# ADR-0011: Supabase DB を 5 schema に分離 (public / admin / marketing / analytics / extensions)

- **Status:** Accepted
- **Date:** 2026-04-21 (schema reorg) / 2026-05-01 (ADR 化)
- **Decision Drivers:** end-user 機能と運営機能の混在によるテーブル名衝突リスク / RLS 設計の可読性 / マーケサブプロダクトの独立した retention 戦略 / append-only テレメトリの容量問題 / 拡張機能 (pg_net 等) を public に置かない原則 / service_role / anon / authenticated のアクセス境界を schema レベルで宣言したい
- **Stakeholders:** dev@parky.co.jp (sole maintainer) / Flutter 業者 (2026-05 投入時に schema 構造を把握する必要)

## Context

### 出発点

2026-04 中盤までの Parky DB は `public` schema に全テーブルが平置きされており、業務ドメイン (parking_lots / app_users) と運営機能 (admins / admin_tasks) と analytics (client_events / error_reports) が混在していた。新規テーブル追加のたびに以下が問題になった:

1. **テーブル名衝突**: `notifications` (ユーザー向け) と `admin_notifications` (管理者向け) のように prefix で区別する慣習が増え、命名疲労が発生
2. **RLS の可読性低下**: `public` schema の policy 数が膨らみ、どの policy が active かレビューが困難
3. **GRANT/REVOKE 漏れ**: 運営テーブル (admin_*) を `anon` ロールに REVOKE し忘れる事故が発生しやすい
4. **append-only テレメトリの混在**: `client_events` / `error_reports` 等は VACUUM / 古いデータ削除戦略が public とは異なるため別 schema が望ましい
5. **拡張機能の汚染**: `pg_net` 等を `public` に CREATE EXTENSION すると関数名が衝突 (`http_post` 等)、Supabase ダッシュボードのテーブル一覧も見づらくなる

### 採用した分離

| Schema | 役割 | アクセス | 例 |
|---|---|---|---|
| `public` | 業務ドメインの中核 (end-user 向け機能・core business) | anon / authenticated + RLS | parking_lots / app_users / user_notifications / articles / revenue_transactions |
| `admin` | 内部運営者データ | service_role only | admins / admin_tasks / admin_activity_logs / roles / role_permissions |
| `marketing` | マーケサブプロダクト (Marketing Portal 駆動) | service_role only | marketing_campaigns / x_posts / newsletter_broadcasts / store_sales_daily |
| `analytics` | append-only テレメトリ (retention 戦略が public と別) | service_role only | client_events / error_reports / boost_*_logs / sns_follower_snapshots |
| `extensions` | 拡張機能の encapsulation 先 | USAGE は全 role | pg_net 等を順次移設 |

### 防御の構造

- `admin` / `marketing` / `analytics` は `REVOKE ALL ON SCHEMA ... FROM anon, authenticated` で **schema レベルで anon 不可**。テーブル個別の GRANT 漏れがあっても schema 防御で守れる
- `bff_only` schema (ADR-0012) と組み合わせて、function 配置と data 配置の両方で「client が直接届かない領域」を schema 名で表現
- baseline strategy (ADR-0003) と整合: `infra/supabase/baseline/` 配下が schema 別ディレクトリ (`public/`, `admin/`, ...) に分かれており、grep / レビューが schema 単位で可能

## Decision

新規テーブルは必ず schema を判定して `CREATE TABLE <schema>.<table_name>` で作成する。`public` 直置きは「end-user が直接 anon / authenticated で触る業務データ」に限定する。

### 運用ルール

1. **新規テーブル作成時に schema 明示必須** (`CREATE TABLE marketing.<name>` 等)
2. **新規 BFF SQL も schema 明示で書く** (`FROM marketing.x_posts`)。既存の裸名参照は `search_path` fallback で動くが新規は禁止
3. **Edge Function (supabase-js) からは `supabase.schema('marketing').from(...)` または `db: { schema: 'marketing' }` で初期化**
4. **admin / marketing / analytics に anon / authenticated を GRANT してはいけない** (schema 単位の REVOKE で防御)
5. **拡張機能追加は `CREATE EXTENSION ... SCHEMA extensions`**

## Consequences

### Positive

