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

v0.1.2

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.

Downloads

374

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

Database Per Tenant

Separate PostgreSQL database per tenant, with connection pooling.

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

const tenantDbUrl = getTenantDatabaseUrl("acme-corp");
// Returns: "postgresql://user:pass@localhost/nebutra_acme_corp"

// Create new Prisma client for this database:
const tenantPrisma = new PrismaClient({
  datasources: {
    db: { url: tenantDbUrl },
  },
});

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
  • getTenantSchema(tenantId) — Get schema name for schema-per-tenant
  • getTenantDatabaseUrl(tenantId, baseUrl?) — Get DB URL for database-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