Skip to content

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.

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

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 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.

  • The access token lives in memory only.
  • The refresh token is an httpOnly Secure cookie (nexisomni_{plane}_rt, SameSite=Strict, path-scoped to its plane).
  • Send credentials: include on /refresh and /logout, and run over HTTPS even in dev - the Secure cookie 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.

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.