Skip to Content
Auth & Routing

Authentication & Routing

Numa’s canonical guide for authentication, product platforms, JWT, proxy.ts, and [lang] routes.

Conceptual overview

The app uses three cooperating signals to determine user identity and platform access:

SignalSourceUsed for
JWT (access token)Cookie VAR_ACCESS_TOKEN, decoded in proxyRoute gating before /me runs
GET /meAPI, called from RSC and/or React QueryAuthoritative profile; drives cookie sync
Cookie VAR_PERMITTED_PLATFORMSJSON array, set client-side after /meWhen JWT has no derivable platforms, proxy still knows

Platforms

Defined in @numa/config/platforms:

PlatformPermission stringRoute prefix
ENERGYENERGY_APP/energy
OPERATIONSOPERATIONS_APP/operations
FINANCEFINANCE_APP/finance

Access is determined by exact match: a user can open /energy only if their permissions array contains ENERGY_APP.

Cookies

CookiePurpose
VAR_ACCESS_TOKENJWT access token (sent as Bearer on API calls)
VAR_REFRESH_TOKENRefresh token (used by Axios interceptor)
VAR_PERMITTED_PLATFORMSJSON TPlatform[], updated after every /me
VAR_SELECTED_LOCALELocale preference

proxy.ts runs on the edge and must decide whether a path is allowed before React renders. Calling /me from the edge on every request would add latency. The JWT alone often doesn’t list product permissions the same way /me does. So the app keeps a mirror: whatever /me last returned, derived to TPlatform[], stored in VAR_PERMITTED_PLATFORMS.

JWT decoding

File: utils/funcs/get-user-from-token.ts

  • platforms — derived from permissions / Permissions claims using derivePlatformsFromMe
  • primaryRole — optional explicit claim, else first permitted platform in PLATFORMS order
  1. getUserFromToken(token).platforms (from JWT permissions)
  2. If empty, parse VAR_PERMITTED_PLATFORMS cookie
  3. If still empty, return [] → user goes to shared profile, not a product dashboard

proxy.ts (edge routing)

Each app’s proxy.ts handles:

  1. Locale — paths without locale prefix are normalized
  2. Unauthenticated + private route → redirect to login
  3. Authenticated + public route → redirect to default home
  4. Authenticated + platform route → if user not allowed, redirect to default home
  5. Headers — sets x-pathname on forwarded request

Post-login redirect flow

After login, the cookie may not yet reflect /me permissions. Auth flows call resolvePostLoginPath(accessToken):

  1. Fetches /me with explicit Authorization: Bearer header
  2. Writes VAR_PERMITTED_PLATFORMS from derivePlatformsFromMe
  3. Returns the correct home path (or shared profile)
  4. Login hooks await this, then router.push to /{locale}${path}

Private app shell

Server (fetchUserEssentials):

  • Calls GET /me with Bearer token from cookie
  • If successful, wraps children in EssentialsProvider
  • If token exists but fetch fails, falls back to HydrateEssentialsFromClient

Client (useAppEssentials):

  • Runs useGetMeQuery unless already provided via EssentialsProvider
  • On every /me update, syncs the VAR_PERMITTED_PLATFORMS cookie

Cross-app authentication

All apps share cookies under the same parent domain (e.g., .numalogistics.com). When a user logs in:

  1. Auth tokens stored as cookies on the shared domain
  2. Auth app redirects to the user’s permitted platform
  3. Platform apps read auth cookies to verify access

OIDC multi-subdomain

When NEXT_PUBLIC_AUTH_COOKIE_DOMAIN is set:

  • Shared auth cookies become the cross-subdomain source of truth for OIDC user state
  • Stale per-tab OIDC state is cleared when shared cookies disappear
  • Periodic re-checks on focus/visibility

Environment

VariablePurpose
NEXT_PUBLIC_API_BASE_URLBase URL for API calls and server fetch
NEXT_PUBLIC_AUTH_COOKIE_DOMAINParent domain for shared auth cookies (omit on localhost)
NEXT_PUBLIC_GATEWAY_APP_URLGateway app URL for cross-app redirects
Last updated on