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

@zerotal/tenancy

v1.7.5

Published

Multi-tenancy primitives for Zerotal applications.

Readme

@zerotal/tenancy

First-class multi-tenancy: resolve the active tenant per request and scope ORM, storage, and cache.

Resolves the active tenant from every incoming request and makes it available everywhere in the call stack — ORM queries, storage paths, and cache keys — with no manual thread-through. Supports both single-database (a tenant_id column, scoped automatically by Tenantable) and multi-database (a connection per tenant, routed automatically from a one-line connect factory) strategies — in both, your models "just work" with no per-query wiring. Stable — the public API follows SemVer strictly for the rest of the 1.x line.

Part of the Zerotal framework. Requires Bun ≥ 1.3.14.

Installation

bun add @zerotal/tenancy

Setup

Register the provider in bootstrap/providers.ts:

import { TenancyProvider } from "@zerotal/tenancy";
import tenancyConfig from "../config/tenancy.ts";

export default [
  // …your other providers
  TenancyProvider.withConfig(tenancyConfig),
];

Configure it in config/tenancy.ts:

// config/tenancy.ts
import { TenancyConfig, SubdomainResolver } from "@zerotal/tenancy";
import { env } from "@zerotal/core";

export default TenancyConfig({
  strategy: env("TENANCY_STRATEGY", "single-database"),
  tenantColumn: "tenant_id",
  resolvers: [new SubdomainResolver("myapp.com")],
});

The tenant registry is owned by the package — the tenants and tenant_members tables are provisioned automatically on boot (no migration, no app Tenant model, no findTenant callback). Reach tenants through the Tenant facade.

Then add TenancyMiddleware to the global pipeline (or a route group):

import { TenancyMiddleware } from "@zerotal/tenancy";

Application.create({ providers }).use([TenancyMiddleware]);

Usage

Compose Tenantable via Model.using onto any tenant-owned model (the same flat form used for every other mixin). Every query is scoped with WHERE tenant_id = <current tenant> and create() injects the tenant_id:

import { Model, column, table } from "@zerotal/orm";
import { Tenantable } from "@zerotal/tenancy";

@table("projects")
export class Project extends Model.using(Tenantable) {
  @column() name!: string;
  @column() tenantId!: number;
}

// Inside a TenancyMiddleware boundary (tenant id = 7):
await Project.all(); // SELECT * FROM projects WHERE tenant_id = 7
await Project.create({ name: "A" }); // INSERT … (tenant_id, name) VALUES (7, 'A')

Bypass scoping when you need cross-tenant access, or run a job under a specific tenant:

import { TenantContext } from "@zerotal/tenancy";

await Project.query().withoutTenancy().get(); // all tenants

await TenantContext.run(tenant, async () => {
  // queries here run scoped to `tenant`
});

Access the active tenant anywhere in the async call chain, and scope storage and cache to it:

import { TenantContext, tenantDisk, tenantCache } from "@zerotal/tenancy";

const tenant = TenantContext.get(); // throws outside a boundary
const id = TenantContext.id(); // number | null

await tenantDisk().put("avatars/alice.jpg", buffer); // → tenants/<slug>/avatars/alice.jpg
await tenantCache().set("dashboard:stats", data, 300); // key: tenant:<slug>:dashboard:stats

The Tenant facade

The Tenant facade carries the day-to-day tenancy API — the current tenant, tenant CRUD, and membership (the tenant_members pivot). Mutations are admin-gated against the authenticated user; force* variants bypass the check for trusted code.

import { Tenant } from "@zerotal/tenancy";

// Current tenant (inside a TenancyMiddleware boundary)
Tenant.current(); // the active tenant, or null
Tenant.check(); // true when there is an active, enabled tenant
Tenant.id(); // number | null

// Lifecycle — the authenticated creator becomes the first admin member
const acme = await Tenant.create({ slug: "acme", name: "Acme Inc." });
await Tenant.update({ name: "Acme LLC" }); // requires the current user to be an admin
await Tenant.forceUpdate({ name: "Acme LLC" }); // bypasses the admin check
await Tenant.delete(); // admin-gated; removes the tenant + its memberships
await Tenant.forceDelete();

// Membership — members() hydrates full User models from the ORM registry
await Tenant.addMember(userId, { admin: true });
await Tenant.members(); // User[]
await Tenant.member(); // the authenticated user as a member, or null
await Tenant.isMember(userId);
await Tenant.isMemberAdmin(userId);
await Tenant.promote(userId);
await Tenant.demote(userId);
await Tenant.removeMember(userId);

// Reads + running work under a tenant
await Tenant.find("acme"); // by slug
await Tenant.findById(1);
await Tenant.exists("acme");
await Tenant.all();
await Tenant.forId(1, () => Project.all()); // run a callback inside tenant #1's context

Membership hydration needs an authenticatable User model — compose Authenticatable from @zerotal/auth on it. Tenancy discovers it via the ORM registry, so there's no hard dependency between the two packages.

Clean up tenant-owned data when a tenant is deleted:

import { Tenant } from "@zerotal/tenancy";

Tenant.onTenantDeleted(async (tenant) => {
  await Project.query().withoutTenancy().where("tenant_id", tenant.id).delete();
});

Multi-database

For the database-per-tenant strategy, set strategy: "multi-database" and supply a one-line connect factory. Every model query inside a tenant boundary is then routed to that tenant's connection automatically — no TenantManager wiring, no raw SQL, the same Project.all() you already write:

// config/tenancy.ts
import { TenancyConfig, SubdomainResolver } from "@zerotal/tenancy";
import { SQL } from "bun";

export default TenancyConfig({
  strategy: "multi-database",
  resolvers: [new SubdomainResolver("myapp.com")],
  connect: (tenant) => new SQL(`file:./storage/tenants/${tenant.database}`),
});

Routing is AsyncLocalStorage-scoped, so concurrent requests for different tenants stay isolated. See the tenancy guide for details.

Exports

  • Tenant — the facade: current tenant, tenant CRUD, and membership.
  • Tenancy — the service behind the facade (bound as "tenancy").
  • TenantModel — the internal tenant record (owned by the package; reach it via Tenant).
  • TenantDeletedHook — type of the Tenant.onTenantDeleted callback.
  • TenantContext — access the active tenant (get, tryGet, id, slug, run).
  • TenancyMiddleware — resolves the tenant per request.
  • TenancyProvider — wires tenancy (.withConfig(...)).
  • TenantManager / TenantManagerOptions — per-tenant connections (multi-database).
  • Resolvers: SubdomainResolver, HeaderResolver, PathResolver.
  • Tenantable — ORM mixin that scopes queries to the current tenant.
  • tenantDisk, tenantCache — tenant-scoped storage and cache helpers.
  • TenancyConfig, tenancyConfig — config factory.
  • Errors: TenantNotFoundError, TenantInactiveError, TenantForbiddenError, TenancyNotConfiguredError, TenancyConfigError, NoActiveTenantError.
  • Types: Tenant, MultiDbTenant, TenantResolver, TenantResolverResult, TenancyStrategy, TenancyConfigShape.

Documentation