npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@open-xchange/fastify-sdk

v0.4.0

Published

Shared foundation package for OX App Suite Node.js services

Downloads

1,004

Readme

@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.listen({ host: '0.0.0.0', port: 8080 })

That single call replaces ~500 lines of boilerplate (logger, CORS, Helmet, metrics, JWT, Swagger, autoload).

Exports

| Import path | What it provides | |---|---| | @open-xchange/fastify-sdk | createApp, createMetricsServer, createLogger, logger, loadEnv, requireEnv, jwtAuthHook, 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 | | @open-xchange/fastify-sdk/config | createConfigRegistry, readConfigurationFile, Joi helpers (defaultTrue, defaultFalse, customString, 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: false | true | { key },       // Default: false (see JWT section below)
    swagger: false | { enabled, openapi }, // Default: false
    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)

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.

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

  1. /ready flips to 503 immediately — K8s stops routing new traffic to this pod
  2. The process waits shutdownDelay ms (default 5000) — gives K8s time to remove the pod from service endpoints so in-flight requests complete
  3. app.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.

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

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

  1. Extracts the iss claim from the JWT and checks it against the allowlist
  2. On first request per issuer, tries OIDC discovery (.well-known/openid-configuration) to find jwks_uri
  3. Falls back to .well-known/jwks.json if discovery is unavailable
  4. 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() — from @fastify/jwt, 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: disabled)

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