本文へ移動

設計の指針/ Architectural rules

クリーンアーキテクチャ 4 層、bulletproof-react による配置の 2 軸、Hono RPC と Protocol Buffers を用いた型共有、行レベルセキュリティ(RLS)を含む三層認可。この 4 つを核に、コードベース全体に通底する設計ルールを定めています。本ページではフロントエンド・バックエンド・データベース・モノレポ・テスト・CI/CD の各層で何をどのように作るかを示します。

01

設計の核となる 4 つの判断Four core decisions

以下の 4 つは、ほぼすべてのファイルが影響を受ける土台となる判断です。これらを最初に固めることで、機能を追加しても秩序が保たれる構造になります。

Clean Architecture

4 層構造(Domain / Application / Infrastructure / Presentation)

依存方向を「外から内へ」に固定し、業務ルールを Domain 層に集めます。UseCase から Prisma を直接呼ぶことは禁止し、Repository インターフェースを介して Gateway 実装に委譲します。

Bulletproof React

features × pages の 2 軸配置

共通利用される機能は features/{entity}/、ページ固有の機能は features/pages/{page}/。app/ 配下にはルーティングのみを置き、ロジックは features に集約します。

Type Sharing

Hono RPC と Protocol Buffers で型共有

Web ↔ API は Hono RPC(hc<AppType>())で TypeScript の型を完全共有。API ↔ Drive 同期ワーカー(Go)は Protocol Buffers + Buf で TS と Go の両方を生成します。

Three-Layer Auth

プロキシ・ルート・DB の三層認可

同じ認可判断を 3 箇所で重ねがけします。プロキシ層でセッション検証、ルート層でロール判定、DB 層で行レベルセキュリティ。どこかで漏れても次の層が止めます。

02

クリーンアーキテクチャ 4 層Clean Architecture layers

ロバート・C・マーティン の 4 層構造を踏襲しています。中心の Domain 層は他の層に依存せず、外側の Presentation 層が DI(依存性注入)の起点となります。

図 01 · 依存方向 Presentation 層Hono Router · features/{x}/api/Infrastructure 層Gateway 実装 (xx-gateway.ts) · Prisma · GCS · DriveApplication 層UseCase · features/{x}/server/Domain 層純粋関数 + Repository インターフェース依存方向 — 外側から内側のみ。逆向きは禁止。

依存方向は常に外側から内側のみです。Domain 層が他層を import することはなく、Infrastructure 層は Domain 層が宣言した Repository インターフェースを実装します。Presentation 層が UseCase インスタンスを組み立て、Repository 実装を注入する DI の起点になります。

層置き場所主な責務
Domainfeatures/{x}/domain/純粋関数のドメインサービス + Repository インターフェース。エンティティは @repo/shared/entities から re-export
Applicationfeatures/{x}/server/UseCase クラス。Repository インターフェースを DI で受け取り、業務手順を組み立てる
Infrastructurexx-gateway.tsRepository 実装(Gateway)。Prisma の生型を mapToEntity でエンティティに変換
Presentationfeatures/{x}/api/Hono Router。UseCase インスタンス化 + 依存注入、dbFor(userId, fn) で RLS JWT を SET LOCAL

鉄則: UseCase から basePrisma() を直接 import することは ESLint で禁止します。必ず Repository インターフェースを経由して Gateway 実装に委譲します。これにより業務ロジックがインフラ詳細に汚染されません。

03

三層認可Three-layer authorization

認可判断はプロキシ層・ルート層・データベース層の 3 箇所で重ねがけします。アプリ層をすり抜けても DB 層が拒否するため、IDOR(オブジェクト参照の改ざん)や認可漏れに対する強い防御層となります。

図 02 · 三層認可の流れ リクエストHTTP from userProxy 層 — proxy.tsBetter Auth セッション検証 / 2FA pending / system_admin チェック未認証 → /sign-in リダイレクトredirect/ 403ルート層 — Hono ミドルウェアBetter Auth accessControl · 4 ロール × 5 statement権限不足 → DomainError → 403deny 403DB 層 — PostgreSQL RLScurrent_user_role_for_box() · SECURITY DEFINER ヘルパ行が見えない / INSERT 拒否empty / error許可されたデータのみ返却

プロキシ層は 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 ヘルパ関数が参照します。

Layer 1 · proxy.ts

プロキシ層

Better Auth のセッションを検証します。未認証ユーザを /sign-in に飛ばし、twoFactorPending なら /two-factor に固定します。/admin/* へのアクセスは system_admin ロールを必須化します。

Layer 2 · Hono middleware

ルート層

API ルートごとに必要な権限を requirePermission() で宣言します。accessControl の statement は box / folder / file / member / share_link の 5 種類で、4 ロール × アクションのマトリクスで判定します。

Layer 3 · PostgreSQL RLS

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 ヘルパで判定します。

04

フロントエンド設計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 は不採用(アクセシビリティ標準対応のため)
UIshadcn/ui + Tailwind CSS v4 + Matsuba トークン(@theme inline)
05

バックエンド設計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 + 指数バックオフ(回数を限って再試行)
06

データベース設計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 単数形
IDUUIDv7(text、時系列ソート可)。uuid_generate_v7() 関数で生成
日時timestamptz UTC で保存、表示時にタイムゾーン変換
EnumPostgreSQL 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 透過暗号化はリリース後拡張
07

モノレポ設計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 を集約
TurborepodependsOn: ["^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 直接呼び出しで代替
08

テスト戦略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 を使うことで、本番に近い経路で検証できます。

09

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 に注入