読了 約 8 分 Cosoado Lab

4 本目のマッチングアプリ — 釣り仲間マッチング「TsuriMate」ロンチで学んだこと

3 本目を作ったときは「これでテンプレ化は完成」と思っていた。4 本目を作って、まだ見えていない穴が 5 つあったことに気づく話。船割りマッチングという釣り固有のウェッジと、同じコードベースに新ジャンルを載せるときに立ち上げスクリプトが面倒を見てくれない地雷たちをまとめます。

同じコードベースから 4 本のマッチングアプリが動いている経緯は 別記事 にまとめてあります。この記事は、そこで「テンプレ化できた」と書いた後に、4 本目で露呈した粗の話です。

4 本目は「釣り」に決めた

これまでに作った 3 本(SparMate / NetaPair / BoardLink)は、どれも「条件で合う仲間が近くにいない」という同じ形の痛みを扱っていた。

4 本目に選んだのは釣り。TsuriMate(ツリメイト)

きっかけは単純で、海釣りをやっている知人から繰り返し同じ愚痴を聞かされたことだった。

「タイラバ船、4 人で割れば 1 人 8,500 円。でも毎回 2 人しか集まらない」

これが核心だった。釣り人口 510 万人、年間 4.4 万円使うソロアングラー。一人で船は高くてチャーターできないし、4 人のチャーター枠に 1〜2 人足りないまま時期が過ぎる。この 1〜2 人の空席が、毎週全国で何万席も発生している計算になる。

船割り という明確な解決対象があり、かつ既存の SNS では解決できていない。これは作る価値がある、と判断した。

既存アプリと違う "条件の語彙"

3 本目までで学んだのは、マッチングの核は「条件で絞れる」ことで、その条件の語彙はジャンルごとにまったく別物だということ。

項目釣り固有の語彙
ジャンル海釣り(ショア)/海釣り(オフショア・船)/淡水/その他(SUP・カヤック)
釣法ショアジギング・エギング・タイラバ・ジギング・バス釣り・渓流フライ・フカセ・サーフ(ヒラメ/マゴチ)…
魚種ブリ、マダイ、アオリイカ、ブラックバス、ヤマメ、ヒラメ、マゴチ、タチウオ…
レベルビギナー(釣り歴1年未満)→ 初級 → 中級 → 上級 → エキスパート
装備・免許マイボート所有/小型船舶 1 級・2 級/ウェーダー/桜マーク認定ライフジャケット
安全フラグ天候悪化時は即撤退/単独行動禁止/GPS 共有を希望

ここで 1 つ強いルールを設けている。他ジャンルの語彙を流用しない。

たとえば格闘技の「重量級」はボドゲでも使っている BGG Weight(ゲームの思考負荷)と混ざると概念が壊れる。釣りの「重量級」が仮にあったとしても(実際はない)、それは前 2 者とは無関係な意味になる。各ジャンルはネイティブの語彙だけで組む、を徹底している。

船割りモード — TsuriMate 固有の機能

釣り仲間マッチングの他に、このアプリ固有の機能として「船割りモード」を入れた。

釣行募集を立てるとき、船割りトグルを ON にすると、以下が一括で出現する:

カード表示側でもこれは強調される。船割りモードの釣行は水面風のアクセントバーが入り、料金情報がサブカードで目立つ。「船割りのみ表示」トグルで一発フィルタもできる。

これは他のどのマッチングアプリにもない機能で、釣り文化の具体的な pain に最短で刺さるウェッジとして設計した。他ジャンルでは不要なので、featureFlags でなく釣り固有の UI 分岐にしている。

釣果ログと魚種図鑑 — リテンションの仕掛け

もう 1 つ、釣り文化で強いのが「釣果を記録・共有したい」という欲求。

TsuriMate には 23 種の日本の代表的な釣魚をマスタデータで持たせた(ブリ・マダイ・アオリイカ・ブラックバス・ヤマメ・ヒラメ・マゴチ等)。ユーザーが釣果を記録すると、その魚種が図鑑に色付きで記録される。未釣果の魚はモノクロのまま残っていて、次に釣りたい魚が視覚的に見える。

月間・年間ランキングも搭載した:

自分の順位が TOP20 圏内ならバナーで表示し、「○月のマダイ部門 1 位」を X にワンタップ共有できるようにした。SNS 拡散の導線を最初から用意しておく、という設計判断。

テンプレに乗るはずが、乗らなかった

3 本目を出した時点で、node scripts/add-genre.mjs fishing 1 行で新ジャンルが立ち上がる「はず」だった。実際は、隠れた地雷が 5 つ発火した。

地雷 1: genre-split スキーマの bootstrap

テンプレの DB は 1 Supabase プロジェクトにジャンル別スキーマを作って分離している。sparmate.profiles / netapair.profiles / boardlink.profiles が別物として並存する。

僕は最初、新テーブルを public.profiles に紐付けようとした。失敗した。public.profiles は存在しない(genre-split 時に全ジャンルスキーマに移動済み)。

