同じコードベースから 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 にすると、以下が一括で出現する:
- 船名(例: 館山◯◯丸)
- 船の定員(4〜20 名)
- 1 人あたりの目安料金(例: ¥8,500)
- 費用分担方法(船代割り勘/エサ・仕掛けは各自/全額主催者負担…)
カード表示側でもこれは強調される。船割りモードの釣行は水面風のアクセントバーが入り、料金情報がサブカードで目立つ。「船割りのみ表示」トグルで一発フィルタもできる。
これは他のどのマッチングアプリにもない機能で、釣り文化の具体的な pain に最短で刺さるウェッジとして設計した。他ジャンルでは不要なので、featureFlags でなく釣り固有の UI 分岐にしている。
釣果ログと魚種図鑑 — リテンションの仕掛け
もう 1 つ、釣り文化で強いのが「釣果を記録・共有したい」という欲求。
TsuriMate には 23 種の日本の代表的な釣魚をマスタデータで持たせた(ブリ・マダイ・アオリイカ・ブラックバス・ヤマメ・ヒラメ・マゴチ等)。ユーザーが釣果を記録すると、その魚種が図鑑に色付きで記録される。未釣果の魚はモノクロのまま残っていて、次に釣りたい魚が視覚的に見える。
月間・年間ランキングも搭載した:
- 総釣果数 TOP10: 表彰台(1/2/3 位) + 4〜10 位のリスト
- 魚種別 最大サイズ TOP3: マダイ部門、ブリ部門、ブラックバス部門… 各魚種で最大サイズを競う
自分の順位が 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.sql を sed '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.sql と 20260424_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 を作る前に:
src/config/genre.tsに<genre>エントリがあるかsrc/lib/supabase/schema.tsに分岐があるかopengraph-image.tsx/twitter-image.tsxに分岐があるか(これが最初に弾けていれば地雷 4 は発火しなかった)public/icons/<genre>/が揃っているか(不足なら sharp で自動生成)
後工程 で:
- Supabase Management API トークンがあれば、Auth Redirect URL を自動追加
- なければ手動手順を表示
- 最後に「genre-split bootstrap migration は別途必要」等のチェックリストを echo
失敗 1 回でテンプレが強くなる。個人開発の醍醐味だと思う。
SEO キーワードは完全にジャンル固有
最後にひとつ、4 本目で改めて実感したこと。ジャンルごとに狙うべきキーワードが丸ごと違う。
TsuriMate で仕込んだキーワード群 (一部):
- 釣り仲間 / 釣り仲間 募集 / 釣り 同行者 / 釣り マッチング
- 船釣り 割り勘 / 船釣り 相乗り / 乗合船 仲間
- タイラバ 仲間 / ジギング 仲間 / バス釣り 同船 / 霞ヶ浦 バス
- エギング 仲間 / サーフ ヒラメ 仲間 / 渓流 釣り 仲間
- 釣果 記録 アプリ / 釣果ログ / 魚種図鑑
- 釣りガール / ファミリーフィッシング
- 東京湾 釣り 仲間 / 玄界灘 釣り / 琵琶湖 バス
- fishing buddy app / split charter fishing / bass fishing buddy
SparMate の「BJJ training partner」、NetaPair の「M-1 相方募集」、BoardLink の「TRPG 同卓」とは 1 語も被らない。同じコードベースで 4 つの検索インデックスを別々に育てている感覚。
FAQ 構造化データ(JSON-LD)も 4 ジャンルぶん別々に書いた。リッチスニペット露出を狙う場合、ジャンルごとの質問文を自然な口語で書くのがおそらく一番効く。「船代を割り勘できる仲間を探せますか?」みたいな、検索者がそのまま打ちそうな疑問形。
ロードマップ
β0 でリリースした TsuriMate は、現在:
- 釣行募集・船割り・釣果ログ・魚種図鑑・月間/年間ランキングが稼働
- 完全無料(クレカ不要)
- 10 人のデモユーザーが仕込まれていて、初ログインからすぐ使える
次のフェーズで足す予定:
/spots釣り場マップ(国土地理院タイル + 遊漁船 DB)- 本人確認バッジと相互レビュー
- 釣行当日のみ有効な GPS 共有
- β3 で課金 live 切り替え(月額 800 円 / 釣果記録無制限・優先マッチング)
おわりに
「あと 1 人足りない」の構造は、格闘技でもお笑いでもボドゲでも釣りでも変わらない。違うのは語彙とユーザー文化だけ、というテンプレ化の仮説は、4 本目でも成立した。
ただし、「テンプレに乗る」と「新ジャンルを立ち上げる」の間には、毎回新しい地雷がある。3 本目までで気づいていなかった genre-split スキーマ、CHECK サブクエリ、OG alt フォールバック、アイコン自動生成、migration version 衝突——これらは 4 本目で初めて浮上した問題で、もし 5 本目を作るときには別の地雷が出るだろうと思っている。
その "まだ見えていない穴" をつぶし続ける開発をやるなら、一番効くのは 「失敗を script に戻す」 こと。preflight チェックを書けば、同じ地雷は 2 度と踏まない。
TsuriMate は、ソロアングラーの週末を変えるかもしれないアプリだと、自分では思っている。船割りで釣行が 1 回でも成立すれば、このアプリを作った意味はあった。
もし釣りをやっていて、今週末の 1 人分が空いている人がいたら、登録 してみてほしい。