Architecture and Error Contracts
Understand Sushi SaaS's enforced application layers, browser data flow, authorization boundary, and translated no-leak error system.
Synced with starter commit
7d452a6.
One Direction Through the Server
src/app/** routes and pages: HTTP in, HTTP out
↓
src/services/** business rules, orchestration, invariants
↓
src/models/** typed persistence; the only layer allowed to call db()
↓
src/db/** schema, migrations, and connectionRoutes authenticate, validate, and translate HTTP. Writes always go through a service. Models own queries, tenant predicates, and database transactions. tests/unit/architecture.test.ts rejects imports that cross these boundaries.
Do not create src/features/; each domain is spread across the horizontal layers. For example, reservations use models/reservation.ts, services/reservations/, config/reservations.ts, and components/reservations/.
Browser Data Flow
Server Component → service directly
Client Component → src/api/** → shared API client → /api/**Server Components never fetch the application's own API. Client Components never call raw fetch; they use a domain wrapper under src/api/, which consistently unwraps the response envelope and raises a typed client error.
Authorization Has Two Questions
Organization roles and subscription plans are intentionally separate:
const ctx = await getOrgContext(request);
if (!ctx || !can(ctx, "file:delete", file)) return respForbidden();
await requireEntitlement(ctx.orgUuid, "storage.upload");can() answers whether the member's role permits an action. Entitlement functions answer whether the organization's effective plan includes the capability or capacity.
No-Leak Error Contract
Server code throws AppError with a stable catalog code. Every route boundary ends in respError; it logs internal detail and returns translated safe copy:
throw new AppError("CREDITS_INSUFFICIENT", {
message: `org ${orgUuid} could not spend ${cost}`,
details: { required: cost, available: balance }
});{
"code": -1,
"message": "You do not have enough credits for this.",
"error_code": "CREDITS_INSUFFICIENT"
}The UI branches on error_code and resolves copy through resolveErrorMessage or resolveAuthError. It never renders error.message. Error translations live in src/lib/errors/i18n/locales/; adding a code requires all five locales.
Adding a Domain Safely
- Define constants and environment-derived flags in
src/config/. - Add typed CRUD under
src/models/. - Put invariants, authorization, idempotency, and side effects in
src/services/. - Keep route handlers thin and translate failures with the error catalog.
- Add the appropriate unit, service, API, component, or database tests.
- Run
pnpm lint,pnpm test:run, andpnpm build.
The detailed engineering contracts remain in docs/errors.md, docs/frontend.md, and AGENTS.md inside the starter repository.