正しい手順は: 新スキーマ tsurimate を作成 → genre_schema_template.sqlsed 's/@SCHEMA@/tsurimate/g' で展開して適用 → PostgREST の db_schemas に追加 → Realtime publication に追加。1494 行の SQL をつなぎ直す作業が必要だった。

地雷 2: PostgreSQL CHECK 制約で SELECT サブクエリが使えない

釣果写真の URL を「catches_tsurimate バケットの公開 URL パターン」だけに制限したくて、こう書いた:

ALTER TABLE catch_logs ADD CONSTRAINT ... CHECK (
  photo_urls IS NULL OR
  (SELECT bool_and(u ~ '^https://...') FROM unnest(photo_urls) AS u)
);

結果: ERROR: cannot use subquery in check constraint (SQLSTATE 0A000)

正しくは IMMUTABLE 関数にラップする:

CREATE OR REPLACE FUNCTION catches_urls_valid(urls text[])
RETURNS boolean LANGUAGE sql IMMUTABLE AS $$
  SELECT urls IS NULL OR (SELECT bool_and(u ~ '...') FROM unnest(urls) AS u);
$$;
ALTER TABLE ... ADD CONSTRAINT ... CHECK (catches_urls_valid(photo_urls));

CHECK 内の副問い合わせ禁止ルール。15 年書いてて初めてハマった。

地雷 3: migration ファイル名の version 衝突

Supabase CLI はファイル名の先頭数字を schema_migrations.version に INSERT する。20260424_catch_logs.sql20260424_fish_species_seed.sql は両方 version=20260424 で UNIQUE 制約違反。

14 桁タイムスタンプ YYYYMMDDhhmmss_... にリネームして衝突回避。同日複数の migration は時刻で区別する。

地雷 4: OG/Twitter image alt のフォールバック

本番にデプロイした直後、Twitter に URL を貼ったら SparMate のブランド名が表示された。原因は src/app/opengraph-image.tsx の alt が martial / comedy / boardgame の 3 ブランチしかなく、fishing は SparMate の fallback に落ちていたこと。

地雷 5: favicon 一式の手動生成

public/icons/<genre>/{favicon.png, icon-192.png, icon.png} は add-genre.mjs が面倒を見ない。新ジャンルのアイコンはゼロから用意しないと 404 が出る。

学んだことをスクリプトに戻す

5 つ全部を踏んだ後、add-genre.mjs を改修した。

preflight フェーズで Vercel を作る前に:

後工程 で:

失敗 1 回でテンプレが強くなる。個人開発の醍醐味だと思う。

SEO キーワードは完全にジャンル固有

最後にひとつ、4 本目で改めて実感したこと。ジャンルごとに狙うべきキーワードが丸ごと違う

TsuriMate で仕込んだキーワード群 (一部):

SparMate の「BJJ training partner」、NetaPair の「M-1 相方募集」、BoardLink の「TRPG 同卓」とは 1 語も被らない。同じコードベースで 4 つの検索インデックスを別々に育てている感覚。

FAQ 構造化データ(JSON-LD)も 4 ジャンルぶん別々に書いた。リッチスニペット露出を狙う場合、ジャンルごとの質問文を自然な口語で書くのがおそらく一番効く。「船代を割り勘できる仲間を探せますか?」みたいな、検索者がそのまま打ちそうな疑問形。

ロードマップ

β0 でリリースした TsuriMate は、現在:

次のフェーズで足す予定:

おわりに

「あと 1 人足りない」の構造は、格闘技でもお笑いでもボドゲでも釣りでも変わらない。違うのは語彙とユーザー文化だけ、というテンプレ化の仮説は、4 本目でも成立した。

ただし、「テンプレに乗る」と「新ジャンルを立ち上げる」の間には、毎回新しい地雷がある。3 本目までで気づいていなかった genre-split スキーマ、CHECK サブクエリ、OG alt フォールバック、アイコン自動生成、migration version 衝突——これらは 4 本目で初めて浮上した問題で、もし 5 本目を作るときには別の地雷が出るだろうと思っている。

その "まだ見えていない穴" をつぶし続ける開発をやるなら、一番効くのは 「失敗を script に戻す」 こと。preflight チェックを書けば、同じ地雷は 2 度と踏まない。

TsuriMate は、ソロアングラーの週末を変えるかもしれないアプリだと、自分では思っている。船割りで釣行が 1 回でも成立すれば、このアプリを作った意味はあった。

もし釣りをやっていて、今週末の 1 人分が空いている人がいたら、登録 してみてほしい。

本記事で言及したマッチングアプリ + 関連プロダクト

TsuriMate(本記事の主役) 釣り仲間・船割りメンバーマッチング、釣果ログ・魚種図鑑搭載 SparMate BJJ・Boxing・MMA など格闘技の練習相手マッチング NetaPair 漫才・コント・ピン芸人の相方マッチング BoardLink ボドゲ・TRPG・カードゲームの同卓メンバー募集 YUMELIA 夢占い・数秘術が無料でできる AI 占い Web アプリ OshiVista 推し活を 1 つにまとめる管理アプリ(多言語対応)