認証・テナント運用ガイド¶
waste-link の認証モデル、テナント割当、招待運用についての実務ハンドブック。
1. 認証モデル¶
1.1 全体像¶
auth.users (Supabase)
├── id : UUID
├── email
├── raw_app_meta_data : サーバ側のみ書込可
│ ├── tenant_id : tenants.id
│ ├── tenant_type : 'discharger' | 'collector' | 'external_partner'
│ │ (テナント種別を JWT に
│ │ denormalize)
│ └── role : 'admin' | 'normal' | 'driver' (collector のみ意味あり)
└── raw_user_meta_data : ユーザー編集可
│
│ サインイン時に JWT へ自動エンコード
▼
JWT claims
├── sub : auth.users.id
├── email
├── app_metadata : 上記 raw_app_meta_data の中身
└── user_metadata : 同上
│
│ RLS / アプリ層から参照
▼
DB helpers
├── public.current_tenant_id() → tenants.id
├── public.current_tenant_type() → 'discharger' | 'collector' | 'external_partner'
└── public.current_user_role() → 'admin' | 'normal' | 'driver' | NULL
1.2 テナント種別¶
tenants.type |
説明 | アプリ内画面 (デフォルト遷移先) |
|---|---|---|
discharger |
排出業者 (廃棄物を出す側) | /discharger |
collector |
処理業者 (収集・運搬・処分) | admin/normal → /collector / driver → /driver |
external_partner |
委託先 (衛星テナント・KYO-106 Phase3)。自前業務データを持たず、collector が保持する自社帰属の委託料精算を越境読取のみで閲覧 (role='admin' 固定) |
/external-partner/settlements |
external_partner(委託先) は §1.1 の 3 値目。tenants.type制約 (mig97tenants_type_check) がdischarger/collector/external_partnerを許可し、Web のTENANT_TYPESallowlist もこの 3 値 (AuthContext.tsx:140)。衛星テナントは運営スクリプト + service-role INSERT でのみ作成し (公開サインアップは 3 値化しない)、越境精算閲覧は DEFINER RPC (mig98) 経由に限定される。権威:apps/web/src/contexts/AuthContext.tsx:16-21,244-246+ mig97。
1.3 ロール (role)¶
ロール値は profiles_role_check 制約 (mig27) で admin / normal / driver に限定される
(public.is_valid_role)。
app_metadata.role |
対応テナント種別 | 用途 |
|---|---|---|
admin |
discharger, collector の両方 | 管理者画面・全機能アクセス。工場/車両の新規作成 (INSERT) は admin 限定 (mig44) |
normal |
collector (事務) | 事務系の全操作。RLS の書込 (factories/vehicles/cost_entries 等の UPDATE/DELETE・cost_entries の INSERT) は admin と同水準 (mig27/mig43)。工場/車両の新規作成のみ不可 (mig44) |
driver |
collector のみ | ドライバー画面 (配車・現場操作)。事務操作・課金 API は不可 |
| 未設定 (null) | collector | 実効権限なし。RoleGuard / RLS / isCollectorAdmin いずれも通らない |
排出業者は当面ロール区分なし。role を設定しなくても admin 相当として動く
(resolveLandingPath が discharger は無条件に /discharger へ送る)。
collector で role が未設定 (null) の場合、resolveLandingPath は admin/未設定をまとめて
/collector (admin Dashboard) へ送るが、実効権限は付かない (RLS の current_user_role() が
null のため書込ポリシーに非合致)。この誤解を避けるため、UI のロール表示は未設定を
「処理業者 - 権限未設定」 と正直に出す (管理者と偽らない。PR #135・apps/web/src/lib/role-display.ts)。
修復は §3 の運用 (チーム管理 UI または scripts/set-member-role.mjs) で role を付与する。
実効ロールの権威 (JWT app_metadata.role) と profiles.role¶
権限の権威は auth.users.raw_app_meta_data.role (JWT app_metadata.role 経由)。RLS の
current_user_role() はこれを JWT から読み、フロントの useAuth().role も app_metadata.role 由来。
profiles.role は表示用の副次コピーであり、認可判定には使わない。チーム管理 UI (apps/api routes/team.ts) や
scripts/set-member-role.mjs は両者を同時に更新する。
⚠️ 反映タイミング:
app_metadata.roleの変更は、対象ユーザーの JWT が更新される (再ログイン / トークンリフレッシュ) まで RLS・画面に反映されない。ロールを変えた直後は 対象者に再ログインを促すこと。
1.4 採用方針サマリー¶
| 項目 | 採用 | 理由 |
|---|---|---|
| 認証方式 | メール+パスワード + マジックリンク | 業務アプリで一般的、パスワード忘れも OTP で吸収 |
| サインアップ | 招待制 (enable_signup = false) |
勝手な登録を防ぐ |
| テナントモデル | 1ユーザー = 1テナント | シンプル。将来 memberships で多対多へ拡張可能 |
| テナント種別の付与 | auth.users.raw_app_meta_data.tenant_type |
JWT に自動投入、RLS / アプリ層共通 |
| Auth Hook | 不要 | app_metadata 経路で十分 |
1.5 RLS / アクセス制御¶
public.current_tenant_id() の挙動:
| 状態 | 返却値 |
|---|---|
| 未ログイン (anon) | NULL |
| ログイン済 / tenant_id 未割当 | NULL |
| ログイン済 / tenant_id 設定済み | テナントの UUID |
tenants テーブルや業務テーブルの RLS は tenant_id = public.current_tenant_id()
で絞り込み。tenant_id 未割当のユーザーは何もアクセスできない (フェイルセーフ)。
current_user_role() を使ったロール差分は既に導入済み:
- 書込 (INSERT/UPDATE/DELETE) は概ね admin|normal に限定 (mig27)。
- 工場・車両の新規作成 (INSERT) は admin 限定 (mig44)。cost_entries/daily_logs の INSERT は admin|normal (mig43)。
- 工場スコープの READ: admin=テナント全工場 / normal・driver=所属工場のみ (current_user_can_access_factory / mig43)。
- driver は自ルートの dispatch_stops のみ更新可・cost_entries は不可 (mig30)。
サーバ側 (Hono API) にもロール判定ミドルウェアがある:
- requireCollectorAdmin — collector かつ role='admin' (例: メンバー招待)。
- requireCollectorStaff — collector かつ role∈{admin,normal} (例: POST /vehicle-costs/ocr。RLS の
cost_entries INSERT=admin|normal と同じ判定面。driver には課金 API を開放しない)。
- requireDischarger — discharger のみ (例: POST /manifests/ocr)。
2. 新規テナント + 初回ユーザーの作成¶
2.0 α 検証用デモアカウント (推奨: Initial Setup ワークフロー)¶
dev Supabase プロジェクト (gkpzjbzhknghwclrofhc) を α 検証用に wipe → 再投入 できる
GitHub Actions ワークフロー。DB に余計なごみが溜まっても 1 クリックで真っさらに戻る。
(Supabase Branching は廃止済み。dev / 本番は独立した 2 プロジェクト構成。)
仕組み¶
.github/workflows/initial-setup.yml (workflow_dispatch only / develop 固定)
├── ① psql で wipe
│ → truncate public.* cascade
│ → auth.users から旧 demo-*@waste-link.test と新テスト login_id メールを限定削除
├── ② psql で docs/test-data.sql 投入
│ → tenants / permits / service_areas / vehicles / pricing_rules / jobs
├── ③ scripts/setup-demo-users.mjs (Node)
│ → Supabase Admin API でデモアカウントを upsert (login_id でログイン可能化)
│ → app_metadata に tenant_id / tenant_type / role を埋める
└── ④ psql で docs/test-data-driver-deps.sql 投入 (profile 依存の配車/車両ログ)
どのブランチから起動しても、checkout は必ず ref: develop で固定される
ため、参照する test-data.sql / script は常に develop 最新になる
(適用先は git ブランチではなく dev プロジェクト)。
auth.users への直接 SQL INSERT は GoTrue 内部スキーマと相性が悪く
("Database error querying schema" を発生させる)、必ず Admin API 経由
で作成する。
必要な GitHub Secrets¶
| Secret | 取得場所 | 用途 |
|---|---|---|
SUPABASE_DEV_API_URL |
dev プロジェクトの API URL をそのまま (例: https://gkpzjbzhknghwclrofhc.supabase.co) |
Admin API の base URL |
SUPABASE_DEV_DB_URL |
dev プロジェクトの Connection string > Session pooler タブの URI (パスワード入りでコピー) | psql で wipe + test-data 投入 |
SUPABASE_DEV_SERVICE_ROLE_KEY |
dev プロジェクトの API > "Project API keys" の service_role の Reveal | デモユーザー作成 (Admin API) |
service_role キーは DB 全権限を持つ root レベル の鍵。漏洩すると DB が
丸ごと触られるため、GitHub Secrets でのみ保管し UI / git / Slack に絶対
に貼らないこと。
SUPABASE_DEV_DB_URL も同様にパスワードを含むので機密扱い。
直結エンドポイント (db.<ref>.supabase.co) は IPv6 only で GitHub Actions
ランナーから到達できないため、必ず Session pooler の URI を使うこと。
実行手順¶
- GitHub Secrets に上記 3 つが揃っていることを確認
- Actions タブの "Initial Setup (develop branch test data)" workflow を開く
Run workflowをクリック- 入力フォーム:
target:develop(production は選べない設計)confirm:yesを手入力 (誤実行防止)Run workflowをクリック- 1〜2 分で完了。Step Summary にログイン情報が出る。ログインは
login_id (メールアドレスではない。内部で
<login_id>@members.waste-link.localに決定的変換される)。共通パスワードはDemo!2026。例:
全アカウントの一覧 (処理業者 A/B・排出業者・閲覧ポータル用) は
README.md「テストアカウント」節と scripts/setup-demo-users.mjs を参照。
冪等なので何度走らせても OK。実行のたびに wipe → 再投入されるため、 画面操作で増えたごみデータも一掃される。
⚠️ production には絶対に流さないこと。 workflow_dispatch の入力で
target選択肢をdevelopのみに限定しているが、二重チェックとしてSUPABASE_DEV_*系の secret しか参照しないようにしている。
手作業派の場合¶
自分の手で投入したい場合は §2.1〜§2.4 に従う(以下のセクション)。 ただしデモアカウント作成は手数が多いので、本 §2.0 の workflow を 強く推奨。
2.1 共通: テナント作成¶
Supabase Dashboard (dev プロジェクト または 本番プロジェクト) の SQL Editor で:
-- 排出業者を作る場合
insert into public.tenants (name, type)
values ('株式会社○○商事', 'discharger')
returning id;
-- 処理業者を作る場合
insert into public.tenants (name, type)
values ('株式会社○○環境', 'collector')
returning id;
返ってきた id をコピー (例: 123e4567-...)。
2.2 排出業者ユーザーを作る¶
Step 1: Authentication > Users > Add user¶
| 項目 | 値 |
|---|---|
admin@kaisha.example.jp |
|
| Password | 強パスワード |
| Auto Confirm User | ✅ |
Step 2: tenant_id + tenant_type を紐付け¶
update auth.users
set raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb)
|| jsonb_build_object(
'tenant_id', '<2.1 でコピーした tenant_id>',
'tenant_type', 'discharger'
)
where email = 'admin@kaisha.example.jp';
roleは省略可 (排出業者はロール区分なし)。
Step 3: 動作確認¶
/signin でログイン → / → 自動で /discharger へリダイレクトされる。
2.3 処理業者 (管理者) を作る¶
Step 1: Add user¶
同上。Email を該当の管理者にする。
Step 2: tenant_id + tenant_type + role(admin) を紐付け¶
update auth.users
set raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb)
|| jsonb_build_object(
'tenant_id', '<処理業者 tenant の id>',
'tenant_type', 'collector',
'role', 'admin'
)
where email = 'admin@shorigyo.example.jp';
Step 3: 動作確認¶
ログイン → / → /collector (admin Dashboard) へリダイレクト。
2.4 ドライバーを追加する¶
事前に処理業者テナント (collector) が作成済みである前提。
Step 1: Add user¶
driver01@shorigyo.example.jp 等の専用メールでユーザー作成。
Step 2: role = 'driver' を紐付け¶
update auth.users
set raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb)
|| jsonb_build_object(
'tenant_id', '<同じ処理業者 tenant の id>',
'tenant_type', 'collector',
'role', 'driver'
)
where email = 'driver01@shorigyo.example.jp';
Step 3: 動作確認¶
ログイン → / → /driver へリダイレクト。
モバイル幅レイアウトのシンプル画面が出れば成功。
3. ユーザーのテナント / ロール変更¶
テナント間移動¶
update auth.users
set raw_app_meta_data = raw_app_meta_data
|| jsonb_build_object('tenant_id', '<新tenant_id>')
where email = 'user@example.com';
ユーザーは次回サインイン時から新テナントのデータを参照するようになる。
ロール昇格 (driver → admin / normal など)¶
通常はチーム管理 UI (apps/api routes/team.ts の /members/:id/role) から変更する
(app_metadata.role と profiles.role の両方を更新)。SQL で直接変える場合:
update auth.users
set raw_app_meta_data = raw_app_meta_data
|| jsonb_build_object('role', 'admin') -- 'admin' | 'normal' | 'driver'
where email = 'user@example.com';
SQL 直変更は
profiles.role(表示用の副次コピー) を更新しない。表示のズレを避けるため 可能ならチーム管理 UI か下記スクリプトを使う。いずれも再ログインまで反映されない。
実効 admin が 1 人もいないとき (卵と鶏)¶
チーム管理 UI は呼び出し元に有効な admin が必要なため、「テナント内に実効 admin
(app_metadata.role='admin') が 1 人もいない」状態は UI から修復できない。この場合は
service-role で解く運用スクリプトを使う:
SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... node scripts/set-member-role.mjs \
--email <対象メール> --collector-tenant-id <uuid> --role admin [--dry-run]
team.ts /members/:id/role と同一セマンティクス (app_metadata + profiles 両更新・
「最後の管理者」降格ガード・失敗時ロールバック)。service_role は RLS をバイパスするため
--collector-tenant-id は必須で、対象ユーザーの tenant_id 一致を明示検証する
(AGENTS.md 不変条件)。反映は対象者の再ログイン後。
不正な値が混入していないか確認¶
select email, raw_app_meta_data
from auth.users
where coalesce(raw_app_meta_data ->> 'tenant_type', '') not in ('discharger', 'collector', 'external_partner')
or coalesce(raw_app_meta_data ->> 'role', '') not in ('admin', 'normal', 'driver')
or raw_app_meta_data ->> 'tenant_id' is null;
-- coalesce で NULL を検出対象に含める(`NULL not in (...)` は NULL 評価で検出漏れするため、
-- tenant_type / role が欠落している行も「不正」として拾う)。
該当行が出たら設定漏れ。SQL で修復。
4. パスワードリセット¶
現状アプリ内には UI なし。以下の 2 つの代替 がある:
4.1 ユーザー自身が SignIn 画面でマジックリンク利用¶
/signin でタブを「マジックリンク」に切り替え → メール送信 → メールから直接ログイン。
4.2 管理者が Dashboard から再発行¶
Authentication → Users → 該当ユーザー → Send password recovery。
将来:
/reset-password画面を実装し、Supabase の標準フローに統合する。
5. 制限事項と将来の改善¶
現状の制限¶
- サインアップ機能なし (招待制を意図)
- 多対多テナント関係不可
- パスワード再設定 UI なし (マジックリンクで代替)
- 招待メールテンプレートは Supabase デフォルトのまま (日本語化未対応)
- ロール権限は UI 出し分けに加え、RLS とサーバミドルウェアでも制御済み (§1.3 / §1.5)。 書込・工場スコープ READ は admin|normal|driver で差がある (admin/normal がテナント全データを 見えるとは限らない: normal/driver は工場スコープ)。
将来検討する改善¶
| 項目 | 概要 | 想定タイミング |
|---|---|---|
| 招待 Edge Function | UI から招待 → Edge Function が auth.admin.inviteUserByEmail を叩く → 招待リンクで自動的に tenant_id / tenant_type / role 紐付け |
管理者画面実装と同時 |
| memberships テーブル | 1ユーザー複数テナント。会計事務所が複数排出業者をまとめて見る | 会計事務所ユース時 |
| role-based RLS (追加分) | 基本のロール差分・工場スコープは導入済み (§1.5)。残る細粒度 (例: jobs の driver=assigned のみ) は業務機能に応じて拡張 | 業務機能実装時 |
| メールテンプレ日本語化 | Authentication > Email Templates | 本番導入直前 |
| SAML / OIDC SSO | 大口顧客向け | 50社規模到達後 |
6. トラブルシューティング¶
ログインできるが / で「テナント未割当」表示¶
→ Step 2 の SQL UPDATE が漏れている、または tenant_type を入れ忘れた。
tenant_id と tenant_type の両方があるか確認。
ドライバーなのに /collector (admin Dashboard) に飛ぶ¶
→ role: 'driver' が設定されていない。Step 2 の SQL を確認・再実行。
/driver を開いてもすぐ /collector にリダイレクトされる¶
→ 自分が can_drive を持たない admin/normal なので RoleGuard が弾いている。これは正常動作。
ただし can_drive=true を付与された admin/normal は /driver に入れる(判定は
canActAsDriver = role が driver、または can_drive 付きの admin/normal を許可・
AuthContext.tsx の canActAsDriver / resolveLandingPath)。純粋な admin(can_drive なし)のみ弾かれる。
マジックリンクをクリックしても /signin に戻される¶
→ Supabase の該当プロジェクト (dev / 本番) の
Authentication > URL Configuration に Preview ドメインの redirect URL が登録されていない。
auth.users の SQL UPDATE が失敗する¶
→ SQL Editor 上部の Run 設定で 「Run as: service_role」 を選んで実行する。