Skip to content

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.

Authentication uses a dual-token strategy:

  1. Session tokens — Long-lived (30 days), stored in both KV (fast lookup) and D1 (audit trail). Used for server-side session validation.
  2. JWT access tokens — Short-lived (15 minutes), stateless. Used for API calls where you want to avoid a database round-trip.
  3. 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.

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 registration
const 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 ID

Password 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).

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.

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 verified

Login checks:

  • Identity exists with matching email
  • Account is not banned (isBanned flag)
  • Identity has a password (OAuth-only accounts cannot use password login)
  • Password matches bcrypt hash

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.

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).

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 expired

All auth endpoints are tRPC mutations/queries on the auth router:

EndpointTypeAuthDescription
auth.registermutationpublicCreate account, returns session
auth.loginmutationpublicAuthenticate, returns session
auth.logoutmutationprotectedDestroy current session
auth.sessionqueryprotectedGet current session + user + orgs
auth.verifyEmailmutationpublicVerify email with token
auth.requestPasswordResetmutationpublicSend password reset email
auth.resetPasswordmutationpublicReset password with token
auth.refreshTokenmutationpublicExchange refresh token for new JWT

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();

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).

VariableRequiredDescription
JWT_SECRETYesSecret key for signing JWT access tokens
APP_URLYesBase URL for verification/reset email links
REGISTRATION_INVITE_CODENoIf set, required during registration

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
},
});