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. An
object (a table/view definition) lives in one /object/{name} space, with the record mode
encoded in the path; wizards live at /form/{name}:
| Route | Mode | Renders | Hook |
|---|---|---|---|
/object/{name}?params |
grid (listing) | GenieTable |
useGenieTable |
/object/{name}?id={pk} |
view a record | GenieView |
useGenieForm (read-only) |
/object/{name}/edit?id={pk} |
edit | GenieForm |
useGenieForm |
/object/{name}/create |
create | GenieForm |
useGenieForm |
/form/{key}?id=&flow= |
wizard | GenieWizardRunner |
— |
The record’s primary key travels as the reserved lowercase id query param (a bare /object/{name}
with id is a single-record view; without it, the grid). Internally the client canonicalizes id →
Id for the engine’s form/view contract — so a model field literally named lowercase id collides
with the pk (the pk wins); avoid it.
Beyond these, the router handles the report page (/reports/{slug}, plural — see
Report pages), the visual designers (/workflow-designer, /wizard-designer,
/navbar-designer — System-role only — and /report-designer, which takes the
definition to edit as ?Id={slug} and opens a blank new document without one) 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 view/edit/create branches are keyed by route so navigating between records remounts the
component with fresh state rather than leaking the previous record’s values (the grid is keyed by
object name). The router also owns the create/edit flow: a top-level grid navigates to an
/object/{name}/create|edit page by default, while an object 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 grid
it was opened from (carried in the URL), which keeps that object’s navbar item highlighted
throughout the view/edit/create flow.
Legacy routes. The former /table/{name}, /view/{name}?Id=, and /wizard/{name} URLs still
resolve — they are normalized to the new scheme (and rewritten in the address bar). The one exception
is the old /form/{name} entity-form URL: /form/ now means wizard, so those old links no
longer resolve.
useGenieRoute, navigate & hrefs
Section titled “useGenieRoute, navigate & hrefs”useGenieRoute (in shell/useGenieRoute.ts) parses window.location into a GenieRoute
({ kind, name, params, mode?, raw } — mode is set for kind: "object") 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.buildObjectHref(name, { mode, id?, params? })builds a canonical object href, e.g.buildObjectHref("Orders", { mode: "grid", params: { Status: "Active" } })→/object/Orders?Status=Active,buildObjectHref("Orders", { mode: "edit", id: "5" })→/object/Orders/edit?id=5.buildGenieHref("wizard", name, params)builds a wizard href →/form/{name}?….normalizeGenieHrefrewrites legacy routes from the navbar/DB (hash#/table/Name/c/k=v, or browser/table,/view,/wizard) into the canonical/object//formscheme, so older stored routes keep working with no data migration.
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.