Skip to content

Web dashboards

The nexis-omni-web repository holds the online dashboards: a pnpm + Turborepo workspace of two apps plus shared packages. The offline POS is a separate Flutter app and is not in this repo.

App Plane Who uses it
apps/hq-dashboard TENANT a business’s staff (Owner / Manager / Cashier)
apps/platform-admin ADMIN the platform operator

The two never mix planes. The tenant app sends a Tenant-ID header on every request (including login) and reaches the API under /api; the admin app never sends Tenant-ID and reaches the API under /admin. See Auth planes & tenancy.

  • @workspace/api-client - the hey-api typed client, generated from the backend’s OpenAPI, plus the Big.js money schema.
  • @workspace/auth - the two-plane client and single-flight refresh-on-401.
  • @workspace/ui, @workspace/tokens - the component library and design tokens.
  • @workspace/i18n - Paraglide messages (English, Sinhala, Tamil).
  • @workspace/config - shared config, including the runtime environment schema.
  • Stack & app layout - the React SPA monorepo stack and which of the two apps a screen belongs in.
  • Calling the API - the two planes, the refresh cookie, money-as-string, error codes, and the generated client.
  • nexis-omni-web/docs/backend-integration.md - the full API contract from the client’s side: auth, 2FA, the refresh cookie, the Tenant-ID header, money-as-string, error shapes, and the endpoint inventory. Read it before writing code that calls the API.
  • nexis-omni-web/docs/adr/0001-frontend-stack.md - the stack decision.
  • nexis-omni-web/docs/reference/apps.md - which app a given screen belongs in.
  • Gate off the server, mirror it in the UI. Authorization comes from GET /me/permissions and GET /capabilities; the UI hides what the user cannot do, but the server is the real gate. Do not over-gate read-only screens that the server leaves open.
  • Money is a string. Decode it through the money schema; never Number() it.
  • Environment is validated at runtime in the browser. VITE_API_BASE_URL must be a full URL (z.url()), inlined at build time. A wrong or empty value builds a bundle that fails on load - which is why the deployment Dockerfiles validate it at build time.