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

@nebutra/tenant

v4.0.1

Published

> **Status: Foundation** — Type definitions, factory pattern, and provider stubs are complete. Provider implementations require external service credentials to activate. See inline TODOs for integration points.

Readme

Status: Foundation — Type definitions, factory pattern, and provider stubs are complete. Provider implementations require external service credentials to activate. See inline TODOs for integration points.

@nebutra/tenant

Multi-tenancy context and isolation for the Nebutra platform.

Provides:

  • Request-scoped tenant context via AsyncLocalStorage (zero-copy, type-safe)
  • Tenant resolution strategies (header, subdomain, path, JWT, API key)
  • Middleware integration (Hono + Next.js)
  • Database isolation (RLS, schema-per-tenant, database-per-tenant)
  • Deterministic RLS SQL generation for migration-reviewed shared-schema policies
  • React hooks for client-side tenant access

Installation

pnpm add @nebutra/tenant

Quick Start

1. Set Tenant Context in Middleware

Hono (API Gateway)

import { Hono } from "hono";
import { tenantMiddleware } from "@nebutra/tenant/middleware";
import { getCurrentTenant } from "@nebutra/tenant";

const app = new Hono();

// Resolve tenant from x-tenant-id header
app.use(
  tenantMiddleware({
    headerName: "x-tenant-id",
    requireTenant: true,
  })
);

app.get("/api/data", async (c) => {
  const tenant = getCurrentTenant();
  return c.json({ tenantId: tenant.id });
});

export default app;

Next.js API Route

// pages/api/data.ts
import { withTenant } from "@nebutra/tenant/middleware";
import { getCurrentTenant } from "@nebutra/tenant";

export default withTenant(async (req, res) => {
  const tenant = getCurrentTenant();
  res.json({ tenantId: tenant.id });
});

Next.js Server Action

// lib/actions.ts
"use server";
import { withServerAction } from "@nebutra/tenant/middleware";
import { getCurrentTenant } from "@nebutra/tenant";
import { getSession } from "@/lib/auth";

export const fetchUserData = withServerAction(
  async (userId: string) => {
    const tenant = getCurrentTenant();
    const user = await db.user.findUnique({
      where: { id: userId, tenantId: tenant.id },
    });
    return user;
  },
  async () => {
    const session = await getSession();
    return session?.tenantId || null;
  }
);

2. Access Tenant Context

In Server Code

import { getCurrentTenant, getTenantOrNull } from "@nebutra/tenant";

// Throws error if tenant not found
const tenant = getCurrentTenant();
console.log(tenant.id);

// Returns null if tenant not found
const tenantOpt = getTenantOrNull();
if (tenantOpt) {
  console.log(tenantOpt.id);
}

In Client Components

"use client";
import { useTenant, useTenantId } from "@nebutra/tenant/react";
import { TenantProvider } from "@nebutra/tenant/react";

// In layout or root component
export default function RootLayout({ tenant, children }) {
  return (
    <TenantProvider value={tenant}>
      {children}
    </TenantProvider>
  );
}

// In child components
function Dashboard() {
  const tenantId = useTenantId();
  return <div>Tenant: {tenantId}</div>;
}

3. Database Isolation (Shared Schema + RLS)

import { PrismaClient } from "@prisma/client";
import { withRls } from "@nebutra/tenant/isolation";
import { getCurrentTenant } from "@nebutra/tenant";

const prisma = new PrismaClient();

// In a request handler
const tenant = getCurrentTenant();
const client = withRls(prisma, tenant.id);

// All queries now enforce RLS policies
const users = await client.user.findMany();

Generate migration SQL for the backing PostgreSQL policies:

import { generateRlsPolicySql } from "@nebutra/tenant/isolation";

const sql = generateRlsPolicySql({
  tables: ["users", "audit_logs"],
});
// Review and apply through your migration tool.

Tenant Resolution Strategies

Header (Default)

import { tenantMiddleware } from "@nebutra/tenant/middleware";

app.use(
  tenantMiddleware({
    headerName: "x-tenant-id", // or "x-org-id", etc
  })
);

Subdomain

import { tenantMiddleware } from "@nebutra/tenant/middleware";

app.use(
  tenantMiddleware({
    subdomainPattern: "^([a-z0-9-]+)\\.app\\.nebutra\\.com$",
  })
);
// Resolves tenant ID from "acme.app.nebutra.com" → "acme"

