8.9 KiB
Route names: Next.js app/ to TanStack Start src/routes/
The web shell moved from Next.js to TanStack Start route by route (M-wave M6). This table is the name each
Next file took, so a change that still names a Next path finds its file here. The rules are F10 in
docs/reference/app-kit.md; the worked
example is the notes module through $org.$workspace.$project.$module.$.tsx and src/server/page.ts. A route
file holds its server function and its view; what the loader does is in src/server/account-pages.ts (outside an
organization) and src/server/org-pages.ts (under one), imported inside the handler.
Naming
- Flat file names under
src/routes/, one dot per path segment:$org._settings.members.$workspace.tsxis/<org>/members/<workspace>. No folders. [param]becomes$param;[...rest]and[[...rest]]become$(the splat, read asparams._splat, empty at the parent's own path).- A Next
layout.tsxbecomes the route file of its own path ($org.tsx), rendering<Outlet />; thepage.tsxbeside it becomes<path>.index.tsx. A layout's checks move to itsloader(orbeforeLoadwhen a child needs the result as context), so they finish before the response starts. - A
route.tsbecomes a route file withserver: { handlers: { GET, ... } }and no component. - A
loading.tsxbecomespendingComponent: RoutePendingon that path's route (src/ui/route-pending.tsx), which rendersLoadingPagewith the label from the root loader'sShellData. error.tsx,global-error.tsx,not-found.tsxandforbidden.tsxare the root route'serrorComponentandnotFoundComponent(done,__root.tsx).- Static segments win over
$paramat the same depth, so$org._settings.members.tsxand$org.$workspace.tsxlive side by side, as in Next. src/routeTree.gen.tsis generated by the Start plugin onvite devandvite build. It is not committed.- Organization pages share the frame in
$org.tsx. A validated workspace is retained bysrc/ui/workspace-context.tswhile Settings is open, so its sidebar and workspace settings use the workspace just visited even when the organization loader has not rerun. A full reload resolves the workspace preference on the server. In a workspace, the project picker and the open project’s sections follow the workspace destinations in the sidebar. - The organization home lists accessible workspaces with the kit's
ResourceList. Projects are opened within their workspace. The workspace overview stays at/<org>/<workspace>and the dedicated directory is at/<org>/<workspace>/modules. The picker offers?create=truethere when the workspace grant allows project creation. A successful submission opens the new project overview at/<org>/<workspace>/<project>. Workspace and project breadcrumbs start at the workspace. Organization settings begin at the organization. /settings/accountopens Personal settings in the last workspace's organization when that organization is still among the signed-in person's memberships. With one organization it uses that one; with none or an ambiguous choice it stays on the standalone account page. A preference cookie never grants access.- Root 404, 403 and error states use the kit's
PageStateand keep the generic not-found response for inaccessible organizations; they do not display the standalone document header. - The multi-organization chooser at
/uses the same centeredAuthPageandAuthCardas onboarding. Its organization choices are links, and account, sign-out, language and theme controls remain available within the card; they use the shared kit and the shell's existing account and preference handling.
Page layout contract
- List pages give their primary action to
PageHeaderorPageSection.actions. Role creation, workspace creation, the overview's project creation and document upload use the kit'sFormPanel. The project directory also has a directly addressable create form. A panel form is mounted on demand in the same right panel as records, Agent and Help. A phone gets the shared full-screen sheet. The panel protects drafts and cannot close during a pending submission. defineResourceandResourcePageremain the default for repeatable record CRUD (Notes is the worked example): one declaration owns columns, fields, mutations, record addresses and list refresh.- Keep settings forms inline when each section is saved independently. Keep short invitation forms with their one-time link result. Onboarding and identity stay on their dedicated entry layout; filter forms remain beside the list they filter.
- Full permission matrices, document review and long-running work use full-page editors. A detail layout is appropriate for related record facts and context, rather than a permanent create form.
- Optional modules remain disabled by default. Documents upload was verified separately with that module selected in an isolated checkout; this does not establish a live OCR/provider integration.
The table
Next file (apps/web/app/...) |
Start file (apps/web/src/routes/...) |
State |
|---|---|---|
layout.tsx |
__root.tsx (loader shellData, shellComponent) |
done (M6a) |
error.tsx, global-error.tsx |
__root.tsx errorComponent |
done (M6a) |
not-found.tsx, forbidden.tsx |
__root.tsx notFoundComponent (data.forbidden is the 403 page) |
done (M6a) |
globals.css |
src/ui/globals.css, linked by __root.tsx, then modules/styles.css |
done (M6a) |
api/[[...rpc]]/route.ts, api/auth/[...all]/route.ts |
api.$.ts |
done (M6a) |
auth/callback/route.ts, auth/dev-login/route.ts, auth/sign-in/start/route.ts, auth/sign-out/route.ts |
auth.$.ts |
done (M6a) |
[org]/[workspace]/[project]/[module]/[[...rest]]/page.tsx |
$org.$workspace.$project.$module.$.tsx |
done (M6a, notes) |
page.tsx |
index.tsx |
done (M6b) |
onboarding/page.tsx |
onboarding.tsx |
done (M6b) |
members/page.tsx |
members.tsx |
done (M6b) |
settings/account/page.tsx |
settings.account.tsx |
done (M6b) |
invitations/[org]/[token]/page.tsx |
invitations.$org.$token.tsx |
done (M6b) |
auth/sign-in/page.tsx |
auth.sign-in.tsx |
done (M6b) |
auth/sign-up/page.tsx |
auth.sign-up.tsx |
done (M6b) |
auth/signed-out/page.tsx |
auth.signed-out.tsx |
done (M6b) |
api/brand/og/route.ts |
api.brand.og.ts |
done (M6b) |
[org]/layout.tsx |
$org.tsx |
done (M6c) |
[org]/page.tsx |
$org.index.tsx |
done (M6c) |
[org]/brand/logo/[variant]/route.ts |
$org.brand.logo.$variant.ts |
done (M6c) |
[org]/brand/og/route.ts |
$org.brand.og.ts |
done (M6c) |
[org]/branding/page.tsx |
$org._settings.branding.tsx |
done (M6c) |
[org]/settings/page.tsx |
$org._settings.settings.index.tsx |
done (M6c) |
[org]/workspaces/page.tsx |
$org._settings.workspaces.tsx |
done (M6c) |
[org]/access-audit/{layout,loading,page}.tsx |
$org._settings.access-audit.tsx |
done (M6c) |
[org]/audit/{layout,loading,page}.tsx |
$org._settings.audit.tsx |
done (M6c); a tab of Settings since 2026-11-03 |
[org]/invitations/{layout,loading,page}.tsx |
$org._settings.invitations.tsx |
done (M6c) |
[org]/members/layout.tsx, loading.tsx |
$org._settings.members.tsx |
done (M6c) |
[org]/members/page.tsx |
$org._settings.members.index.tsx |
done (M6c) |
[org]/members/[workspace]/page.tsx |
$org._settings.members.$workspace.index.tsx |
done (M6c) |
[org]/members/[workspace]/[project]/page.tsx |
$org._settings.members.$workspace.$project.tsx |
done (M6c) |
[org]/roles/layout.tsx, loading.tsx |
$org._settings.roles.tsx |
done (M6c) |
[org]/roles/page.tsx |
$org._settings.roles.index.tsx |
done (M6c) |
[org]/roles/[role]/page.tsx |
$org._settings.roles.$role.tsx |
done (M6c) |
[org]/[workspace]/layout.tsx |
$org.$workspace.tsx |
done (M6c) |
[org]/[workspace]/page.tsx |
$org.$workspace.index.tsx |
done (M6c) |
[org]/[workspace]/[project]/layout.tsx |
$org.$workspace.$project.tsx |
done (M6c) |
[org]/[workspace]/[project]/page.tsx |
$org.$workspace.$project.index.tsx |
done (M6c) |
[org]/[workspace]/[project]/documents/page.tsx |
$org.$workspace.$project.documents.index.tsx |
done (M6c) |
[org]/[workspace]/[project]/documents/[fileId]/page.tsx |
$org.$workspace.$project.documents.$fileId.tsx |
done (M6c) |
A module's own pages (modules/<id>/ui/pages.tsx) need no row: each joins modules/pages.ts when its page
is a ProjectPage of F10 (load and View), as notes is. A module's page above a project is a
WorkspacePage in workspacePages of the same file, served by one route with no Next ancestor:
$org.$workspace.modules.$module.$.tsx, /<org>/<workspace>/modules/<module>/.... modules is reserved from
project slugs (RESERVED_PROJECT_SLUGS), as a bare /<org>/<workspace>/<module> would be a project's slug.