
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
@open-xchange/fastify-sdk
Advanced tools
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.
pnpm add @open-xchange/fastify-sdk
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.
| Import path | What it provides |
|---|---|
@open-xchange/fastify-sdk | createApp, createMetricsServer, createLogger, logger, pinoConfig, loadEnv, requireEnv, jwtAuthHook, 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 |
@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 |
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)
swagger: true | false | { enabled, openapi }, // Default: true (but only registers when EXPOSE_API_DOCS=true)
static: false | true | { root, preCompressed, ... }, // Default: false
},
metricsServer: true, // Default: true (separate Fastify on port 9090)
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 (SIGINT/SIGTERM/SIGHUP, uncaughtException, unhandledRejection)
shutdownDelay: 5000, // Default: 5000 (ms to wait before closing connections after signal)
}
Defaults applied:
requestIdLogLabel: 'requestId'disableRequestLogging: trueconnectionTimeout: 30000genReqId: () => randomUUID()createLogger(options)Returns a Pino logger with the standard OX configuration:
headers.authorization, headers.cookie, headers.host, key, password, salt, hashPass custom options to override defaults (e.g. createLogger({ level: 'debug' })).
loadEnv()Loads environment variables from .env.defaults then .env using Node's built-in process.loadEnvFile(). 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'])
}
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.
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)
const pool = createMariaDBPoolFromEnv()
// Multi-database from env (DB_<NAME>_HOST, DB_<NAME>_PORT, etc.)
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)
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()
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-indexes
Both functions use convention-based config by default: pool name "x" maps to glob migrations/x/*.mjs and table migrations_x.
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)
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')
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)
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')
Use @open-xchange/lint directly as a devDependency:
// eslint.config.js
import config from '@open-xchange/lint'
export default [
...config
]
Foundation 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 outputThese 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'
Reads ORIGINS env var (comma-separated). Defaults: methods GET, POST, maxAge 86400.
Standard security headers. contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, crossOriginOpenerPolicy: same-origin-allow-popups.
preHandler: trace-logs request bodyonResponse: debug-logs URL, status, responseTime (includes headers at trace level)A separate Fastify instance on port 9090, serving health probes and Prometheus metrics.
| Endpoint | Purpose | Response |
|---|---|---|
GET /live | K8s liveness probe | 200 {"status":"ok"} or 503 {"status":"error"} |
GET /ready | K8s readiness probe | 200 {"status":"ok"} or 503 {"status":"error"} |
GET /metrics | Prometheus scraping | Prometheus text format |
/live runs checks registered via registerHealthCheck(). /ready runs checks registered via registerReadinessCheck(). With no checks registered, /live returns 200.
Startup lifecycle (matches lightship behavior): The metrics server starts at the very beginning of createApp(), before any plugins, database readiness checks, or other initialization. This ensures K8s liveness probes work immediately while the app initializes. /ready returns 503 until app.start() completes — a built-in readiness check gates it automatically. This means consuming projects can safely perform long-running initialization (Redis, S3, DB, config) between createApp() and app.start() without K8s killing the pod.
| Phase | /live | /ready |
|---|---|---|
During createApp() (plugins, DB readiness, etc.) | 200 | 503 |
Between createApp() and app.start() (custom init) | 200 | 503 |
After app.start() | 200 | 200 |
Graceful shutdown (default: enabled via processHandlers: true): The SDK registers handlers for SIGINT, SIGTERM, and SIGHUP (plus uncaughtException and unhandledRejection). When a signal is received:
/ready flips to 503 immediately — K8s stops routing new traffic to this podshutdownDelay ms (default 5000) — gives K8s time to remove the pod from service endpoints so in-flight requests completeapp.close() runs — triggers all onClose hooks (database pool cleanup, config watcher teardown, metrics server shutdown, etc.)| Phase | /live | /ready |
|---|---|---|
| Signal received | 200 | 503 |
| During shutdown delay | 200 | 503 |
After app.close() | — | — |
This matches lightship's shutdownDelay: 5000 behavior. Set shutdownDelay: 0 in tests that trigger signal-based shutdown to avoid slow teardown.
All signal listeners are named functions and are removed in the onClose hook to prevent MaxListenersExceededWarning.
Disable with processHandlers: false (used by createTestApp() to skip signal handlers in tests).
Disable with metricsServer: false (used by createTestApp() to avoid port binding in tests).
Port 9090 is hardcoded to match all existing K8s probe and Prometheus configs.
Registers fastify-metrics collectors on the main app (request duration, etc.) but does not serve an endpoint — metrics are read from prom-client's registry by the metrics server on port 9090.
Registers @fastify/sensible, providing reply.notFound(), reply.badRequest(), app.httpErrors, request.to(), and other convenience utilities on every app.
JWKS-based JWT verification using jose.createRemoteJWKSet. Verifies tokens against remote JWKS endpoints with OIDC discovery support. Also supports a custom key resolver for project-specific verification (e.g. local X.509 certificates).
Env vars:
OIDC_ISSUER — comma-separated allowed issuers (e.g. auth.example.com, *.example.org). Supports wildcard subdomains.Modes:
OIDC_ISSUER | key option | Behavior |
|---|---|---|
| Set | — | OIDC/JWKS verification via createRemoteJWKSet |
| Not set | Provided | Custom key resolver (e.g. local certificates) |
| Not set | Not provided | app.verifyJWT always returns 401 |
OIDC mode (plugins: { jwt: true } with OIDC_ISSUER set):
iss claim from the JWT and checks it against the allowlist.well-known/openid-configuration) to find jwks_uri.well-known/jwks.json if discovery is unavailablejose.createRemoteJWKSet for key resolution — jose handles caching and key rotation internallyCustom key mode (plugins: { jwt: { key: fn } } without OIDC_ISSUER):
For services that verify tokens against project-specific keys (e.g. local X.509 certificates):
import { getLocalPublicKey } from './modules/jwks.js'
const app = await createApp({
plugins: {
jwt: { key: (request, token) => getLocalPublicKey(token) }
}
})
The key function receives (request, token) where token has header (with kid, alg) and payload (with iss, sub, etc.). It should return the verification key (SPKI public key string or {} to reject).
Decorators:
app.verifyJWT — async function for use as onRequest hook. Returns 401 if token is invalid or JWT is not configured.request.jwtVerify() — verifies the Bearer token and sets request.user to the JWT payload. Available when OIDC_ISSUER is set or key is provided.jwtAuthHook — ready-made autohook that requires a valid JWT on every request in the encapsulated scope. Use as the default export of an autohooks.js file to protect all routes in that directory:
// src/routes/api/autohooks.js
export { jwtAuthHook as default } from '@open-xchange/fastify-sdk'
This is equivalent to:
export default async function (app) {
app.addHook('onRequest', app.verifyJWT)
}
Serves static files using @fastify/static. Defaults to public/ directory with pre-compressed file support.
plugins: {
static: true, // Serve from public/ with preCompressed: true
static: { root: 'dist', prefix: '/assets' } // Customize
}
EXPOSE_API_DOCS=true)Conditional on enabled option. Serves Swagger UI at /api-docs.
Foundation runs Fastify standalone. Health endpoints, metrics, and logging are all built in:
import { createApp } from '@open-xchange/fastify-sdk'
// Metrics server starts here — K8s /live probe works immediately.
// /ready returns 503 until app.start() completes.
const app = await createApp({ dirname: import.meta.dirname })
// Safe to perform long-running initialization here.
// K8s sees /live → 200 (pod is alive) and /ready → 503 (not accepting traffic yet).
// await connectRedis()
// await loadConfig()
// Start listening. /ready flips to 200 after this completes.
await app.start()
This starts:
createApp(): GET /live (200), GET /ready (503 until app.start()), GET /metricsroutes/), with CORS, Helmet, logging, JWT — starts in app.start()pnpm install
pnpm test
pnpm lint
AGPL-3.0-or-later
FAQs
Shared foundation package for OX App Suite Node.js services
The npm package @open-xchange/fastify-sdk receives a total of 887 weekly downloads. As such, @open-xchange/fastify-sdk popularity was classified as not popular.
We found that @open-xchange/fastify-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 14 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.