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

@molecule/api-multi-tenancy-schema

v1.0.1

Published

Schema-based multi-tenancy provider for molecule.dev — tenant isolation via database schemas with HTTP header middleware

Readme

@molecule/api-multi-tenancy-schema

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Multi-tenancy provider for molecule.dev (@molecule/api-multi-tenancy-schema).

Implements the TenancyProvider interface as a request-scoped tenant-context tracker: an active-tenant context (AsyncLocalStorage), an in-process tenant registry, and header-based tenant resolution (x-tenant-id by default). Despite the package name it does NOT create or select database schemas and does NOT scope queries — it provides tenant CONTEXT, not data isolation. The application must isolate its own data from getTenant(); see the remarks for what "schema" does and does not mean here.

Quick Start

import { setProvider, getTenantMiddleware } from '@molecule/api-multi-tenancy'
import { provider, createProvider } from '@molecule/api-multi-tenancy-schema'

// Wire the provider at startup (default config)
setProvider(provider)

// SECURE wiring: authorize the header against the authenticated principal.
// `req.user` is populated by your auth middleware mounted earlier in the chain.
const secureProvider = createProvider({
  tenantHeader: 'x-org-id',
  resolveAuthorizedTenantIds: (req) => {
    const user = req.user as { tenantIds?: string[] } | undefined
    return user?.tenantIds ?? []
  },
})
setProvider(secureProvider)
// app.use(authMiddleware, getTenantMiddleware())

// ISOLATION IS YOUR JOB: this bond only tracks the active tenant. In your
// data layer, scope every query by getTenant() — e.g.:
//   import { getTenant } from '@molecule/api-multi-tenancy'
//   store.findMany('records', { where: { tenantId: getTenant() } })

Type

provider

Installation

npm install @molecule/api-multi-tenancy-schema @molecule/api-multi-tenancy

API

Interfaces

SchemaConfig

Configuration options for the multi-tenancy provider.

NOTE: this provider tracks tenant context only — it does no database work and does not scope queries. There is intentionally no schemaPrefix option, because no schema is ever created or selected; per-tenant DATA isolation is the application's responsibility (filter queries by getTenant()).

interface SchemaConfig {
  /**
   * The HTTP header name used to extract the tenant identifier from
   * incoming requests. Case-insensitive (headers are lowercased).
   *
   * @default 'x-tenant-id'
   */
  tenantHeader?: string

  /**
   * Default tenant ID to use when no tenant is resolved from the request.
   * If not set and no tenant is found, the middleware returns a 400 error.
   *
   * SECURITY: this value is *server-supplied configuration* (trusted) — unlike
   * the request header, it is not attacker-controlled, so it is activated
   * without the membership/existence checks applied to header-derived tenants.
   * Only set this to a tenant every unauthenticated caller is allowed to use.
   */
  defaultTenantId?: string

  /**
   * Resolver that returns the tenant id(s) the *authenticated principal* is a
   * member of, used to authorize the (attacker-controlled) tenant header.
   *
   * SECURITY: the tenant header is client-supplied and MUST NOT be trusted on
   * its own — any caller can send `x-tenant-id: <victim-tenant>`. When this
   * resolver is provided, the middleware rejects (403) every request whose
   * header tenant is not among the ids it returns. When it is omitted, the
   * raw-header middleware is *unauthenticated* and must be composed strictly
   * behind your own auth + tenant-membership gate (see the module `@remarks`).
   */
  resolveAuthorizedTenantIds?: AuthorizedTenantResolver

  /**
   * [M5-2] Opt-in to honor the raw (attacker-controlled) tenant header WITHOUT a
   * `resolveAuthorizedTenantIds` resolver. Default `false` (secure by default): when no
   * resolver is configured the middleware refuses (403) to activate a header-named tenant,
   * because trusting the bare header lets any caller send `x-tenant-id: <victim-tenant>`
   * and read/write another tenant's data (cross-tenant IDOR). Set to `true` ONLY when the
   * middleware is mounted strictly behind your own auth + tenant-membership gate that has
   * already validated the header — an explicit, audited choice, not the default.
   */
  allowUnauthorizedTenantHeader?: boolean
}

Types

AuthorizedTenantResolver

Resolves the tenant id(s) the authenticated principal of a request is permitted to act as — typically read from a verified session/JWT on the request (e.g. req.user.tenantIds), never from the client-supplied header.