URL Path

import { tenantMiddleware } from "@nebutra/tenant/middleware";

app.use(
  tenantMiddleware({
    pathPrefix: "/org/:tenantId",
  })
);
// Resolves tenant ID from "/org/acme/dashboard" → "acme"

JWT Claim

import { tenantMiddleware } from "@nebutra/tenant/middleware";

app.use(
  tenantMiddleware({
    jwtClaimName: "tenant_id", // or "org_id", etc
  })
);

API Key (Custom)

import { fromApiKey, compose } from "@nebutra/tenant";
import { tenantMiddleware } from "@nebutra/tenant/middleware";

const keyResolver = fromApiKey(async (apiKey) => {
  const key = await db.apiKey.findUnique({
    where: { key: apiKey },
  });
  return key?.tenantId ?? null;
});

app.use(
  tenantMiddleware({
    resolver: keyResolver,
  })
);

Composite (Fallback Chain)

import { compose, fromHeader, fromSubdomain, fromPath } from "@nebutra/tenant";
import { tenantMiddleware } from "@nebutra/tenant/middleware";

const resolver = compose(
  fromHeader("x-tenant-id"),
  fromSubdomain("^([a-z0-9-]+)\\.app\\.nebutra\\.com$"),
  fromPath("/org/:tenantId")
);

app.use(
  tenantMiddleware({
    resolver,
  })
);
// Tries header first, then subdomain, then path

Isolation Strategies

Shared Schema + RLS (Default)

Single PostgreSQL schema with Row-Level Security (RLS) policies enforced at the database level.

import { withRls } from "@nebutra/tenant/isolation";

const prisma = new PrismaClient();
const client = withRls(prisma, tenantId);

// All queries filtered by RLS policy:
// WHERE current_setting('app.current_tenant_id') = table.tenant_id

Schema Per Tenant

Separate PostgreSQL schema per tenant (e.g., org_acme_public, org_customer_public).

import { getTenantSchema } from "@nebutra/tenant/isolation";

const schemaName = getTenantSchema("acme-corp");
// Returns: "org_acme_corp_public"

// Use with migration tool:
// pnpm prisma migrate deploy --schema org_acme_corp_public

Types

TenantContext

Runtime tenant context passed through requests.

interface TenantContext {
  id: string; // Unique tenant identifier
  slug?: string; // URL-friendly slug
  plan?: "free" | "pro" | "enterprise"; // Subscription tier
  features?: string[]; // Feature flags
  limits?: Record<string, number>; // Rate limits and quotas
  metadata?: Record<string, unknown>; // Custom data
}

TenantInfo

Persistent tenant information, usually loaded from database.

interface TenantInfo {
  id: string;
  slug: string;
  name: string;
  plan: "free" | "pro" | "enterprise";
  createdAt: Date;
  settings: Record<string, unknown>;
  parentTenantId?: string; // For hierarchical orgs
}

TenantConfig

Configuration for tenant extraction and isolation.

interface TenantConfig {
  strategy?: "shared_schema" | "schema_per_tenant" | "database_per_tenant";
  headerName?: string; // Default: "x-tenant-id"
  subdomainPattern?: string; // Regex for subdomain extraction
  pathPrefix?: string; // URL path prefix
  jwtClaimName?: string; // JWT claim name
  requireTenant?: boolean; // Throw error if not found (default: true)
  resolver?: TenantResolver; // Custom resolver function
}

API Reference

Context

  • runWithTenant(context, fn) — Execute function with tenant context
  • getCurrentTenant() — Get current tenant (throws if missing)
  • getTenantOrNull() — Get current tenant or null
  • requireTenant(tenant, context?) — Assertion helper
  • getCurrentTenantId() — Get tenant ID (throws if missing)
  • getTenantIdOrNull() — Get tenant ID or null

Resolvers

  • fromHeader(headerName?) — Resolve from HTTP header
  • fromSubdomain(pattern) — Resolve from subdomain regex
  • fromPath(prefix) — Resolve from URL path
  • fromJwtClaim(claimName) — Resolve from JWT claim
  • fromApiKey(lookupFn) — Resolve from API key lookup
  • compose(...resolvers) — Composite resolver with fallback

Middleware

  • tenantMiddleware(config) — Hono middleware
  • withTenant(handler, config) — Next.js API route wrapper
  • withServerAction(handler, getTenantId) — Next.js Server Action wrapper

