@open-xchange/fastify-sdk
v0.10.0
Published
Shared foundation package for OX App Suite Node.js services
Maintainers
Keywords
Readme
@open-xchange/fastify-sdk
Shared foundation package for OX App Suite Node.js services. Extracts common infrastructure — Fastify setup, logging, health checks, plugins, database pools, config loading — so consuming projects consist almost entirely of business logic.
Install
pnpm add @open-xchange/fastify-sdkQuick start
import { createApp } from '@open-xchange/fastify-sdk'
const app = await createApp({
dirname: import.meta.dirname,
plugins: {
jwt: true,
swagger: { enabled: process.env.EXPOSE_API_DOCS === 'true' }
}
})
await app.start()That single call replaces ~500 lines of boilerplate (logger, CORS, Helmet, metrics, JWT, Swagger, autoload). app.start() reads PORT (default 8080) and BIND_ADDR from env vars, and flips the K8s /ready probe to 200.
TypeScript
The source is JSDoc-typed and the package ships generated .d.ts declarations next to every entry point (./types/**). Consumer tsconfigs resolve them automatically as long as moduleResolution is "bundler" or "nodenext" (needed to honour package exports conditions — the classic "node" resolver skips them). Pure JavaScript consumers get editor IntelliSense for free.
Exports
| Import path | What it provides |
|---|---|
| @open-xchange/fastify-sdk | createApp, createMetricsServer, createLogger, logger, pinoConfig, loadEnv, requireEnv, jwtAuthHook, sessionAuthHook, extractAppSuiteSession, fetchAppSuiteUser, discoverAppSuiteApi, isKubernetesAvailable, metricsPlugin, health check helpers, re-exports of fastify, fp, pino, promClient, createError, jose |
| @open-xchange/fastify-sdk/mariadb | createMariaDBPool, createMariaDBPoolFromEnv, getMariaDBPools, mariadbReadyCheck, createUUID |
| @open-xchange/fastify-sdk/postgres | createPostgresPool, createPostgresPoolFromEnv |
| @open-xchange/fastify-sdk/migrations | createMigrationRunner, executeMigrations, runMigrations, runMigrationsCLI, checkPendingMigrations, ensureMigrationsTable, normalizeMigrationsConfig, MigrationsNotReadyError |
| @open-xchange/fastify-sdk/config | createConfigRegistry, readConfigurationFile, validateAndUpdateConfiguration, fileExists, getConfigPath, Joi helpers (defaultTrue, defaultFalse, customString, optionalCustomString, customURL), Joi |
| @open-xchange/fastify-sdk/redis | createRedisClient, createRedisClientFromEnv, redisReadyCheck |
| @open-xchange/fastify-sdk/testing | createTestApp, generateTokenForJwks, getJwks |
API reference
createApp(options)
Creates a configured Fastify instance with standard OX defaults.
Options:
{
dirname: import.meta.dirname, // For resolving plugins/routes dirs
pluginsDir: 'plugins', // Relative to dirname (auto-loaded)
routesDir: 'routes', // Relative to dirname (auto-loaded)
routes: { prefix, ...autoloadOpts }, // Extra @fastify/autoload options for routes
fastify: {}, // Merged into Fastify constructor options
plugins: {
cors: true | { origin, methods }, // Default: true
helmet: true | { options }, // Default: true
logging: true, // Default: true (request/response hooks)
metrics: true, // Default: true (fastify-metrics collectors, no endpoint)
jwt: true | false | { key }, // Default: true (OIDC via OIDC_ISSUER env, or noop 401 if unset)
sessionAuth: false | true | { appSuiteApi, discover, apiPath, userAgent, timeout, scope }, // Default: false (App Suite session forwarding; pair with jwt: false). appSuiteApi falls back to APP_SUITE_API env, then in-cluster K8s discovery
swagger: true | false | { enabled, openapi }, // Default: true (but only registers when EXPOSE_API_DOCS=true)
static: false | true | { root, preCompressed, ... }, // Default: false
cookie: false | true | { secret, parseOptions }, // Default: false (@fastify/cookie)
formbody: false | true | { bodyLimit }, // Default: false (@fastify/formbody, URL-encoded form bodies)
},
metricsServer: true, // Default: true (separate Fastify on port 9090, or METRICS_PORT env var)
database: { mariadb: true }, // Auto-manages pool readiness, health checks, shutdown
config: { // YAML config file watching
filename: 'config.yaml',
schema, // Joi schema for validation
optional: true,
callback: (data) => { ... }
},
onReady: async () => {}, // Called in Fastify onReady hook
onClose: async () => {}, // Called in Fastify onClose hook
processHandlers: true, // Default: true (close-with-grace: signals, uncaughtException, unhandledRejection)
shutdownDelay: 5000, // Default: 5000 (ms to wait before closing connections after signal)
closeGraceDelay: 10000, // Default: 10000 (ms hard timeout — force exit if shutdown hangs)
migrationRetries: 30, // Default: 30 (poll attempts for the migration job to finish before giving up)
migrationRetryDelay: 10000, // Default: 10000 (ms between migration-readiness attempts)
}Defaults applied:
requestIdLogLabel: 'requestId'disableRequestLogging: trueconnectionTimeout: 30000genReqId: () => randomUUID()
createLogger(options)
Returns a Pino logger with the standard OX configuration:
- Custom level mapping (trace→8, debug→7, info→6, warn→4, error→3, fatal→0)
- Redaction of
headers.authorization,headers.cookie,headers.host,key,password,salt,hash - Epoch millisecond timestamps
- No base (omits pid/hostname)
Pass custom options to override defaults (e.g. createLogger({ level: 'debug' })).
loadEnv()
Loads environment variables from .env first (higher precedence), then .env.defaults fills in any gaps, using Node's built-in process.loadEnvFile() (it does not overwrite already-set vars). No external dependency needed.
requireEnv(keys)
Validates that the given environment variables are set and non-empty. Prints a clear error message and exits the process if any are missing. Call after loadEnv() and before createApp().
import { loadEnv, requireEnv } from '@open-xchange/fastify-sdk'
loadEnv()
requireEnv(['PORT', 'ORIGINS', 'REDIS_HOSTS'])
if (process.env.SQL_ENABLED === 'true') {
requireEnv(['SQL_HOST', 'SQL_USER', 'SQL_PASS', 'SQL_DB'])
}Health check helpers
import { registerReadinessCheck, registerHealthCheck, mariadbHealthCheck } from '@open-xchange/fastify-sdk'
registerReadinessCheck(async () => { await mariadbHealthCheck(pool) })
registerHealthCheck(async () => { await mariadbHealthCheck(pool) })Registered checks are run by the metrics server (GET /ready and GET /live on port 9090). See Metrics server below.
MariaDB
import { createMariaDBPool, createMariaDBPoolFromEnv, mariadbReadyCheck, createUUID } from '@open-xchange/fastify-sdk/mariadb'
// From explicit options
const pool = createMariaDBPool({ host: 'localhost', database: 'mydb', user: 'root', password: '' })
// From env vars (SQL_HOST, SQL_PORT, SQL_DB, SQL_USER, SQL_PASS, SQL_CONNECTIONS)
// Optional TLS via SQL_SSL=true, SQL_SSL_CA (PEM contents),
// SQL_SSL_REJECT_UNAUTHORIZED=false (disable cert verification)
const pool = createMariaDBPoolFromEnv()
// Multi-database from env (DB_<NAME>_HOST, DB_<NAME>_PORT, etc.)
// Optional TLS via DB_<NAME>_SSL=true, DB_<NAME>_SSL_CA, DB_<NAME>_SSL_REJECT_UNAUTHORIZED=false
const pools = createMariaDBPoolFromEnv({ names: 'users,analytics' })
// Retry-based readiness check
await mariadbReadyCheck(pool, { retries: 12, delay: 10_000, logger })
// MariaDB UUID generation
const uuid = await createUUID(pool)PostgreSQL
import { createPostgresPool, createPostgresPoolFromEnv } from '@open-xchange/fastify-sdk/postgres'
// From env vars (DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD)
// Supports SSL via DATABASE_SSL, DATABASE_SSL_CA_PATH, etc.
const pool = createPostgresPoolFromEnv()Migrations (Umzug + MariaDB)
K8s migration job — use runMigrations() as the entry point for a Kubernetes migration job. Connects to all configured pools, waits for readiness, runs pending migrations, then exits:
// server/migrate.js (called by K8s job)
import { runMigrations } from '@open-xchange/fastify-sdk/migrations'
await runMigrations()CLI tool — use runMigrationsCLI() for interactive migration management (up, down, pending, executed, create):
// server/migrations/migrate.js
import { runMigrationsCLI } from '@open-xchange/fastify-sdk/migrations'
await runMigrationsCLI({ dirname: join(dirname(fileURLToPath(import.meta.url)), '..') })node server/migrations/migrate.js --db bimi up
node server/migrations/migrate.js --db bimi pending
node server/migrations/migrate.js --db bimi create --name add-indexesBoth functions use convention-based config by default: pool name "x" maps to glob migrations/x/*.mjs and table migrations_x.
Startup migration guard (graceful). When createApp is given database.migrations, it performs a read-only check (no DDL) that the schema is up to date before the app starts — execution is the migration job's responsibility. On a deploy the migration job typically races the app pod, so the app does not crash if migrations aren't applied yet: it polls (migrationRetries × migrationRetryDelay, default 30 × 10s = 5 min) with calm warn logs while the job catches up. If they're still not applied when the window elapses, it logs one clean line (no stack trace) and exits, so Kubernetes restarts the pod — which self-heals once the job completes.
This relies on the app exiting itself: liveness is served on
/liveand stays green during startup, so the kubelet will not kill a pod that is merely unready. If you instead want the app to wait indefinitely, setmigrationRetries: Infinityand configure astartupProbeon/readyso Kubernetes owns the deadline. With no startupProbe, infinite retries hang the pod silently.
Low-level API — for custom setups, use createMigrationRunner and executeMigrations directly:
import { createMigrationRunner, executeMigrations } from '@open-xchange/fastify-sdk/migrations'
const runner = createMigrationRunner({
pool,
migrationsGlob: 'src/migrations/*.mjs',
tableName: 'migrations',
logger
})
await executeMigrations(runner)Config (YAML + Joi + hot-reload)
import { createConfigRegistry, Joi, defaultTrue } from '@open-xchange/fastify-sdk/config'
const { registerConfigurationFile, getCurrent } = createConfigRegistry({ logger })
const schema = Joi.object({
features: Joi.object({ chat: defaultTrue }).default()
})
await registerConfigurationFile('config.yaml', { schema, watch: true }, (data) => {
Object.assign(config, data)
})
const current = getCurrent('config.yaml')Redis
import { createRedisClient, redisReadyCheck } from '@open-xchange/fastify-sdk/redis'
// Reads REDIS_HOSTS, REDIS_MODE (standalone|sentinel|cluster), REDIS_PASSWORD, etc.
const client = createRedisClient()
await redisReadyCheck(client)Testing
import { createTestApp, generateTokenForJwks, getJwks } from '@open-xchange/fastify-sdk/testing'
// Creates Fastify with CORS/Helmet/Metrics/Logging disabled, metrics server off
const app = await createTestApp({
dirname: import.meta.dirname,
routesDir: '../src/routes',
plugins: { jwt: true }
})
const token = await generateTokenForJwks({ userId: '1' }, 'kid', 'issuer.com')
const jwks = await getJwks('kid')Lint
Use @open-xchange/lint directly as a devDependency:
// eslint.config.js
import config from '@open-xchange/lint'
export default [
...config
]Logging
The SDK configures Pino with syslog-level mapping, redaction, and epoch timestamps. When running in a TTY (e.g. local development), logs are automatically pretty-printed with colors — no pino-pretty pipe needed.
Override TTY detection with LOG_PRETTY:
LOG_PRETTY=true— force pretty printing (useful in CI or non-TTY environments)LOG_PRETTY=false— force JSON output
Re-exports
These are re-exported so consuming projects don't need to install them separately:
import { fastify, fp, pino, promClient, createError, jose } from '@open-xchange/fastify-sdk'Plugins
All plugins are registered automatically by createApp(). See the plugin reference for detailed behavioral documentation including lifecycle tables, shutdown sequences, JWT modes, and code examples.
| Plugin | Default | Summary |
|---|---|---|
| CORS | enabled | Reads ORIGINS env var (comma-separated) |
| Helmet | enabled | Standard security headers |
| Logging | enabled | Request/response hooks (trace + debug level) |
| Metrics server | enabled | Separate server on port 9090: /live, /ready, /metrics |
| Metrics plugin | enabled | fastify-metrics collectors (no endpoint on main app) |
| Sensible | always | @fastify/sensible convenience utilities |
| JWT | enabled | JWKS verification via jose, OIDC discovery, custom key resolver |
| Session auth | disabled | App Suite session forwarding (same-domain); resolves identity via the MW /user/me. MW URL from APP_SUITE_API or in-cluster K8s discovery. Pair with jwt: false |
| Static files | disabled | @fastify/static with pre-compressed support |
| Swagger | enabled | Gated by EXPOSE_API_DOCS=true, serves /api-docs |
The SDK runs Fastify standalone with two ports: 9090 (metrics/health, starts in createApp()) and 8080 (app routes, starts in app.start()). See Running standalone for the full lifecycle explanation.
LLM integration reference
An LLM-optimized reference ships with the package at llm-reference.md. Point your LLM coding assistant at it, e.g. in a CLAUDE.md:
@node_modules/@open-xchange/fastify-sdk/llm-reference.mdDevelopment
See docs/development/ for contributor documentation, project structure, and conventions.
License
AGPL-3.0-or-later
