Skip to content

Foundation — Routes

Scope note: Foundation is not a feature module — it does not own any business pages. This doc describes (a) the route skeleton that exists today, (b) the small handful of system routes foundation adds (account stub, forbidden), and (c) the route registry convention that all subsequent module routes will follow.

URL stems per module (reference)

These are the URL prefixes each BE module will use once it ships. None of these are routed today — the table is here so the route registry is shaped correctly from day one. Authoritative source: CLAUDE.md § Modules.

BE module URL stem
identity /login, /account
item_catalog /catalog
manufacturing /manufacturing
warehouse /warehouse
traceability /traceability
crm /crm
invoicing /invoicing

Current route tree (after M0 cleanup)

src/routes/
├── __root.tsx           # providers, devtools, outlet
└── _auth.tsx            # authenticated layout group — mounts AppShell
    └── _auth/
        └── index.tsx    # / — dashboard placeholder

src/routes/login.tsx is deleted by the M0 cleanup ticket. It was vibe-coded without a real auth contract; identity's first ticket adds it back against the real BE login endpoint.

The _auth layout group renders without an auth guard until identity ships — there's nothing to gate against. Identity's first ticket adds the <RequireAuth> wrapper at this layer.

All routes are file-based; TanStack Router regenerates routeTree.gen.ts on save.

Foundation route additions

One thin system route ships as part of foundation:

Route File Owner ticket
/forbidden src/routes/_auth/forbidden.tsx M2 — AppShell ticket (rendered when a 403 FORBIDDEN_INSUFFICIENT_PERMISSION is intercepted)

The /forbidden route is functional from day one because the error parser routes 403s here. /account and /login are deliberately not added by foundation — those land with identity's first ticket against a real auth contract.

Routes & navigation convention (new, owned by M2)

Foundation introduces two narrow files with two clear concerns:

  • src/lib/routes.ts — every URL path in the app, organised by module. Knows about paths and their parameters, nothing else. Paths are not internationalised.
  • src/lib/navigation.ts — the curated subset of routes that appear in the AppShell sidebar (label, icon, group, optional permission).

Every route in the app registers a path in routes.ts. Components reference routes.<module>.<name> or routes.<module>.<name>(id) — string-literal paths are not allowed. The sidebar reads navigation.ts; breadcrumb labels are page-driven (set on the route file itself when AppShell lands), not derived from either file.

src/lib/routes.ts shape

// Static routes are strings; parameterised routes are arrow functions.
export const sharedRoutes = {
  dashboard: '/',
  forbidden: '/forbidden',
} as const

// Example shape a module ticket will land:
export const itemCatalogRoutes = {
  items:      '/catalog/items',
  itemDetail: (id: string) => `/catalog/items/${id}`,
  itemEdit:   (id: string) => `/catalog/items/${id}/edit`,
} as const

export const routes = {
  shared: sharedRoutes,
  identity: identityRoutes,
  itemCatalog: itemCatalogRoutes,
  // …seven modules
} as const

Foundation seeds only sharedRoutes; the seven module constants ship empty. Each module's first ticket fills in its own module constant in place — module tickets never edit the aggregating routes object.

src/lib/navigation.ts shape

export enum NavGroup {
  IDENTITY        = 'identity',
  ITEM_CATALOG    = 'item_catalog',
  MANUFACTURING   = 'manufacturing',
  WAREHOUSE       = 'warehouse',
  TRACEABILITY    = 'traceability',
  CRM             = 'crm',
  INVOICING       = 'invoicing',
}

export type NavItem = {
  path: string                     // from routes.*
  i18nKey: string                  // sidebar label
  icon: LucideIcon
  group?: NavGroup                 // omitted => ungrouped, rendered at the top
  requiredPermission?: Permission  // controls sidebar visibility (M4+)
}

export const navigation: NavItem[] = [
  { path: routes.shared.dashboard, i18nKey: 'common:nav.dashboard', icon: Home },
  // …module tickets add their entries here
]

NavGroup values match BE snake_case (matches ux-tests/prototypes/action-flow-prototypes/sidebar-template.txt). The sidebar renders items in array order; items with no group render ungrouped above the first group header; items sharing a group cluster under that group's translated header.

Not every route is a NavItem. Detail / edit / parameterised pages, plus /forbidden, are reachable via routes but never live in navigation.

The convention is documented authoritatively in ../../standards.md § Routes & Navigation (added in the M2 route-registry ticket). This doc references that section rather than duplicating it.

Search params

No search params are introduced by foundation. The pattern — validateSearch: zodSchema with the draft/committed split — is established and documented in ../../standards.md § Routing and gets exercised the moment the first list page (in item_catalog) lands.

Route file size budget

Per CLAUDE.md § Key rules — route files are entry points only, ≤20 lines. The foundation route (forbidden.tsx) holds to this — it's a single static screen with one i18n string, kept inline because there's nothing to extract.