How OpenWork Den Implements Multi-Tenancy: JWT Parsing, PostgreSQL RLS, and Tenant Context Propagation

OpenWork Den enforces strict multi-tenancy by extracting organization IDs from JWT tokens, applying PostgreSQL row-level security policies on every query, and propagating tenant context through the EnterpriseDenOrigin gateway to ensure complete data isolation between organizations.

OpenWork Den, part of the different-ai/openwork repository, operates as a single-tenant-per-organization SaaS platform where every request is strictly scoped to a specific organization. The multi-tenancy architecture implements defense-in-depth isolation across three layers: authentication token parsing, database-level row-level security (RLS), and request context propagation. This technical deep-dive examines the complete implementation, from JWT extraction in apps/server/src/runtime-db.ts to PostgreSQL RLS enforcement.

Token-Based Tenant Identification

The multi-tenancy enforcement begins at the authentication layer. When a request arrives at the Den API, the server extracts the Authorization: Bearer <jwt> header and decodes the organization ID from the token payload.

In apps/server/src/runtime-db.ts, the decodeOrgIdFromBearer function validates the JWT signature against the OpenWork public key and extracts the custom org_id claim:

export function decodeOrgIdFromBearer(bearer: string): string {
  const token = bearer.replace(/^Bearer\s+/i, '');
  const payload = jwt.verify(token, PUBLIC_KEY) as { org_id: string };
  if (!payload.org_id) throw new Error('Missing org_id claim');
  return payload.org_id;
}

This function serves as the primary entry point for tenant identification, ensuring that every authenticated request carries a valid organization identifier before proceeding to database operations.

Database Isolation with Row-Level Security

The Den backend enforces data isolation at the database layer using PostgreSQL Row-Level Security (RLS) policies. Every persistent table that stores user- or organization-specific data—including capability_credentials, workspace_files, and session_tokens—includes a mandatory org_id column defined in schemas like ee/packages/den-db/src/schema/sharables/capability-credentials.ts.

Database migrations, such as ee/packages/den-db/drizzle/0019_scim_sso_identity.sql, enable RLS and create policies that automatically filter rows based on the current session variable:

ALTER TABLE capability_credentials
ADD COLUMN org_id UUID NOT NULL;

ALTER TABLE capability_credentials ENABLE ROW LEVEL SECURITY;
CREATE POLICY org_isolation ON capability_credentials
USING (org_id = current_setting('app.current_org_id')::uuid);

The runtime-db.ts module provides the withTenant helper to set this session variable before executing queries:

export async function withTenant<T>(orgId: string, fn: () => Promise<T>) {
  await db.execute(sql`SET LOCAL app.current_org_id = ${orgId}`);
  return fn();
}

All repository queries wrap database operations inside withTenant, guaranteeing that SQL queries cannot cross tenant boundaries even if application logic contains bugs.

Request Context Propagation via EnterpriseDenOrigin

All Den-specific routes flow through the EnterpriseDenOrigin class in apps/server/src/enterprise-den-origin.ts, which acts as the central tenant gateway. This class constructs a TenantContext object containing the orgId and optional tenantId (for multi-tenant MCP connections), then injects this context into downstream services.

export class EnterpriseDenOrigin {
  async handle(req: Request) {
    const orgId = decodeOrgIdFromBearer(req.headers.get('authorization')!);
    await withTenant(orgId, async () => {
      const ctx = new TenantContext(orgId, req);
      // Route dispatch using ctx
    });
  }
}

This centralized approach ensures that workspace handlers, credential managers, and MCP connectors receive the correct tenant context without manual propagation through every function call.

Multi-Tenant MCP Connection Management

For integrations with external multi-tenant providers like Microsoft Entra, Den maintains mappings between OpenWork organization IDs and remote tenant IDs. The ManagedProviderAuth layer in apps/server/src/managed-provider-auth.ts generates tenant-scoped OAuth URLs using the stored mcp_tenant_id:

export function makeAuthorizeUrl(tenantId: string, clientId: string) {
  return `${BASE_URL}/entra/${tenantId}/oauth2/v2.0/authorize?client_id=${clientId}`;
}

When creating MCP connections, the system stores both the OpenWork orgId and the remote mcp_tenant_id, ensuring OAuth flows target the correct external tenant while maintaining isolation within the Den database.

