JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 47
  • Score
    100M100P100Q92445F
  • 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/feature-flags
  • @tenence/sdk/middleware

Readme

@tenence/sdk

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

Tenence compiles JSON-based rules into native PostgreSQL RLS policies so your data isolation happens at the database level — not in application code.

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

Quick Start

1. Initialize the database

Run the CLI to install the required PostgreSQL stored procedures:

npx tenence init

This creates the rls schema with stored procedures for compiling and applying RLS policies.

2. Create a client

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

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

3. Add the Express middleware

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,
  }),
}));

4. Query with automatic tenant isolation

app.get("/api/tasks", async (req, res) => {
  // RLS policies filter rows automatically based on the tenant context
  const result = await req.tenence.query("SELECT * FROM tasks");
  res.json(result.rows);
});

API Reference

Core

createClient(config)

Creates a TenenceClient instance.

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

const tenence = createClient({
  databaseUrl: "postgres://...",
  poolSize: 10,          // default: 10
  sessionVars: {         // maps context keys to PostgreSQL session variables
    tenantId: "app.tenant_id",
    userId: "auth.uid",
    role: "auth.role",
    projectId: "app.project_id",
  },
});

TenenceClient

Method Description
query(sql, params?, context?) Execute a SQL query. When context is provided, session variables are set within a transaction.
withContext(context, fn) Run a callback with a database client scoped to a tenant context.
enableRls(tableName) Enable Row Level Security on a table.
disableRls(tableName) Disable Row Level Security on a table.
applyRule(config) Apply an RLS policy to a table via the stored procedures.
dropRule(tableName, ruleName) Drop an RLS policy from a table.
testRule(tableName, expression, context?) Simulate a rule against real data without persisting changes.
compileExpression(expression) Compile a JSON expression into its SQL equivalent.
close() Close the connection pool.

Setup Helpers

Programmatically install or verify the Tenence stored procedures (same operations the CLI performs):

import { installTenence, verifyInstallation } from "@tenence/sdk";

const result = await installTenence(process.env.DATABASE_URL);
if (!result.success) {
  console.error(result.message);
}

const status = await verifyInstallation(process.env.DATABASE_URL);
console.log(status.message);

defineRule(config)

Declare an RLS policy configuration object with full type safety:

import { defineRule, templates } from "@tenence/sdk";

const policy = defineRule({
  table: "tasks",
  name: "tenant_isolation",
  command: "ALL",
  permissive: true,
  usingExpression: templates.tenantIsolation(),
});

await tenence.applyRule(policy);

Expression Builder

Build RLS policy expressions with a type-safe DSL instead of writing raw JSON.

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

// Simple tenant isolation
const rule = eq("tenant_id", sessionVar("app.tenant_id", "uuid"));

// Compound isolation (project + tenant)
const compound = and(
  eq("project_id", sessionVar("app.project_id", "uuid")),
  eq("tenant_id", sessionVar("app.tenant_id", "uuid")),
);

// Or use a built-in template
const isolation = templates.tenantIsolation();
const ownerOnly = templates.ownerOnly("created_by");

Available functions:

Function Example
eq(field, value) eq("status", "active")
neq(field, value) neq("role", "guest")
gt, gte, lt, lte gte("age", 18)
isNull(field) isNull("deleted_at")
isNotNull(field) isNotNull("id")
ilike(field, pattern) ilike("name", "%search%")
inList(field, values) inList("status", ["active", "pending"])
contains(field, value) contains("tags", "premium")
and(...exprs) and(eq("a", 1), eq("b", 2))
or(...exprs) or(eq("role", "admin"), eq("role", "owner"))
not(expr) not(eq("archived", true))
sessionVar(name, type?) sessionVar("app.tenant_id", "uuid")
col(name) col("parent_id")

Built-in templates:

Template Description
templates.tenantIsolation(column?) Restrict rows to the current tenant. Default column: tenant_id.
templates.projectIsolation(column?) Restrict rows to the current project. Default column: project_id.
templates.compoundIsolation(projectCol?, tenantCol?) Project + tenant compound key isolation.
templates.ownerOnly(column?) Only the row owner can access it. Default column: user_id.
templates.adminBypass(primaryKey?) Allow admin roles unrestricted access.
templates.softDeleteFilter(column?) Hide soft-deleted rows. Default column: deleted_at.

Middleware

Express

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

app.use(tenenceMiddleware({
  client: tenence,
  resolveContext: (req) => ({
    tenantId: req.user.orgId,
    userId: req.user.id,
  }),
  skip: (req) => req.path.startsWith("/public"),
  onError: (err, req, res) => {
    res.status(500).json({ error: "Context resolution failed" });
  },
}));

Once applied, every request has req.tenence with:

  • req.tenence.query(sql, params?) — query with tenant context
  • req.tenence.withConnection(fn) — use a raw PoolClient with context set
  • req.tenence.context — the resolved TenenceContext

Generic (non-Express)

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

const handler = createTenenceHandler({
  client: tenence,
  resolveContext: (req) => ({ tenantId: req.orgId }),
});