- end-user 機能と運営機能の境界が schema 名で明示され、RLS policy / GRANT 設計が schema ごとに完結
- `admin` / `marketing` / `analytics` への anon 流出は schema REVOKE で物理 deny されるため、テーブル個別の GRANT 設計で事故を起こせない
- 命名衝突 (notifications vs admin_notifications) が schema prefix で自然解消
- baseline split (ADR-0003) と組み合わせて、schema 単位のレビューが可能 (PR で `git diff infra/supabase/baseline/marketing/` で marketing スコープのみ確認)
- マーケサブプロダクト / analytics の retention / VACUUM 設計を public と独立に設計できる
- 拡張機能 (pg_net 等) が `extensions` schema に隔離され、public のテーブル一覧が綺麗に保たれる

### Negative / Trade-offs

- 新規テーブル作成時に schema 判定が必要 (どこに置くか迷うコスト)
- BFF コードで `FROM marketing.x_posts` のように schema 明示を徹底する必要がある (裸名は postgres / authenticator の search_path fallback でしか動かない)
- supabase-js (Edge Function) は `schema('marketing')` チェーンが必要で、デフォルト (public) と書き分けを意識する必要
- migration / seed ファイルが schema 数だけ増える (`infra/supabase/baseline/{public,admin,marketing,analytics}/30_tables/...`)
- 横断クエリ (admin から marketing データを SELECT 等) が schema 越境を意識する形になる

### Mitigations

- CLAUDE.md (`parky/CLAUDE.md` の DB スキーマ構成節) に schema 判定表を明文化
- `baseline-drift` workflow が schema 別 split を含めて drift 検出 (commit 漏れの schema を block)
- `dependency-cruiser` / data 層命名規則 で「marketing schema を扱う data ファイルは marketing prefix」のような慣習を持つ (将来的なルール追加候補)
- `bff_only` schema (ADR-0012) と組み合わせ、function 配置と data 配置の両方で schema 名から責務が読める設計

## Alternatives Considered

- **Alternative A: 全テーブルを `public` のままにし prefix で区別 (`admin_*` / `marketing_*` / `analytics_*`)**
  - 不採用理由: 命名疲労 + GRANT/REVOKE が個別テーブル単位になるため anon 流出事故のリスクが高い。schema レベルの防御を持てない

- **Alternative B: schema を機能別ではなく階層別 (`hot` / `cold` / `archive`)**
  - 不採用理由: アクセス境界 (anon vs service_role) と一致せず、RLS / GRANT 設計の言語化が難しい

- **Alternative C: マーケサブプロダクトを別 Supabase プロジェクトに分離**
  - 不採用理由: cross-project JOIN が不可能 (campaign の対象 article が public.articles に存在するため)。1 人開発 + 単一 Cloudflare Workers (ADR-0006) との整合性が崩れる

- **Alternative D: schema 分割せず PostgreSQL の Row-Level Security のみで境界を引く**
  - 不採用理由: RLS は anon / authenticated に対する防御で、テーブル群を「触らせない」表現には schema REVOKE のほうが simpler で robust

## References

- 関連 memory:
  - `parky/feedback_db_function_default_bff_only.md` (ADR-0012 の前段)
  - `parky/feedback_parky_layer_first_architecture.md` (ADR-0005)
- 関連 docs:
  - `parky/CLAUDE.md` の「DB スキーマ構成」節
  - `infra/supabase/baseline/` の schema 別ディレクトリ構造
- 関連 ADR:
  - [ADR-0003](./0003-snapshot-baseline-strategy.md) (baseline strategy)
  - [ADR-0005](./0005-layer-first-mechanical-enforcement.md) (Layer-First / data 層命名規則)
  - [ADR-0007](./0007-supabase-auth-integration.md) (service_role 運用)
  - [ADR-0012](./0012-bff-only-rpc-default.md) (bff_only schema)

## Revisit Triggers

- マーケサブプロダクトが Parky 本体から独立した SaaS に成長し、別 Supabase プロジェクトへの分離が現実的なコストで可能になった時
- analytics が ClickHouse / BigQuery 等の外部 OLAP に移行する時 (`analytics` schema を Postgres に置く意義が薄れる)
- end-user 向け機能が爆発的に増え、`public` 内で更なる細分化 (`public.parking` / `public.gamification` 等) が必要になった時
