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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@giogaspa/fastify-multitenant

v1.2.0

Published

Fastify plugin for multi-tenancy

Readme

Fastify Multi-Tenancy Plugin v1

A flexible multi-tenancy plugin for Fastify that simplifies building multi-tenant applications by handling tenant detection, configuration, and resource isolation.

Examples:

Table of Contents

Features

Done

  • 🔍 Composable tenant detection via identifier strategies (e.g., headerIdentifierStrategy, queryIdentifierStrategy, cookieIdentifierStrategy,...)
  • 🧩 Register tenant-specific resources (DB, Mailer, OpenAI, in-memory stores, etc.)
  • 🔁 Access other tenant resources inside a resource factory
  • ✨ Written in TypeScript with full type safety and autocompletion

In progress

  • Cache tenant configs and tenant resources with expiration and manual reset
  • 🪝 Lifecycle hooks per resource

In future/Maybe

  • 📈 Resources memory consumption

💡 The plugin is not tied to any specific database or ORM.

Installation

npm install @giogaspa/fastify-multitenant

Compatibility

| Plugin version | Fastify version | | ---------------|-----------------| | >=1.x | ^5.x | | ^0.x | ^4.x |

Quick Start

import { FastifyInstance, FastifyPluginAsync } from 'fastify'
import fastifyMultitenant, { headerIdentifierStrategy, FastifyMultitenantOptions, ResourceFactoryConfig } from '@giogaspa/fastify-multitenant'

declare module "fastify" {
    interface FastifyRequest {
        tenant: {
            db: TenantDatabase
            greeting: GreetingsClient
        }
    }
}

// Define custom tenant config TS type
type TenantConfigType = {
    id: string
    name: string
    dbConnectionUrl: string
}

// Define custom tenant resources TS type
type TenantResourcesType = {
    db: TenantDatabase
    greeting: GreetingsClient
}

// In this example we used a Map for simplicity, but you can manage the configurations however you prefer.
// Check the Playground folder for an example based on SQLite and Drizzle ORM.
const tenantConfigs = new Map<string, TenantConfig>()

export const app: FastifyPluginAsync = async function App(server: FastifyInstance) {

    const options: FastifyMultitenantOptions<TenantConfigType, TenantResourcesType> = {
        tenantIdentifierStrategies: [
            headerIdentifierStrategy('X-TENANT-ID'),
        ],
        tenantConfigResolver: async (tenantId) => {
            return tenantConfigs.get(tenantId)
        },
        resources: {
            // You can use the `createTenantResourceConfig` helper to create the configuration...
            ...createTenantResourceConfig({
                name: 'db',
                factory: async ({ tenantConfig }) => {
                    const dbClient = createTenantClient({ url: tenantConfig.dbConnectionUrl })

                    return dbClient
                },
                onDelete: async (resource) => {
                    // Here you can perform any cleanup if needed
                    server.log.debug(`[${resource}]: Deleting db resource`)
                }
            }),
            // ... or you can directly define the configuration object as specified by the type `TenantResourceFactory`
            'greeting': async ({ config, resources }: ResourceFactoryConfig<TenantConfig>) => {
                const tenantGreetings = await resources.db!.greetings.findMany()

                const greetingsClient = greetingFactory(config.id, tenantGreetings)

                return greetingsClient
            },
        }
    }

    await server.register(fastifyMultitenant, options)

    // Example of a tenant's route that uses the tenant greeting service
    server.get('/greetings', async (request) => {
        return {
            greeting: request.tenant.greeting(),
        }
    })

    // Example of a route that is excluded from multitenancy
    server.get(
        '/no-tenant',
        {
            config: {
                multitenant: {
                    exclude: true
                }
            }
        },
        async (request, reply) => {
            return { msg: 'No tenant route' }
        }
    )
}

How It Works

