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

Published

Shared foundation package for OX App Suite Node.js services

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

Both 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 /live and 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, set migrationRetries: Infinity and configure a startupProbe on /ready so 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.md

Development

See docs/development/ for contributor documentation, project structure, and conventions.

License

AGPL-3.0-or-later