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):
- GraphQL at
/{prefix}/graphql— code-first (autoSchemaFile: true, no committedschema.gql). All reads and any write without a file. Only some modules are in the GraphQLinclude: admins, catalogs, users, orders, promotions, journals. - REST controllers — create/update that uploads a file (multipart
FormData), for examplePOST catalogs/products. The admin frontend uses axios for these; the storefront rarely needs them. - 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 / CMS | Storefront / member | |
|---|---|---|
| Login op | login, loginWithGoogle | loginAsMember |
| Strategy | jwt (JWT_ACCESS_TOKEN_SECRET_KEY), jwt-refresh | jwt-member (JWT_ACCESS_TOKEN_STOREFRONT_SECRET_KEY), jwt-member-refresh |
| Guards | JwtAuthGuard, LastActivityGuard, RoleAbilityGuard | JwtMemberAuthGuard |
| Extra | Google OAuth (passport-google-oauth20); idle auto-logout | email 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 roles ↔ roles_menus ↔ menus.
- Backend enforces it with
@RoleCheck({ slug, action })+RoleAbilityGuard(src/admins/guards/role-check.guard.ts), which checks the admin'srole.menusfor that menuslugand 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 backendmequery, 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 thejwtcallback callsfrontRefreshToken; 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.