Skip to content

Modules ​

Module system ​

The API uses a plug-and-play module system that allows features to be enabled or disabled independently via environment variables. Each module is a self-contained unit that can:

  • Register Fastify decorators and routes.
  • Bootstrap data after the database is ready.
  • Release resources on graceful shutdown.

A module implements the AppModule interface:

ts
interface AppModule {
  name: string
  register: (app: FastifyInstance) => Promise<void>  // decorators + routes
  onReady?: (ctx: ModuleContext) => Promise<void>     // post-DB bootstrap
  onClose?: (ctx: ModuleContext) => Promise<void>     // graceful shutdown
}

When a module is disabled, no-op decorators are installed so route code can always reference app.requireAuth / app.requireRole without conditional imports.

Toggling modules ​

sh
# Disable the auth module entirely (no-op decorators, no auth routes)
MODULES__AUTH=false

# Enable optional modules
MODULES__TENANT=true
MODULES__AUDIT=true

Adding a new module ​

  1. Create apps/api/src/modules/<name>/index.ts that exports a default AppModule.
  2. Add a toggle entry (config.modules.<name>) to ConfigSchema in config.ts.
  3. Import and register it inside setupModules() in apps/api/src/modules/index.ts.

Auth module ​

The auth module is built on top of BetterAuth, a type-safe, batteries-included authentication library. It handles authentication, organization management, and access control — roles and permissions are managed via BetterAuth's organization plugin with typed access control definitions (see access-control.ts).

Features ​

FeatureDetails
Email + passwordBuilt-in sign-up / sign-in / password reset
Bearer tokenMachine-to-machine API access via Authorization: Bearer <token>
Session managementCookie-based sessions with optional Redis secondary storage for scaling
Two-factor authenticationTOTP-based 2FA (compatible with any authenticator app)
Admin APIServer-side user management (create, ban, set roles) via BetterAuth admin plugin
Organization managementMulti-tenant organizations with members, invitations, and role-based access (owner / admin / member)
API keysPer-user or per-organization API keys with prefix, rate limiting, and permissions
JWT / JWKSJWT token issuance with automatic key rotation via a JWKS endpoint
OpenAPI referenceAuto-generated OpenAPI 3.0 schema and interactive Scalar UI at /api/v1/auth/reference
Keycloak OIDC federationOptional SSO via Keycloak with configurable role & group mapping
Profile fieldsfirstname, lastname, bio additional user fields

Middleware ​

ts
// Require a valid session (cookie or Bearer token)
{ preHandler: [app.requireAuth] }

// Require one of the listed roles (calls requireAuth internally)
{ preHandler: [app.requireRole('admin')] }

Keycloak OIDC mapping ​

When Keycloak is enabled, users can authenticate via OIDC (SSO). Profile fields (given_name, family_name) are always mapped to firstname / lastname. Role and group mapping is opt-in via configuration:

ModeMAP_ROLESMAP_GROUPSRoles sourceUse case
Integrated (default)falsefalseBetterAuth admin pluginPublic app with social IDPs — roles managed in-app
Enterprise rolestruefalseKeycloak realm_roles claimCorp Keycloak — realm roles synced on every login
Enterprise groupsfalsetrueKeycloak groups claimCorp Keycloak — groups used as roles
Full enterprisetruetrueBoth (merged, deduplicated)Keycloak is the single source of truth

Admin bootstrap ​

On first startup, if BOOTSTRAP__EMAIL and BOOTSTRAP__PASSWORD are set, a default admin user is created automatically. The operation is idempotent — it is safely skipped if the user already exists.

Audit module ​

The audit module provides structured audit logging backed by a Prisma repository. It exposes an auditLogger decorator on the Fastify instance.

Enable: MODULES__AUDIT=true

Usage in route handlers ​

ts
// Synchronous (awaits write, errors surface to the route handler)
await app.auditLogger!.log({
  actorId: req.session!.user.id,
  action: 'delete',
  resourceType: 'project',
  resourceId: projectId,
  details: { reason: 'cleanup' },
})

// Fire-and-forget (non-blocking, errors are swallowed)
app.auditLogger!.logAsync({
  actorId: req.session!.user.id,
  action: 'update',
  resourceType: 'organization',
  resourceId: orgId,
})

Audit entry schema ​

FieldTypeRequiredDescription
actorIdstring✓ID of the user performing the action
actionstring✓Action name (e.g. create, delete)
resourceTypestring✓Resource type (e.g. user, project)
resourceIdstring✓ID of the affected resource
detailsobjectArbitrary metadata about the action