B2B2C Architecture Contract V1
Use this as the non-negotiable contract for scaffolding, route boundaries, data boundaries, and feature growth.
For enforcement levels and rollout strategy, see:
Scope Model
Every request resolves one and only one runtime scope:
platformGlobal operator surface across all tenants.orgTenant business operator surface.customerEnd-customer portal surface.
Unknown scope fails closed.
Surface Boundaries
Treat each scope as a first-class product surface with explicit boundaries:
- route boundary
- data boundary
- auth boundary
- UI boundary
Do not collapse boundaries through convenience imports or shared ad-hoc logic.
Each surface may intentionally diverge in visual language and product experience.
platform,org, andportalmay use different layouts, navigation models, density, motion, and theme treatment.- Shared primitives are opt-in infrastructure, not a requirement to make all surfaces look or behave the same.
- Do not centralize page inventory just because two surfaces currently look similar.
Source Layout Contract
src/app/*Route entrypoints, layout composition, and route-owned page composition.src/app/<surface>/**/_components/*Route-local page composition that belongs to one surface tree and is not intended for cross-surface reuse.src/features/platform/*Platform-only feature modules.src/features/org/*Org-admin feature modules.src/features/customer/*Customer-portal feature modules.src/core/*Shared infrastructure only (auth/session plumbing, host context, wrappers, design primitives).
src/core/* must not become a domain business-logic catch-all.
Feature Boundary Contract
- Cross-feature imports must go through the target feature public entrypoint (
index.ts). - Do not deep-import another feature's internals (
components/*,lib/*,hooks/*,types/*). - Route files stay thin and only compose feature pages, access guards, and redirects.
src/components/*is for reusable surface composition and shared UI, not route-page inventory.- If page composition belongs to one route tree, keep it with that tree instead of promoting it into shared components prematurely.
- Default to route-local ownership first. Promotion order is: route tree
_components/*-> surface-local shared folder inside that route tree -> same-surface reusable component undersrc/components/<surface>/*when reuse escapes the tree -> cross-surface primitive undersrc/components/ui/*orsrc/lib/*. - Do not treat promoted component folders as catch-all storage for everything in a surface.
AuthZ Contract
- Keep platform access separate from org access.
- Keep org access separate from customer access.
- Prefer capability checks over inline role branching.
- Deny by default when membership or capability cannot be resolved.
Data Contract
- Keep identity and memberships explicit.
- Keep tenant ownership explicit on tenant-owned records.
- Never rely on host inference alone for authorization decisions.
- Use widen -> migrate -> narrow for breaking schema changes.
Convex Access Contract
- UI pages/components call app wrapper modules, not generated Convex API paths directly.
- Generated Convex API imports in
src/*are allowed only in wrapper modules and proxy/middleware entrypoints. - Keep backend module names domain-specific, not generic.
Naming Contract
- File and folder names: kebab-case.
- React components: PascalCase.
- Helpers/utilities/variables: camelCase.
- Hooks:
use*. - Constants: UPPER_SNAKE_CASE when truly constant.
Test Placement Contract
- App unit tests live in
apps/app/tests/*by default. - Do not scatter tests under
src/*unless there is a strong colocated-testing reason. - Use
*.test.tsand*.test.tsxnaming.
Rule Gates For New Work
Before implementing a new feature, answer these in the design note or PR description:
- Which scope owns this feature (
platform,org, orcustomer)? - Which data boundary does it read/write?
- What capabilities are required?
- Does it cross feature boundaries through a public entrypoint?
- Does it require migration-safe schema rollout?
If any answer is unclear, pause and resolve design before implementation.