This plugin operates in a three-step process to manage multi-tenancy:

  1. Tenant Identification - Detects which tenant is making the request using configurable strategies
  2. Configuration Resolution - Retrieves tenant-specific configuration data (connection strings, API keys, etc.)
  3. Resource Initialization - Creates and caches tenant-specific resources based on the configuration

Tenant Identification

The plugin uses composable strategies to identify which tenant is making a request. You can configure multiple strategies that will be executed in sequence until one returns a valid tenant ID. By default, identification strategies are executed during the onRequest hook, but you can configure which hook to use with the hook option.

Built-in strategies

headerIdentifierStrategy(headerName: string): IdentifierStrategy
cookieIdentifierStrategy(cookieName: string): IdentifierStrategy
queryParamIdentifierStrategy(param: string): IdentifierStrategy

Example:

tenantIdentifierStrategies: [
  headerIdentifierStrategy('X-TENANT-ID'),
  queryParamIdentifierStrategy('tenant_id'),
]

⚠️ Order matters. First match wins.

Custom strategies

You can also write your own:

const myStrategy = () => (req) => req.headers['my-header'];

Configuration Resolution

After identifying the tenant ID through the configured strategies, the plugin invokes your tenantConfigResolver function to retrieve tenant-specific configuration data. This configuration serves as the foundation for initializing tenant-specific resources.

Purpose

The tenant configuration object should include information needed by the resource factories, e.g.:

  • Database connection string,
  • API keys (e.g., OpenAI, Stripe,...),
  • Feature flags or environment settings,
  • etc.

Example:

tenantConfigResolver: async (tenantId) => {
  ...

  return {
    id: tenantId,
    db_url: `postgres://.../${tenantId}`,
    openaiKey: 'sk-xxx',
    mailerConfig: { from: '[email protected]' }
  }
}

This configuration is retrieved once per tenant and then passed to all resource factories for that tenant, serving as the foundation for initializing tenant-specific resources such as databases, APIs, or services.

Clearing tenant configuration cache programmatically

await fastify.multitenant.configProvider.invalidate('tenantId');
await fastify.multitenant.configProvider.invalidateAll();

Resource Initialization

The plugin allows you to define and initialize tenant-specific resources (like databases, API clients, or services) that are automatically managed based on tenant context. Each resource is:

  1. Created on-demand when first needed
  2. Cached for subsequent requests to the same tenant
  3. Isolated between different tenants

Resources are defined as a collection of factory functions that can depend on previously initialized resources, creating a dependency chain.

Factory function signature

(args: { tenantConfig: TenantConfig, resources: Partial<TenantResources>}) => Promise<Resource>
  • tenantConfig: result of resolveTenantConfig(tenantId)
  • resources: other initialized resources so far (in declaration order)

Example:

resources: {
  db: {
    factory: async ({ config }) => new PrismaClient({ ... }),
  },
  mailer: async ({ tenantConfig, resources }) => {
      const settings = await resources.db.settings.findFirst();
      return createMailer({ ...config.mailerConfig, from: settings.sender });
  }
}

Plugin Options

| Option | Type | Description | |-----------------------------|------------------------------------------------------------|-----------------------------------------------------------------------------| | tenantIdentifierStrategies | Array<IdentifierStrategy> | Strategies to extract tenant ID from request. | | tenantConfigResolver | TenantConfigResolver<TenantConfig> | Fetch tenant-specific configuration. | | resources | TenantResourceConfigs<TenantConfig, TenantResources> | Defines how to create tenant-specific resources. | | hook | 'onRequest' \| 'preParsing' \| 'preValidation' \| 'preHandler' | Define in which lifecycle hook the current tenant is identified and its resources resolved. Default onRequest |

TenantResourceConfig<TenantConfig, TenantResources, Resource>

This type defines how to create and manage tenant-specific resources:

| Property | Type | Description | |------------------|-----------------------------------------|-------------------------------------------------------| | factory | TenantResourceFactory<TenantConfig, TenantResources, ResourceType> | Function that create the resource. | | onDelete? | TenantResourceOnDeleteHook<ResourceType> | Function that runs before a resource is deleted by resources provider. Here you can perform any cleanup if needed, es: close DB connection,... |

Plugin Decorators

| Property | Type | Description | |-----------------|---------------|------------------------------------------------| | request.tenant | Record<string, Resources> | Tenant-scoped resources for the current request. | | fastify.multitenant.resourceProvider.createAll | (tenantId: TenantId) => Promise<TenantResources \| undefined> | ... | | fastify.multitenant.resourceProvider.getAll | (tenantId: TenantId) => Promise<TenantResources \| undefined> | ... | | fastify.multitenant.resourceProvider.invalidate | (tenantId: TenantId) => Promise<void> | ... | | fastify.multitenant.resourceProvider.invalidateAll | () => Promise<void> | ... | | fastify.multitenant.configProvider.get | (tenantId: TenantId) => Promise<TenantConfig \| undefined> | ... | | fastify.multitenant.configProvider.invalidate | (tenantId: TenantId) => Promise<void> | Invalidate the cached configuration for a specific tenant. | | fastify.multitenant.configProvider.invalidateAll | () => Promise<void> | Invalidate all cached tenant configurations. |

Route Options

| Option | Type | Description | |-----------------------------|------------------------------------------------------------|-----------------------------------------------------------------------------| | exclude? | boolean | Skip the tenant identification process entirely. | | identifierStrategy? | IdentifierStrategy | Specify a custom tenant identification strategy function. |

The exclude option allows you to specify routes that should skip the tenant identification process entirely. When set to true for a specific route, the plugin won't attempt to identify the tenant or initialize tenant-specific resources for requests to that route.

This option is particularly useful for:

  • Public endpoints that don't require tenant-specific data,
  • Authentication endpoints before a tenant is known

Usage Example:

    server.get(
        '/no-tenant',
        {
            config: {
                multitenant: {
                    exclude: true
                }
            }
        },
        async (request, reply) => {
            return { message: 'This endpoint works without a tenant' }
        }

The identifierStrategy option allows you to specify a custom tenant identification method for individual routes. This overrides the global tenant identification strategies defined when registering the plugin.

    server.get(
        '/no-tenant',
        {
            config: {
                multitenant: {
                    identifierStrategy: (request) => {
                        // Route custom strategy for identify tenant id
                    }
                }
            }
        },

TypeScript: Declaration Merging

import 'fastify';
import type { PrismaClient } from '@prisma/client';
import type { OpenAI } from 'openai';

declare module 'fastify' {
  interface FastifyRequest {
    tenant: {
      db: PrismaClient;
      openai: OpenAI;
    };
  }
}

Advanced: Tenant Resources Context

Access tenant resources from anywhere in your code using the async local storage-based context:

| Method | Type | Description | |--------|------|-------------| | tenantResourcesContext.get(key) | (key: ResourceName) => unknown | Retrieves a specific tenant resource for the current request | | tenantResourcesContext.getAll() | () => TenantResourcesStore \| undefined | Gets all tenant resources for the current request |

Example:

import { tenantResourcesContext } from '@giogaspa/fastify-multitenant'

// This function can be called from anywhere in your application code
// and will have access to the current tenant's resources
async function getUsers() {
    const db = tenantResourcesContext.get('db');
    return await db.query('SELECT * FROM users');
}

// Use in a route handler
fastify.get('/users', async () => {
    return getUsers();
})

// Get all resources at once
fastify.get('/tenant-info', async () => {
    const resources = tenantResourcesContext.getAll();
    return {
        dbStatus: await resources.db.status(),
        apiConnected: resources.api.isConnected()
    };
})

Note: The tenant resources context is tied to the request lifecycle. Make sure you're accessing it within the scope of a request.

Performance Considerations

  • The plugin caches tenant configurations and resources to minimize overhead.
  • Resources are created on-demand and cached for subsequent requests.
  • The plugin uses AsyncLocalStorage to bind tenant resources to the current request context.

License

MIT