マッチングサイトを一人で作って気づいた「3つの現実」|ホテル支配人がエンジニアに転身してJINZAIを作った話

記事
IT・テクノロジー
はじめに
ホテルの支配人を13年やった後、フリーランスエンジニアに転身し、今は JINZAI(ホスピタリティ特化の人材マッチングプラットフォーム)を一人で開発している。
スタックは Next.js 14 / TypeScript / Supabase / Vercel。正直に言うと、マッチングサービスを作るのがこんなに大変だとは思っていなかった。
この記事では、実際に手を動かして痛感した「3つの現実」を書く。同じように一人でマッチングサイトを作ろうとしているエンジニア、またはSaaSを0から立ち上げようとしている人の参考になれば。

現実① マッチングサイトのDB設計は「関係者が多すぎる」問題が必ず起きる
最初に設計した時、テーブルの数をなめていた。
ECサイトなら users / products / orders の3本柱でだいたい話が成立する。でもマッチングサイトは違う。登場人物が多い。
JINZAIの場合、主要なテーブルだけでこうなった:
profiles // 共通ユーザー情報
workers // 求職者プロフィール
clients // 採用企業プロフィール
jobs // 求人票
applications // 応募
messages // メッセージ
notifications // 通知
earnings // 収益・報酬管理
documents // 提出書類(在留カード、資格証明など)
verifications // 書類審査ステータス
そして設計の難所は「誰が誰の何を見ていいか」という権限設計だ。
Supabaseは RLS(Row Level Security)で行レベルのアクセス制御ができる。これ自体は強力な機能なのだが、マッチングサービスで「worker は自分の application だけ見える」「client は自社の job に紐づく application だけ見える」「admin はすべて見える」という3者が絡む権限設計をRLSで書くと、ポリシーが複雑になる。
実際に書いたポリシーの一部:
sql-- workerは自分のapplicationのみ参照可
CREATE POLICY "workers_select_own_applications"
ON applications FOR SELECT
USING (
  auth.uid() = (
    SELECT user_id FROM workers WHERE id = applications.worker_id
  )
);

-- clientは自社jobのapplicationのみ参照可
CREATE POLICY "clients_select_job_applications"
ON applications FOR SELECT
USING (
  EXISTS (
    SELECT 1 FROM jobs j
    JOIN clients c ON c.id = j.client_id
    WHERE j.id = applications.job_id
    AND c.user_id = auth.uid()
  )
);
これを書いて、ローカルでテストして、本番にデプロイして……「あれ、なぜかworkerがclientのデータを見れてしまう」というバグを踏む。あるある。
RLSのデバッグが特にしんどいのは、エラーメッセージが「空の配列が返る」だけで、「RLSに弾かれたのか」「そもそもデータがないのか」が区別しにくいことだ。

現実② Supabase/SQLの検証作業は「信じるな、確かめろ」の連続
SupabaseはUIが便利で、初期開発は爆速で進む。しかし本番に近づくほど「本当にこのデータ、意図通りに入ってるか?」という確認作業が増える。
特に手間がかかったのは、型の生成と実際のスキーマのズレだ。
Supabaseには supabase gen types typescript でDB定義からTypeScriptの型を自動生成できる機能がある。これを使って database.types.ts を作成し、フロントエンドと共有するのが基本構成なのだが——
スキーマを変更するたびに型を再生成しないと、コンパイルエラーが出ないのに実行時に壊れるというケースが起きる。TypeScriptの「型が合ってる」は「DBのスキーマと合ってる」を保証しない。
実際にやらかした例:

カラム名を worker_type から employment_type に変えた
型を再生成し忘れた
フロントでは worker_type を参照していたが、コンパイルエラーは出ない(any が漏れていた)
本番デプロイ後、特定の条件分岐が常にfalseになっていることに気づいた

