Stack & app layout
The web frontend is a two-app React SPA monorepo. The stack decision lives in nexis-omni-web/docs/adr/0001-frontend-stack.md, and the rule for which of the two apps a given screen belongs in lives in nexis-omni-web/docs/reference/apps.md. This page orients you to both; those repo-local docs are the ground truth.
The stack at a glance
Section titled “The stack at a glance”ADR-0001 adopts a React SPA monorepo and keeps-and-extends the existing scaffold rather than re-scaffolding. There is deliberately no SSR: every surface is internal and auth-gated, so there is no SEO or anonymous-first-paint requirement to justify a server runtime.
| Concern | Choice |
|---|---|
| Framework / build | React 19 + Vite SPA, React Compiler on |
| Routing | TanStack Router (file-based) |
| Server state | TanStack Query v5 (polling for realtime; push deferred until the backend hub exists) |
| API client | @hey-api/openapi-ts (fetch client + TanStack Query v5 plugin + Zod 4 plugin), generated from the .NET OpenAPI, behind a packages/api-client facade |
| UI primitives | shadcn/ui (own-source) on Base UI (@base-ui/react, not Radix) + Tailwind v4 |
| Forms / tables / dates | TanStack Form + Zod, TanStack Table, react-day-picker + date-fns |
| i18n | Paraglide JS - Sinhala / Tamil / English; money renders as Rs. (en-LK) |
| Lint / format | Biome (replacing the template’s ESLint + Prettier) |
| Monorepo | pnpm + Turborepo |
| Component docs | Storybook in packages/ui |
Monorepo layout: apps hq-dashboard and platform-admin; packages ui, api-client, auth, tokens, config (and i18n).
What the frontend owns
Section titled “What the frontend owns”Codegen removes hand-written DTO drift across a large surface, but a few things are deliberately hand-built and live behind stable seams so the generated client stays swappable:
- Two-plane client. Two client instances: an admin client (admin bearer,
/admin/*, never sendsTenant-ID) and a tenant client (tenant bearer + aTenant-IDGUID header). Switching tenant is a full re-login, because the tenant token is cryptographically bound to onetenant_id. - Single-flight refresh-on-401. A response interceptor with a module-level guard so concurrent 401s trigger exactly one refresh.
- Money is a decimal, never a JS
number. No OSS generator parsesformat: decimalat runtime, so one shared Zod schema inpackages/api-clientdecodes the quoted decimal string with Big.js and encodes back with.toString(). A lint rule forbidsNumber()on money. - A codegen guard. The generated client output is committed, and CI regenerates-and-diffs it against a snapshot of the OpenAPI doc, so a drifting contract fails the build instead of silently changing types.
hey-api is adopted defensively (pinned exact version, OSS CLI only, committed output, the CI diff, and the packages/api-client facade) so a fallback to Orval would stay localized.
Two apps, two auth planes
Section titled “Two apps, two auth planes”The monorepo ships two React SPAs that share the same stack and shell, so they look almost identical in code - but they are not interchangeable. They serve different users and talk to two different backend auth planes. Putting a feature in the wrong app is a real, recurring mistake.
hq-dashboard |
platform-admin |
|
|---|---|---|
| One-liner | One tenant business’s online HQ / back-office | NexisOmni’s own platform-operator console |
| Who signs in | Tenant business users - owners, managers, back-office staff (AppUser) |
NexisOmni platform staff (PlatformUser) |
| Backend plane | Tenant / business plane | Central / admin plane |
| Route surface | /identity/* + the POS/CRM surface (/branches, /products, /orders, /inventory, /reports, …) |
/admin/* only |
Tenant-ID header |
Required on every request | Never sent |
| api-client factory | createTenantApi() |
createAdminApi() |
| i18n | Yes - EN / SI / TA | No - English-only internal tool |
hq-dashboard is the online back-office for a single tenant business: catalogue, multi-branch inventory, sales review, cash sessions, credit, returns, purchasing, reporting, and loyalty. Within the tenant plane, cost / money / valuation operations are Manager-gated, and whole modules can be switched off per tenant (read GET /capabilities and suppress the affordance when a module is off).
platform-admin is the console for NexisOmni’s own staff to administer the SaaS across all tenants: provisioning tenants (POST /admin/tenants), granting a tenant’s built-in roles, toggling per-tenant modules, and gated admin onboarding. It is cross-tenant and never inside one tenant, and it must never send a Tenant-ID.
Which app does this screen go in?
Section titled “Which app does this screen go in?”The backend enforces the split cryptographically, not by convention: two JWT bearer schemes with distinct audiences. An admin token on a tenant route is a 403, and a tenant token on an admin route is a 403. A tenant token is additionally bound to one tenant_id. So a feature belongs to whichever plane its endpoints live on, not to whichever app is handy.
| If the screen / feature is about… | Build it in | Plane |
|---|---|---|
| Creating / listing / configuring tenants (companies) | platform-admin |
admin |
| Granting a tenant’s built-in roles; flipping a tenant’s modules | platform-admin |
admin |
| Anything NexisOmni’s own ops team uses to run the platform | platform-admin |
admin |
| Products, inventory, orders, cash, returns, purchasing, reports, loyalty for one business | hq-dashboard |
tenant |
| A screen a store’s manager / back-office uses | hq-dashboard |
tenant |
Anything that needs a Tenant-ID header |
hq-dashboard |
tenant |
Anything that must never send Tenant-ID |
platform-admin |
admin |
Canonical sources
Section titled “Canonical sources”nexis-omni-web/docs/adr/0001-frontend-stack.md- the full stack decision, its rationale, trade-offs, and the two-plane client design. Ground truth for the stack.nexis-omni-web/docs/reference/apps.md- the authoritative guide to which app a screen belongs in, including the code pointers (packages/api-client/src/client.ts,packages/auth) that wire each plane.- The two-plane split mirrors the backend; for the auth contract itself see the API contract and the backend developer guide at
NexisOmni/docs/developer-guide.md(§2 “Hybrid Identity” and §6 “How authentication works”).