Multi-Tenancy

Isolated workspaces, members, and invites - resolved per request with withTenant.

A tenant is an isolated workspace. Membership links users to tenants with a role. Requests select the active tenant via the X-TENANT-ID header.

Resolving the Tenant - withTenant

import { withTenant } from "@/plugins/Huruf-Tech/thunder-core/utils/withTenant.ts";

const { tenant, member, user, owner, getParentMember } = await withTenant(req);

const maybe = await withTenant(req, true); // silent mode

withTenant resolves the auth session, requires X-TENANT-ID, then loads the tenant plus the caller's membership. It throws Response.forbidden() if the user is not a member or the tenant doesn't exist.

If you only need access to the user object, use withAuthSession() directly. Using withTenant() adds additional processing overhead and is unnecessary in this case. See the withAuthSession example.

For non-owner members it also returns:

  • owner - the tenant creator.
  • getParentMember() - a lazy lookup of the inviter's membership, used for RBAC role cascading.

Pair withTenant with createCRUD's isolationFields to scope a collection per tenant automatically. See the Posts CMS example for a complete multi-tenant CRUD walkthrough.

Tenancy Routes

Read each file for endpoints and exact behavior:

Route fileResponsibility
routes/tenants.tsTenants. Creating one atomically creates an owner member; deleting one removes its members.
routes/tenantMembers.tsMembers. The create handler consumes a tenantInvite.
routes/tenantInvites.tsInvites. Validates the invited role against the caller's subRoles, renders the invite URL, and emails it.
routes/tenantMemberships.tsLists the current user's memberships with the joined tenant document.

The X-TENANT-ID header is allow-listed for CORS by the essentials hook, so browser clients can send it cross-origin.


On this page