Appearance
マルチ主催組織 / アカウント体系
複数の 主催組織 が同じプラットフォーム上で大会を運営できるようにするための、アカウント・権限モデルと DB スキーマ計画。
ステータス(方針確定 / 実装前)
方針は確定。MVP では「土台(データ構造+権限スコープ+切替)」を入れ、重い機能(共同掲載UI・レベル対応表UI・項目別権限・決済分離)は受け皿だけ用意して後で有効化する。重い機能を「後で足せる」前提は、この土台(紐づけテーブル・organizer_id・can() 集約)を MVPで必ず入れ切る ことが条件。
前提・方針
- 公開(参加者)側は1サイトに集約。主催組織で画面は分けない。分かれるのは 管理側だけ
- 後から作り直すのが大変なもの(データの形・権限スコープ)は MVP から入れる。追加の画面・機能は後回し可
- 既存データは仮。移行はせず、主催組織つきで再シードする
- 公開時 Snapshot 方針(データモデル)は維持。主催組織スコープは主に live / draft に効き、公開済みの snapshot は不変
アカウント体系
原則
- 1人 = 1アカウント(システム共通・複数アカウント不可)
- 1アカウントが複数のロールを持てる(兼任OK。ロールは「紐づけ」として何件でも)
ロール
| ロール | 範囲 | 説明 |
|---|---|---|
| 一般ユーザ | — | 公開サイトで大会申込・マイページ(管理画面には入れない)。全アカウント共通の土台 |
スタッフ (staff) | 紐づく主催組織 | 現場運営(MVPはアカウントのみ/操作機能・シフトは後) |
主催組織の 管理者/編集/閲覧 (admin/editor/viewer) | 紐づく主催組織だけ | その組織を admin(全管理+メンバー管理)/editor(編集)/viewer(閲覧)のいずれかで |
| システムアドミン | 全主催組織 | 全権・全機能を横断(users.is_system_admin) |
ロール(
admin/editor/viewer/staff)は 「機能 × アクセス(編集/閲覧/なし)」のプリセット。詳細は後述の「権限判定」。
アカウントと主催組織の関係(多対多)
- 1つの主催組織に、管理アカウントを 何人でも 紐づけられる
- 1つのアカウントが 複数の主催組織を掛け持ち できる(ログインし直し不要)
- 1つの主催組織につきロールは1つ(
admin/editor/viewer/staff)。組織が違えば別のロールを持てる - システムアドミンは紐づけ不要で全主催組織を扱える
例(1アカウントの兼任):一般ユーザ + A組織は
editor+ B組織はviewer+ C組織はstaff
ログインと主催組織の切り替え
- 1ログインのまま、操作する主催組織を画面で切り替える(再ログイン不要)
- システムアドミン=どの主催組織でも/主催組織管理者・スタッフ=自分に紐づく主催組織の中から
- 管理画面の各機能は 「いま選んでいる主催組織」のデータだけ を表示・編集
DB スキーマ計画
設計の要点
- ロールは
users.role(単一 enum)をやめ、「紐づけ」テーブルorganization_members(アカウント×組織×ロール1つ)に移す - システム全権だけは
users.is_system_adminフラグで持つ - 権限の判定は
can(機能, 操作)に統一。ロールは「機能×アクセス」のプリセットで、機能ごとの上書きはorganization_member_features(MVPは空・将来用)に入れる → “機能ごとON/OFF”を後付けでも呼び出し側を変えずに足せる - 大会の 主催 は
tournaments.organizer_id(単一)を維持し、共同管理(掲載) は別テーブルで足す - 主催組織で分けるリソースは
organizer_idを追加。ただし 規定/審査基準/ルールはレベル経由(自前の組織列は不要)。共通リソース(競技・会場)は据え置き
主催組織(既存テーブルを流用)
organizers(既存)を 主催組織 とする。id / code / name / short_name / kind / color / banner_id。変更なし。
新規テーブル
organization_members — アカウント × 主催組織 × ロール(このモデルの核。MVPで持つのはコレ)
| 列 | 型 | 説明 |
|---|---|---|
user_id | uuid (FK users) | アカウント |
organizer_id | uuid (FK organizers) | 主催組織 |
role | enum admin | editor | viewer | staff | その組織でのロール(1組織1つ) |
PK (user_id, organizer_id) |
organization_member_features — 人 × 組織 × 機能 → アクセス(機能ごとの上書き。MVPは空・将来用)
| 列 | 型 | 説明 |
|---|---|---|
user_id | uuid (FK users) | |
organizer_id | uuid (FK organizers) | |
feature | enum(tournaments/registrations/masters/reports/members …) | 機能 |
access | enum none | view | edit | この機能のアクセス |
PK (user_id, organizer_id, feature)。行があればロール既定より優先 |
tournament_co_organizers — 大会の共同管理(掲載)。主催は tournaments.organizer_id、ここは“追加で管理できる組織”
| 列 | 型 | 説明 |
|---|---|---|
tournament_id | uuid (FK tournaments) | 対象大会 |
organizer_id | uuid (FK organizers) | 共同管理する主催組織 |
PK (tournament_id, organizer_id)。MVPは空、掲載開始で行追加 |
organizer_venues — 主催組織が使える会場(会場は共通のまま、利用可リスト)
| 列 | 型 |
|---|---|
organizer_id | uuid (FK organizers) |
venue_id | uuid (FK venues) |
PK (organizer_id, venue_id) |
level_tiers — 共通の「難易度の段」(レベル対応表の軸)
| 列 | 型 | 説明 |
|---|---|---|
id | uuid PK | |
name | text | 例:中級 |
sort_index | int | 並び順 |
level_external_aliases — 段 ⇄ 外部サービスの呼び名(将来の labola 対応の器)
| 列 | 型 | 説明 |
|---|---|---|
id | uuid PK | |
tier_id | uuid (FK level_tiers) | どの段か |
system | text | 例:labola |
label | text | 例:エンジョイ |
権限判定(ロール=プリセット + can())
- ロールは「機能 → アクセス」の既定表(プリセット)として展開する。テーブルではなく定数。
ts
// 機能ごとの既定アクセス(ロール → 機能 → none/view/edit)
const ROLE_PRESETS = {
admin: { tournaments:"edit", registrations:"edit", masters:"edit", reports:"edit", members:"edit" },
editor: { tournaments:"edit", registrations:"edit", masters:"edit", reports:"view", members:"none" },
viewer: { tournaments:"view", registrations:"view", masters:"view", reports:"view", members:"none" },
staff: { tournaments:"view", registrations:"view", masters:"none", reports:"none", members:"none" },
};
// 判定はすべてこの関数を通す(機能ごとの上書きがあれば優先、無ければロール既定)
function can(user, organizerId, feature, need /* "view"|"edit" */): boolean {
if (user.isSystemAdmin) return true;
const m = membershipOf(user, organizerId); // organization_members の行
if (!m) return false;
const level = featureOverride(user, organizerId, feature) // organization_member_features
?? ROLE_PRESETS[m.role][feature];
return need === "edit" ? level === "edit" : level !== "none";
}- MVP:
organization_member_featuresは空 → 判定はロール既定だけ - 将来(機能ごとON/OFF):
organization_member_featuresに行を足すだけ。can()の呼び出し側は不変
既存テーブルへの変更
| テーブル | 変更 |
|---|---|
users | + is_system_admin boolean。role(単一 enum)と organizer_id(単一 FK)は廃止予定 → ロールは organization_members へ。一般ユーザ=どの紐づけも持たないアカウント |
tournaments | organizer_id = 主催 として維持。共同管理は tournament_co_organizers |
levels | + organizer_id(主催組織ごと)、+ tier_id(FK level_tiers, nullable) |
prizes / discount_templates / media / announcements | + organizer_id(レベルに紐づかないので直接持つ) |
一意制約の地雷(移行時に必須)
organizer_id を足すテーブルは、いま code を グローバル一意(.unique()) で持っている(levels.code / discount_templates.code など)。このままだと別の主催組織が同じ code(例:早割の early)を作れない。code の一意制約は (organizer_id, code) の複合一意へ変更すること。 ※ 規定/審査基準/ルールの (sport_id, level_id) 一意は、レベルが主催組織ごとになるので自然に組織別になり変更不要。
| regulation_templates / judging_templates / rule_templates | 変更なし。level_id でレベルを参照しているので、主催組織はレベル経由で決まる(一意制約 (競技 × レベル) は、レベルが主催組織ごとなので実質 (主催組織 × 競技 × レベル))| | sports | 変更なし(共通)| | venues | 変更なし(共通。利用可は organizer_venues)|
アクティブ主催組織(切替)と権限スコープ
- 「操作中の主催組織」は セッション/Cookie に保持(テーブルではない)
- 管理 API は全て 「操作中の主催組織」でスコープ +
organization_membersの権限チェックを通す(is_system_adminは全許可) - ※ これが一番“後付けが大変”なので、土台として最初から全エンドポイントに通す
レベル対応表のイメージ
| 段(level_tiers) | A組織のレベル | B組織のレベル | 外部(labola) |
|---|---|---|---|
| 中級 | Lv.3 | エンジョイ | エンジョイ |
各 levels 行が tier_id で「段」を指し、同じ段=同じ実力帯として横断対応。外部の呼び名は level_external_aliases。
スコープの線引き(MVP / 後回し)
| 区分 | 内容 |
|---|---|
| MVPで作る | 上記スキーマ一式・organization_members による権限/スコープ・主催組織の切替・各リソースが主催組織ごとに見える/編集・レベル+段のデータ・会場の利用可リスト |
| 器だけ(UI/有効化は後) | 大会の共同掲載(tournament_co_organizers はあるが投入は後)・レベル対応表の編集UI・項目別/機能別の細かい権限 |
| Ver.2 / 後 | スタッフのシフト→名簿閲覧・決済分離(Stripe Connect)・LaBOLA連携 |
既存データ
仮データのため 移行しない。スキーマ変更後、主催組織つきで再シードする。
確定済みの判断(クライアント確認不要)
| 項目 | 判断 |
|---|---|
共同掲載(tournament_co_organizers) | MVP は 器のみ(行は投入しない)。掲載UIは後 |
| 機能ごとの権限(項目別) | MVP は ロール単位(admin/editor/viewer/staff)。機能ごとON/OFFは organization_member_features で後付け |
| スタッフ | MVP は アカウントのみ。シフト→名簿閲覧は Ver.2 |
| 決済分離(Stripe Connect) | MVP 後。organizer_id で受け皿だけ用意済み |
関連
- データモデル — 全体のテーブル責務・Snapshot 方針
- 認証 / 認可 — Better Auth とロールの基盤
- メンバー管理の仕様 —
organization_membersを使ったメンバー追加/ロール変更/除名の運用仕様 - 会場の利用可リストの仕様 —
organizer_venues(利用可リスト)の運用仕様