How CRUD Screens Are Generated
How Thunder UI turns SDK module names, groups and permissions into a route tree with list, form and detail pages.
src/core/router.tsx builds the entire route tree at module load from the installed SDK, plus whatever you registered in src/overrides/routes.tsx.
A backend module that does not appear here is usually a stale SDK, not a routing problem. See Two Kinds of Update.
The Build Order
| Step | What happens |
|---|---|
| 1. Module names | Come from ThunderSDK.getModuleNames(), plus any key in src/overrides/routes.tsx that is neither an SDK group nor a built-in route key. |
| 2. Per module | If an override exists without merge: true, it replaces the generated route entirely. Otherwise the route is generated and the override is spread over it - so merge: true changes name/icon/priority while keeping the generated children. |
| 3. Grouping | Uses ThunderSDK.getGroup(name) - the .group("Label") on the backend router, deliberately shared across several route files. Each group becomes a parent route whose path is the group name lowercased with whitespace replaced by dashes. A group's index redirects to its first visible child. |
| 4. Visibility | Requires ThunderSDK.isPermitted(name, "get") or isPermitted(name, "create"), and the override's display must allow it. A group shows only if a child is visible. |
| 5. Built-ins | overview, wallet and notifications are prepended, and are replaceable by name. |
Step 3 is why .group("Label") on the backend matters for more than the OpenAPI spec: it is what organises the dashboard's navigation. See Controlling the UI From the Backend and Routes.
Generated Children Per Module
| Child path | Component | Condition |
|---|---|---|
| index | ListPage, or the lists override | always |
form, form/:id | FormPage | only if the module has create or update |
:id | ViewPage | always |
A detail component fetches its own data - see Views. When no views override is registered for a module, its detail route goes to that module's edit form.
URL Shape
| URL | Page |
|---|---|
/ | redirects to /select-tenant |
/select-tenant | tenant picker |
/:tenant/overview | landing page |
/:tenant/<group>/<module> | list |
/:tenant/<group>/<module>/form | create |
/:tenant/<group>/<module>/form/:id | edit |
/:tenant/<group>/<module>/:id | detail |
The :tenant segment is pushed into the SDK via ThunderSDK.plugins.essentials.setTenant(). See Authentication & Multi-Tenancy.
TRouteObject
Every entry in the tree - generated or yours - is a React Router RouteObject with Thunder UI's navigation metadata on top:
type TRouteObject = {
name?: string // nav label; passed through t()
priority?: number | (() => number) // ascending; ties keep declaration order
group?: string
icon?: TablerIcon
display?: boolean | (() => boolean)
children?: TRouteObject[]
button?: React.ComponentType // rendered beside the nav entry
} & RouteObjectdisplay and priority may be functions, re-evaluated whenever navigation renders - which is what makes permission-gated visibility work. The full set of patterns lives in Route Overrides.