JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 47
  • Score
    100M100P100Q92615F
  • License MIT

Row-level access control for PostgreSQL. Multi-tenant isolation, built-in auth, and embeddable UI components.

Package Exports

  • @tenence/sdk
  • @tenence/sdk/auth
  • @tenence/sdk/components
  • @tenence/sdk/core
  • @tenence/sdk/feature-flags
  • @tenence/sdk/middleware
  • @tenence/sdk/organizations

Readme

@tenence/sdk

Build secure, multi-tenant applications without worrying about data isolation. Tenence handles tenant separation at the database level using PostgreSQL Row Level Security — so you can focus on building your product.

For full documentation, guides, and tutorials, visit tenence.io/docs.

🚧 Under Construction — Early Access

Tenence is currently under construction and not yet publicly available. If you're interested in early access, join the waitlist on tenence.io to be notified when we launch.

Install

npm install @tenence/sdk

Peer dependencies (install if not already present):

npm install pg
npm install @openfeature/server-sdk  # only if using feature flags

Neon serverless alternative: If your database is on Neon and you're deploying to a serverless or edge environment, you can use @neondatabase/serverless instead of pg. There are two approaches:

  1. WebSocket Pool — The Neon driver provides a WebSocket-based Pool class that is API-compatible with pg.Pool. Use this with createClient when you need SET LOCAL-based tenant isolation (the standard approach).

  2. Neon HTTP driver with JWT auth — Use the stateless neon() HTTP driver with createNeonClient for fully serverless/edge deployments. Tenant isolation is enforced via JWT claims embedded in the auth token, with no persistent connection required.

npm install @neondatabase/serverless

Quick Start

The fastest way to get started is with the guided setup wizard:

npx tenence setup

This walks you through authenticating, selecting a project, connecting your database, installing the RLS stored procedures, and generating a config file — all in one step.

Or, if you prefer to do it step by step:

1. Authenticate

Log in to your Tenence account from the terminal (similar to gh auth login):

npx tenence auth login

A browser window will open asking you to approve the CLI. Once approved, your credentials are stored locally at ~/.config/tenence/auth.json.

2. Set up your database

Install the required PostgreSQL stored procedures:

npx tenence init

3. Connect your project

Register your database with the Tenence console:

npx tenence register

4. Create a client

import { createClient } from "@tenence/sdk";

const tenence = createClient({
  databaseUrl: process.env.DATABASE_URL,
});

5. Add the middleware

The middleware automatically connects each incoming request to the right tenant:

import { tenenceMiddleware } from "@tenence/sdk/middleware";

app.use(tenenceMiddleware({
  client: tenence,
  resolveContext: (req) => ({
    tenantId: req.user.orgId,
    userId: req.user.id,
    role: req.user.role,
  }),
}));

6. Query with automatic tenant isolation

Every database query is automatically scoped to the current tenant — no manual filtering needed:

app.get("/api/tasks", async (req, res) => {
  const result = await req.tenence.query("SELECT * FROM tasks");
  res.json(result.rows);
});

For lower-level control, use withConnection to get a raw pg client with tenant context already set:

app.get("/api/tasks", async (req, res) => {
  const rows = await req.tenence.withConnection(async (client) => {
    const result = await client.query("SELECT * FROM tasks WHERE status = $1", ["active"]);
    return result.rows;
  });
  res.json(rows);
});

Neon Serverless with JWT Auth

For fully stateless serverless or edge deployments on Neon, use createNeonClient with the Neon HTTP driver. Instead of SET LOCAL session variables, tenant isolation is enforced through JWT claims embedded in the auth token that Neon validates at the database level.

1. Install the Neon driver

npm install @neondatabase/serverless

2. Create a Neon client

import { createNeonClient } from "@tenence/sdk";

const tenence = createNeonClient({
  databaseUrl: process.env.DATABASE_URL!,
  authToken: () => getJwtTokenForCurrentUser(),
});

The authToken can be a static string or an async function that returns a fresh JWT per request.

3. Define RLS policies with JWT claims

Use jwtClaim instead of sessionVar when writing RLS expressions for the Neon HTTP driver:

import { eq, and, jwtClaim, templates } from "@tenence/sdk";

const isolation = templates.neonTenantIsolation();

const customRule = and(
  eq("tenant_id", jwtClaim("org_id", "uuid")),
  eq("user_id", jwtClaim("sub", "uuid")),
);

JWT-aware templates:

Template What it does
templates.neonTenantIsolation() Restricts rows by org_id JWT claim
templates.neonOwnerOnly() Restricts rows by sub JWT claim
templates.neonCompoundIsolation() Tenant + user compound isolation via JWT claims

4. Use neonMiddleware in Express

The neonMiddleware attaches a per-request Neon HTTP client to req.tenence, using the JWT from each request:

import { neonMiddleware } from "@tenence/sdk/middleware";

app.use(neonMiddleware({
  databaseUrl: process.env.DATABASE_URL!,
  resolveAuthToken: (req) => req.headers.authorization?.replace("Bearer ", "") ?? "",
}));

