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.
Install dependencies
Section titled “Install dependencies”yarn add @stratal/frameworkConfigure AuthModule
Section titled “Configure AuthModule”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.
Auth controller
Section titled “Auth controller”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
Section titled “AuthContext”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) }}AuthContext API
Section titled “AuthContext API”| Method | Return type | Description |
|---|---|---|
isAuthenticated() | boolean | Whether the current request has a valid session |
getUser() | AuthUser | undefined | The authenticated user, or undefined |
requireUser() | AuthUser | The authenticated user. Throws UserNotAuthenticatedError if not authenticated |
getUserId() | string | undefined | The authenticated user’s ID, or undefined |
requireUserId() | string | The authenticated user’s ID. Throws if not authenticated |
getRole() | string | undefined | Raw 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() | AuthInfo | The full authentication context object ({ user }). Throws AuthError if no user is authenticated |
clearAuthContext() | void | Clears the authentication state |
Augmenting AuthUser
Section titled “Augmenting AuthUser”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.
AuthService
Section titled “AuthService”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.
Middleware pipeline
Section titled “Middleware pipeline”The AuthModule automatically registers two middleware components:
- AuthContextMiddleware: Creates an
AuthContextinstance in the request-scoped container for every request. - SessionVerificationMiddleware: Verifies the session token from the request and populates
AuthContextwith the authenticated user’s information.
These middleware run before your controller methods, so AuthContext is always available and populated when your code executes.
Auth errors
Section titled “Auth errors”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.
Fresh-session requirement
Section titled “Fresh-session requirement”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.
| Error | HTTP status |
|---|---|
FreshSessionRequiredError | 403 |
Full error reference
Section titled “Full error reference”Every error below extends HttpException and is thrown when the corresponding Better Auth condition occurs during a request handled by your auth controller.
| Error | HTTP status | When |
|---|---|---|
InvalidCredentialsError | 401 | Email and password combination is invalid |
InvalidPasswordError | 401 | Supplied password is incorrect |
SessionExpiredError | 401 | The session has expired |
TokenExpiredError | 401 | A verification or reset token has expired |
InvalidTokenError | 401 | A verification or reset token is invalid or has been used too many times |
TokenRequiredError | 401 | A verification token is required but was not provided |
FreshSessionRequiredError | 403 | The session is not fresh enough for a sensitive operation |
EmailNotVerifiedError | 403 | The account’s email address has not been verified |
InvalidOriginError | 403 | The request origin is not allowed |
UserNotFoundError | 404 | No user matches the request |
UserEmailNotFoundError | 404 | The user has no email on record |
AccountNotFoundError | 404 | No linked account matches the request |
CredentialAccountNotFoundError | 404 | No password (credential) account exists for the user |
ProviderNotFoundError | 404 | The requested social provider is not configured |
AccountAlreadyExistsError | 409 | An account already exists for the email |
SocialAccountLinkedError | 409 | The social account is already linked to a user |
CannotUnlinkLastAccountError | 409 | The user’s last remaining account cannot be unlinked |
UserAlreadyHasPasswordError | 409 | The user already has a password set |
EmailAlreadyVerifiedError | 409 | The email address is already verified |
InvalidEmailError | 422 | The email address is malformed |
PasswordTooShortError | 422 | The password is shorter than the minimum length |
PasswordTooLongError | 422 | The password exceeds the maximum length |
EmailCannotBeUpdatedError | 422 | The email address cannot be changed |
EmailMismatchError | 422 | The supplied email does not match the expected one |
IdTokenNotSupportedError | 422 | The provider does not support ID-token sign-in |
InvalidCallbackUrlError | 422 | A callback or redirect URL is not allowed |
AuthValidationFailedError | 422 | The request failed Better Auth’s field validation |
Next steps
Section titled “Next steps”- 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.