# ADR-0012: DB 関数 (RPC) は default `bff_only` schema 配置 + client 直叩き禁止

- **Status:** Accepted
- **Date:** 2026-04-22 (bff_only schema 導入) / 2026-05-01 (ADR 化)
- **Decision Drivers:** Supabase の `supabase.rpc('foo')` が anon / authenticated から直接叩ける構造の脆弱性 / 認可ロジックを RLS と関数 SECURITY DEFINER の二重定義で書きたくない / BFF (Cloudflare Workers) が認可・rate-limit・audit log の単一通過点であってほしい / Flutter / Web client が「叩ける関数」を機械的に絞りたい
- **Stakeholders:** dev@parky.co.jp (sole maintainer) / Flutter 業者 (2026-05 投入)

## Context

### Supabase の標準パターンの落とし穴

Supabase は `supabase.rpc('function_name', args)` で client から PostgreSQL 関数を叩ける。標準の使い方では:

1. 関数を `public` schema に作る
2. `GRANT EXECUTE ON FUNCTION ... TO anon, authenticated`
3. 関数内で `auth.uid()` を見て認可判定 / RLS bypass のために `SECURITY DEFINER`

これだと **client が叩ける関数の表面積が無防備に広がる**。事故パターン:

- 関数の認可漏れ (auth.uid() チェック忘れ) で他人のデータが取れる
- service_role 経由でのみ実行されるべき内部処理が anon に GRANT された
- `SECURITY DEFINER` 関数は postgres 権限で動くため、内部実装の変更で意図しない権限昇格になる
- audit log / rate-limit / 入力 validation を関数ごとに書き散らす羽目になる

### Parky の方針

ADR-0005 (Layer-First) と ADR-0006 (Cloudflare Workers) により、Parky は **BFF (Cloudflare Workers) が単一の認可・rate-limit・audit ゲート**である構造。client (Flutter / Web Astro / Admin Portal / Owner Portal) はすべて BFF を経由する。直接 Supabase RPC を叩く設計を排除したい。

### 採用したガード構造 (4 層)

L1. **schema 分離**: 新規関数は `bff_only.<name>` に作成。`public` schema には作らない (default rule)
L2. **EXECUTE GRANT**: `REVOKE ALL ON FUNCTION ... FROM PUBLIC` + `GRANT EXECUTE TO service_role`。anon / authenticated には GRANT しない
L3. **BFF endpoint 1:1**: 関数を呼ぶのは `/v1/{channel}/actions/{name}` の Cloudflare Workers endpoint だけ。BFF が認可・rate-limit・audit を担当
L4. **client lint gate**: client コード内の `supabase.rpc(...)` を ESLint + CI grep gate (`.github/workflows/banned-rpc-check.yml`) で block

例外 (client 直叩きが必要な関数) は `public.<name>` に作り、明示 GRANT + CLAUDE.md コメント + ESLint allowlist の **4 点セット**でのみ許容。

## Decision

新規 DB 関数は **default で `bff_only.<name>` schema に作成し、client GRANT を行わない**。

### 関数追加チェックリスト

```sql
CREATE OR REPLACE FUNCTION bff_only.<name>(...)
RETURNS ... AS $$
  ...
$$ LANGUAGE sql SECURITY DEFINER STABLE;

REVOKE ALL ON FUNCTION bff_only.<name>(...) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION bff_only.<name>(...) TO service_role;
```

加えて:

1. BFF endpoint (`/v1/{channel}/actions/{name}`) を 1 対 1 で作成
2. `routes-manifest.ts` に entry 追加 (idempotent: true 必要に応じて)
3. Flutter / Web の client は **BFF endpoint 経由のみで呼ぶ**

### 例外: client 直叩きが必要な関数

`public.<name>` に CREATE + 明示 GRANT + CLAUDE.md にコメント追加 + ESLint allowlist 追記の **4 点セット**でのみ許容する。`bff_only` から `public` への昇格は別 PR で明示的にレビュー。

## Consequences

### Positive

- **client が叩ける関数の表面積が schema レベルで物理的にゼロ**: `bff_only` には GRANT がないため anon / authenticated から `supabase.rpc('bff_only.foo')` は権限不足で必ず失敗
- BFF が認可・rate-limit・audit log の単一通過点になり、横断ロジックを 1 箇所に集約できる ([ADR-0005](./0005-layer-first-mechanical-enforcement.md) の Layer-First と整合)
- 関数内で `auth.uid()` 判定の Boy Scout 漏れが起きない (BFF が JWT を検証してから `service_role` で関数を呼ぶ)
- ESLint + CI grep の 2 段ガードで client 直叩きを禁止 → コードレビューで毎回確認しなくて良い
- `bff_only` schema 名そのものが「ここは BFF だけ通る」という設計意図を表現
- service_role でのみ呼ばれるため、関数は `SECURITY DEFINER` を自由に使え、複雑なクロステーブル更新も atomicity を保てる

