@anarchitects/auth-nest
NestJS services, controllers, and infrastructure for the Anarchitecture authentication domain. This package wires contract-driven DTOs from @anarchitects/auth-ts, uses Better Auth as the canonical internal auth engine, keeps email/password always enabled, and layers repo-owned RBAC on top of Better Auth-backed user/session state.
Migration guidance for the Better Auth realignment lives in the auth migration guide. Migration guidance for the contract-driven auth profile model lives in the auth contract migration guide.
Developer + AI Agent Start Here
- Read this README before generating integration code for
@anarchitects/auth-nest. - Start with
AuthModule.forRoot(...)orAuthModule.forRootFromConfig(...)from the root entry point unless you need explicit layered composition. - Keep shared mail transport setup at app root via
@anarchitects/common-nest-mailer; keep auth mailer infrastructure adapter-only. - Use DTO contracts from
@anarchitects/auth-tsand preservepresentation -> application <- infrastructureboundaries.
Features
- Application layer – Better Auth-backed
AuthService,AuthPrincipalResolver,BcryptHashService, CASL-basedPoliciesService,ResourceAuthorizationService, andAbilityFactoryencapsulating business rules for sessions, passwords, principal hydration, and fine-grained access control. - Presentation layer –
AuthControllerexposing the package-owned core session-oriented auth lifecycle, global-readyAuthenticationGuardandAuthorizationGuard, compatibility guards for policy/resource authorization, plus internal plugin controllers such as JWT when enabled. - Infrastructure persistence – TypeORM entities and repositories for users, roles, permissions, and core Better Auth tables in the
authschema. Better Auth database operations are bridged internally through the published@anarchitects/better-auth-typeorm-adapter, while this repo keeps the Nest wrapper, entity registration, and migrations local. Plugin-specific tables and plugin-owned persistence such as JWT invalidation stay with their plugin modules. - Infrastructure mailer –
AuthMailerModulewrapper over sharedCommonMailerModule.forRoot(...)provider wiring;NodeMailerAdapteris re-exported for compatibility. - Config – Typed
authConfignamespace using@nestjs/configwith a Better Auth core config branch and typed plugin configuration.
Installation
npm install @anarchitects/auth-nest @nestjs/common @nestjs/config @nestjs/core @nestjs/jwt @nestjs/platform-fastify @nestjs/typeorm typeorm
# or
yarn add @anarchitects/auth-nest @nestjs/common @nestjs/config @nestjs/core @nestjs/jwt @nestjs/platform-fastify @nestjs/typeorm typeorm
Peer requirements:
@nestjs/common,@nestjs/core,@nestjs/jwt,@nestjs/typeorm,@nestjs/config@nestjs/platform-fastify,typeorm
The internal @anarchitects/auth-ts and @anarchitects/common-nest-mailer packages are installed transitively. The published community package @anarchitects/better-auth-typeorm-adapter is also installed transitively and used internally by @anarchitects/auth-nest; consumers do not need to wire it directly when using this package facade. Runtime utilities such as @casl/ability, bcrypt, better-auth, and @better-auth/passkey are direct dependencies of this package. Add @nestjs-modules/mailer only when your host app enables the shared/common mailer integration.
Better Auth Adapter Boundary
@anarchitects/auth-nest now consumes the published @anarchitects/better-auth-typeorm-adapter package internally for Better Auth database composition.
- The community package provides only the framework-neutral Better Auth
databaseadapter. - This repo still owns the Nest wrapper, dependency injection, TypeORM entities, migrations, and plugin model registration.
- This internal swap does not add a new public Nest API or change the existing
AuthModule/AuthApplicationModuleintegration surface.
Maintainers can validate the published npm artifact integration path with:
yarn nx run auth-nest:test-published-adapter
That target boots auth-nest against ephemeral PostgreSQL and exercises the real published adapter package through the host repo's Nest integration path. It requires Docker or another supported local container runtime because the suite provisions PostgreSQL through testcontainers.
Exports
| Import path | Contents |
|---|---|
@anarchitects/auth-nest |
Facade-first runtime entry point: AuthModule.forRoot(...), AuthModule.forRootFromConfig(...), provideAuthRuntimeGuards(), @AuthorizedResource(), plus layered re-exports for convenience |
@anarchitects/auth-nest/application |
AuthApplicationModule, AuthService, AuthPrincipalResolver, PoliciesService, ResourceAuthorizationService, AbilityFactory, resource-authorization helpers/types |
@anarchitects/auth-nest/presentation |
Runtime presentation internals for advanced composition: AuthPresentationModule, AuthController, AuthenticationGuard, AuthorizationGuard, PoliciesGuard, ResourceAuthorizationGuard, provideAuthRuntimeGuards(), @AuthorizedResource() |
@anarchitects/auth-nest/infrastructure-persistence |
AuthPersistenceModule, core auth persistence entities, CreateAuthSchema1720200000000, AuthAccountRepository, AuthUserRepository, and persistence module option types |
@anarchitects/auth-nest/infrastructure-mailer |
AuthMailerModule, NodeMailerAdapter |
@anarchitects/auth-nest/config |
authConfig, AuthConfig type, InjectAuthConfig() |
Controller-facing security declarations that should not pull in this runtime
package live in @anarchitects/auth-declarations. Use that package for
@Public(), @Policies(...), and @AuthorizeResource(...). Runtime
enforcement, guards, modules, principal resolution, and app wiring remain owned
by @anarchitects/auth-nest.
RoutePolicy likewise belongs to the contract/declaration side. Import it from
@anarchitects/auth-ts/models or use the declaration helpers from
@anarchitects/auth-declarations; do not treat auth-nest as the source of
truth for route metadata contracts.
Runtime Security Enforcement
auth-nest owns the runtime side of authentication and authorization.
AuthenticationGuardis ready for global registration by a host app. It honors@Public(), resolves the Better Auth session throughAuthPrincipalResolver, hydrates the repo-owned user principal, and attaches it torequest.user.AuthorizationGuardis ready for global authorization registration by a host app. It honors@Public(), then reads@Policies(...)and@AuthorizeResource(...)metadata from@anarchitects/auth-declarations.PoliciesServiceperforms the lightweight route pass check against the CASL-aligned{ action, subject }shape. Conditional and field-scoped persisted rules may allow the route attempt, but they do not prove access to a specific resource instance.ResourceAuthorizationServiceperforms the concrete resource check after a configured loader returns the subject instance. This is where CASL evaluates conditions against loaded data.
Use the app shell to activate runtime security centrally:
import { Module } from '@nestjs/common';
import { AuthModule, provideAuthRuntimeGuards } from '@anarchitects/auth-nest';
@Module({
imports: [AuthModule.forRoot({})],
providers: [...provideAuthRuntimeGuards()],
})
export class AppModule {}
provideAuthRuntimeGuards() registers AuthenticationGuard first and AuthorizationGuard second as APP_GUARD providers, so principal resolution always runs before route-policy or resource checks. Controller-level UseGuards(...) wiring is no longer the default integration path.
The intended split is:
- controllers declare security intent with
@anarchitects/auth-declarations - host apps activate runtime guards once through
@anarchitects/auth-nest auth-nestreads controller metadata and performs the actual enforcement
Wiring Migrations And Entities
Prefer symbol imports from @anarchitects/auth-nest/infrastructure-persistence when wiring TypeORM DataSource migrations or host-level entity registration. This gives consumers a stable public contract instead of depending on package-internal file paths.
Recommended:
import { AccountEntity, CreateAuthSchema1720200000000, PermissionEntity, RoleEntity, SessionEntity, UserEntity, VerificationEntity } from '@anarchitects/auth-nest/infrastructure-persistence';
import { DataSource } from 'typeorm';
export const AppDataSource = new DataSource({
type: 'postgres',
migrations: [CreateAuthSchema1720200000000],
entities: [AccountEntity, PermissionEntity, RoleEntity, SessionEntity, UserEntity, VerificationEntity],
});
Legacy glob-based wiring still works, but it depends on package internals and is not the recommended integration path:
import { join } from 'node:path';
export const AppDataSource = new DataSource({
type: 'postgres',
migrations: [join(process.cwd(), 'node_modules/@anarchitects/auth-nest/src/infrastructure-persistence/migrations/*.js')],
});
Use the symbol-based approach for production apps and reusable templates. Keep plugin-specific migrations with their owning plugin modules; the infrastructure-persistence entry point only exposes the core auth schema migration.
Configuration
The library reads configuration through @nestjs/config using a namespaced authConfig registered under the key auth. Set the following environment variables to customise behaviour:
| Variable | Description | Default |
|---|---|---|
AUTH_BETTER_AUTH_BASE_URL |
Better Auth base URL used for internal route generation. | http://localhost:3000/api/auth |
AUTH_BETTER_AUTH_SECRET |
Better Auth secret. Must be overridden in production. | better-auth-secret-32-chars-minimum |
AUTH_BETTER_AUTH_VERIFY_EMAIL_CALLBACK_URL |
App-facing callback URL embedded in verification emails. | <base-url origin>/verify-email |
AUTH_BETTER_AUTH_RESET_PASSWORD_CALLBACK_URL |
App-facing callback URL embedded in password reset emails. | <base-url origin>/reset-password |
AUTH_PLUGIN_JWT_ENABLED |
Enables the internal JWT plugin routes. | false |
AUTH_PLUGIN_JWT_SECRET |
Secret key used by the JWT plugin. Must be overridden when enabled. | default_jwt_secret |
AUTH_PLUGIN_JWT_EXPIRATION |
JWT plugin token lifetime (e.g. 3600s, 15m, 1d). |
3600s |
AUTH_PLUGIN_JWT_AUDIENCE |
Expected aud claim for JWT plugin tokens. |
your_audience |
AUTH_PLUGIN_JWT_ISSUER |
Expected iss claim for JWT plugin tokens. |
your_issuer |
AUTH_PLUGIN_PASSKEYS_ENABLED |
Enables the passkeys plugin. | false |
AUTH_PLUGIN_PASSKEY_RP_ID |
Passkey relying-party ID. | localhost |
AUTH_PLUGIN_PASSKEY_RP_NAME |
Passkey relying-party display name. | Anarchitecture Auth |
AUTH_PLUGIN_PASSKEY_ORIGIN |
Explicit passkey origin when needed. | unset |
AUTH_PLUGIN_SOCIAL_ENABLED |
Enables social auth plugins. | false |
AUTH_PLUGIN_SOCIAL_GITHUB_CLIENT_ID |
GitHub social sign-in client ID. | unset |
AUTH_PLUGIN_SOCIAL_GITHUB_CLIENT_SECRET |
GitHub social sign-in client secret. | unset |
AUTH_PLUGIN_OIDC_ENABLED |
Enables future OIDC plugin wiring. | false |
AUTH_ENCRYPTION_ALGORITHM |
Password hashing algorithm (bcrypt). |
bcrypt |
AUTH_ENCRYPTION_KEY |
Symmetric key for additional encryption needs. Must be overridden. | default_encryption_key |
AUTH_MAILER_PROVIDER |
Domain mailer provider for forRootFromConfig(...) (node or noop). |
node |
Security note: The defaults for
AUTH_BETTER_AUTH_SECRET,AUTH_PLUGIN_JWT_SECRET, andAUTH_ENCRYPTION_KEYare intentionally insecure placeholders. Always provide strong, unique values in any deployed environment.
Injecting the config
import { InjectAuthConfig, AuthConfig } from '@anarchitects/auth-nest/config';
@Injectable()
export class MyService {
constructor(@InjectAuthConfig() private readonly config: AuthConfig) {}
someMethod() {
const secret = this.config.betterAuth.secret;
}
}
Make sure to import authConfig into your module's ConfigModule:
import { ConfigModule } from '@nestjs/config';
import { authConfig } from '@anarchitects/auth-nest/config';
@Module({
imports: [ConfigModule.forRoot({ load: [authConfig] })],
})
export class AppModule {}
Usage
Contract-driven route validation
@anarchitects/auth-nest now resolves request-body validation from an auth contract profile at module bootstrap.
- If you omit
contracts, the module usesDefaultAuthContractConfig. - Pass explicit overrides with
AuthModule.forRoot({ contracts: ... })for deterministic app-local configuration. - Pass explicit overrides with
AuthModule.forRootFromConfig({ contracts: ... })when the rest of the module is config-driven but the contract profile still needs code-level control. forRootFromConfig(...)intentionally overlays explicitcontractsoverrides onto the default profile today; there is no dedicatedAUTH_*contract env tree yet.
Easy mode (single facade import)
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CommonMailerModule, mailerConfig } from '@anarchitects/common-nest-mailer';
import { AuthModule, provideAuthRuntimeGuards } from '@anarchitects/auth-nest';
import { authConfig } from '@anarchitects/auth-nest/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [authConfig, mailerConfig],
}),
CommonMailerModule.forRootFromConfig(),
AuthModule.forRoot({
contracts: {
register: {
name: {
required: true,
},
},
},
presentation: {
application: {
encryption: {
algorithm: 'bcrypt',
key: process.env.AUTH_ENCRYPTION_KEY!,
},
},
},
mailer: {
provider: 'node',
},
}),
],
providers: [...provideAuthRuntimeGuards()],
})
export class AuthApiModule {}
AuthModule.forRoot(...) is the preferred integration path when you want a full auth stack with minimal host-module wiring.
Use AuthModule.forRootFromConfig() when you want module composition fully driven by AUTH_*
variables exposed via authConfig.
Apply contract-profile overrides without adding new AUTH_* variables:
AuthModule.forRootFromConfig({
contracts: {
register: {
name: {
required: true,
},
},
},
});
The same contract profile drives Fastify request validation for the core auth routes:
POST /auth/registerPOST /auth/loginPOST /auth/logoutPOST /auth/forgot-passwordPOST /auth/reset-passwordPOST /auth/verify-emailPATCH /auth/change-password/:userId
Disable domain mailer wiring when not needed:
AuthModule.forRoot({
presentation: { application: { ... } },
mailer: { provider: 'noop' },
});
Layered composition (advanced)
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CommonMailerModule, mailerConfig } from '@anarchitects/common-nest-mailer';
import { authConfig } from '@anarchitects/auth-nest/config';
import { AuthApplicationModule } from '@anarchitects/auth-nest/application';
import { AuthPresentationModule, provideAuthRuntimeGuards } from '@anarchitects/auth-nest/presentation';
import { AuthMailerModule } from '@anarchitects/auth-nest/infrastructure-mailer';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [authConfig, mailerConfig],
}),
CommonMailerModule.forRootFromConfig(),
AuthApplicationModule.forRoot({
encryption: {
algorithm: 'bcrypt',
key: process.env.AUTH_ENCRYPTION_KEY!,
},
}),
AuthPresentationModule.forRoot({
contracts: {
register: {
name: {
required: true,
},
},
},
application: {
encryption: {
algorithm: 'bcrypt',
key: process.env.AUTH_ENCRYPTION_KEY!,
},
},
}),
AuthMailerModule.forRoot({
provider: 'node',
}),
],
providers: [...provideAuthRuntimeGuards()],
})
export class AuthApiModule {}
Use layered composition when you need to replace or selectively compose infrastructure/application concerns.
Optional JWT plugin
Core auth remains session-first. Only enable the JWT plugin when the host app explicitly needs token-based routes:
AuthModule.forRoot({
presentation: {
application: {
plugins: {
jwt: {
enabled: true,
secret: process.env.AUTH_PLUGIN_JWT_SECRET!,
},
},
},
},
});
That mounts the plugin-owned /auth/jwt/login, /auth/jwt/logout, and /auth/jwt/refresh routes alongside the package-owned core session routes.
Mailer Migration Note
AuthMailerModule is now adapter-only. It wraps shared CommonMailerModule.forRoot(...)
provider wiring from @anarchitects/common-nest-mailer and no longer configures transport with
MailerModule.forRootAsync(...).
Configure transport once at app root with CommonMailerModule.
Set mailer.provider: 'noop' to disable active delivery behavior per domain.
The shared mailer DI contract (MailerPort) and concrete NodeMailerAdapter now live in
@anarchitects/common-nest-mailer.
Injecting services
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from '@anarchitects/auth-nest/application';
import { LoginRequestDTO } from '@anarchitects/auth-ts/dtos';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
login(@Body() dto: LoginRequestDTO) {
return this.authService.login(dto);
}
}
Route-level authorization with policies
import { Controller, Patch } from '@nestjs/common';
import { AuthorizeResource, Policies } from '@anarchitects/auth-declarations';
import { AuthorizedResource } from '@anarchitects/auth-nest';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Patch(':postId')
@Policies({ action: 'update', subject: 'Post' })
@AuthorizeResource({ action: 'update', subject: 'Post', idParam: 'postId' })
async updatePost(@AuthorizedResource() post: Post) {
return this.postsService.update(post);
}
}
import { AuthModule } from '@anarchitects/auth-nest';
AuthModule.forRoot({
presentation: {
application: {
resourceAuthorization: {
loaders: {
Post: async ({ resourceId }) => postsRepository.findById(resourceId),
},
},
},
},
});
@Policies() remains the coarse route-level declaration. @AuthorizeResource(...) declares the concrete resource check that auth-nest runtime guards can perform after loading the resource through the app-registered loader. The runtime flow attaches the authorized resource to the request so @AuthorizedResource() can read it in the handler.
Authorization Model
CASL integration in @anarchitects/auth-nest is intentionally split into two layers:
@Policies()uses the CASL-aligned{ action, subject }route metadata shape to declare a coarse route-level pre-check@AuthorizeResource(...)declares the concrete instance-level check that runtime enforcement performs after loading the resource@AuthorizedResource()gives the handler access to the already loaded and authorized entity
Use this split to avoid overstating what route metadata can prove. Ownership-sensitive rules such as "writers may only update their own posts" need the concrete resource instance before CASL can decide correctly.
What the library enforces
- persisted permission payloads are validated before they become
PolicyRule[] - malformed persisted permission payloads fail closed with a server-side error
- missing registered resource loader is treated as configuration error
- missing route param yields
400 - missing resource yields
404
What the host app must provide
- subject-specific resource loaders for
@AuthorizeResource(...) - domain resource retrieval logic and repository access
- route resolver/handler composition that fits the app's domain model
The library owns authorization orchestration. The host app still owns how domain resources are found.
REST endpoints
The AuthController exposes the following routes (all prefixed with /auth):
| Method | Path | Description |
|---|---|---|
POST |
/auth/register |
Register a new user |
PATCH |
/auth/activate |
Activate a user account |
POST |
/auth/login |
Log in and establish a Better Auth session |
POST |
/auth/logout |
Log out and clear the Better Auth session |
PATCH |
/auth/change-password/:userId |
Change password for a user |
POST |
/auth/forgot-password |
Request a password-reset email |
POST |
/auth/reset-password |
Reset password with token |
POST |
/auth/verify-email |
Verify an email address |
PATCH |
/auth/update-email/:userId |
Update email for a user |
GET |
/auth/me |
Get logged-in user info and RBAC rules |
When the JWT plugin is enabled, these plugin-owned routes are also mounted:
| Method | Path | Description |
|---|---|---|
POST |
/auth/jwt/login |
Log in and receive JWT tokens |
POST |
/auth/jwt/logout |
Invalidate JWT plugin tokens |
POST |
/auth/jwt/refresh |
Refresh JWT plugin token pairs |
Nx scripts
nx build auth-nest– bundle the Nest library.nx test auth-nest– execute Jest unit tests.nx lint auth-nest– run ESLint checks.
Development notes
- DTO shapes live in
@anarchitects/auth-ts; update the contract and regenerate DTOs before extending this library. - Default persistence is TypeORM with schema-qualified tables (see
libs/auth/nest/src/infrastructure-persistence). - Invalidated tokens use an unlogged cache table for quick revocation lookups.
- Route schemas are defined in
@anarchitects/auth-ts/dtosand imported into controller@RouteSchemadecorators — do not define inline schemas. - Keep
@Policies()guidance coarse in docs and examples; use@AuthorizeResource(...)for instance-sensitive authorization. - OpenAPI metadata (
operationId,tags) is assigned intools/api-specs/route-metadata.ts, not in controllers.
License
Licensed under the Apache License, Version 2.0. Unless required by applicable law or agreed to in writing, software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.