The API contract
Both halves of NexisOmni - the dashboards and the POS - are built around one API contract. This is the “big picture” that requires reading across repositories. The authoritative sources are the backend ADRs (0014 / 0015 / 0016) and nexis-omni-web/docs/backend-integration.md; this page is the orientation.
Two auth planes, never mixed
Section titled “Two auth planes, never mixed”There are two separate authentication planes. A token minted for one is rejected on the other (each plane has its own JWT audience).
| ADMIN plane | TENANT plane | |
|---|---|---|
| Endpoints | /admin/* |
/identity/*, and the rest |
| Identity | PlatformUser in the central DB |
AppUser in that tenant’s own DB |
| Token | admin bearer | tenant bearer |
Tenant-ID header |
never sent | required on every request, including login |
| Web app | apps/platform-admin |
apps/hq-dashboard |
Database-per-tenant
Section titled “Database-per-tenant”One central admin database, plus one isolated database per tenant. There is no shared TenantId column - isolation is at the database level. A request resolves its tenant from the Tenant-ID header, and TenantDbContext binds to that tenant’s database for the rest of the request.
Because a tenant token is cryptographically bound to one tenant_id, switching tenant means a full re-login.
Authorization is resolved per request, not baked into the JWT
Section titled “Authorization is resolved per request, not baked into the JWT”The client gates its UI off GET /me/permissions (verb-based permissions) plus GET /capabilities (per-tenant module entitlements). The server re-checks permission on every request regardless of what the UI showed. The rule of thumb: cost is back-office - anything touching cost, money totals, or valuation is Manager-gated.
Money is a string on the wire
Section titled “Money is a string on the wire”Money is always a quoted string ("118.00", pattern ^-?\d+(\.\d+)?$), never a JSON number and never a float. Enums are PascalCase strings too (for example "Created").
The web client decodes money through a Big.js schema in @workspace/api-client, and a lint rule forbids calling Number() on it. The POS uses decimal. Treat any money value as an opaque decimal string until you deliberately parse it.
Tokens and cookies
Section titled “Tokens and cookies”- The access token lives in memory only.
- The refresh token is an httpOnly
Securecookie (nexisomni_{plane}_rt,SameSite=Strict, path-scoped to its plane). - Send
credentials: includeon/refreshand/logout, and run over HTTPS even in dev - theSecurecookie is dropped on plain HTTP.
The SameSite=Strict + path-scoping has a direct deployment consequence: a dashboard and the API it calls must share one origin. See Auth planes & tenancy and Deploying.
When the contract changes
Section titled “When the contract changes”Regenerate the web client. The hey-api client in nexis-omni-web/packages/api-client is generated from the backend’s /openapi/v1.json. The workflow is: snapshot the backend’s OpenAPI document into the client package, then run the client’s generate script.