Service Startup
The IAM service ships two entry points. The @riavzon/auth/service export provides a complete standalone server that reads an age-encrypted configuration file, creates the Express application, and begins listening for requests. The @riavzon/auth export provides the same building blocks as individual functions and routers so you can mount them into your own Express application.
Both paths converge on the same core function: bootstrapApp. This function takes a validated configuration object, creates every pool, table, middleware, and route the service needs, and returns a ready to use Express app.
Package Exports
The package exposes two subpath exports:
| Export | Entry | Purpose |
|---|---|---|
@riavzon/auth | dist/main.mjs | Library mode. Individual functions, middleware, routers, and utilities. |
@riavzon/auth/service | dist/service.mjs | Standalone mode. Exports bootstrapApp and startServer. Auto-starts when NODE_ENV !== 'test'. |
When you import from @riavzon/auth, nothing boots automatically. When you import from @riavzon/auth/service, the module calls startServer() immediately unless NODE_ENV is test. In production, this is the module you point your container's CMD at.
For a complete list of every export, see the API reference.
startServer
The startServer function is the outermost entry point in standalone mode. It handles configuration loading, application bootstrap, config file cleanup, and HTTP listening.
Startup Flow
Locate the configuration file
The function reads the CONFIG_PATH environment variable. If the variable is not set, it defaults to /run/app/config.json. It then checks that the file exists using fs.access. If the file is missing, the process exits with a fatal error.
Parse configuration
The file is read as UTF-8 and parsed with JSON.parse. The resulting object is passed directly to bootstrapApp, which validates it against the Zod schema internally. See the Configuration reference for the full schema.
Bootstrap the application
Calls bootstrapApp(config), which initializes all pools, tables, middleware, and routes. Returns a fully configured Express app.
Delete the configuration file
After bootstrapping, the function deletes the configuration file from disk with fs.unlink. This ensures that sensitive credentials (database passwords, JWT secrets, HMAC keys, API keys) do not persist on the filesystem after they have been loaded into memory. If the SKIP_CONFIG_UNLINK environment variable is set to 'true', the file is kept.
Start listening
The app begins listening on the configured service.port (default 10000) and service.ipAddress (default 0.0.0.0). Once the server is ready, background data-refresh tasks are scheduled.
import { startServer } from '@riavzon/auth/service'
// In production, this runs automatically.
// In tests (NODE_ENV=test), you call it manually:
await startServer()
decrypt.sh runs on every container start.bootstrapApp
The core bootstrap function. It accepts a ConfigurationInput object, performs all initialization, and returns an Express Application. Both the standalone server and library consumers use this function.
import { bootstrapApp } from '@riavzon/auth/service'
import type { ConfigurationInput } from '@riavzon/auth'
const app = await bootstrapApp(config)
Bootstrap Sequence
The function executes the following steps in order:
Configure OAuth providers
If the configuration includes a providers array, each entry is instantiated as an OAuthProvider object. Providers defined with a schema field use that Zod schema for profile validation. Providers defined with fields or useStandardProfile use the built-in StandardProfileSchema. See OAuth for provider configuration details.
Initialize the configuration manager
Calls configuration(config), which passes the raw config through the Zod schema for validation, then runs three initialization tasks concurrently:
- Main MySQL pool - Creates a
mysql2/promiseconnection pool fromconfig.store.main. This pool handles all authentication queries (users, refresh tokens, MFA codes). - Rate limiter pool - Creates a callback-based
mysql2connection pool fromconfig.store.rate_limiters_pool.store. This separate pool is used exclusively by rate-limiter-flexible for counter storage. - Disposable email database - Opens an LMDB read-only database containing known disposable email domains. Used by the signup flow to reject temporary email addresses.
Once all three tasks complete, the configuration is frozen with Object.freeze and stored as a module-level singleton. Any subsequent call to getConfiguration() returns this frozen object.
Create the Express application
Creates a new Express app and conditionally configures trust proxy. When service.proxy.trust is true, the app trusts the configured proxy IP (service.proxy.server or service.ipAddress) and the client-facing proxy IP (service.proxy.ipToTrust). This ensures req.ip reflects the real client IP rather than the proxy's address.
Register the health endpoint
Mounts GET /health before any middleware. The handler returns 200 OK as plain text. Because it is registered before HMAC authentication, it is accessible without credentials. The Docker health check (healthcheck.js) polls this endpoint every 30 seconds.
Configure the bot detector
Initializes the @riavzon/bot-detector module. If the configuration has botDetector.enableBotDetector: true with custom settings, those settings are used. Otherwise, a default configuration is applied. If bot detection is disabled (enableBotDetector: false), a passive-mode configuration is loaded that disables all active checks but still provides the ApiResponse middleware for visitor tracking.
Create database tables
Creates the bot-detector tables (via createTables) and the IAM tables (via
makeTables). The IAM schema includes four tables: users, api_tokens,
refresh_tokens, and mfa_codes. All table creation statements use
CREATE TABLE IF NOT EXISTS, so repeated calls are safe. After creating
api_tokens, makeTables() also ensures the three API-token indexes exist and
ignores duplicate-key errors on repeated runs. See
Database for the full schema.
Mount middleware stack
The middleware is registered in a specific order. Each layer depends on the layers before it.
Mount route groups
The public API verification router is registered first. After that, the authenticated route groups are mounted in order: authentication routes, token rotation routes, magic link routes, BFF access routes, and API token management routes. The standalone operational config endpoint is mounted after those routers.
Warm up the bot detector
Calls warmUp() from the bot-detector module to preload geo, ASN, and threat databases into memory. This runs after route registration so that the first incoming request does not pay the cold-start penalty.
Register terminal handlers
Mounts the notFoundHandler (returns 404 for unmatched paths) and finalUnHandledErrors (catches unhandled errors and returns 500). These are registered last so they only trigger when no route or middleware handled the request.
Middleware Stack
The middleware is mounted in a fixed order. Every request passes through each layer sequentially:
| Order | Middleware | Purpose | Reference |
|---|---|---|---|
| 1 | httpLogger | Structured request logging via pino-http. Assigns a unique X-Request-Id header to every request. Redacts Authorization headers and session cookies from logs. | Logging |
| 2 | x-powered-by disabled | Removes the default Express X-Powered-By header to avoid fingerprinting the server technology. | |
| 3 | helmet | Sets security headers: X-Frame-Options: DENY, Content-Security-Policy: frame-ancestors 'none', Cross-Origin-Embedder-Policy, Referrer-Policy: origin. | |
| 4 | headers | Sets Cache-Control: no-cache, private, max-age=0, Pragma: no-cache, Expires: 0 on every response. Prevents browsers and proxies from caching sensitive authentication responses. | |
| 5 | validateIp | Validates that req.ip is a valid IP address using Node.js net.isIP(). Returns 403 Forbidden if the IP is missing or invalid. | |
| 6 | hmacAuth (conditional) | Only mounted when service.Hmac is configured. Verifies the X-Client-Id, X-Timestamp, X-Signature, and X-Request-ID headers against a shared secret. Rejects requests with missing headers, unknown client IDs, expired timestamps, replayed nonces, or invalid signatures. Bypasses the health endpoint on localhost. | HMAC |
| 7 | apiVerificationRoute() | Mounts GET /api/public/verify before body and cookie parsing. | API Tokens |
| 8 | express.json() | Parses JSON request bodies. | |
| 9 | cookieParser() | Parses Cookie headers into req.cookies. | Cookies |
| 10 | ApiResponse | Bot-detector middleware that attaches visitor data and geo information to every request. | Bot Detection |
ApiResponse middleware is last because it relies on cookies being parsed.Route Groups
After the early health check and service-wide middleware, six route groups are mounted. Five are authenticated or semi-public routers, and the operational config endpoint is mounted afterward as a direct handler.
Public API Verification Route
Handles machine-to-machine API token verification.
| Method | Path | Purpose | Reference |
|---|---|---|---|
| GET | /api/public/verify | Verify an API token against a required privilege | API Tokens |
This route is mounted before express.json(), cookieParser(), and the
bot-detector middleware. It reads the token from the x-api-key header and the
required privilege from the privilege query parameter.
Authentication Routes
Handles account creation, login, and OAuth flows.
| Method | Path | Purpose | Middleware | Reference |
|---|---|---|---|---|
| POST | /signup | Create a new account | contentType('application/json'), json({ limit: '1kb' }), canary cookie check | Signup |
| POST | /login | Authenticate with email and password | contentType('application/json'), json({ limit: '1kb' }), canary cookie check | Login |
| POST | /auth/OAuth/:providerName | Authenticate via an OAuth provider | contentType('application/json'), json({ limit: '4kb' }), canary cookie check | OAuth |
All three routes enforce a Content-Type: application/json header via the contentType middleware. The json() middleware limits request bodies to 1 KB (4 KB for OAuth) and rejects empty bodies with a 403. Each route also requires the canary_id cookie from the bot-detector and returns 400 if it is missing.
Token Rotation Routes
Handles token refresh and logout.
| Method | Path | Purpose | Middleware | Reference |
|---|---|---|---|---|
| POST | /auth/user/refresh-session | Rotate both access and refresh tokens | requireRefreshToken, acceptCookieOnly, getFingerPrint, checkForActiveMfa | Refresh Tokens |
| POST | /auth/logout | Revoke the current session | requireRefreshToken, requireAccessToken, acceptCookieOnly | Logout |
The acceptCookieOnly middleware enforces that these requests send only cookies: no JSON body, no query string, and no Content-Type header. This prevents CSRF attacks by ensuring the request cannot carry attacker-controlled data beyond the browser's automatic cookie attachment.
Magic Link Routes
Handles MFA verification, password reset, custom MFA flows, and email changes.
| Method | Path | Purpose | Reference |
|---|---|---|---|
| GET | /auth/verify-mfa | Verify an email MFA link (redirects) | MFA |
| POST | /auth/verify-mfa | Submit an MFA code | MFA |
| POST | /custom/mfa/:reason | Initiate a custom MFA flow | MFA |
| GET | /auth/verify-custom-mfa | Verify a custom MFA link | MFA |
| POST | /auth/verify-custom-mfa | Submit a custom MFA code | MFA |
| POST | /update/email | Change the user's email address | MFA |
| POST | /auth/forgot-password | Initiate a password reset | Password Reset |
| GET | /auth/reset-password | Verify a password reset link | Password Reset |
| POST | /auth/reset-password | Submit a new password | Password Reset |
Custom MFA and email update routes require a valid access token, refresh
token, fingerprint, and a checkForActiveMfa pass before protectRoute runs
full anomaly detection on these requests.
BFF Access Routes
Backend-for-Frontend routes used by your application server to verify user sessions.
| Method | Path | Purpose | Reference |
|---|---|---|---|
| GET | /secret/data | Verify the user's session and return authorization data | BFF |
| GET | /secret/accesstoken/metadata | Return decoded access token payload and TTL | BFF |
Both routes require an access token (Authorization: Bearer header), a refresh
token (session cookie), and a valid fingerprint. The routes also call
checkForActiveMfa before protectRoute performs full JWT verification and
anomaly detection.
API Token Management Routes
Handles authenticated creation and management of API tokens.
| Method | Path | Purpose | Reference |
|---|---|---|---|
| POST | /api/manage/new-token | Create a new API token | Creating Tokens |
| GET | /api/manage/list-metadata | List the user's valid API tokens | Token Listing |
| POST | /api/manage/revoke | Revoke an API token | Revocation |
| POST | /api/manage/metadata | Return metadata for one API token | Metadata |
| POST | /api/manage/rotate | Rotate an API token | Rotation |
| POST | /api/manage/ip-restriction-update | Update a token IP whitelist | IP Restriction |
| POST | /api/manage/privilege-update | Update a token privilege | Privileges |
All API token management routes require requireAccessToken,
requireRefreshToken, getFingerPrint, checkForActiveMfa, and
protectRoute. The POST routes also enforce
contentType('application/json') and express.json({ limit: '1kb' }). The
GET /api/manage/list-metadata branch skips the JSON guards because it does
not accept a request body.
Operational Config Endpoint
A single endpoint mounted outside the route groups:
| Method | Path | Purpose |
|---|---|---|
| GET | /operational/config | Return the cookie domain and access token TTL to a trusted client |
This endpoint is restricted by physical IP address. It compares req.socket.remoteAddress against service.clientIp or service.proxy.ipToTrust. Only the configured trusted client can access it.
Terminal Handlers
Two handlers are registered after all routes:
| Handler | Status | Body |
|---|---|---|
notFoundHandler | 404 | { "error": "The page you are looking for doesn't exists" } |
finalUnHandledErrors | 500 (or res.statusCode if > 415) | { "error": "<message>" } |
The error handler logs the full error via pino before sending the response. It picks the status code from the current res.statusCode if it is above 415, otherwise defaults to 500.
Configuration
The configuration object is validated at startup by a strict Zod schema. Every field is type-checked at runtime. If validation fails, the process exits with a detailed error showing which fields failed and why.
The schema covers database connections, JWT settings, password hashing, bot detection, rate limiting, magic links, OAuth providers, email credentials, and standalone server options. Each section is documented in its own essentials page, and the complete schema reference is available in the Configuration reference.
Docker Deployment
The standalone service is designed to run as a Docker container with encrypted configuration.
Container Architecture
The Dockerfile uses a multi-stage build:
Go builder stage
Compiles the age encryption tool and mmdbctl from source. These binaries are copied into later stages.
Base stage
Starts from node:lts-slim. Installs minimal runtime dependencies (libatomic1, curl, ca-certificates). Creates a non-root user (appuser, UID 10001) for the production stage.
Builder stage
Installs npm dependencies with npm ci, runs bot-detector init to generate the shield-base data sources, and builds the TypeScript source with the configured build tool.
Production stage
Copies only the built artifacts, production node_modules, bot-detector data sources, and the decrypt.sh and healthcheck.js scripts. Drops all development tools, npm itself, and build caches. Runs as appuser (non-root) with no-new-privileges enforced.
Entrypoint Flow
The container entrypoint is decrypt.sh, which runs before the main process:
Check for existing config
If /run/app/config.json already exists (from a previous run or manual mount), the script skips decryption and proceeds directly to the main command.
Decrypt the configuration
Reads the age private key from /run/secrets/age_key and the encrypted config from /run/secrets/encrypted_config. Decrypts using age -d and writes the result to /run/app/config.json with 0400 permissions (read-only for the owner).
Launch the service
Calls exec "$@", which replaces the shell process with the Docker CMD: node dist/service.mjs. The service startServer function then reads the config from the default path (/run/app/config.json).
Health Check
Docker runs a health check every 30 seconds:
http.request({ host: '127.0.0.1', port: 10000, path: '/health', timeout: 2000 })
The check hits GET /health on localhost. A 2xx or 401 response means the service is healthy. The 401 case covers scenarios where HMAC is enabled but the health check runs without headers. Because the HMAC middleware skips localhost health checks, this typically returns 200.
Background Tasks
After the server starts listening, startServer schedules three recurring background tasks:
| Task | Command | Default Interval | Purpose |
|---|---|---|---|
| Bot detector refresh | bot-detector refresh | 24 hours | Updates IP reputation, geo, and threat intelligence data sources |
| Bot detector generate | bot-detector generate | 3 days | Regenerates compiled MMDB databases from updated sources |
| Disposable email list | shield-base --email --path=dist | 7 days | Updates the LMDB database of known disposable email domains |
Each task runs with nice -n 19 (lowest CPU priority) to minimize impact on request handling. Tasks are scheduled with setTimeout and reschedule themselves after completion, so the interval measures the gap between the end of one run and the start of the next.
Library Integration
When using @riavzon/auth as a library in your own Express application, you skip startServer and either call bootstrapApp or mount the individual pieces yourself.
Using bootstrapApp
The simplest integration. Provides the full IAM service as a sub-application:
import express from 'express'
import { bootstrapApp } from '@riavzon/auth/service'
const mainApp = express()
const authApp = await bootstrapApp({
store: { main: { host: 'localhost', ... }, rate_limiters_pool: { store: { host: 'localhost', ... }, dbName: 'rate_limiters' } },
password: { pepper: 'your-pepper' },
botDetector: { enableBotDetector: false },
htmlSanitizer: {},
magic_links: { jwt_secret_key: '...', domain: 'https://example.com', notificationEmail: { ... } },
jwt: { jwt_secret_key: '...', access_tokens: {}, refresh_tokens: { refresh_ttl: 604800000, domain: 'example.com', MAX_SESSION_LIFE: 2592000000, maxAllowedSessionsPerUser: 5, byPassAnomaliesFor: 86400000 } },
email: { resend_key: '...', email: '[email protected]' },
})
mainApp.use('/auth', authApp)
mainApp.listen(3000)
/auth, remember that the internal routes already include their own prefixes (e.g., /auth/user/refresh-session). Adjust your mount point to avoid doubled path segments.Using Individual Routers
For more control, call configuration() to initialize the pools and config, then mount only the routers you need:
import express from 'express'
import cookieParser from 'cookie-parser'
import {
configuration,
authenticationRoutes,
tokenRotationRoutes,
magicLinks,
bffAccessRoute,
apiVerificationRoute,
apiProtectedRoutes,
} from '@riavzon/auth'
const app = express()
await configuration({
store: { ... },
password: { pepper: '...' },
// ... rest of config
})
// Mount only what you need
app.use(apiVerificationRoute())
app.use(cookieParser())
app.use(authenticationRoutes)
app.use(tokenRotationRoutes)
app.use(magicLinks)
app.use(bffAccessRoute)
app.use(apiProtectedRoutes())
app.listen(3000)
configuration() before mounting any routers. The routers and
controllers call getConfiguration() internally, which throws if the config
has not been initialized. Register cookieParser() before any router that
reads req.cookies. The same applies to getPool(), poolForLibrary(), and
getDisposableEmailList().CLI Initialization
The package also exposes a CLI binary (auth) and an initAuthData function for one-time setup tasks like creating database tables and downloading the disposable email list:
import { initAuthData } from '@riavzon/auth'
await initAuthData(config)
Or from the command line:
npx auth ./config.json
This runs configuration(), bot-detector setup, makeTables(), createTables(), and the disposable email LMDB download in sequence.
Complete Route Map
For reference, here is every route the IAM service registers:
| Method | Path | Auth Required | Body | Reference |
|---|---|---|---|---|
| GET | /health | No | None | |
| POST | /signup | canary cookie | JSON (1 KB) | Signup |
| POST | /login | canary cookie | JSON (1 KB) | Login |
| POST | /auth/OAuth/:providerName | canary cookie | JSON (4 KB) | OAuth |
| POST | /auth/user/refresh-session | Refresh cookie | None (cookie only) | Refresh Tokens |
| POST | /auth/logout | Access + Refresh | None (cookie only) | Logout |
| GET | /auth/verify-mfa | Magic link token | None | MFA |
| POST | /auth/verify-mfa | Magic link token | JSON (1 KB) | MFA |
| POST | /custom/mfa/:reason | Access + Refresh + JWT | JSON (1 KB) | MFA |
| GET | /auth/verify-custom-mfa | Access + Refresh + JWT | None | MFA |
| POST | /auth/verify-custom-mfa | Access + Refresh + JWT | JSON (1 KB) | MFA |
| POST | /update/email | Access + Refresh + JWT | JSON (1 KB) | MFA |
| POST | /auth/forgot-password | None | JSON (1 KB) | Password Reset |
| GET | /auth/reset-password | Magic link token | None | Password Reset |
| POST | /auth/reset-password | Magic link token | JSON (1 KB) | Password Reset |
| GET | /secret/data | Access + Refresh + JWT | None | BFF |
| GET | /secret/accesstoken/metadata | Access + Refresh + JWT | None | BFF |
| GET | /operational/config | Trusted IP only | None |
Summary
| Concept | Description |
|---|---|
| Two entry points | @riavzon/auth for library usage, @riavzon/auth/service for standalone deployment |
startServer | Reads encrypted config, calls bootstrapApp, deletes config from disk, listens on port |
bootstrapApp | Initializes pools, tables, middleware, routes. Returns Express app |
| Config lifecycle | Decrypted at container start, parsed at boot, deleted from disk, frozen in memory |
| Middleware order | Logger, helmet, cache headers, IP validation, HMAC (optional), JSON parser, cookie parser, bot detector |
| Route groups | Authentication, token rotation, magic links, BFF access, operational config |
| Background tasks | Bot detector data refresh (daily), database generation (3 days), disposable email update (weekly) |
| Docker security | Read-only filesystem, no capabilities, non-root user, tmpfs for secrets, PID limits |
| Health check | GET /health returns 200 OK, polled every 30 seconds by Docker |