### Negative / Trade-offs

- BFF endpoint を 1 関数につき 1 つ作る必要があり、定型コード (validation / audit / response) が増える
- client 直叩きが本当に必要なケース (低レイテンシ要件 / オフライン同期等) に対応するには 4 点セット例外申請が必要
- BFF を経由する分のレイテンシ追加 (Cloudflare Workers + Supabase が edge 配置されているので実測 +30〜80ms)
- `bff_only` / `public` のどちらに置くかの判断が新規関数ごとに必要

### Mitigations

- BFF 側に `withAdminActivity()` / `withPgError()` / `runCronBatch()` 等の wrapper を整備し定型コードを最小化 ([ADR-0005](./0005-layer-first-mechanical-enforcement.md) の `api/CLAUDE.md` の§1)
- ESLint + grep gate (`banned-rpc-check.yml`) で client 直叩きを CI で必ず block。手動レビューに頼らない
- 4 点セットは「明示申請の儀式コスト」として例外昇格の心理的ハードルを上げる役割を兼ねる
- レイテンシは Hyperdrive (Cloudflare Postgres connection pool) の利用で実用範囲に収まっている

## Alternatives Considered

- **Alternative A: `public` schema に関数を置き個別 `REVOKE FROM anon` で守る**
  - 不採用理由: REVOKE 漏れの human error が混入する。schema 単位の物理 deny のほうが robust

- **Alternative B: PostgREST の RLS と組み合わせて anon にも GRANT し関数内で認可判定**
  - 不採用理由: 認可ロジックが BFF と関数の二重定義になる。BFF を単一通過点にする ADR-0005 の整合性が崩れる

- **Alternative C: `bff_only` ではなく `internal` 等の別命名**
  - 不採用理由: 名前で「BFF からだけ呼ばれる」意図を表現したい。`internal` だとサーバー内部の private を連想するが、Parky では BFF (Cloudflare Workers, 別プロセス) が呼ぶため `bff_only` のほうが正確

- **Alternative D: 関数を全廃し SQL を BFF 内に直書き**
  - 不採用理由: cron batch / 複数 table を atomic に更新するロジックは関数化したほうが SQL 1 往復で済み性能が良い。関数の存在自体は捨てない、配置を `bff_only` に絞る

## References

- 関連 memory:
  - `parky/feedback_db_function_default_bff_only.md` (本 ADR の原典)
  - `parky/feedback_api_natural_key_strategy.md` (BFF 層の natural key 戦略)
- 関連 code:
  - `infra/supabase/baseline/bff_only/` (関数定義の SSoT)
  - `.github/workflows/banned-rpc-check.yml` (client 直叩き検出)
  - `api/eslint.config.js` の `no-restricted-syntax` rule (`supabase.rpc(...)` block)
- 関連 ADR:
  - [ADR-0005](./0005-layer-first-mechanical-enforcement.md) (Layer-First の機械強制)
  - [ADR-0006](./0006-cloudflare-workers-platform.md) (Cloudflare Workers BFF)
  - [ADR-0007](./0007-supabase-auth-integration.md) (Supabase Auth + JWKS)
  - [ADR-0011](./0011-db-schema-5-way-split.md) (DB schema 5 分離)

## Revisit Triggers

- Cloudflare Workers から別ランタイムに移行し、BFF 経由の前提が崩れる時
- リアルタイム性が極端に必要な機能 (1ms 以下のレイテンシ要件) で BFF 経由が現実的でなくなる時
- Supabase 側に schema レベルではなく function レベルの細粒度 ACL が導入され、`bff_only` schema 命名が陳腐化した時
- client 直叩き例外が 5 件を超え、4 点セットの運用コストが「default を `public` に変える」を正当化するレベルになった時

## 2026-05-10: 完全達成と機械強制化

ADR 採択以降の運用結果として、以下を確定:

- **移管完了 (37 関数 / 5 migrations)** — `public.* SECURITY DEFINER = 0` を達成。詳細は [docs/db/bff-only-sweep.md §6](../db/bff-only-sweep.md) 参照。
- **TRIGGER 関数も bff_only に統合** — 当初「TRIGGER 関数は移管対象外」としていたが、pg_trigger は OID 参照で SET SCHEMA に追従するため bff_only に移動可能と判明。schema 分離の物理 deny 効果を最大化するため統合した。
- **3 audit による継続維持** — public-sd-zero / migration-no-public-secdef / db-invariants-weekly の 3 系統を CI / pre-commit / pre-push で常時稼働 ([docs/db/bff-only-sweep.md §7](../db/bff-only-sweep.md))。

ADR 自体の方針は変更なし — 達成状況と維持メカニズムの追記のみ。
