Skip to content

Multi-tenant

Pass tenantId and Rungs keeps tenants apart: requests, inboxes, lists, flows, delegation rules and idempotency keys. A tenant can have its own version of a flow; tenants without one use the global flow (EC-56).

// A global flow for everyone, and Acme's own version with an extra step.
await rungs.flows.register({
key: "expense",
steps: [{ id: "manager", approvers: { type: "users", ids: ["u_mgr"] } }],
});
await rungs.flows.register(
{
key: "expense",
steps: [
{ id: "manager", approvers: { type: "users", ids: ["u_mgr"] } },
{ id: "controller", approvers: { type: "users", ids: ["u_ctrl"] } },
],
},
{ tenantId: "acme" },
);

Runs in CI:examples/docs-snippets/src/guides/multi-tenant.ts

const acme = await rungs.submit({
flow: "expense",
tenantId: "acme", // uses Acme's flow; tenants without one use the global flow
requesterId: "u_req",
subject: { type: "expense", id: "EXP-1" },
payload: { amount: 90 },
});
const globex = await rungs.submit({
flow: "expense",
tenantId: "globex",
requesterId: "u_req",
subject: { type: "expense", id: "EXP-1" },
payload: { amount: 90 },
});

Runs in CI:examples/docs-snippets/src/guides/multi-tenant.ts

Every list matches the tenant exactly, so a missing tenantId never shows another tenant’s data (EC-62):

// Every list matches the tenant exactly. Without tenantId, only requests without a tenant.
const acmeInbox = await rungs.inbox({ userId: "u_mgr", tenantId: "acme" });
const acmeRequests = await rungs.requests({ tenantId: "acme" });
const noTenant = await rungs.requests();

Runs in CI:examples/docs-snippets/src/guides/multi-tenant.ts

Callbacks receive the tenant too (ctx.tenantId in roles, request.tenantId in resolvers), so roles and org data can be looked up per tenant. Idempotency keys are scoped per tenant (EC-57).

Rungs does not decide which tenant a user belongs to: derive tenantId from the signed-in user, never from a request parameter. PostgreSQL row level security is not used.