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.