Organization-Specific Configuration and Feature Gating

Per-organization feature flags and configuration settings are managed through the runtime-config store in apps/server/src/runtime-config.ts. The getOrgConfig function merges global defaults with organization-specific overrides retrieved from the org_config table:

export async function getOrgConfig(orgId: string) {
  const global = await loadGlobalConfig();
  const orgOverrides = await db.select().from(org_config).where(eq(org_config.org_id, orgId));
  return { ...global, ...orgOverrides };
}

This pattern prevents organizations from accessing features they have not purchased, such as MCP connections, by checking flags like enableMcp before executing restricted operations.

Practical Implementation Examples

The following examples demonstrate common multi-tenancy patterns in the Den codebase.

Revoking Credentials with Tenant Scoping

The credential revocation handler in ee/apps/den-api/src/credential-revocation.ts shows how database operations automatically respect tenant boundaries:

export async function revokeCredential(req: Request) {
  const orgId = decodeOrgIdFromBearer(req.headers.get('authorization')!);
  return withTenant(orgId, async () => {
    await db.delete(credentialTable).where(eq(credentialTable.org_id, orgId));
    return { success: true };
  });
}

Querying with Row-Level Security

Custom queries in repository layers use withTenant to ensure RLS policies apply:

export async function listCredentials(orgId: string) {
  return withTenant(orgId, async () => {
    return db.select().from(capability_credentials).all();
  });
}

Feature Flag Enforcement

Before creating MCP connections, the system validates organizational entitlements:

export async function canCreateMcpConnection(orgId: string) {
  const cfg = await getOrgConfig(orgId);
  if (!cfg.enableMcp) throw new Error('MCP connections disabled for this org');
}

Summary

OpenWork Den implements multi-tenancy through a defense-in-depth strategy that combines application-level and database-level controls:

  • JWT Extraction: The decodeOrgIdFromBearer function in apps/server/src/runtime-db.ts extracts organization IDs from authentication tokens
  • PostgreSQL RLS: Row-level security policies in migrations like ee/packages/den-db/drizzle/0019_scim_sso_identity.sql enforce data isolation at the database layer
  • Context Propagation: The EnterpriseDenOrigin class centralizes tenant context creation and injection for all Den routes
  • Session Scoping: The withTenant helper sets PostgreSQL session variables to ensure queries cannot cross tenant boundaries
  • Feature Gating: The runtime-config system enforces organization-specific feature access and plan limits

Frequently Asked Questions

How does OpenWork Den prevent cross-tenant data leakage?

OpenWork Den prevents cross-tenant data leakage through PostgreSQL Row-Level Security (RLS) policies that automatically filter rows based on the app.current_org_id session variable. Even if application code accidentally omits a WHERE org_id clause, the database layer will only return rows matching the current session's organization ID. Additionally, the withTenant wrapper in apps/server/src/runtime-db.ts sets this session variable for every request, creating a database-level safety net.

What is the role of the EnterpriseDenOrigin class in multi-tenancy?

The EnterpriseDenOrigin class in apps/server/src/enterprise-den-origin.ts serves as the central gateway for all Den-specific API routes. It extracts the organization ID from the JWT token using decodeOrgIdFromBearer, initializes the tenant context, and wraps all downstream operations in a withTenant block. This centralized approach ensures that every handler receives the correct tenant context without requiring manual token parsing in individual route handlers.

How are multi-tenant MCP connections handled differently from standard connections?

Multi-tenant MCP connections store two identifiers: the OpenWork organization ID (orgId) and the external provider's tenant ID (mcp_tenant_id). When generating OAuth URLs in apps/server/src/managed-provider-auth.ts, the system uses the remote tenantId to construct provider-specific endpoints (e.g., /entra/{tenantId}/oauth2/v2.0/authorize), ensuring the OAuth flow targets the correct external tenant while the Den database records maintain strict isolation through the org_id column.

Where is the tenant context initialized in the request lifecycle?

The tenant context initializes at the entry point of the Den API routing layer in apps/server/src/enterprise-den-origin.ts. When a request arrives, the handle method immediately extracts the bearer token, decodes the org_id claim, creates a TenantContext instance, and invokes withTenant to set the PostgreSQL session variable before any database queries execute. This ensures the tenant context is established before any service logic runs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →