Skip to content

API and auth

How the frontends reach the backend, and how the backend decides who may do what.

How this page was verified

Read on 2026-09-15 from the backend GraphQL config, main.ts, the guards and strategies, and both frontends' Apollo/axios setup.

Three ways in

The backend exposes three surfaces, all under the global prefix APP_PREFIX (be-api locally, gapi in prod):

  1. GraphQL at /{prefix}/graphql — code-first (autoSchemaFile: true, no committed schema.gql). All reads and any write without a file. Only some modules are in the GraphQL include: admins, catalogs, users, orders, promotions, journals.
  2. REST controllers — create/update that uploads a file (multipart FormData), for example POST catalogs/products. The admin frontend uses axios for these; the storefront rarely needs them.
  3. REST webhooks/* — inbound callbacks from DOKU, Biteship and the WMS. Not GraphQL. See Third parties.

Operation naming: storefront GraphQL is prefixed front* (frontProducts, frontMemberCart); admin/CMS GraphQL is plain (products, createVoucher).

There is one global ValidationPipe (custom exceptionFactory that flattens class-validator errors) and no global exception filter, interceptor, or guard. GraphQL has a custom formatError for the add-to-cart error (returns message, code, and an optional cartId).

Two auth realms

Admin users and storefront members are separate JWT realms, each with its own secrets and strategies. Guards are applied per-resolver — there is no global guard.

Admin / CMSStorefront / member
Login oplogin, loginWithGoogleloginAsMember
Strategyjwt (JWT_ACCESS_TOKEN_SECRET_KEY), jwt-refreshjwt-member (JWT_ACCESS_TOKEN_STOREFRONT_SECRET_KEY), jwt-member-refresh
GuardsJwtAuthGuard, LastActivityGuard, RoleAbilityGuardJwtMemberAuthGuard
ExtraGoogle OAuth (passport-google-oauth20); idle auto-logoutemail verification on register; passwords bcrypt

The typical admin resolver stacks all three guards: @UseGuards(JwtAuthGuard, LastActivityGuard, RoleAbilityGuard). @Public() bypasses them (login, reset-password, storefront-facing homepage). LastActivityGuard enforces idle auto-logout after CMS_AUTO_LOGOUT_MAX_INACTIVE_SECONDS (default 1800).

RBAC (admin only)

Roles carry a set of menus, each with five abilities: view, add, update, delete, export. The model is rolesroles_menusmenus.

  • Backend enforces it with @RoleCheck({ slug, action }) + RoleAbilityGuard (src/admins/guards/role-check.guard.ts), which checks the admin's role.menus for that menu slug and ability.
  • Admin frontend mirrors it client-side with useAccess(slug, action) (src/hooks/useAccess.tsx): it hides sidebar items and redirects denied pages to /404. Permissions come from the backend me query, not hard-coded.

Members have no role/ability layer — a member is authenticated or not.

How the frontends send the token

Both frontends store the token in the NextAuth JWT session, not in localStorage, and send it as Authorization: Bearer <token> on every request.

  • Storefront — NextAuth Credentials provider → loginAsMember. The Apollo link and the axios client both read the session and attach the bearer. On an expired access token the jwt callback calls frontRefreshToken; on failure the client forces sign-out to /auth/signin?autologout=true.
  • Admin — NextAuth Credentials + Google. The Apollo link attaches the bearer; the axios client (src/lib/http.ts) does the same for uploads. It auto-logs-out on the backend messages "no activity" and "Access token expired".

The admin GraphQL client uses cache: "no-store", so it does not rely on the Apollo cache.