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

@ferrow/multi-tenant-router

v1.0.0

Published

Tenant resolution with isolation: hostname, header, and path-based routing

Readme

multi-tenant-router

CI

Tenant resolution + isolation. Extract tenant from hostname (acme.app.com → acme), header (X-Tenant-Id), or path (/t/acme/...) in configurable order. Per-tenant registry with isolated get/set — no cross-tenant data leaks.

Quickstart

import { MultiTenantRouter } from 'multi-tenant-router';

const router = new MultiTenantRouter();

// Register resolvers in order
router.registerResolver(MultiTenantRouter.headerResolver('x-tenant-id'));
router.registerResolver(MultiTenantRouter.hostnameResolver(req));
router.registerResolver(MultiTenantRouter.pathResolver());

// Set fallback
router.setDefaultTenant('default');

// Resolve request
const { tenant, context } = router.resolve(req);

// Isolated tenant config
router.set(tenant, 'apiKey', 'secret123');
router.set(tenant, 'features', ['feature1', 'feature2']);

const config = router.get(tenant, 'apiKey'); // 'secret123'

API

Constructor

new MultiTenantRouter()

Methods

registerResolver(resolver)

Register a tenant resolver function. Resolvers run in order; first match wins.

router.registerResolver((req) => {
  // Return tenant string or null
  return req.headers['x-tenant-id'] || null;
});

hostnameResolver(req), headerResolver(headerName), pathResolver()

Built-in resolvers:

  • hostname: Extract subdomain from hostname (acme.app.com → acme)
  • header: Read header value (default: x-tenant-id)
  • path: Extract from path prefix /t/:tenant/
router.registerResolver(MultiTenantRouter.hostnameResolver(req));
router.registerResolver(MultiTenantRouter.headerResolver('x-tenant-id'));
router.registerResolver(MultiTenantRouter.pathResolver());

resolve(req)

Resolve tenant from request using registered resolvers in order.

Returns: { tenant: string, context: { source: 'resolver' | 'default' } }

Throws: UnknownTenantError if no tenant resolved and no default set

try {
  const { tenant, context } = router.resolve(req);
} catch (e) {
  // UnknownTenantError
}

get(tenant, key) / set(tenant, key, value)

Isolated per-tenant configuration. Data is never leaked between tenants.

router.set('acme', 'database', 'acme_db');
router.set('stripe', 'database', 'stripe_db');

router.get('acme', 'database'); // 'acme_db'
router.get('stripe', 'database'); // 'stripe_db' — no cross-tenant leak

setDefaultTenant(tenant)

Set fallback tenant if no resolver matches.

getTenants()

List all registered tenants.

Scope & Limits

  • Resolution only — doesn't route requests to handlers; you implement that
  • Isolation via registry — no kernel-level isolation; application must use tenant context
  • Async resolver not supported — all resolvers are sync (url-based only)
  • No wildcard matching — hostname matches first level only (acme.* not supported)
  • Memory-only — tenant configs not persisted

Example: Isolation Proof

const router = new MultiTenantRouter();

// Set up two tenants
router.set('acme', 'secret', 'acme-secret');
router.set('stripe', 'secret', 'stripe-secret');

// Verify isolation
console.log(router.get('acme', 'secret')); // 'acme-secret'
console.log(router.get('stripe', 'secret')); // 'stripe-secret'

// Different tenants cannot access each other's data
try {
  router.get('acme', 'secret');  // Works
  router.get('evil', 'secret');  // Throws UnknownTenantError
} catch (e) {
  console.log('Isolation verified:', e.message);
}

License

MIT


Sponsored by Ferrow


Part of the ferrow-toolkit collection · Sponsored by Ferrow