Isolation

  • withRls(prisma, tenantId) — Apply RLS extension to Prisma
  • generateRlsPolicySql(options) — Generate deterministic PostgreSQL RLS policy SQL
  • applyTenantSession(tx, tenantId, options?) / tenantSessionOperations(prisma, tenantId, options?) — The tenant session core: the transaction-local SET LOCAL ROLE (from APP_DB_ROLE) and set_config('app.current_tenant_id', …, true) statements. withRls runs it as a batch transaction; withTenantContext in @nebutra/db/rls runs it inside an interactive transaction. One implementation, two shapes — new wrappers run the core rather than restating the statements. (getTenantDb in @nebutra/db still carries its own copy until the P1.2 follow-up routes it through tenantSessionOperations.)
  • resolveRlsRole(env?) / isValidDbRole(role) — APP_DB_ROLE resolved as a bare SQL identifier (permissive: null on invalid)
  • resolveRlsRoleOrThrow(env?) — the same resolution, but throws TenantIsolationError when APP_DB_ROLE is set to something that is not a bare SQL identifier, instead of returning null. applyTenantSession/tenantSessionOperations resolve the role through this when no explicit role option is given, and so does withRls (closure P1.3 — an unusable APP_DB_ROLE refuses to run rather than silently dropping the role switch)
  • TENANT_SESSION_SETTING / TENANT_SESSION_EXPRESSION — the setting key the policies read and the wrappers write
  • getTenantSchema(tenantId) — Get schema name for schema-per-tenant
  • TenantAwarePrismaClient — Wrapper class for tenant isolation
  • createTenantPrismaProxy(prisma, tenantId, strategy) — Factory for isolation proxies

React

  • TenantProvider — Context provider component
  • useTenant() — Hook to get current tenant
  • useTenantOrNull() — Hook to get current tenant or null
  • useTenantId() — Hook to get tenant ID
  • useTenantIdOrNull() — Hook to get tenant ID or null
  • useTenantPlan() — Hook to get tenant plan tier
  • useTenantFeature(feature) — Hook to check feature flag
  • useTenantLimit(limitName, defaultValue?) — Hook to get rate limit
  • withTenantGuard(Component, errorFallback?) — HOC requiring tenant
  • TenantBoundary — Component requiring tenant context

Errors

TenantRequiredError

Thrown when tenant context is required but not found.

import { TenantRequiredError } from "@nebutra/tenant";

try {
  const tenant = getCurrentTenant();
} catch (err) {
  if (err instanceof TenantRequiredError) {
    console.log(err.statusCode); // 400
  }
}

TenantIsolationError

Thrown when database isolation fails.

import { TenantIsolationError } from "@nebutra/tenant";

try {
  const schema = getTenantSchema(invalidId);
} catch (err) {
  if (err instanceof TenantIsolationError) {
    console.log(err.strategy); // "schema_per_tenant"
  }
}

Examples

Multi-Tenant SaaS Dashboard

// middleware.ts
import { tenantMiddleware } from "@nebutra/tenant/middleware";
import { compose, fromHeader, fromSubdomain } from "@nebutra/tenant";

const resolver = compose(
  fromHeader("x-tenant-id"),
  fromSubdomain("^([a-z0-9-]+)\\.app\\.nebutra\\.com$")
);

export const middleware = tenantMiddleware({ resolver });

// pages/api/dashboard/stats.ts
import { getCurrentTenant } from "@nebutra/tenant";
import { withRls } from "@nebutra/tenant/isolation";

export default withTenant(async (req, res) => {
  const tenant = getCurrentTenant();
  const prisma = withRls(db, tenant.id);

  const stats = await prisma.stat.aggregate({
    where: { tenantId: tenant.id },
  });

  res.json(stats);
});

// components/Dashboard.tsx
"use client";
import { useTenantId } from "@nebutra/tenant/react";

export function Dashboard() {
  const tenantId = useTenantId();
  return <div>Dashboard for {tenantId}</div>;
}

Feature-Gated Functionality

"use client";
import { useTenantFeature, useTenantPlan } from "@nebutra/tenant/react";

function AdvancedAnalytics() {
  const hasAdvanced = useTenantFeature("advanced_analytics");
  const plan = useTenantPlan();

  if (!hasAdvanced && plan === "free") {
    return <UpgradePrompt />;
  }

  return <AnalyticsPanel />;
}

See Also