Skip to Content
Apps & PortalsAuthAuthentication & Authorization

Authentication & Authorization

Leadmetrics uses a unified JWT auth system across all three web portals (Dashboard, DM Portal, Manage) and the mobile app. The Fastify API is the single identity issuer. Each portal validates tokens independently using shared middleware from @leadmetrics/middleware.

Related: API Auth Endpoints | Multi-tenancy


How It Works

┌─────────────────────────────────────────────────────────┐ │ Fastify API /auth/v1 │ │ │ │ POST /auth/v1/login → access + refresh tokens │ │ POST /auth/v1/refresh → new access token │ │ POST /auth/v1/forgot-password → send reset email │ │ POST /auth/v1/reset-password → apply new password │ └─────────────────────────────────────────────────────────┘ ▲ ▲ ▲ Dashboard :3000 DM Portal :3002 Manage :3001 (Next.js) (Next.js) (Next.js) Each portal runs its own Next.js middleware that: 1. Reads the portal's httpOnly access-token cookie 2. Verifies the JWT with JWT_SECRET (HS256) 3. On 401: attempts a silent refresh via /auth/v1/refresh 4. On refresh failure: redirects to /login

Token Model

TokenLifetimeStoragePurpose
Access token15 minuteshttpOnly cookie per portalAuthorize API requests; verified locally (no DB hit)
Refresh token7 dayshttpOnly cookie per portalObtain a new access token via /auth/v1/refresh

Signing: HS256 (HMAC-SHA256) with JWT_SECRET. All portals and the API share the same secret.

Access token payload:

{ sub: string; // user ID (ULID) role: string; // 'admin' | 'member' | 'reviewer' | 'super_admin' appAccess: string[]; // apps this user is allowed to access tenantId?: string; // absent for super_admin name?: string; email?: string; iat: number; exp: number; // iat + 15 min }

Each portal uses its own cookie names so sessions are completely isolated:

PortalAccess token cookieRefresh token cookie
Dashboarddashboard_access_tokendashboard_refresh_token
DM Portaldm_access_tokendm_refresh_token
Managemanage_access_tokenmanage_refresh_token

Cookie names are exported as PORTAL_COOKIES from @leadmetrics/middleware.


Shared Middleware Package

@leadmetrics/middleware provides the shared auth primitives:

ExportPurpose
createJwtAuthMiddleware(options)Factory that returns a Next.js middleware function with JWT verify + silent refresh
verifyJwt(token)Verify a JWT and return the payload (used in server components / server actions)
setPortalAuthCookies(cookieSetter, tokens, cookieNames)Set access + refresh cookies after login
clearPortalAuthCookies(cookieDeleter, cookieNames)Delete both cookies on logout
PORTAL_COOKIESConstant with cookie name pairs for each portal

Each portal’s middleware.ts is a thin wrapper:

// apps/dashboard/src/middleware.ts import { createJwtAuthMiddleware } from "@leadmetrics/middleware"; export const middleware = createJwtAuthMiddleware({ accessTokenCookie: "dashboard_access_token", refreshTokenCookie: "dashboard_refresh_token", apiUrl: process.env.API_URL ?? "http://localhost:3003", publicPaths: ["/login", "/signup", "/forgot-password", "/reset-password", "/api/auth"], }); export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };

Per-Portal Auth Helpers

Each portal has:

FilePurpose
src/middleware.tsNext.js middleware (JWT verify + silent refresh)
src/lib/auth-cookies.tssetAuthCookies() / clearAuthCookies() — thin wrappers over shared helpers
src/lib/server-auth.tsrequireSession() / requireAuth() — server component / action guards
src/app/actions/auth.tsloginAction, logoutAction, forgotPasswordAction, resetPasswordAction

Server Auth Guards

Use these at the top of every server component, layout, and server action that requires authentication.

PortalFunctionRedirects on failure
DashboardrequireSession() from @/lib/server-auth/login
DM PortalrequireAuth() from @/lib/server-auth/login
ManagerequireSuperAdmin() from @/lib/server-auth/login

For API routes that must return JSON (not redirect), use getSession() instead — it returns null on failure.


Roles & Permissions

RolePortalsScope
adminDashboard, MobileSingle tenant — full access
memberDashboard, MobileSingle tenant — read + limited write
reviewerDM PortalAssigned tenants — approval + editorial
super_adminManageAll tenants — platform-wide

Roles are embedded in the JWT. Role checks are enforced per API route in the Fastify API.


Login Flow

1. User submits email + password on /login 2. loginAction() calls POST /auth/v1/login { email, password, app: "dashboard" } 3. API verifies credentials, checks app access, returns { accessToken, refreshToken, user } 4. setAuthCookies() sets both tokens as httpOnly cookies 5. User is redirected to /overview

Silent Refresh

1. Request arrives; middleware reads access token cookie 2. JWT verify fails (expired) 3. Middleware reads refresh token cookie 4. POST /auth/v1/refresh { refreshToken } 5. API returns new accessToken 6. Middleware sets new access token cookie, continues with request 7. If refresh fails → redirect to /login

Password Reset Flow

1. User submits email on /forgot-password 2. forgotPasswordAction() calls POST /auth/v1/forgot-password { email } 3. API sends reset email with a one-time token (1-hour expiry) 4. User clicks link → /reset-password?token=<token> 5. resetPasswordAction() calls POST /auth/v1/reset-password { token, newPassword } 6. API validates token, updates password, invalidates token

Features Not Yet Implemented

The following are planned but not built:

  • Social login (Google, LinkedIn, Microsoft, Apple) — see Social Providers
  • OAuth 2.0 / OIDC server (external client integrations) — see OAuth Server
  • Two-factor authentication (TOTP)
  • Biometric auth (mobile)
  • Email verification on registration

© 2026 Leadmetrics — Internal use only