シーケンス図¶
waste-link の主要な処理フローを Mermaid の sequenceDiagram で示します。各図は
実コード (apps/** / supabase/migrations/** / scripts/**) を典拠にしています。
関連する設計判断は各図末尾の ADR リンクを参照してください。
登場する RLS ヘルパー
DB (Supabase Postgres) の行レベルセキュリティ (RLS) は JWT の app_metadata を
読む SQL 関数で判定します:
current_tenant_id() / current_tenant_type() / current_user_role()
(migration 0001 / 0003)。クライアントは認証後の JWT を付けて Supabase に直接
アクセスし、特権操作 (招待・メンバー管理) のみ API (service_role) を経由します。
(a) login_id 方式のログイン¶
メールアドレスではなく login_id でログインします。Web は login_id を合成メール
<login_id>@members.waste-link.local に変換 (loginIdToEmail) してから Supabase Auth
の signInWithPassword を呼びます。発行される JWT の app_metadata に
tenant_id / tenant_type / role / can_drive が入り、以降の RLS 判定の権威に
なります。
典拠: packages/shared/src/utils/login-id.ts (LOGIN_EMAIL_DOMAIN = 'members.waste-link.local')、
apps/web/src/lib/auth-login-id.ts、apps/web/src/contexts/AuthContext.tsx。
sequenceDiagram
autonumber
actor U as 利用者
participant Web as Web SPA (AuthContext)
participant Auth as Supabase Auth (GoTrue)
participant DB as Supabase Postgres (RLS)
U->>Web: login_id + パスワードを入力
Web->>Web: loginIdToEmail(login_id)<br/>→ id@members.waste-link.local
Web->>Auth: signInWithPassword({ email, password })
Auth-->>Web: session + JWT<br/>app_metadata: tenant_id / tenant_type / role / can_drive
Web->>Web: onAuthStateChange で session 反映<br/>resolveLandingPath(tenant_type, role)
Note over Web: discharger→/discharger<br/>collector admin/normal→/collector<br/>collector driver→/driver
U->>Web: 業務画面を開く
Web->>DB: select ... (Authorization: Bearer JWT)
DB->>DB: RLS: current_tenant_id() 等が<br/>JWT app_metadata を読む
DB-->>Web: 自テナントの行のみ返す
関連 ADR: 0004 login_id 方式の認証、 0003 RLS マルチテナント分離、 0005 ロールモデル。
(b) メール招待によるオンボーディング (組織登録 + 初回 admin 作成)¶
アカウント未保有から「組織 (tenant) 登録 + 初回 admin 作成」を行う公開フローです。
運営スクリプトが宛先メールに 6 桁コードを送り (平文は DB に保存せず code_hash のみ)、
受信者が /register で入力した内容を公開 API POST /signup (service_role) が検証して
組織と管理者を作成します。試行上限はアトミックゲートで TOCTOU を防ぎます。
典拠: scripts/invite-org.mjs、apps/api/src/routes/signup.ts、
migration 0048 / 0049 / 0050 (register_signup_invite_attempt、SIGNUP_MAX_ATTEMPTS = 5)。
sequenceDiagram
autonumber
actor Op as 運営
participant Script as scripts/invite-org.mjs
participant DB as Supabase (service_role)
participant Resend as Resend API
actor Rec as 受信者
participant Web as Web (/register)
participant API as API POST /signup (service_role)
participant Auth as Supabase Auth
Op->>Script: node invite-org.mjs --email ... [--tenant-type ...]
Script->>Script: 6桁コード生成 + id=uuid<br/>code_hash = sha256("id:code")
Script->>DB: 同一メールの未使用行を delete → insert<br/>(email, code_hash, attempts=0, expires_at, tenant_type?)
Script->>Resend: POST /emails (本文に平文コード)
Resend-->>Rec: 招待メール (6桁コード)
Note over Script,Resend: 送信失敗時は保存した招待行をロールバック
Rec->>Web: メール + コード + 組織情報を入力
Web->>API: POST /signup (認証不要・公開)
API->>API: validateSignupBody (形式・種別・login_id・PW)
API->>DB: email で最新の未使用招待行を取得
API->>API: emailInviteUsable 事前フィルタ<br/>(存在/未使用/期限内/attempts<5)
API->>DB: rpc register_signup_invite_attempt(id, max=5)
DB-->>API: admitted (used_at null かつ attempts<5 のとき<br/>attempts+1 して true / 行ロックで直列化)
alt admitted = false (上限/使用済み/不存在)
API-->>Web: 403 (汎用文言・条件はリークしない)
else admitted = true
API->>API: matchInviteCode<br/>(code_hash 照合 + tenant_type 整合)
API->>DB: insert tenants (type)
API->>Auth: createUser(app_metadata:<br/>tenant_id / tenant_type / role=admin)
opt collector の場合のみ
API->>DB: insert profiles (admin)
end
API->>DB: 招待消費 update used_at<br/>(used_at is null ガード・単回使用)
Note over API,DB: いずれかの失敗で profile/auth/tenant を全ロールバック
API-->>Web: { ok, loginId }
end
Rec->>Web: 発行した login_id でログイン (フロー a)
関連 ADR: 0011 公開オンボーディング (招待コード)。 詳細は オンボーディング手引き も参照。
(c) 配車ボードの生成とドラッグ&ドロップ編集¶
当日配車ボードは 自動生成しません。担当者 (admin / normal) が「コース割当から
(再)生成」ボタンを押したときだけ RPC を呼びます。RPC は当日 (曜日 × 週序数) に
発火するコース割当を展開し、ドライバー毎に stop_order を 0 始まりで採番して
dispatch_stops を作ります。生成後はドラッグ&ドロップで列間移動・並べ替え・
未割当プールへの差し戻しができます。
典拠: apps/web/src/pages/collector/dispatch/DispatchBoardPage.tsx、
apps/web/src/lib/db/dispatch-stops.ts、
migration 0045 (generate_dispatch_stops_from_courses(date, force))、
migration 0025 (driver は status/completed_at/note のみ更新可)。
sequenceDiagram
autonumber
actor Staff as 担当者 (admin/normal)
participant Web as Web (DispatchBoardPage)
participant DB as Supabase (RPC + RLS)
Staff->>Web: 配車ボードを開く / 日付選択
Web->>DB: select dispatch_stops where dispatch_date = ?
DB-->>Web: stops (RLS: 自テナント)
Note over Web,DB: 自動生成はしない。生成は手動ボタンのみ
Staff->>Web: 「コース割当から(再)生成」をクリック
Web->>DB: rpc generate_dispatch_stops_from_courses(date, force=true)
DB->>DB: role ∈ (admin, normal) を検証
DB->>DB: force=true → 当日 (tenant,date) の stops を削除
DB->>DB: dispatch_course_assignments(weekday × week_ordinal)<br/>→ active な dispatch_courses → dispatch_course_stops<br/>→ (driver, site) 重複を畳み込み<br/>→ driver 毎に stop_order を 0 採番し insert
DB-->>Web: 生成件数
Staff->>Web: ストップを D&D
alt 未割当プール → ドライバー列
Web->>DB: insert dispatch_stop (stop_order = max+1)
else 列内 / 別ドライバーへ移動
Web->>DB: update stop_order / driver_id (N件を一括)
else ストップ → プールへ戻す
Web->>DB: delete dispatch_stop (配車から外す)
end
DB-->>Web: onSettled で invalidate → 再フェッチし DB の並びへ再同期
Note over Staff,DB: driver は完了 (status/completed_at/note) のみ可。<br/>reorder/割当は admin (RLS + トリガで担保)
関連 ADR: 0008 配車コースモデル、 0006 工場スコープのアクセス制御。
(d) ドライバーの日次運行ログ入力¶
ドライバー (または can_drive の社員) が担当車両の日次運行を記録します。選択した
(車両, 日付) の既存ログがあればプリフィルし、新規入力時は 出庫メーターの既定値を
前回入庫メーターにします。給油は軽油 / アドブルー / ガソリン (L) を種別ごとに入力
します。保存は (tenant_id, vehicle_id, log_date) の upsert です。
典拠: apps/web/src/pages/driver/DriverDailyLogPage.tsx、
apps/web/src/lib/db/vehicle-daily-logs.ts、
migration 0029 / 0047 (diesel_liters / adblue_liters / gasoline_liters)。
sequenceDiagram
autonumber
actor Drv as driver / can_drive
participant Web as Web (DriverDailyLogPage)
participant DB as Supabase (RLS: 工場スコープ)
Drv->>Web: 運行ログ登録を開く
Web->>DB: 自工場の車両一覧を取得 (profile_factories で絞り込み)
Web->>Web: 既定車両を選択 / 日付は既定で本日
Drv->>Web: 車両・日付を選択
Web->>DB: select vehicle_daily_logs (vehicle, date) 既存ログ
Web->>DB: select 直近1件 (date より前で最新)
alt 既存ログあり
Web->>Web: 既存値をプリフィル
else 新規入力
Web->>Web: 出庫メーター既定 = 前回入庫メーター
end
Drv->>Web: 軽油 / アドブルー / ガソリン (L) + 出入庫メーター + メモ
Web->>DB: upsert vehicle_daily_logs<br/>(onConflict: tenant_id, vehicle_id, log_date)
Note over Web,DB: 新規時のみ driver_id = 自分。更新時は driver_id を送らず<br/>ON CONFLICT UPDATE が old.driver_id を保持
DB->>DB: distance_km = odometer_in - odometer_out (生成列)
DB-->>Web: 保存結果
関連 ADR: 0012 日次運行ログと燃料の記録、 0007 can_drive 能力フラグ。
(e) メンバーの役割 / 運転可フラグの変更¶
admin がメンバーの役割 (admin / normal / driver) または運転可フラグ (can_drive) を
変更します。権威は JWT の app_metadata のため、API (service_role) が
updateUserById で app_metadata をマージ更新し、表示用に profiles も更新します。
service_role は RLS をバイパスするため、同一テナント検証と「最後の管理者」ガードを
明示的に行います。反映は対象ユーザーの次回 JWT 更新時です。
典拠: apps/api/src/routes/team.ts (/members/:id/role、/members/:id/can-drive、
mergeAppMetadataRole / mergeAppMetadataCanDrive / isLastAdminViolation)。
sequenceDiagram
autonumber
actor Adm as admin
participant Web as Web (CollectorMembersPage)
participant API as API /team (service_role)
participant Auth as Supabase Auth
participant DB as Supabase Postgres
actor Tgt as 対象ユーザー
Adm->>Web: 役割を変更 / 運転可をトグル
Web->>API: POST /team/members/:id/role (または /can-drive)
API->>API: requireCollectorAdmin + parseRoleChange / parseCanDrive
API->>DB: 対象 profile を取得
alt 別テナント
API-->>Web: 403 (他テナントは操作不可)
else 同一テナント
opt role を admin → 非admin に降格
API->>DB: テナント内の admin 数をカウント
alt 最後の管理者
API-->>Web: 409 (最後の管理者は変更不可)
end
end
API->>Auth: getUserById → updateUserById<br/>app_metadata をマージ (role/can_drive 差替・他キー保持)
API->>DB: update profiles.role / can_drive (副次コピー)
Note over API,DB: profiles 更新が失敗したら auth を元の値へロールバック
API-->>Web: { ok }
end
Note over Tgt: 反映は対象ユーザーの<br/>次回ログイン / トークンリフレッシュ時
関連 ADR: 0005 ロールモデル、 0007 can_drive 能力フラグ。
(f) メンバー削除 (オフボーディング)¶
admin がメンバーを削除します。本質は auth.users 行の削除で、これにより対象の
既存セッション / リフレッシュトークンが失効します。profiles は user_id の FK
on delete cascade (migration 0008) で連鎖削除されます。auth ユーザーを持たない
不完全メンバーは profile を直接削除します。
典拠: apps/api/src/routes/team.ts (/members/:id/delete、decideMemberDeletion)。
sequenceDiagram
autonumber
actor Adm as admin
participant Web as Web (CollectorMembersPage)
participant API as API /team (service_role)
participant Auth as Supabase Auth
participant DB as Supabase Postgres
Adm->>Web: メンバーを削除
Web->>API: POST /team/members/:id/delete
API->>DB: 対象 profile を取得
opt 対象が admin
API->>DB: テナント内の admin 数をカウント
end
API->>API: decideMemberDeletion<br/>(404 profile無 / 403 別テナント / 409 最後のadmin)
alt user_id あり (action = deleteUser)
API->>Auth: auth.admin.deleteUser(userId)
Note over Auth: 既存セッション / リフレッシュトークンを失効
Auth->>DB: profiles を cascade 削除 (user_id FK)
else user_id 無し (不完全メンバー / action = deleteProfile)
API->>DB: profiles を直接削除
end
API-->>Web: { ok }
関連 ADR: 0005 ロールモデル、 0003 RLS マルチテナント分離。
(g) 横乗り (同乗者) の追加 / 削除¶
配車ボードのドライバー列ヘッダーに、当日ルートへ同行する 横乗り (同乗者) を付与します。
紐付け単位は ドライバーの当日ルート (driver_id × dispatch_date) で、ストップ単位ではありません。
追加候補は computeRideAlongCandidates で 全メンバー − そのルートのドライバー本人 − 既存の同乗者 に
絞り、groupRideAlongsByDriver で列ヘッダーのチップにまとめます。stops / stop_order・生成 RPC・
D&D には一切影響しない付与のみ です。書込は admin / normal のみで、RLS の WITH CHECK が
driver_id / profile_id の同一テナントを検証します。
典拠: apps/web/src/pages/collector/dispatch/DispatchBoardPage.tsx、
apps/web/src/lib/dispatch-ride-along.ts (computeRideAlongCandidates / groupRideAlongsByDriver)、
apps/web/src/lib/db/dispatch-ride-alongs.ts、
migration 0057 (dispatch_ride_alongs、driver_id <> profile_id、unique・WITH CHECK 越境参照検証)。
sequenceDiagram
autonumber
actor Staff as 担当者 (admin/normal)
participant Web as Web (DispatchBoardPage)
participant DB as Supabase (RLS)
Staff->>Web: 配車ボードを開く / 日付選択
Web->>DB: select dispatch_ride_alongs where dispatch_date = ?
DB-->>Web: 横乗り (RLS: 自テナント全件)
Web->>Web: groupRideAlongsByDriver<br/>→ 列ヘッダーに同乗者チップ
Staff->>Web: ある列の「横乗りを追加」
Web->>Web: computeRideAlongCandidates<br/>(全メンバー − ドライバー本人 − 既存)
Note over Web: 候補が空なら追加導線を出さない
Staff->>Web: メンバーを選択
Web->>DB: insert dispatch_ride_alongs<br/>(tenant_id, dispatch_date, driver_id, profile_id)
DB->>DB: WITH CHECK: role ∈ (admin, normal)<br/>+ driver_id / profile_id が同一テナント (exists)
DB->>DB: check driver_id <> profile_id<br/>unique (tenant, date, driver, profile)
alt 二重登録 (unique 違反)
DB-->>Web: error → isUniqueViolation で案内
else 成功
DB-->>Web: 追加した行
Web->>DB: invalidate → 再フェッチ
end
Staff->>Web: 横乗りチップの削除
Web->>DB: delete dispatch_ride_alongs where id = ?
DB-->>Web: 削除 → invalidate
Note over Staff,DB: dispatch_stops / stop_order・生成 RPC・D&D には影響しない (付与と表示のみ)
関連 ADR: 0016 横乗り (当日ルート単位の同乗者)、 0006 工場スコープのアクセス制御。 画面項目の算出は 画面項目の算出方法 (横乗り) を参照。