設計の指針/ Architectural rules
クリーンアーキテクチャ 4 層、bulletproof-react による配置の 2 軸、Hono RPC と Protocol Buffers を用いた型共有、行レベルセキュリティ(RLS)を含む三層認可。この 4 つを核に、コードベース全体に通底する設計ルールを定めています。本ページではフロントエンド・バックエンド・データベース・モノレポ・テスト・CI/CD の各層で何をどのように作るかを示します。
設計の核となる 4 つの判断Four core decisions
以下の 4 つは、ほぼすべてのファイルが影響を受ける土台となる判断です。これらを最初に固めることで、機能を追加しても秩序が保たれる構造になります。
4 層構造(Domain / Application / Infrastructure / Presentation)
依存方向を「外から内へ」に固定し、業務ルールを Domain 層に集めます。UseCase から Prisma を直接呼ぶことは禁止し、Repository インターフェースを介して Gateway 実装に委譲します。
features × pages の 2 軸配置
共通利用される機能は features/{entity}/、ページ固有の機能は features/pages/{page}/。app/ 配下にはルーティングのみを置き、ロジックは features に集約します。
Hono RPC と Protocol Buffers で型共有
Web ↔ API は Hono RPC(hc<AppType>())で TypeScript の型を完全共有。API ↔ Drive 同期ワーカー(Go)は Protocol Buffers + Buf で TS と Go の両方を生成します。
プロキシ・ルート・DB の三層認可
同じ認可判断を 3 箇所で重ねがけします。プロキシ層でセッション検証、ルート層でロール判定、DB 層で行レベルセキュリティ。どこかで漏れても次の層が止めます。
クリーンアーキテクチャ 4 層Clean Architecture layers
ロバート・C・マーティン の 4 層構造を踏襲しています。中心の Domain 層は他の層に依存せず、外側の Presentation 層が DI(依存性注入)の起点となります。
依存方向は常に外側から内側のみです。Domain 層が他層を import することはなく、Infrastructure 層は Domain 層が宣言した Repository インターフェースを実装します。Presentation 層が UseCase インスタンスを組み立て、Repository 実装を注入する DI の起点になります。
| 層 | 置き場所 | 主な責務 |
|---|---|---|
| Domain | features/{x}/domain/ | 純粋関数のドメインサービス + Repository インターフェース。エンティティは @repo/shared/entities から re-export |
| Application | features/{x}/server/ | UseCase クラス。Repository インターフェースを DI で受け取り、業務手順を組み立てる |
| Infrastructure | xx-gateway.ts | Repository 実装(Gateway)。Prisma の生型を mapToEntity でエンティティに変換 |
| Presentation | features/{x}/api/ | Hono Router。UseCase インスタンス化 + 依存注入、dbFor(userId, fn) で RLS JWT を SET LOCAL |
鉄則: UseCase から basePrisma() を直接 import することは ESLint で禁止します。必ず Repository インターフェースを経由して Gateway 実装に委譲します。これにより業務ロジックがインフラ詳細に汚染されません。
三層認可Three-layer authorization
認可判断はプロキシ層・ルート層・データベース層の 3 箇所で重ねがけします。アプリ層をすり抜けても DB 層が拒否するため、IDOR(オブジェクト参照の改ざん)や認可漏れに対する強い防御層となります。
プロキシ層は Next.js の proxy.ts(旧 middleware.ts)で実行し、Cloud Run の Node.js ランタイム上で動きます。ルート層は Hono のミドルウェアで、Better Auth の accessControl によって 4 ロール(Viewer / Uploader / Editor / Owner)の判断を行います。DB 層は PostgreSQL の RLS(行レベルセキュリティ)で、SET LOCAL request.jwt.claims 経由で渡される Better Auth の短命 JWT を SECURITY DEFINER ヘルパ関数が参照します。
プロキシ層
Better Auth のセッションを検証します。未認証ユーザを /sign-in に飛ばし、twoFactorPending なら /two-factor に固定します。/admin/* へのアクセスは system_admin ロールを必須化します。
ルート層
API ルートごとに必要な権限を requirePermission() で宣言します。accessControl の statement は box / folder / file / member / share_link の 5 種類で、4 ロール × アクションのマトリクスで判定します。
DB 層
SELECT / INSERT / UPDATE / DELETE のすべてに RLS ポリシーを適用します。current_user_id() / current_user_box_ids() / current_user_role_for_box(p_box_id) / is_system_admin() などの SECURITY DEFINER ヘルパで判定します。
フロントエンド設計Frontend rules
Next.js 16 App Router を基盤に、bulletproof-react の 5 層レイヤード + Container / Presentational パターンで配置します。Server Components ファースト、状態変更は Hono RPC 経由を徹底します。
| 項目 | 採用ルール |
|---|---|
| ディレクトリ配置 | bulletproof-react ベース。features/{entity}/(共通) + features/pages/{page}/(ページ固有)の 2 軸。app/ 配下は薄い page.tsx と proxy.ts のみ |
| レンダリング | Next.js 16 Cache Components + PPR を有効化。cacheComponents: true + <Suspense> で静的シェル + 動的部分を分離 |
| データ取得 | Server Components で初期値を取得し、TanStack Query に hydration。変更系は Hono RPC(hc<AppType>())+ Server Action + revalidatePath |
| 状態管理 | サーバ状態 = TanStack Query、URL 状態 = nuqs(サーバサイド優先)、クライアント永続化 = Zustand |
| フォーム | React Hook Form + Zod + @hookform/resolvers/zod。スキーマは必ず @repo/shared から import、FE 側での再定義を禁止 |
| アイコン | React Icons は apps/web/src/config/icons/ に集約。コンポーネント側での分散 import を禁止 |
| DnD | @hello-pangea/dnd 採用。dnd-kit は不採用(アクセシビリティ標準対応のため) |
| UI | shadcn/ui + Tailwind CSS v4 + Matsuba トークン(@theme inline) |
バックエンド設計Backend rules
Hono 4 を中心に、OpenAPI 自動生成と Hono RPC による型共有を組み合わせます。Better Auth の organization プラグインで Box を単一テーブル管理する設計と整合させています。
| 項目 | 採用ルール |
|---|---|
| ルーティング | OpenAPIHono + zod-openapi。pnpm openapi:gen でスキーマ自動生成、/api/swagger は system_admin 限定で閲覧可 |
| 認証 | Better Auth 1.3+ + プラグイン(admin / organization / jwt / twoFactor / emailOTP) |
| Box の管理 | Better Auth の organizations テーブル単一管理。additionalFields で visibility / driveSyncEnabled / retentionDays 等を保持。boxes / box_members テーブルは作らない |
| 認可 | accessControl の 5 statement(box / folder / file / member / share_link) × 4 ロール(Viewer / Uploader / Editor / Owner) |
| RLS 連携 | dbFor(userId, fn) ヘルパで Better Auth の短命 JWT を SET LOCAL request.jwt.claims に注入。1 リクエスト = 1 トランザクション |
| バリデーション | HTTP 層: Zod + @hono/zod-validator。UseCase 層: 状態遷移・所有者判定を if + throw DomainError。Domain 層: エンティティ不変条件 |
| エラー | DomainError 抽象クラス + サブクラス(Validation / Unauthorized / Forbidden / NotFound / Conflict / RateLimit / Internal)。handle-domain-error.ts で HTTP に変換 |
| ジョブ・ワーカー | 3 種類: apps/sync(Go、Pub/Sub Push) / apps/scanner(Go + ClamAV、Eventarc) / docshare-batch(Cloud Run Jobs、18 種のバッチを --args で振り分け) |
| 信頼性パターン | Outbox パターン + 冪等性(event_id = UUIDv7 + processed_events) + DLQ + 指数バックオフ(回数を限って再試行) |
データベース設計Database rules
Supabase Postgres 16+(東京リージョン)を採用し、Prisma 7 の Driver Adapter で接続します。すべてのテーブルに RLS(行レベルセキュリティ)を適用し、監査ログは追記専用のポリシーで UPDATE と DELETE を拒否する構造にします。
| 項目 | 採用ルール |
|---|---|
| 命名規則 | テーブル: snake_case 複数形 / カラム: snake_case / インデックス: idx_{table}_{cols} / FK: fk_{table}_{ref} / Enum 型: snake_case 単数形 |
| ID | UUIDv7(text、時系列ソート可)。uuid_generate_v7() 関数で生成 |
| 日時 | timestamptz UTC で保存、表示時にタイムゾーン変換 |
| Enum | PostgreSQL native enum を採用し、Prisma で定義 → @repo/database/enums で re-export → Zod で z.nativeEnum() 参照。z.enum([...]) 手書きは禁止 |
| 共通カラム | すべてのテーブルに id / created_at / updated_at(Prisma @updatedAt) / created_by / updated_by。例外: Better Auth 管理テーブル、audit_logs、processed_events |
| 監査ログ | 追記専用 RLS(INSERT / SELECT ポリシーのみ、UPDATE / DELETE は拒否)+ pg_partman による月次パーティション + 25 ヶ月超を GCS archive class へ退避。ハッシュチェーンは不採用 |
| インデックス | FK 全部に必須 / 論理削除テーブルは部分インデックス(WHERE deleted_at IS NULL) / retention バッチ用 retention_expires_at WHERE deleted_at IS NULL AND hold = false / 共有リンク UNIQUE (token) WHERE revoked_at IS NULL AND expires_at > now() |
| マイグレーション | prisma migrate deploy && psql -f rls.sql を必ずセット実行。CI で prisma migrate diff --exit-code。破壊的変更は 2 段階デプロイ(新カラム追加 → データ移行 → 旧削除) |
| 接続文字列 | DATABASE_URL(PgBouncer pooled、?pgbouncer=true&connection_limit=1) / DIRECT_URL(migration 用) |
| 暗号化 | at-rest = Supabase AES-256 標準、in-transit = TLS 1.2 以上、個人情報カラムの pgsodium 透過暗号化はリリース後拡張 |
モノレポ設計Monorepo
ポリリポ風モノレポとして、pnpm workspace(TS) + go.work(Go) + Turborepo(統一エントリ) + Buf CLI(Protocol Buffers)で構成します。apps と packages の責務を厳密に分けて、循環依存を防ぎます。
| 項目 | 採用ルール |
|---|---|
| apps(4 種) | apps/web(Next.js) / apps/api(Hono on Node.js) / apps/sync(Go、Drive 同期) / apps/scanner(Go + ClamAV) |
| packages(7 種) | database(@repo/database、「db」ではない) / shared(共通スキーマ・型・定数・ユーティリティ) / auth(Better Auth 設定) / events(protobuf) / ui(最小) / eslint-config / typescript-config |
| 共通スキーマ・型 | packages/shared/{entities, schemas, types, constants, utils} に集約。FE / BE で二重定義は禁止 |
| 型共有戦略 | TS ↔ TS: Hono RPC(hc<AppType>()、zero-runtime) / TS ↔ Go: Protocol Buffers + Buf(packages/events/proto/*.proto) |
| Better Auth スキーマ | packages/auth に server.ts / client.ts / permissions.ts(accessControl) / roles.ts / types.ts を集約 |
| Turborepo | dependsOn: ["^build", "proto:gen", "db:generate"] で依存順を保証。.next / dist / bin を outputs にキャッシュ |
| ローカル環境 | docker-compose で postgres:16-alpine + fake-gcs-server + Pub/Sub emulator + Resend dev mode。Eventarc / Cloud Tasks は HTTP 直接呼び出しで代替 |
テスト戦略Test strategy
Vitest(TypeScript 単体)+ Playwright(E2E)+ Go testing の組み合わせで、モックの使用を原則禁止します。実物の Postgres / fake-gcs-server / Pub/Sub emulator / Resend dev mode に対して走らせます。
| 項目 | 採用ルール |
|---|---|
| テストランナー | Vitest(TS unit + integration) / Playwright(E2E) / Go testing(Go unit) |
| モック原則禁止 | vi.mock('@repo/database') 等のモジュール全体モック、Repository「常に成功スタブ」、fetch.mock は禁止 |
| 代替手段 | 実 Postgres(docker-compose) / fake-gcs-server / Pub/Sub emulator / Resend dev mode / Cloud Tasks は HTTP 直接呼び出し / Drive API は fixture + replay |
| 例外許可 | 時計(vi.useFakeTimers()) / Repository のインメモリ実装(モックではなく interface の軽量実装) / ランダム値の DI 注入 |
| E2E シナリオ | 20 シナリオ以内、クリティカルパスのみ(認証 4 / Box 3 / ファイル 5 / 共有リンク 2 / プレビュー 1 / 通知 1 / 管理画面 2 等) |
| カバレッジ目標 | 数値目標は持たない(ソロ開発の負担抑制が目的)。代わりにテスト対象の優先順位を「RLS ポリシー / retention-calculator / permission-checker / E2E クリティカルパス」と明示 |
| パフォーマンステスト | リリース直前のみ k6 / Artillery で、同時アクセス時の Box 一覧 P95 が目標値以内、100 MB UL の並列でエラー 0 を検証 |
なぜモックを禁止するか: モックを許すと「テストは通るが本番で壊れる」状態を作りやすく、特に RLS のような DB 層の重要な挙動は実 Postgres でしか検証できません。fake サーバや emulator を使うことで、本番に近い経路で検証できます。
CI / CD 設計CI / CD
GitHub Actions と Workload Identity Federation(WIF)を中心に、サービスアカウント鍵を一切発行しないデプロイパイプラインを構築します。変更パスに応じて該当サービスだけをデプロイするため、CI 時間も最小化されます。
| 項目 | 採用ルール |
|---|---|
| CI 基盤 | GitHub Actions + Turborepo 差分ビルド |
| 認証 | Workload Identity Federation(WIF)。GitHub の OIDC トークン → GCP STS 交換 → SA impersonate。SA キー(JSON)は発行しない |
| デプロイトリガ | dorny/paths-filter で変更パスを検知。apps/web/** → docshare-web、apps/api/** / packages/{database,auth,shared}/** → docshare-api + docshare-batch、packages/events/** → 3 サービス全部 |
| 環境 | 完成まで 1 環境(本番 = テスト・ステージング、docshare-prod のみ)。リリース後にステージング分離 |
| マイグレーション | cd-api.yml 内で prisma migrate deploy && psql -f rls.sql(DIRECT_URL 使用)を deploy job の前段で実行 |
| Docker | マルチステージビルド。pnpm deploy --prod --ignore-scripts /prod で node_modules/ をフラット化(シンボリックリンク壊れ回避)。Go は distroless |
| ロールバック | Cloud Run 自動ロールバック(新リビジョン失敗時 traffic は前リビジョンに残る) + 手動ロールバック gcloud run services update-traffic --to-revisions <previous>=100 |
| シークレット | Secret Manager に集約。GitHub Secrets は WIF_PROVIDER / DEPLOY_SA / GCP_PROJECT_ID / PROD_DIRECT_URL のみ。アプリ実行時は --update-secrets で Cloud Run に注入 |