app.get("/api/tasks", async (req, res) => {
  const result = await req.tenence.query("SELECT * FROM tasks");
  res.json(result.rows);
});

When to use each approach

WebSocket Pool (createClient) Neon HTTP (createNeonClient)
Connection model Persistent pool via WebSocket Stateless HTTP per query
Isolation mechanism SET LOCAL session variables JWT claims verified by Neon
Best for Long-running servers, traditional hosting Edge functions, serverless (Vercel, Cloudflare Workers)
Cold start Pool initialization overhead No pool, instant cold start
Driver @neondatabase/serverless Pool or pg.Pool @neondatabase/serverless neon()

CLI

The CLI provides GitHub-style browser-based authentication and tools for managing your Tenence setup.

npx tenence setup          # Guided setup wizard (recommended for new projects)

npx tenence auth login     # Authenticate via browser
npx tenence auth logout    # Clear stored credentials
npx tenence auth status    # Show current auth status

npx tenence init           # Install RLS stored procedures into your database
npx tenence status         # Check if stored procedures are installed
npx tenence register       # Connect your database to the Tenence console

Organization Management

npx tenence orgs list                        # List all organizations
npx tenence orgs create --name="Acme" --slug=acme   # Create an organization
npx tenence orgs get <orgId>                 # Get organization details
npx tenence orgs update <orgId> --name="New Name"    # Update an organization
npx tenence orgs suspend <orgId>             # Suspend an organization

npx tenence orgs members list <orgId>        # List members
npx tenence orgs members add <orgId> --email=user@example.com --role=member
npx tenence orgs members update <orgId> --user=<userId> --role=admin
npx tenence orgs members remove <orgId> --user=<userId>
npx tenence orgs members import <orgId> --emails=a@ex.com,b@ex.com --role=member

npx tenence orgs keys list <orgId>           # List API keys
npx tenence orgs keys create <orgId> --name="My Key"
npx tenence orgs keys revoke <orgId> --key=<keyId>

All orgs commands support --project=ID, --json for machine-readable output, and --help.

Options:

  • --host=URL — Override the console URL (default: https://console.tenence.io)
  • TENENCE_CONSOLE_URL env var — Alternative way to set the console URL
  • TENENCE_API_KEY env var — Use an API key instead of browser auth

Credentials are stored in ~/.config/tenence/auth.json. Tokens expire after 90 days.

What's Included

Data Isolation

Tenence compiles your access rules into native PostgreSQL RLS policies. Each tenant only sees their own data, enforced at the database level. Learn more in the Concepts Guide.

Authentication

Connect your preferred auth provider — or use the built-in Tenence Auth Gateway. Out-of-the-box adapters for:

  • Tenence Auth Gateway — JWT-based with automatic JWKS verification
  • Clerk — Drop-in adapter
  • Custom providers — Bring your own auth logic
  • Built-in sessions — Password-based auth with Express sessions

Gateway adapter:

import { createGatewayAdapter } from "@tenence/sdk/auth";

// API-key-only — jwksUrl is auto-resolved
const auth = createGatewayAdapter({
  apiKey: process.env.TENENCE_API_KEY,
});

// Or provide jwksUrl explicitly:
// const auth = createGatewayAdapter({
//   jwksUrl: "https://console.tenence.io/api/gateway/your-project/.well-known/jwks.json",
// });

Clerk adapter:

import { createClerkAdapter } from "@tenence/sdk/auth";

const auth = createClerkAdapter();

Custom adapter:

import { createCustomAdapter } from "@tenence/sdk/auth";

const auth = createCustomAdapter({
  resolveUser: async (req) => ({
    userId: req.user.id,
    email: req.user.email,
    tenantId: req.user.orgId,
    role: req.user.role,
  }),
  resolveContext: async (req) => ({
    tenantId: req.user.orgId,
    userId: req.user.id,
    role: req.user.role,
  }),
});

See the Auth Gateway Docs for more details.

Entitlements & Billing

Control access to features based on each tenant's subscription plan. Gate premium features, enforce usage limits, and manage trials — all from the Tenence Console.

import { entitlementMiddleware } from "@tenence/sdk";

app.use("/api/premium", entitlementMiddleware({
  feature: "advanced_analytics",
  mode: "hard_block",
}));

You can also check entitlements programmatically:

import { checkEntitlement, parseEntitlementClaims } from "@tenence/sdk";

const claims = parseEntitlementClaims(req.user.metadata.entitlements);
const result = checkEntitlement(claims, { feature: "advanced_analytics" });

if (!result.allowed) {
  return res.status(403).json({ message: result.reason });
}

Learn more in the Billing Docs.

Feature Flags

Toggle features per tenant using the OpenFeature standard:

import { OpenFeature } from "@openfeature/server-sdk";
import { TenenceProvider } from "@tenence/sdk/feature-flags";

// API-key-only — apiUrl and projectId are auto-resolved
OpenFeature.setProvider(new TenenceProvider({
  apiKey: process.env.TENENCE_API_KEY,
}));

const client = OpenFeature.getClient();
const showBeta = await client.getBooleanValue("beta_feature", false, {
  targetingKey: orgId,
});

See the Feature Flags Docs for configuration details.

UI Components

Drop-in Web Components for common multi-tenant UI patterns:

import "@tenence/sdk/components";
Component What it does
<tenence-tenant-switcher> Lets users switch between organizations
<tenence-user-badge> Shows the current user's info
<tenence-role-indicator> Displays the user's role
<tenence-paywall> Shows or hides content based on subscription
<tenence-feature-gate> Shows or hides content based on feature flags
<tenence-impersonation-bar> Banner when an admin is impersonating a user
<tenence-theme-provider> Applies per-organization branding

Organization Management

Programmatically manage the full tenant lifecycle — create organizations, add and remove members, manage subscriptions, and more. The OrgClient supports two modes:

  • Admin mode (tenenceKey) — Full CRUD access for backend services and scripts
  • Portal mode (bearerToken) — Self-service access for end-user-facing features
import { createOrgClient } from "@tenence/sdk";

// API-key-only — apiUrl and projectId are auto-resolved
const orgs = createOrgClient({
  tenenceKey: process.env.TENENCE_API_KEY,
});

const org = await orgs.create({ name: "Acme Corp", slug: "acme" });
await orgs.addMember(org.id, { email: "alice@acme.com", role: "admin" });

const { data, total } = await orgs.list({ search: "acme" });

See the API Reference for the full OrgClient method list.

Theming

Load and apply per-organization branding dynamically — logos, colors, favicon, and app name:

import { createTheme } from "@tenence/sdk";

const theme = createTheme({
  baseUrl: "https://console.tenence.io",
  projectId: "your-project-id",
  orgId: "org-id",
  autoApply: true, // sets CSS variables and favicon automatically
});

await theme.load();

// Access theme values
theme.getLogoUrl();      // organization logo URL
theme.getAppName();      // branded app name
theme.getPrimaryColor(); // primary brand color
theme.getAccentColor();  // accent color

CSS custom properties set by the theme:

  • --tenence-primary — Primary color
  • --tenence-accent — Accent color
  • --tenence-bg — Background color

autoApply defaults to true in browser environments and false in SSR/Node.js.

Rule Builder

Define access rules with a type-safe builder instead of writing raw SQL. Common patterns are available as one-line templates:

import { eq, and, sessionVar, templates } from "@tenence/sdk";

// Tenant isolation in one line
const isolation = templates.tenantIsolation();

// Or build custom rules
const rule = and(
  eq("project_id", sessionVar("app.project_id", "uuid")),
  eq("tenant_id", sessionVar("app.tenant_id", "uuid")),
);

Built-in templates:

Template What it does
templates.tenantIsolation() Restricts rows to the current tenant
templates.projectIsolation() Restricts rows to the current project
templates.compoundIsolation() Project + tenant compound isolation
templates.ownerOnly() Only the row creator can access it
templates.adminBypass() Admins can access all rows
templates.softDeleteFilter() Hides soft-deleted rows

Additional expression functions: col, neq, gt, gte, lt, lte, isNull, isNotNull, ilike, inList, contains, not, or, defineRule

For the full expression builder reference, see the API Reference.

Session Variables

The client maps context keys to PostgreSQL session variables using SET LOCAL. The defaults are:

Context Key PostgreSQL Variable Description
tenantId app.tenant_id Current tenant/organization ID
userId auth.uid Authenticated user ID
role auth.role User's role
projectId app.project_id Current project ID

You can customize these mappings:

const tenence = createClient({
  databaseUrl: process.env.DATABASE_URL,
  poolSize: 10,
  sessionVars: {
    tenantId: "app.tenant_id",
    userId: "auth.uid",
    role: "auth.role",
    projectId: "app.project_id",
  },
});

Client API

The TenenceClient provides these methods:

Method Description
query(sql, params?, context?) Run a SQL query, optionally with tenant context
withContext(context, fn) Execute a function with a tenant-scoped connection
compileExpression(expression) Compile a rule expression to SQL
applyRule(config) Apply an RLS policy to a table
dropRule(table, name) Remove an RLS policy
enableRls(table) Enable RLS on a table
disableRls(table) Disable RLS on a table
testRule(table, expression, context?) Simulate a policy and preview matched rows
close() Close the connection pool

AI-Powered Development

The Tenence Console includes an MCP (Model Context Protocol) server that lets AI coding assistants manage your data access rules through natural language. Works with Replit Agent, Cursor, Windsurf, and Claude Desktop.

See the Replit Extension Docs for setup instructions.

Import Paths

Import What's inside
@tenence/sdk Client, expressions, templates, middleware, entitlements, theming, OrgClient
@tenence/sdk/organizations Organization management (OrgClient, createOrgClient)
@tenence/sdk/middleware Express middleware (tenenceMiddleware)
@tenence/sdk/auth Auth adapters (Gateway, Clerk, Custom) and password utilities
@tenence/sdk/components Web Components (tenant switcher, paywall, etc.)
@tenence/sdk/feature-flags OpenFeature provider (TenenceProvider)

Learn More

License

MIT