Authentication
CruzJS provides a complete authentication system built on session tokens stored in D1, JWT access tokens for stateless API calls, and refresh tokens for token rotation. The AuthService in @cruzjs/core handles registration, login, email verification, and password reset flows.
Architecture overview
Section titled “Architecture overview”Authentication uses a dual-token strategy:
- Session tokens — Long-lived (30 days), stored in both KV (fast lookup) and D1 (audit trail). Used for server-side session validation.
- JWT access tokens — Short-lived (15 minutes), stateless. Used for API calls where you want to avoid a database round-trip.
- Refresh tokens — Long-lived (30 days), stored hashed in D1. Used to obtain new access tokens without re-authentication.
All tokens are generated using crypto.randomBytes(32) and session/refresh tokens are SHA-256 hashed before database storage.
Registration flow
Section titled “Registration flow”The auth.register tRPC mutation creates a new identity, dispatches an IdentityCreatedEvent (which your app listens to for creating a user profile), queues a verification email, and returns a session.
// Client-side registrationconst result = await trpc.auth.register.mutate({ email: 'user@example.com', password: 'SecurePass1', name: 'Jane Doe',});
// result.session.token -- store this for subsequent requests// result.user.id -- the new user's identity IDPassword requirements: Minimum 8 characters, at least one uppercase letter, one lowercase letter, and one number. Passwords are hashed with bcrypt (configurable rounds via config.auth.bcryptRounds, default 10).
Gated registration with invite codes
Section titled “Gated registration with invite codes”Set the REGISTRATION_INVITE_CODE environment variable to require an invite code during registration. When set, the inviteCode field in the registration input must match:
const result = await trpc.auth.register.mutate({ email: 'user@example.com', password: 'SecurePass1', name: 'Jane Doe', inviteCode: 'my-secret-code', // must match REGISTRATION_INVITE_CODE});If REGISTRATION_INVITE_CODE is not set, registration is open to everyone.
Login flow
Section titled “Login flow”The auth.login mutation validates credentials and creates a new session:
const result = await trpc.auth.login.mutate({ email: 'user@example.com', password: 'SecurePass1',});
// result.session.token -- session token (30-day TTL)// result.session.expiresAt -- ISO string expiry timestamp// result.user.emailVerified -- null if not yet verifiedLogin checks:
- Identity exists with matching email
- Account is not banned (
isBannedflag) - Identity has a password (OAuth-only accounts cannot use password login)
- Password matches bcrypt hash
Session creation
Section titled “Session creation”On both registration and login, SessionService.createSession() is called:
const session = await sessionService.createSession({ userId: identity.id, currentOrgId: null, // org context set later by Pro layer userAgent, // from request headers ipAddress, // from x-forwarded-for});The session token is stored in KV for fast access and in D1 for persistence. The raw token is returned to the client; only the SHA-256 hash is stored server-side.
JWT access tokens
Section titled “JWT access tokens”For stateless API authentication, use the auth.refreshToken mutation to exchange a refresh token for a short-lived JWT:
const tokens = await trpc.auth.refreshToken.mutate({ refreshToken: storedRefreshToken,});
// tokens.accessToken -- JWT, 15-minute expiry// tokens.refreshToken -- new refresh token (rotation)// tokens.expiresIn -- 900 (seconds)The JWT payload contains { userId, exp, iat } and is signed with the JWT_SECRET environment variable. On each refresh, the old refresh token is revoked and a new one is issued (token rotation).
Verifying access tokens
Section titled “Verifying access tokens”import { TokenService } from '@cruzjs/core/auth/token.service';
const tokenService = container.get<TokenService>(TokenService);const payload = tokenService.verifyAccessToken(jwt);// payload.userId -- the authenticated user ID// Returns null if token is invalid or expiredAuth router endpoints
Section titled “Auth router endpoints”All auth endpoints are tRPC mutations/queries on the auth router:
| Endpoint | Type | Auth | Description |
|---|---|---|---|
auth.register | mutation | public | Create account, returns session |
auth.login | mutation | public | Authenticate, returns session |
auth.logout | mutation | protected | Destroy current session |
auth.session | query | protected | Get current session + user + orgs |
auth.verifyEmail | mutation | public | Verify email with token |
auth.requestPasswordReset | mutation | public | Send password reset email |
auth.resetPassword | mutation | public | Reset password with token |
auth.refreshToken | mutation | public | Exchange refresh token for new JWT |
Logout
Section titled “Logout”The auth.logout mutation extracts the session token from the Authorization: Bearer <token> header and deletes it from both KV and D1:
await trpc.auth.logout.mutate();Session query
Section titled “Session query”The auth.session query returns the full authenticated context including user profile data, current org, and all org memberships with roles:
const data = await trpc.auth.session.query();
// data.user -- { id, email, name, emailVerified, avatarUrl, createdAt }// data.session -- { userId, currentOrgId, expiresAt }// data.organizations -- [{ id, name, slug, avatarUrl, role, isCurrent }]This endpoint also triggers session refresh if the session is within the refresh threshold (7 days remaining by default).
Environment variables
Section titled “Environment variables”| Variable | Required | Description |
|---|---|---|
JWT_SECRET | Yes | Secret key for signing JWT access tokens |
APP_URL | Yes | Base URL for verification/reset email links |
REGISTRATION_INVITE_CODE | No | If set, required during registration |
Configuration
Section titled “Configuration”Session and auth settings can be customized in cruz.config.ts:
export default defineConfig({ auth: { bcryptRounds: 12, // default: 10 passwordResetTokenExpiryHours: 48, // default: 24 }, session: { ttlSeconds: 60 * 24 * 60 * 60, // 60 days, default: 30 days refreshThresholdSeconds: 14 * 24 * 60 * 60, // 14 days, default: 7 days },});