Skip to content

Auth

@stratal/framework integrates with Better Auth to provide authentication, session management, and a request-scoped AuthContext for accessing the authenticated user throughout your application.

Terminal window
yarn add @stratal/framework

Use AuthModule.forRootAsync() to configure authentication with async options:

import { Module } from 'stratal/module'
import { DI_TOKENS } from 'stratal/di'
import { CONFIG_TOKENS, type ConfigService } from 'stratal/config'
import { AuthModule } from '@stratal/framework/auth'
@Module({
imports: [
AuthModule.forRootAsync({
inject: [DI_TOKENS.Database, CONFIG_TOKENS.ConfigService],
useFactory: (db, config: ConfigService) => ({
database: db,
secret: config.get('AUTH_SECRET'),
baseURL: config.get('AUTH_BASE_URL'),
// ... other Better Auth options
}),
}),
],
})
export class AppModule {}

The factory receives resolved dependencies and returns a Better Auth options object. Refer to the Better Auth documentation for all available options.

Create a controller to proxy all authentication requests to Better Auth’s handler:

import { Controller, type IController, type RouterContext } from 'stratal/router'
import { inject } from 'stratal/di'
import { AUTH_SERVICE, type AuthService } from '@stratal/framework/auth'
@Controller('/api/auth')
export class AuthController implements IController {
constructor(@inject(AUTH_SERVICE) private readonly authService: AuthService) {}
async handle(ctx: RouterContext) {
return this.authService.auth.handler(ctx.c.req.raw)
}
}

This controller catches all requests under /api/auth/* and forwards them to Better Auth, which handles sign-up, sign-in, session management, and all other auth endpoints.

AuthContext is a request-scoped service that holds the authenticated user’s information for the current request. Inject it using DI_TOKENS.AuthContext:

import { Transient, inject, DI_TOKENS } from 'stratal/di'
import { AuthContext } from '@stratal/framework/context'
@Transient()
export class ProfileService {
constructor(
@inject(DI_TOKENS.AuthContext) private readonly auth: AuthContext,
) {}
async getProfile() {
const user = this.auth.requireUser()
return this.usersRepository.findById(user.id)
}
}
MethodReturn typeDescription
isAuthenticated()booleanWhether the current request has a valid session
getUser()AuthUser | undefinedThe authenticated user, or undefined
requireUser()AuthUserThe authenticated user. Throws UserNotAuthenticatedError if not authenticated
getUserId()string | undefinedThe authenticated user’s ID, or undefined
requireUserId()stringThe authenticated user’s ID. Throws if not authenticated
getRole()string | undefinedRaw role string from user.role (comma-separated for multiple roles)
getRoles()string[]Roles parsed into an array. Returns [] when the user has no role
getAuthInfo()AuthInfoThe full authentication context object ({ user }). Throws AuthError if no user is authenticated
clearAuthContext()voidClears the authentication state

AuthUser extends Better Auth’s base user with name made optional. Augment it via TypeScript module declaration to match whatever additionalFields or plugins your Better Auth config returns:

declare module '@stratal/framework/context' {
interface AuthUser {
firstName: string
lastName: string
role: string
}
}

Once augmented, auth.requireUser() and auth.getUser() return the typed shape, and auth.getRole() is typed as string.

The AuthService provides access to the underlying Better Auth instance for advanced operations like session management:

import { Transient, inject } from 'stratal/di'
import { AUTH_SERVICE, AuthService } from '@stratal/framework/auth'
@Transient()
export class SessionService {
constructor(
@inject(AUTH_SERVICE) private readonly authService: AuthService,
) {}
get auth() {
return this.authService.auth
}
}

The auth property returns the configured Better Auth instance with full access to its API.

The AuthModule automatically registers two middleware components:

  1. AuthContextMiddleware: Creates an AuthContext instance in the request-scoped container for every request.
  2. SessionVerificationMiddleware: Verifies the session token from the request and populates AuthContext with the authenticated user’s information.

These middleware run before your controller methods, so AuthContext is always available and populated when your code executes.

AuthModule translates Better Auth’s API errors into typed HttpException subclasses from @stratal/framework/auth. Stratal’s global error handler renders each one as a localised JSON response carrying the status code listed below, so you never have to inspect raw Better Auth error codes in your controllers.

Sensitive operations (for example deleting an account, changing a password, or revoking sessions) require a session that was authenticated recently. When the current session is older than Better Auth’s configured freshness window, the operation fails with a FreshSessionRequiredError.

ErrorHTTP status
FreshSessionRequiredError403

Every error below extends HttpException and is thrown when the corresponding Better Auth condition occurs during a request handled by your auth controller.

ErrorHTTP statusWhen
InvalidCredentialsError401Email and password combination is invalid
InvalidPasswordError401Supplied password is incorrect
SessionExpiredError401The session has expired
TokenExpiredError401A verification or reset token has expired
InvalidTokenError401A verification or reset token is invalid or has been used too many times
TokenRequiredError401A verification token is required but was not provided
FreshSessionRequiredError403The session is not fresh enough for a sensitive operation
EmailNotVerifiedError403The account’s email address has not been verified
InvalidOriginError403The request origin is not allowed
UserNotFoundError404No user matches the request
UserEmailNotFoundError404The user has no email on record
AccountNotFoundError404No linked account matches the request
CredentialAccountNotFoundError404No password (credential) account exists for the user
ProviderNotFoundError404The requested social provider is not configured
AccountAlreadyExistsError409An account already exists for the email
SocialAccountLinkedError409The social account is already linked to a user
CannotUnlinkLastAccountError409The user’s last remaining account cannot be unlinked
UserAlreadyHasPasswordError409The user already has a password set
EmailAlreadyVerifiedError409The email address is already verified
InvalidEmailError422The email address is malformed
PasswordTooShortError422The password is shorter than the minimum length
PasswordTooLongError422The password exceeds the maximum length
EmailCannotBeUpdatedError422The email address cannot be changed
EmailMismatchError422The supplied email does not match the expected one
IdTokenNotSupportedError422The provider does not support ID-token sign-in
InvalidCallbackUrlError422A callback or redirect URL is not allowed
AuthValidationFailedError422The request failed Better Auth’s field validation
  • Auth Guard for protecting routes with authentication checks.
  • Access Control for role-based permissions on top of auth.
  • Database for configuring the database used by Better Auth.