type AuthorizedTenantResolver = (
  req: TenancyRequest,
) => string | string[] | null | undefined | Promise<string | string[] | null | undefined>

Functions

createProvider(config)

Creates a multi-tenancy provider: request-scoped tenant context, an in-process tenant registry, and secure-by-default header-resolution middleware. It performs no database work and does not scope queries — the application must isolate tenant data itself from getTenant() (see the module docs).

function createProvider(config?: SchemaConfig): TenancyProvider
  • config — Provider configuration.

Returns: A TenancyProvider providing request-scoped tenant context (it does NOT isolate data by itself).

runWithTenant(tenantId, fn)

Runs fn inside a fresh tenant context scope. Use this to establish a tenant for code that runs outside the HTTP middleware (background jobs, scripts, tests) so getTenant()/setTenant() resolve correctly.

function runWithTenant(tenantId: string, fn: () => T): T
  • tenantId — The tenant id to activate for the duration of fn.
  • fn — The function to run within the tenant scope.

Returns: Whatever fn returns.

Constants

provider

Default multi-tenancy provider instance — request-scoped tenant context, an in-process tenant registry, and secure header middleware. Does no database schema work; the application isolates its own data from getTenant().

Lazily initializes on first property access with default configuration.

const provider: TenancyProvider

Core Interface

Implements @molecule/api-multi-tenancy interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-multi-tenancy'
import { provider } from '@molecule/api-multi-tenancy-schema'

export function setupMultiTenancySchema(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-multi-tenancy ^1.0.1

Runtime Dependencies

  • @molecule/api-multi-tenancy

Security model — read before mounting the middleware.

  1. Tenant context is request-scoped (AsyncLocalStorage), not a module global. getTenant() always returns the current request's tenant, even across awaits under concurrency — no cross-request tenant bleed. setTenant() throws outside a request scope; use runWithTenant() for background jobs.
  2. The tenant header is attacker-controlled — secure by default ([M5-2]). Any caller can send x-tenant-id: <victim-tenant>. Without a resolveAuthorizedTenantIds resolver the middleware REFUSES (403) to honor the header at all — so the default never grants cross-tenant access. To authorize the header, pass resolveAuthorizedTenantIds (rejects 403 when the header tenant is not one the authenticated principal is a member of); or, if you gate membership upstream, set allowUnauthorizedTenantHeader: true to opt into the raw-header path and mount the middleware strictly behind that gate. Either way the middleware also validates the header tenant exists and is active (404/403 otherwise) before activating it.
  3. Tenant records are IN-MEMORY and this provider does no database work. createTenant() writes to a per-process Map — tenants are lost on restart (the middleware then 404s every header tenant until they are re-created) and are NOT shared across instances. No database schema is created or selected, and queries are NOT scoped for you: enforcing per-tenant DATA isolation is the application's job — read getTenant() in your data layer and filter every query by it (e.g. a tenant_id column). The package name refers to the intended schema-per-tenant strategy; the actual schema DDL / search_path scoping is not implemented here (there is no schemaPrefix option — it would only mislead), so treat this bond as a tenant-context tracker, not a data-isolation boundary.

E2E Tests

Integration checklist — this bond provides the tenant context + a secure header middleware; it does NOT isolate data, so the app must scope its own queries. Drive the real UI (live preview, no mocks) and check every box:

  • [ ] Secure header handling: with resolveAuthorizedTenantIds wired, a request carrying a spoofed x-tenant-id for a tenant the authenticated caller is NOT a member of is rejected (403) and never activates that tenant; the same call with the caller's own tenant succeeds.
  • [ ] Request-scoped context: inside a request getTenant() returns that request's tenant across awaits, and two concurrent requests never see each other's tenant (no bleed).
  • [ ] App-enforced data isolation (THIS bond does not do it for you): every read/write path filters by getTenant() (e.g. a tenant_id column). Create records as tenant A, then as tenant B confirm none of A's data is visible or reachable anywhere B can look (lists, detail, search, exports), and vice-versa. A box you can't check is an isolation bug in YOUR data layer to fix, never a skip.
  • [ ] No IDOR across the boundary: as tenant B, hitting a record id that belongs to A returns 403/404, never A's data — your handlers re-check tenant membership server-side on every access, not just at list time.
  • [ ] Registry lifecycle: createTenant/listTenants/deleteTenant reflect the in-process registry; tenants are per-process and lost on restart, so back them with a persistent store before relying on them in production.