Auth Adapters

Auth adapters bridge your authentication provider with Tenence's tenant context.

Tenence Auth Gateway

Use JWTs issued by the Tenence Auth Gateway, verified via JWKS:

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

const auth = createGatewayAdapter({
  jwksUrl: "https://console.tenence.io/api/gateway/my-project/.well-known/jwks.json",
  cacheTtlMs: 3600000,  // 1 hour (default)
});

The adapter extracts tenantId, userId, role, and enriched claims (entitlements, feature flags, permissions) from the JWT.

Clerk

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

const auth = createClerkAdapter();

Custom

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

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

Built-in (session-based)

A self-contained auth adapter with password hashing and Express session support:

import { createBuiltinAuth, createSessionLoginHandler } from "@tenence/sdk/auth";

const auth = createBuiltinAuth({
  findUserByEmail: (email) => db.query("SELECT * FROM users WHERE email = $1", [email]),
  findUserById: (id) => db.query("SELECT * FROM users WHERE id = $1", [id]),
  canImpersonate: async (actorId, targetId) => {
    // Return true if the actor is allowed to impersonate the target
    return isAdmin(actorId);
  },
});

const loginHandler = createSessionLoginHandler({
  findUserByEmail: auth.findUserByEmail,
  findUserById: auth.findUserById,
});

app.post("/login", async (req, res) => {
  const user = await loginHandler.login(req, req.body.email, req.body.password);
  if (!user) return res.status(401).json({ error: "Invalid credentials" });
  res.json(user);
});

app.post("/logout", async (req, res) => {
  await loginHandler.logout(req);
  res.json({ ok: true });
});

Password utilities are also exported for user registration:

import { hashPassword, verifyPassword } from "@tenence/sdk/auth";

const hash = await hashPassword("user-password");
const valid = await verifyPassword("user-password", hash);

Entitlements

Check whether a tenant's subscription plan allows access to a feature or resource.

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

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

Or check directly via API:

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

const result = await checkEntitlement({
  apiUrl: "https://console.tenence.io",
  projectId: "your-project-id",
  orgId: "org-id",
  apiKey: process.env.TENENCE_API_KEY,
});

if (!result.entitled) {
  // Tenant is not entitled — check result.plan, result.limits, result.trial
}

Parse entitlement claims from a JWT (e.g., from the Tenence Auth Gateway):

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

const entitlements = parseEntitlementClaims(decodedJwt);
console.log(entitlements.plan?.name, entitlements.entitled);

Feature Flags

An OpenFeature-compatible provider for server-side flag evaluation.

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

OpenFeature.setProvider(new TenenceProvider({
  apiUrl: "https://console.tenence.io",
  apiKey: process.env.TENENCE_API_KEY,
  projectId: "your-project-id",
}));

const client = OpenFeature.getClient();

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

Theme

Load and apply per-organization branding dynamically.

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 on document.documentElement
});

await theme.load();

// Access resolved values
theme.getLogoUrl();
theme.getAppName();
theme.getPrimaryColor();

CSS custom properties set by autoApply:

  • --tenence-primary
  • --tenence-accent
  • --tenence-bg

UI Components

Import @tenence/sdk/components to register framework-agnostic Web Components:

import "@tenence/sdk/components";
Component Description
<tenence-tenant-switcher> Dropdown for switching between organizations
<tenence-user-badge> Displays current user info
<tenence-role-indicator> Shows the user's current role
<tenence-paywall> Conditionally renders content based on subscription
<tenence-feature-gate> Shows/hides content based on feature flags
<tenence-impersonation-bar> Banner shown when an admin is impersonating a user
<tenence-theme-provider> Fetches and applies org-level theme overrides

CLI

The SDK includes a CLI for database setup and management.

npx tenence init       # Install stored procedures and scaffold config
npx tenence status     # Check if Tenence is installed in the database
npx tenence register   # Register your database with the Tenence console

tenence init

Installs the rls schema and stored procedures (rls.compile_expression, rls.apply_policy, rls.simulate_policy, etc.) into your PostgreSQL database. Optionally generates a tenence.config.ts file.

tenence status

Checks the database for the required rls schema and functions.

tenence register

Connects your database to the Tenence Console via API key, enabling remote rule management and the MCP server.

Entry Points

Import Path Contents
@tenence/sdk Client, setup helpers, expressions, middleware, auth, entitlements, theme
@tenence/sdk/middleware tenenceMiddleware, createTenenceHandler
@tenence/sdk/auth Auth adapters (Gateway, Clerk, Custom, Built-in), password utilities
@tenence/sdk/components Web Components (auto-registered on import)
@tenence/sdk/feature-flags OpenFeature provider

MCP Server

The Tenence Console includes a hosted MCP (Model Context Protocol) server that lets AI coding assistants manage your RLS policies via natural language. Compatible with Replit Agent, Cursor, Windsurf, and Claude Desktop.

See the Tenence Docs for setup instructions.

License

MIT