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/sdkPeer dependencies (install if not already present):
npm install pg
npm install @openfeature/server-sdk # only if using feature flagsNeon 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:
WebSocket Pool — The Neon driver provides a WebSocket-based
Poolclass that is API-compatible withpg.Pool. Use this withcreateClientwhen you needSET LOCAL-based tenant isolation (the standard approach).Neon HTTP driver with JWT auth — Use the stateless
neon()HTTP driver withcreateNeonClientfor fully serverless/edge deployments. Tenant isolation is enforced via JWT claims embedded in the auth token, with no persistent connection required.
npm install @neondatabase/serverlessQuick Start
The fastest way to get started is with the guided setup wizard:
npx tenence setupThis 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 loginA 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 init3. Connect your project
Register your database with the Tenence console:
npx tenence register4. 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/serverless2. 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 consoleOrganization 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_URLenv var — Alternative way to set the console URLTENENCE_API_KEYenv 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 colorCSS 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
- Getting Started — Set up your first project
- Concepts — Understand how Tenence works
- Tutorial — Build a complete multi-tenant app
- API Reference — Full SDK reference
- Billing — Entitlements and subscription management
- Feature Flags — Per-tenant feature toggles
- Auth Gateway — Authentication setup
License
MIT