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/sdkPeer dependencies (install if not already present):
npm install pg
npm install @openfeature/server-sdk # only if using feature flagsQuick Start
1. Initialize the database
Run the CLI to install the required PostgreSQL stored procedures:
npx tenence initThis 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 contextreq.tenence.withConnection(fn)— use a rawPoolClientwith context setreq.tenence.context— the resolvedTenenceContext
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 consoletenence 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