New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

@open-xchange/fastify-sdk

Package Overview
Dependencies
Maintainers
14
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@open-xchange/fastify-sdk

Shared foundation package for OX App Suite Node.js services

latest
npmnpm
Version
0.6.2
Version published
Weekly downloads
925
-61.39%
Maintainers
14
Weekly downloads
 
Created
Source

@open-xchange/fastify-sdk

coverage

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-sdk

Quick 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.

Exports

Import pathWhat it provides
@open-xchange/fastify-sdkcreateApp, createMetricsServer, createLogger, logger, pinoConfig, loadEnv, requireEnv, jwtAuthHook, metricsPlugin, health check helpers, re-exports of fastify, fp, pino, promClient, createError, jose
@open-xchange/fastify-sdk/mariadbcreateMariaDBPool, createMariaDBPoolFromEnv, getMariaDBPools, mariadbReadyCheck, createUUID
@open-xchange/fastify-sdk/postgrescreatePostgresPool, createPostgresPoolFromEnv
@open-xchange/fastify-sdk/migrationscreateMigrationRunner, executeMigrations, runMigrations, runMigrationsCLI, checkPendingMigrations, ensureMigrationsTable, normalizeMigrationsConfig
@open-xchange/fastify-sdk/configcreateConfigRegistry, readConfigurationFile, validateAndUpdateConfiguration, fileExists, getConfigPath, Joi helpers (defaultTrue, defaultFalse, customString, optionalCustomString, customURL), Joi
@open-xchange/fastify-sdk/rediscreateRedisClient, createRedisClientFromEnv, redisReadyCheck
@open-xchange/fastify-sdk/testingcreateTestApp, 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)
    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: true
  • connectionTimeout: 30000
  • genReqId: () => 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.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'])
}

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)
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)

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-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)

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

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 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

CORS (default: enabled)

Reads ORIGINS env var (comma-separated). Defaults: methods GET, POST, maxAge 86400.

Helmet (default: enabled)

Standard security headers. contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, crossOriginOpenerPolicy: same-origin-allow-popups.

Logging (default: enabled)

  • preHandler: trace-logs request body
  • onResponse: debug-logs URL, status, responseTime (includes headers at trace level)

Metrics server (default: enabled)

A separate Fastify instance on port 9090, serving health probes and Prometheus metrics.

EndpointPurposeResponse
GET /liveK8s liveness probe200 {"status":"ok"} or 503 {"status":"error"}
GET /readyK8s readiness probe200 {"status":"ok"} or 503 {"status":"error"}
GET /metricsPrometheus scrapingPrometheus 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.)200503
Between createApp() and app.start() (custom init)200503
After app.start()200200

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 pod
  • The process waits shutdownDelay ms (default 5000) — gives K8s time to remove the pod from service endpoints so in-flight requests complete
  • app.close() runs — triggers all onClose hooks (database pool cleanup, config watcher teardown, metrics server shutdown, etc.)
Phase/live/ready
Signal received200503
During shutdown delay200503
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.

Metrics plugin (default: enabled)

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.

Sensible (always enabled)

Registers @fastify/sensible, providing reply.notFound(), reply.badRequest(), app.httpErrors, request.to(), and other convenience utilities on every app.

JWT (default: enabled)

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_ISSUERkey optionBehavior
SetOIDC/JWKS verification via createRemoteJWKSet
Not setProvidedCustom key resolver (e.g. local certificates)
Not setNot providedapp.verifyJWT always returns 401

OIDC mode (plugins: { jwt: true } with OIDC_ISSUER set):

  • Extracts the iss claim from the JWT and checks it against the allowlist
  • On first request per issuer, tries OIDC discovery (.well-known/openid-configuration) to find jwks_uri
  • Falls back to .well-known/jwks.json if discovery is unavailable
  • Uses jose.createRemoteJWKSet for key resolution — jose handles caching and key rotation internally

Custom 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)
}

Static files (default: disabled)

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
}

Swagger (default: enabled, gated by EXPOSE_API_DOCS=true)

Conditional on enabled option. Serves Swagger UI at /api-docs.

Running standalone

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:

  • Port 9090 (metrics server) — starts first in createApp(): GET /live (200), GET /ready (503 until app.start()), GET /metrics
  • Port 8080 (app) — your routes (auto-loaded from routes/), with CORS, Helmet, logging, JWT — starts in app.start()

Development

pnpm install
pnpm test
pnpm lint

License

AGPL-3.0-or-later

FAQs

Package last updated on 10 Apr 2026

Did you know?

Socket

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.

Install

Related posts