アーキテクチャとエラー契約
Sushi SaaS が強制するレイヤー、ブラウザのデータフロー、認可境界、情報を漏らさない多言語エラー方式を説明します。
starter のコミット
7d452a6に同期しています。
サーバーは一方向に流れる
src/app/** ルートとページ:HTTP の入出力
↓
src/services/** 業務ルール、調整、不変条件
↓
src/models/** 型付き永続化;db() を呼べる唯一の層
↓
src/db/** スキーマ、マイグレーション、接続ルートは認証・検証・HTTP 変換だけを担当し、書き込みは必ず service を通ります。Model はクエリ、テナント条件、トランザクションを所有します。tests/unit/architecture.test.ts が境界違反を検出します。
src/features/ は作りません。各ドメインは model、service、config、component の水平レイヤーに配置します。
ブラウザのデータフロー
Server Component → service を直接呼ぶ
Client Component → src/api/** → 共通 API client → /api/**Server Component は自分の API を fetch しません。Client Component は fetch を直接使わず、src/api/ のドメインラッパーを使います。
認可の二つの問い
組織ロールとサブスクリプションプランは別です。
const ctx = await getOrgContext(request);
if (!ctx || !can(ctx, "file:delete", file)) return respForbidden();
await requireEntitlement(ctx.orgUuid, "storage.upload");can() はロールが操作を許すか、entitlement は組織の有効プランが機能や上限を含むかを判断します。
情報を漏らさないエラー契約
サーバーは安定したカタログコードを持つ AppError を投げます。ルート境界の respError が内部詳細をログに残し、安全な翻訳文だけを返します。
UI は error_code で分岐し、resolveErrorMessage または resolveAuthError で文言を解決します。error.message は表示しません。翻訳は src/lib/errors/i18n/locales/ にあり、新しいコードは五言語すべてに追加します。
ドメイン追加の手順
src/config/に定数を置く。src/models/に型付き CRUD を追加する。- 不変条件、認可、冪等性、副作用を
src/services/に置く。 - ルートを薄く保ち、エラーカタログを使う。
- 適切な階層のテストを追加する。
pnpm lint、pnpm test:run、pnpm buildを実行する。
詳細な契約は starter の docs/errors.md、docs/frontend.md、AGENTS.md にあります。