地味にキツかったのは、ビザ書類の審査ステータス管理のSQL設計だ。
JINZAIは外国人材を対象にしているため、在留資格の確認フローが必要になる。書類のステータスを pending / reviewing / approved / rejected で管理し、特定のステータスにある worker だけが応募できるようにする——という仕様を、SQL/RLSで実装するのは思ったより複雑だった。
sql-- approvedなworkerのみ応募可能
CREATE POLICY "approved_workers_can_apply"
ON applications FOR INSERT
WITH CHECK (
  EXISTS (
    SELECT 1 FROM workers w
    JOIN verifications v ON v.worker_id = w.id
    WHERE w.user_id = auth.uid()
    AND v.status = 'approved'
  )
);
このポリシーを書いた後、「テスト用に approved 状態の worker を手動で作って動作確認する」という作業が発生する。Supabase の SQL Editor で直接 INSERT して確認するのだが、RLS が邪魔して service_role を使わないとINSERTできない、などのハマりポイントがある。
検証フローとして今は以下を定番にしている:

Supabase SQL Editor で set role authenticated; set request.jwt.claims... を使い、特定ユーザーの視点でSELECTを確認する
型の再生成(supabase gen types typescript --project-id xxx > src/types/database.types.ts)をスキーマ変更のたびに必ず実行
テストユーザーのIDをメモしておき、各ロールの挙動を横断確認する


現実③ 外国人採用の「法的複雑さ」はDBに落とすのが難しい
ホテル支配人だった時代、外国人スタッフの採用を何度も経験した。ビザの種類によって「できる仕事」「できない仕事」が違う。在留カードの期限管理、就労資格証明書の有無——これは採用担当なら誰でも知っている話だ。
ただ、これをプラットフォームとして実装するのは別次元の話だった。
まず「在留資格」の種類が多い。技術・人文知識・国際業務、特定技能1号・2号、技能実習、永住者、日本人の配偶者等……それぞれで就労制限が違う。これをDBのenumで管理するか、マスターテーブルを作るか、という設計判断から始まる。
JINZAIでは visa_types テーブルを別に切り出し、workers テーブルからFKで参照する設計にした。さらに「この在留資格でこの職種への応募は可能か」というルールをどこで持つか——アプリ側のロジックで判定するか、DBの制約で弾くか。
現状は「UIでガイドする+申込後に審査フローで確認」というハイブリッドにした。完全にDB制約だけで管理しようとすると、法改正のたびにマイグレーションが必要になるため、柔軟性を優先した判断だ。
また、JINZAIは 有料職業紹介事業の許可を取得している。これは法的に必要な要件で、プラットフォームとして求人と求職者をマッチングして報酬を得るには、厚生労働省の認可が必要になる。この許可番号をLPや利用規約に掲載する義務もある。
ツールとして作るだけなら関係ない話だが、「実際に人を動かすプラットフォーム」を作ろうとすると、法的な文脈を無視できない。コードを書くだけでは完結しないのが、マッチングサービスの難しさだと思う。

現在地と今後
JINZAIは現在、Next.js 14 + Supabase + Vercel の構成でα版を開発中。

求人票の投稿・検索
応募・メッセージ機能
書類審査フロー(在留カード等のアップロード・審査ステータス管理)
管理者ダッシュボード(5タブ構成)

課金モデルは成功報酬型(採用1名あたり ¥50,000〜¥100,000)を想定している。ホテル・旅館などのホスピタリティ業界に特化した人材マッチングは、ニッチだが需要は確かにある。

まとめ
マッチングサイトを一人で作って気づいた3つの現実:
① 設計の複雑さ:登場人物が多いサービスは、RLSの権限設計が指数的に複雑になる。「誰が誰の何を見ていいか」を最初に整理してからDBを設計すること。
② 検証の地味さ:SupabaseのRLSバグはエラーが出ない。型生成の手間を惜しむと本番で壊れる。「信じるな、確かめろ」を徹底する習慣が必要。
③ 法的な現実:人を動かすプラットフォームはコードだけで完結しない。特に外国人採用は在留資格・就労制限・有料職業紹介許可など、法的文脈の理解が設計に直結する。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す ココナラコンテンツマーケット ノウハウ記事・テンプレート・デザイン素材はこちら