Authentication And Authorization
Wavemap uses username/password authentication with JWT-backed sessions and role-derived permissions.
This page is the durable developer reference for auth architecture and implementation patterns. Historical rollout notes,
test proof, and unresolved browser-runtime issues live in working-notes/AUTH_AND_AUTHORIZATION.md and
working-notes/AUTH_PUSH_ROADMAP.md.
Core Invariants
Section titled “Core Invariants”- The backend is authoritative for every protected API request and mutation.
- Roles are persisted; permissions are derived from roles at runtime.
- Permission checks should use permission codes, not hard-coded role allowlists, whenever the shared maps can express the rule.
- Access tokens are short-lived and kept in client memory.
- Refresh tokens are longer-lived, stored in an HttpOnly cookie, and rotated by the backend.
- Frontend auth state is TanStack Query state plus a small in-memory token module, not React context.
- Frontend route guards and conditional rendering improve navigation and UX, but they do not replace backend enforcement.
Source Map
Section titled “Source Map”| Area | Primary Source |
|---|---|
| Role and permission codes | packages/api-contracts/src/constants/auth.ts |
| User and auth DTOs | packages/api-contracts/src/types/auth.ts |
| Role-to-permission map | packages/api-contracts/src/utils/rolesPermissionsMap.ts |
| Backend route map | apps/wavemap-back-end/src/utils/routesPermissionsMap.ts |
| Backend auth middleware | apps/wavemap-back-end/src/middleware/isAuthenticated.ts, getCurrentUser.ts, isAuthorized.ts |
| Backend entity helpers | apps/wavemap-back-end/src/utils/entityAuthorization.ts |
| Backend auth handlers | apps/wavemap-back-end/src/handlers/auth/ |
| Frontend auth API layer | apps/wavemap-front-end/src/api/axios/auth.ts |
| Frontend auth hooks | apps/wavemap-front-end/src/api/queries/auth/, apps/wavemap-front-end/src/api/mutations/auth/ |
| Frontend route access | apps/wavemap-front-end/src/utils/auth/routeAccess.ts |
| Frontend permission UI | apps/wavemap-front-end/src/utils/auth/permissions.ts, entityAuthorization.ts, RequiresPermission.tsx |
How Auth Changes Flow
Section titled “How Auth Changes Flow”Auth changes usually cross the package/app boundary, so start with the contract that downstream code should share:
- Put role codes, permission codes, auth DTOs, and frontend route constants in
@wavemap/api-contracts. - Derive role capability from
rolesPermissionsMap; do not copy role allowlists into app code. - Protect backend routes through
routesPermissionsMapand the auth middleware chain. - Wire frontend API functions and query/mutation hooks against the shared DTOs.
- Add frontend route-access rules only for pages that require a session or page-level permission redirect.
- Gate public-page controls with permission utilities instead of making the whole page private.
- Prove the change at the lowest useful test layer before adding browser coverage.
The backend owns authority. The frontend owns navigation, intent preservation, and conditional affordances. Shared contracts keep the two sides from inventing separate names for the same capability.
Session Model
Section titled “Session Model”Access tokens are short-lived JWTs returned by login and refresh responses. The frontend stores them through
tokenOps.ts, a thin wrapper over the JWTManager singleton. They are sent to the API as bearer tokens and are not
persisted in localStorage or sessionStorage.
Refresh tokens are stored in an HttpOnly cookie. The backend rotates the refresh cookie when the refresh endpoint is used and issues a fresh access token. Refresh-token revocation is driven by the user’s refresh-token version, which lets the backend invalidate older refresh tokens without storing every issued token.
The frontend source of truth for user identity is useCurrentUser():
- It uses the stable query key exported as
QUERY_KEY__CURRENT_USER. - Its query function calls
refreshAccessToken()to attempt session restore from the refresh-token cookie. - On success, it stores the returned access token through
setAccessToken()and returns the authenticated user DTO. - On failure, it returns
null; an unauthenticated user is a settled state, not a thrown query error. - It uses
staleTime: Number.POSITIVE_INFINITYandretry: false.
Login and logout mutate this same query cache. useLogin() stores the access token and sets the current user. useLogout()
optimistically clears the access token and current-user cache, rolls back on failure, and broadcasts logout to other tabs.
Backend Enforcement
Section titled “Backend Enforcement”Protected backend handlers use the auth middleware chain:
isAuthenticated -> getCurrentUser -> isAuthorizedisAuthenticated reads the bearer token from the Authorization header and places it in Hono context. getCurrentUser
verifies the access token, loads the current user from the database, and puts the flattened role list into request
context. generateAuthorizationMiddleware(routesPermissionsMap, rolesPermissionsMap) derives permissions from the
user’s roles and rejects requests missing the required permissions for the matched route.
When adding or changing a protected backend route:
- Add or reuse permission codes in
@wavemap/api-contracts. - Make sure the correct roles receive those permissions in
rolesPermissionsMap. - Add the route’s required permissions to
routesPermissionsMap. - Apply
isAuthenticated,getCurrentUser, andisAuthorizedto protected handler arrays. - Keep public read routes public when no session is required.
An empty permission list means “no additional permission requirement.” It does not by itself decide whether a route requires a session; the handler middleware chain decides that.
Entity Authorization Helpers
Section titled “Entity Authorization Helpers”Entity mutation handlers may also call backend authorization helpers after they have resolved the concrete entity rows:
assertCanMutateEntity(...)assertCanManageEntityMedia(...)assertCanAssociateEntity(...)
Today these helpers delegate to the same role-derived permission map used by route authorization. They are intentionally lightweight stubs, not a second ownership model. Their purpose is to give handlers a stable place to pass actor and entity references now, so future ownership, organization, profile-control, or transfer rules can be added without rewriting each handler signature.
Use them when a handler has a meaningful entity reference available. Keep route-level middleware in place as the first authorization gate.
Roles And Permissions
Section titled “Roles And Permissions”Roles are database assignments. Permissions are code-level capabilities derived from the shared role map.
| Role | Intended Scope |
|---|---|
user | Standard authenticated user access. |
admin | Content creation and update workflows for artists, events, series, and venues. |
root | User management, deletion, admin control, and refresh-token revocation. |
dev-debug | Development-only role with root-equivalent permissions. |
Permission codes are kebab-case verb/resource strings such as read-artist, create-event, update-user, and
revoke-refresh-token. Relationship and media permissions should stay separate from profile/content editing permissions
when the action manages an association or asset collection instead of the entity’s scalar fields.
Artists are the current worked example for this grammar:
update-artist-profilecontrols artist scalar/profile fields and profile links.manage-artist-mediacontrols artist media upload, deletion, ordering, and synchronization.manage-artist-event-relationshipscontrols artist-event association routes.delete-artistcontrols artist deletion.
Keep role names and permission codes aligned with @wavemap/api-contracts; do not copy older uppercase, underscore, or
legacy broad spellings from historical notes.
Frontend Auth API And Hooks
Section titled “Frontend Auth API And Hooks”Frontend auth calls are plain axios functions in src/api/axios/auth.ts, with request and response types from
@wavemap/api-contracts.
Current auth functions include:
adminRegistration(args)registration(args)login(args)logout()refreshAccessToken()requestPasswordReset(args)resetPassword(args)changePassword(args)verifyAccount(args)resendAccountVerification(args)
Each user-initiated auth action gets a dedicated mutation hook under src/api/mutations/auth/. This keeps auth actions
consistent with the rest of the frontend API layer. The hooks should own cache side effects instead of pushing them into
page components.
Use these cache rules:
- Login sets the access token and
currentUser. - Logout clears the access token and
currentUser; preserve rollback behavior for failed logout. - Registration and admin registration do not create a logged-in session.
- Password reset, account verification, resend verification, and change-password do not rewrite
currentUserunless the response contract changes to require it.
The axios response interceptor handles expired-token retry by calling refreshAccessToken() and retrying the original
request. Non-refreshable 401s are left to the calling view so auth pages can render local error states. Forbidden
responses are treated as authorization failures and redirect through the app error path.
Frontend Route Protection
Section titled “Frontend Route Protection”Frontend route protection is deliberately split across middleware and client hooks:
| Layer | Can Verify | Cannot Verify | Responsibility |
|---|---|---|---|
| Backend middleware | Session validity and DB-backed permissions | — | Authoritative API enforcement. |
| Next.js middleware | Refresh-token cookie presence | Permissions, roles, token validity | Hard redirect for unauthenticated full-page loads. |
useRequireAuth() | Current user and derived permissions after hydration | Backend mutation authority | Client-side route guard and permission redirect. |
| Permission hooks/components | Current user and derived permissions after hydration | Backend mutation authority | Conditional controls, nav items, and page sections. |
routeAccess.ts is the centralized frontend route-access config. Each rule names a route constant, declares
requiresAuth: true, and may include required permission codes. Dynamic route templates are normalized to static prefixes
for middleware matching.
The Next.js authGuard consumes GUARDED_ROUTE_PREFIXES from routeAccess.ts. It only answers “does this request need a
session cookie?” If a guarded route is requested without the refresh-token cookie, the middleware redirects to localized
login and preserves the original path in a redirect query parameter.
Page components that require auth should also call useRequireAuth():
const { isAuthorized } = useRequireAuth()
if (!isAuthorized) return nullThis covers client-side navigations where middleware may not run and handles permission-based redirects to
/access-denied.
Routes that are public but contain permission-gated controls should not be added to ROUTE_ACCESS_RULES. Gate the
specific controls instead.
Route Access Alignment
Section titled “Route Access Alignment”Frontend route access is a UX and navigation contract, not the backend permission system.
Keep these rules aligned:
- A protected frontend page should have a
ROUTE_ACCESS_RULESentry and calluseRequireAuth()in the page component or route-owned inner component. - A route that only contains a protected button, link, or panel should stay public and use
<RequiresPermission>oruseCurrentUserPermissions(). - Dynamic frontend routes should use shared route constants so
routeAccess.tscan normalize route templates to guarded prefixes for middleware. - Middleware should only answer “does this route need a session cookie?” It cannot decide whether a user still exists, whether the refresh token is valid, or whether the user has the right permissions.
- Permission redirects belong in
useRequireAuth()because that hook can read the current user after hydration. - Backend API protection must still be configured even when the only visible frontend entry point is permission-gated.
For example, a public artist details page can show profile edit controls only to users with update-artist-profile,
media recovery controls only to users with manage-artist-media, and delete controls only to users with delete-artist.
The page itself should stay public, while the artist mutation API routes remain backend-protected.
Permission UI Patterns
Section titled “Permission UI Patterns”Use useCurrentUserPermissions() for imperative checks:
const { hasPermission } = useCurrentUserPermissions()
if (hasPermission(PERMISSION_CODE__UPDATE_ARTIST_PROFILE)) { // enable update affordance}Use <RequiresPermission> for JSX gating:
<RequiresPermission permission={PERMISSION_CODE__UPDATE_ARTIST_PROFILE}> <EditArtistButton /></RequiresPermission>The wrapper renders children when the user holds the permission and fallback otherwise. Its default fallback is
null.
Use the entity-shaped helpers from useCurrentUserPermissions() when a control maps to a concrete entity action:
const { canManageEntityMedia } = useCurrentUserPermissions()
const canShowMediaTools = canManageEntityMedia({ requiredPermission: PERMISSION_CODE__MANAGE_ARTIST_MEDIA, entity: { entityType: "artist", publicId: artistPublicId },})These helpers currently delegate to the same role-derived permission checks as hasPermission(...). They are not an
authorization boundary and should not be trusted by the backend. Their value is shape: UI code can pass the actor’s
intended operation and entity reference now, leaving room for future owner/controller-aware affordances without pushing
that logic into every component.
Prefer permission checks over role checks. For example, an “Add Artist” control should check create-artist; it should
not manually ask whether the user is an admin or root.
Registration And Account Flows
Section titled “Registration And Account Flows”Standard registration and invited admin registration are separate frontend flows with separate request contracts.
- Standard registration uses
TRegistrationArgsand does not require a registration code. - Admin registration uses
TAdminRegistrationArgsand requires a registration code. - Admin registration links may prefill the code from URL params, but the field stays editable.
- Registration requests carry a best-effort locale from the browser/app context; the backend normalizes it for the new
user’s initial
defaultLanguageand account verification email. - A completed registration does not create a session; users still verify their account before login.
Account verification, resend verification, password reset, and admin invitation emails are backend email flows. Their
templates live in the backend email folder, while request and response DTOs live in @wavemap/api-contracts.
Admin And User Management Boundaries
Section titled “Admin And User Management Boundaries”Admin and root capabilities are intentionally modeled as permissions, not as UI assumptions about role names.
Current boundaries:
admincan create and update content surfaces such as artists, events, event series, and venues.admincan currently update artist profiles, manage artist media, and manage artist-event relationships; those remain separate permissions even though the same role receives them today.rootowns user-management and admin-control actions such as updating users, deleting users, enabling/disabling admin privileges, enabling/disabling user accounts, sending admin invitations, and revoking refresh tokens.dev-debugis a development-only root-equivalent role. It should not become a product-facing role or admin UX label.- Admin registration is invite-oriented. The admin-registration route uses a registration code, but it still creates a normal verified-account path and does not create a logged-in session.
- Role and permission display, editing, auditing, and localization should wait for concrete admin/permissions UI surfaces. Do not turn internal permission codes into public copy before the product surface exists.
When a future admin page appears, gate page access with the narrowest permission that matches the page’s job and gate individual destructive controls separately when needed.
Adding Protected Features
Section titled “Adding Protected Features”When adding a protected feature, work from the authority outward:
- Model any new permission or route contract in
@wavemap/api-contracts. - Update
rolesPermissionsMaponly when role capability changes. - Update backend
routesPermissionsMapand handler middleware for protected API routes. - Add or update typed frontend axios functions and mutation/query hooks.
- Add a
routeAccess.tsrule for protected frontend pages. - Call
useRequireAuth()inside protected page components. - Gate individual public-page controls with
<RequiresPermission>oruseCurrentUserPermissions(). - Use entity-shaped frontend helpers for entity-specific controls that may later depend on ownership or profile control.
- Add targeted backend request tests, frontend hook/component tests, and route-guard tests for the changed behavior.
Testing Auth Changes
Section titled “Testing Auth Changes”Choose the smallest layer that proves the auth risk:
- Use backend mocked-request tests for route wiring, request validation, API envelopes, unauthenticated responses, and unauthorized responses.
- Use DB-backed backend tests when token rotation, refresh-token revocation, password hashing, role joins, invitation codes, or persisted account state matters.
- Use front-end integration tests for auth pages, query/mutation cache effects, route-access rules, permission utilities, and protected page guards.
- Use Playwright only for browser-specific confidence such as redirect preservation, full-page middleware behavior, focus/navigation behavior after login, or high-value auth journeys.
The current smoke posture keeps broad auth lifecycle Playwright coverage out of the default gate while browser-safe API paths, built-runtime timing, and selector stability continue to mature. See Testing for the browser-runtime contract and smoke-lane posture.
Current Boundaries
Section titled “Current Boundaries”Keep this page focused on durable architecture and implementation conventions.
Keep working notes for:
- Historical auth-push sequencing and proof history.
- Open Playwright runtime hardening work.
- Browser-safe API path and selector-stability notes.
- Product surfaces that are still placeholders, such as broader profile-editing and MFA flows.
- Run-specific evidence, logs, and exploratory notes.
Related Pages
Section titled “Related Pages”- API Contracts for shared role, permission, route, DTO, and envelope boundaries.
- Feature Slice Workflow for carrying protected features across contracts, apps, tests, and docs.
- Front End Components for page wrappers, workflow hooks, and permission-aware controls.
- Testing for auth test-layer selection and browser runtime scope.
- Local Development Runtime for the local backend/frontend setup used by auth browser checks.