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

@nyalajs/tenancy

v2.1.0

Published

Multi-tenancy for NyalaJS SaaS applications: resolve which tenant a request belongs to, isolate its data, and — the part most frameworks don't give you — move a tenant between **shared** (row-level `tenant_id` isolation, one database) and **dedicated** (i

Readme

@nyalajs/tenancy

Multi-tenancy for NyalaJS SaaS applications: resolve which tenant a request belongs to, isolate its data, and — the part most frameworks don't give you — move a tenant between shared (row-level tenant_id isolation, one database) and dedicated (its own physical database) storage live, at runtime, with no redeploy.

The two isolation modes

| | Shared (default) | Dedicated | |---|---|---| | Storage | One database, tenant_id column on every tenant-owned table | One tenant, one physical database | | Enforcement | @nyalajs/database's Model auto-scopes every query, fail-closed | Same Model classes, routed to that tenant's own connection | | Cost | Lowest — shared infrastructure | Higher — one connection pool, one database per dedicated tenant | | When | The default for most tenants | Compliance/data-residency requirements, a premium plan tier, noisy-neighbor concerns |

A tenant's mode is a row in your own database (TenantRegistry), not a build-time or per-deployment choice — TenantMiddleware checks it on every request, and TenantMigrationService moves a tenant between modes live.

Quick start (shared mode)

import { Module } from "@nyalajs/core";
import { TenantMiddleware, SubdomainTenantResolver, HeaderTenantResolver } from "@nyalajs/tenancy";

@Module({
  providers: [
    { provide: "TENANT_RESOLVERS", useFactory: () => [new SubdomainTenantResolver(), new HeaderTenantResolver()] },
    { provide: "TENANT_REQUIRED", useValue: true },
    TenantMiddleware,
  ],
})
export class AppModule {}
// bootstrap/main.ts
app.use(app.get(TenantMiddleware));
import { Model, Table, Primary, Column, StringColumn } from "@nyalajs/database";

@Table("users")
export class User extends Model {
  @Primary() @StringColumn() id!: string;
  @Column({ name: "tenant_id" }) tenantId!: string; // this exact property name triggers automatic scoping
  @StringColumn() email!: string;
}

// Every Model.all()/find()/create()/save()/delete() call is now automatically
// scoped to the resolved tenant, and throws if none is active.

That's the whole setup for shared mode. Full walkthrough, resolver strategies, and the fail-closed guarantee's exact mechanics: see the docs site's Multi-Tenancy section.

Dedicated databases

import { TenantRegistry, TenantConnectionManager } from "@nyalajs/tenancy";

const registry = new TenantRegistry();
const connections = new TenantConnectionManager({ maxOpenConnections: 100 });

// Wire both into TenantMiddleware's 3rd/4th constructor args (both @Optional() —
// omit them entirely and the middleware behaves exactly as it always has).
const middleware = new TenantMiddleware(resolvers, required, registry, connections);

await registry.register({
  id: "acme",
  name: "Acme Corp",
  isolationMode: "dedicated",
  connectionString: process.env.ACME_DATABASE_URL!,
  driver: "pg",
});

From here, TenantMiddleware handles routing automatically: it resolves the tenant, sees it's dedicated, gets (or lazily opens, then pools/reuses) its connection via TenantConnectionManager, and runs the rest of the request inside @nyalajs/database's ConnectionContext — every Model call in your handler transparently targets acme's own database. No dedicated-mode-specific repository or query code; the same Model classes work unmodified for both modes.

Migrating a tenant live

import { TenantMigrationService } from "@nyalajs/tenancy";
import { User, Order, Invoice } from "../app/models";

const migrations = new TenantMigrationService(registry, connections);

// Upgrade: shared -> dedicated
await migrations.migrateToDedicated({
  tenantId: "acme",
  connectionString: process.env.ACME_DATABASE_URL!,
  driver: "pg",
  models: [User, Order, Invoice], // every tenant-scoped Model to move
  onProgress: (table, count) => console.log(`${table}: ${count} rows copied`),
});

// Downgrade: dedicated -> shared
await migrations.migrateToShared({ tenantId: "acme", models: [User, Order, Invoice] });

Both directions: provision/verify the target's schema, copy every listed table's rows in batches (reusing Model's own tenant-scoped read/write path — no hand-written WHERE/stamping logic), verify the row counts match on both sides, then atomically flip the tenant's registry entry. A verification mismatch aborts before cutover — the tenant stays on its current, working connection; live traffic is never routed to an unverified/incomplete target. The source side is never deleted automatically, in either direction.

What's included

  • TenantMiddleware — resolves the tenant (via pluggable TenantResolvers) and publishes it through TenantContext; optionally routes dedicated tenants through ConnectionContext too.
  • SubdomainTenantResolver, HeaderTenantResolver, JwtTenantResolver — three real resolution strategies, tried in the order you configure them.
  • TenantRegistry — CRUD + cached lookup over a real TenantRecord table (nyala_tenants), the source of truth for each tenant's isolation mode.
  • TenantConnectionManager — pooled, lazily-opened dedicated-tenant connections with LRU eviction and idle sweeping.
  • TenantMigrationService — live shared↔dedicated migration with schema provisioning, batched row copy, verification, and atomic cutover.
  • TenantRepository<T> — an abstract, Model-backed base class for hand-written tenant-scoped repositories that want guard helpers (ensureTenant()/getTenantId()) without reimplementing Model's own scoping.

What's NOT included

  • No schema-per-tenant mode — only shared (row-level) and dedicated (database-per-tenant) are built in. If you need Postgres search_path-based schema switching, you'd wire that yourself, similarly to how dedicated-mode connection routing works.
  • No cross-database joins — a dedicated tenant's data lives in a separate physical database; there's no query layer here that spans a dedicated tenant's database and the shared one in a single query.
  • No automatic schema-drift detection between your Model definitions and an already-provisioned dedicated database (autoCreateSchema: false trusts the target as-is).