Shell & routing
The shell is what wraps the render surfaces: a router that turns a URL into the right component, a
layout with a module rail + sidebar + header, the navbar fed from the engine, the auth screens, and
the API client every part shares. createGenieApp wires these together — see
Frontend configuration.
GenieRouter
Section titled “GenieRouter”GenieRouter reads the current route (via useGenieRoute) and renders the matching component. The
core data routes map a name to a hook + surface:
| Route | Renders | Hook |
|---|---|---|
/table/{name}?params |
GenieTable |
useGenieTable |
/form/{name}?params |
GenieForm |
useGenieForm |
/view/{name}?Id=… |
GenieView |
useGenieForm (read-only) |
/wizard/{key}?id=&flow= |
GenieWizardRunner |
— |
Beyond these, the router handles the visual designers (/workflow-designer, /wizard-designer) and
the admin tools (/object-explorer, /jobs, /hosted-services) — all lazy-loaded, so their
(heavy) chunks download only when the route is opened, and only when the owning module is enabled
(a disabled module shows a “not enabled” pane and never triggers the download). Any other non-root
path becomes a path route — the extension point for host-registered custom pages — and / is home.
The table/view/form branches are keyed by route so navigating between records remounts the
component with fresh state rather than leaking the previous view’s sort/filters. The router also owns
the create/edit flow: a top-level table navigates to a /form page by default, while a view whose
FormStyle is Modal opens the form in a dialog and refreshes in place on save. After a directed
form submits, it returns to the table it was opened from (carried in the URL).
useGenieRoute, navigate & hrefs
Section titled “useGenieRoute, navigate & hrefs”useGenieRoute (in shell/useGenieRoute.ts) parses window.location into a GenieRoute
({ kind, name, params, raw }) and re-renders on popstate and on programmatic navigation.
navigate(href, { replace? })updates history (pushState/replaceState) and dispatches an internal event so the hook re-reads the route — no full-page load.buildGenieHref(kind, name, params)builds a canonical href, e.g.buildGenieHref("table", "Orders", { Status: "Active" })→/table/Orders?Status=Active.normalizeGenieHrefconverts legacy hash routes from the navbar/DB (e.g.#/table/Name/c/k=v) into clean browser paths, so older stored routes keep working.
Routing mode is set by createGenieApp({ routing }): "browser" (default) uses History-API paths
and requires the host to serve index.html for unknown paths (SPA fallback); "hash" avoids that
server requirement. Auth/account pages always use the hash, independent of this setting.
GenieLayout — the app shell
Section titled “GenieLayout — the app shell”GenieLayout is the three-column shell: a module rail, a contextual sidebar, and a header,
on a has-rail grid. Its behaviour:
- Modules are derived from the navbar’s top-level items. Clicking a rail module swaps the sidebar (or navigates, for a direct-link module); the active module is inferred from the current route, with a manual selection winning until navigation moves elsewhere.
- Collapse — the sidebar collapses to a rail-only view (persisted to
localStorage); on mobile the rail + sidebar become an off-canvas drawer that auto-closes on navigation. The full-canvas designers auto-collapse the sidebar for room and restore the preference on exit. - Header — the hamburger, an optional global search (nav + configured datasets, focused with
Ctrl/Cmd-Shift-F when
headerSearchis enabled), the accent picker and light/dark toggle (both fromuseGenieTheme), and a host-supplied right slot (notifications, user menu). - Breadcrumb — rendered from the current route + nav items; a per-route error boundary isolates a render failure to its page.
Theming (data-theme / data-accent, persistence, the picker) is covered in
Theming.
GenieNavbar
Section titled “GenieNavbar”GenieNavbar renders the contextual sidebar: a module-context header, a menu search, and the active
module’s navigation tree (up to three levels, collapsible groups). Typing in the menu search switches
to a flat cross-module result list so nothing hides behind module selection. It consumes the
navbar JSON from the engine — nodes of kind Section (heading), Divider, or a regular
item/group — and renders right-aligned badges: a numeric count pill or a themed status dot. Where
the tree comes from (the permission-filtered /navbar endpoint or a host-supplied static tree) is set
by navbar.source. See Navigation for the navbar model and
Frontend configuration for the config.
Auth screens
Section titled “Auth screens”The auth experience — login, MFA/TOTP, forgot/reset password, change password, profile — lives under
auth/. GenieAuthProvider holds the session; useAuth exposes the current user, roles, and
sign-in/out. The auth screens run on hash routes (independent of the data-page routing mode) and
their affordances adapt to the server’s enabled features, read from GET /auth/config (which login
methods, MFA channels, reCAPTCHA, password rules are on) — so nothing is hardcoded on the client. The
login/forgot/reset screens render beside a gradient brand panel customizable via the auth block
(see Theming). While the session resolves and the
authenticated app-shell chunk downloads, a branded loading splash shows (replaceable via preloader).
GenieApiClient
Section titled “GenieApiClient”GenieApiClient (api/client.ts) is the single fetch layer every part shares. It:
- Carries the JWT — login returns an access token (not a cookie); the client stores it in
localStorage, sends it asAuthorization: Bearer …, and (for the WebSocket transport, which can’t set a header) exposes it for the SignalRaccess_tokenquery. - Unwraps the envelope — the engine wraps every response in
{ Success, Error, Data }; the client returnsData(or throws aGenieApiErrorcarrying the server’s message) so callers work with plain payloads. Error bodies are reduced to a human-readable message; raw JSON is never shown to the user. - Sends context headers — the antiforgery token (
X-XSRF-TOKEN) on unsafe requests and the browser’s IANA time zone (X-TimeZone) on every request, pluscredentials: "include"for the cookie/company/timezone session. - Handles 401 — a 401 on any non-auth endpoint fires the registered unauthorized handler (the auth provider signs out and bounces to login); auth-endpoint 401s are left to their callers so a bad login doesn’t loop.
- Caches structure & de-dupes requests — object metadata is cached in-memory
(
schemaCacheTtlSeconds), and concurrent identical reads (everyGETplus the read-style POSTs) share one in-flight fetch. Mutations are never coalesced. See Request de-duplication.
Because the auth, notification, wizard, workflow, and Hangfire controllers sit as siblings of the
/genie base, the client derives those bases from apiBase (e.g. .../auth, .../notification) — so
you only ever configure the one apiBase.