公開オンボーディング (メール招待 → 組織登録 / signup)¶
運営が宛先メールに 6 桁の認証コードを送信し、受信者がそのコードを使って
- 組織 (tenant) 1 つ
- その最初の管理者 (admin) アカウント 1 つ
を作成する、半クローズドな公開オンボーディング機能です。公開ページから誰でもコードを 要求することはできません (運営がメール宛に招待を発行する方式)。
1. 構成 (DB / API / Web / 運営スクリプト)¶
| 層 | 実体 | 役割 |
|---|---|---|
| DB | public.signup_invite_codes (migration 00000000000048 + 00000000000049) |
招待台帳。email / code_hash / attempts を持つ。service_role のみ読み書き可 (RLS ロックダウン) |
| 運営 | scripts/invite-org.mjs |
6 桁コードを生成 → code_hash を保存 → Resend で宛先にコードをメール送信 |
| API | POST /signup (apps/api/src/routes/signup.ts) |
公開エンドポイント。email + code を検証 → tenant / admin / profiles 作成 → 招待を消費 |
| Web | /register (apps/web/src/pages/RegisterOrgPage.tsx) |
公開ページ。メール + コード + 組織情報を入力 → POST /signup |
セキュリティモデル (主防御は単一の鍵ではなく多層)¶
- メール所有: 運営がそのアドレス宛にコードを送るため、メールを受信できる人だけがコードを知り得る。公開ページから任意のメールへコードを送る導線は無い。
- 6 桁コードのハッシュ照合: 平文コードは DB に保存せず、
code_hash = sha256(id:code)のみ保存する (salt = 招待行のid)。平文はメール本文にだけ出す。 - 試行上限: コード照合の失敗が
attempts >= 5に達すると、その招待行では以後照合できない。 - 短い有効期限: 既定 10 分 (
--expires-minで調整可)。期限切れは登録不可。 - 単回使用: 消費は
used_at is nullをガードにしたアトミック更新。同時実行でも成功は 1 件だけ。 signup_invite_codesは service_role (= API サーバ / 運営スクリプト) だけが読み書きする。anon / authenticated 向け RLS ポリシーは作っていないため、ブラウザのクライアントから台帳は読めない。- Resend の API キーは Worker (API) に置かない。コード発行はこの運営スクリプトだけが行うため、
RESEND_API_KEYはスクリプト実行環境の env にのみ置く。Worker にはSUPABASE_SERVICE_ROLE_KEY等のみ。 - 新規
tenant_idを採番し admin のapp_metadataに埋めることでテナント分離。 - 作成途中で失敗した場合は作成済みリソースをロールバックし、部分作成を残さない。
- エラーメッセージは汎用化し、存在 / 期限 / 種別 / コードのどれで弾いたかをリークしない。
レート制限について¶
Cloudflare Workers 単体 (追加ストレージ無し) で厳密なレート制限を実装するのは難しいため、 上記の 「メール所有 + 6 桁 + 試行上限 5 + 10 分 + 単回使用」の多層が主たる不正登録対策です。 さらに強化する場合は Cloudflare の WAF・Rate Limiting Rules の併用を検討してください。
collector と discharger の差¶
| collector (処理業者) | discharger (排出事業者) | |
|---|---|---|
tenants 1 行 |
✅ type=collector |
✅ type=discharger |
| admin auth ユーザー | ✅ app_metadata={tenant_id, tenant_type, role:'admin'} |
✅ 同左 |
profiles 1 行 |
✅ 作成 (login_id / display_name / role='admin') |
❌ 作らない (app_metadata だけで解決) |
ログイン方式は既存と同じ ログインID + パスワード (email = <login_id>@members.waste-link.local)。
招待行に種別 (tenant_type) が固定されている場合、/register で選ぶ種別が一致しないと 403
になります (受信者にはメールで種別を案内してください)。
2. 運営の準備 (本番で機能させる前提) ⚠️¶
2-1. Worker 側 (/signup 用)¶
POST /signup は SUPABASE_SERVICE_ROLE_KEY が無いと 503 を返します。本番 / staging の
Worker に設定済みであること。
cd apps/api
pnpm exec wrangler secret put SUPABASE_SERVICE_ROLE_KEY --env production
# staging を試すなら --env staging
SUPABASE_URL / SUPABASE_ANON_KEY も同様に設定済みであること (既存機能と共通)。
RESEND_API_KEY は Worker には設定しません (発行は運営スクリプト側のみ)。
2-2. Resend 側 (コード送信用)¶
- Resend アカウントを作成する (https://resend.com)。
- 送信ドメインを登録し、DNS を検証する: Resend が指示する SPF / DKIM レコード (および推奨の DMARC) を DNS に追加し、ドメインを Verified にする。検証が済まないと 到達率が著しく落ちる / 送信できない。
- 検証済みドメインの送信元アドレスを
MAIL_FROMに使う (例onboarding@<verified-domain>)。 - Resend ダッシュボードで API キーを発行し、運営スクリプト実行環境の
RESEND_API_KEYに置く。
3. 招待の発行 (運営が実行)¶
宛先メールに 6 桁コードを送る運営スクリプト。code_hash のみ DB に保存し、平文コードは
メール本文にだけ出ます (ログにも出力しません)。
SUPABASE_URL="https://<project-ref>.supabase.co" \
SUPABASE_SERVICE_ROLE_KEY="<service_role キー>" \
RESEND_API_KEY="<Resend API キー>" \
MAIL_FROM="onboarding@<verified-domain>" \
node scripts/invite-org.mjs \
--email owner@example.com \
--tenant-type collector \
--org-name "サンプル運輸" \
--expires-min 10
| 引数 | 必須 | 説明 |
|---|---|---|
--email <addr> |
✅ | 招待先メール。この宛先にコードが届く |
--tenant-type <collector\|discharger> |
— | 種別を固定 (省略するとどちらの種別でも使える) |
--org-name <text> |
— | 組織名ヒント (note に保存。任意) |
--expires-min <int> |
— | 有効期限の分数 (既定 10) |
--dry-run |
— | DB / メール送信せず、計画だけ表示 (env 不要) |
--dry-runを付ければ env 無しで内容を確認できます (コードは生成・表示しません)。
再送は、クールダウンを考慮しつつ同じコマンドを再実行します。同一メールの「未使用」招待行は 置換され (delete → insert)、常に最新の 1 件だけが有効になります。
招待コード・
SUPABASE_SERVICE_ROLE_KEY・RESEND_API_KEYは秘密情報です。Slack 等に 平文で貼らず、ログにも残さないでください。
(参考) 旧 out-of-band 方式¶
メールを介さず運営が手入力する平文招待コード (signup_invite_codes.code) を発行する旧方式は
廃止しました (bootstrap-org.mjs --issue-invite-code は削除済み)。招待は必ず
scripts/invite-org.mjs で「宛先メール + code_hash」として発行してください (新フロー: メール + 6 桁)。
code 列は後方互換のため残しています。
4. 登録フロー (受信者視点)¶
- 運営から届いた招待メールを開き、6 桁の認証コードを確認する。
- Web アプリの
/registerを開く (ログイン画面の「招待メールをお持ちの方の組織登録」からも遷移可)。 - 招待メールアドレス / 認証コード (6桁) / 組織名 / 事業者種別 / 管理者ログインID / 管理者名 / パスワードを入力して送信。
- 成功すると
/signinに遷移し、ログインID がプリフィルされる。指定したログインID + パスワードでログイン。 - collector admin →
/collector - discharger →
/discharger
エラーの意味 (API レスポンス)¶
| status | 意味 |
|---|---|
400 |
入力不備 (メール形式・コードが6桁でない・組織名・ログインID 形式・パスワード長など) |
403 |
招待が無効 (存在しない / 期限切れ / 試行上限 / コード不一致 / 種別不一致)。原因は区別せず汎用表示 |
409 |
そのログインID が既に使われている |
503 |
サーバに SUPABASE_SERVICE_ROLE_KEY 等が未設定 (本番前提の §2-1 を確認) |
5. 関連ドキュメント¶
- 認証・テナントモデルの全体像:
docs/auth-and-tenants.md - 初回ブートストラップ (一度きりの種火):
docs/bootstrap-org.md