Route Overrides

Hide, reorder, group, deep-link, replace or rebuild routes from src/overrides/routes.tsx - the most used extension point.

The file to edit is src/overrides/routes.tsx. It is the single most used extension point in Thunder UI.

Keys are SDK module names, group names (as the SDK reports them, usually capitalised), or the built-in keys overview, wallet and notifications.

The One Rule That Governs Everything Here

FormEffect
key: { … }Replaces the generated route entirely. Only what you write exists.
key: { merge: true, … }Merges over the generated route. Keeps its path, Component and generated children.

Everything below follows from that one distinction.

Hiding a Module

Two different outcomes, and the difference matters:

src/overrides/routes.tsx
// Remove it completely - the list/form/detail pages stop existing.
// No `merge`, so `{ display: false }` *becomes* the whole route.
tenants: { display: false },
apiKeys: { display: false },
accessControl: { display: false },

// Keep the pages reachable by URL, just hide them from navigation.
// Use this for screens you link to yourself from somewhere else.
attributeValues: { merge: true, display: false },
stores: { merge: true, display: false },

Gating Visibility on a Permission

display may be a function, re-evaluated whenever navigation renders. Any permission works - including a different module's, which is how you hide a screen behind the permission that actually guards its data:

products: { merge: true, display: () => ThunderSDK.isPermitted(ThunderSDK.products.update) },
brands:   { merge: true, display: () => ThunderSDK.isPermitted(ThunderSDK.brands.update) },

// gated by a *different* module's permission
categories: {
  merge: true,
  display: () => ThunderSDK.isPermitted(ThunderSDK.manageOrders.get),
},

Permissions come from thunder-core and are registered before the app renders - see Authentication.

Reordering Modules and Groups

priority sorts ascending; ties keep declaration order. Group keys work exactly like module keys:

orders:             { merge: true, priority: 0 },
orderSubscriptions: { merge: true, priority: 1 },

// group-level overrides - keyed by the group name
Management: { merge: true, priority: 1 },
Admin:      { merge: true, priority: 4 },
Orders:     { merge: true, priority: 4 },

A group's own display is ANDed with "has at least one visible child", so a group disappears automatically once everything inside it is hidden. You do not need to hide the group as well.

Adding Your Own Page Into a Group

Set group to the group you want it to appear under. The group does not have to come from the SDK - naming a new one creates it:

refundableOrders: {
  name: "Refundable Orders",
  path: "refundableOrders",
  group: "Management",                     // joins an existing group
  icon: IconCreditCardRefund,
  display: () => ThunderSDK.isPermitted(ThunderSDK.manageOrders.refundable),
  Component: RefundableOrders,
},

manageCareers: {
  name: "Manage Careers",
  path: "manageCareers",
  group: "Careers",                        // creates a new group
  icon: IconDeviceDesktopCog,
  display: () => ThunderSDK.isPermitted(ThunderSDK.manageCareers.get),
  Component: ManageCareers,
},

Routes are grouped by group ?? "Other". A route with no group therefore lands in a group literally named Other - which is visible if that route is. Always set group on a custom route unless you actually want that. (This is also why a bare { display: false } is safe: with nothing visible inside it, the Other group hides itself.)

Hidden Deep-Linked Routes

A route with display: false is reachable by URL but absent from navigation - the usual shape for a detail page you link to from a list. path can be several segments with parameters:

manageCareerDetail: {
  path: "manageCareers/:id",
  group: "Careers",
  display: false,
  Component: ManageCareerDetail,
},

orderSubscriptionsTriggers: {
  path: "orderSubscriptions/:subscriptionId/triggers/:triggerId",
  group: "Orders",
  display: false,
  Component: SubscriptionTriggerDetailView,
},

Rebuilding the Children Yourself

Because an override without merge replaces the route outright, you can declare the children by hand - and still reuse the generated pages. ListPage, ViewPage and FormPage are exported from core and take group and name, so you keep the schema-driven screens while changing the URL layout.

Here the module is relabelled and its form segment becomes apply:

import { Outlet } from "react-router"
import { ListPage } from "@/core/crud/ListPage.tsx"
import { ViewPage } from "@/core/crud/ViewPage.tsx"
import { FormPage } from "@/core/crud/FormPage.tsx"

careers: {
  name: "Applications",                    // nav label differs from the module name
  path: "careers",
  group: "Careers",
  icon: IconDeviceDesktopCog,
  display: () => ThunderSDK.isPermitted(ThunderSDK.careers.get),
  Component: () => <Outlet />,             // parent renders its children
  children: [
    {
      index: true,
      display: false,
      Component: () => <ListPage group="Careers" name="careers" />,
    },
    {
      path: ":id",
      display: false,
      Component: () => <ViewPage group="Careers" name="careers" />,
    },
    {
      path: "apply",                       // instead of the generated `form`
      display: false,
      Component: () => <FormPage group="Careers" name="careers" />,
    },
  ],
},

Mix this freely with your own components: replace only the form and keep the generated list, wrap ListPage in your own layout, or add extra children the generator never produces.

Replacing a Built-in Page

// keeps the built-in's icon, priority and permission checks
overview: { Component: MyDashboard },

Built-in keys: overview, wallet, notifications. See Built-in Pages.


On this page