Browse documentation

Architecture

Organizations and multi-tenancy

Teams, workspaces, and tenant isolation in Godspeeds: the organization model, member roles, email invitations, and the access pattern that keeps one tenant out of another's data.

Godspeeds ships multi-tenancy out of the box. One running application serves many isolated organizations, each with its own members and roles — the model most B2B products need and most starter kits leave you to build.

The model

  • `organizations` — the tenant itself: name, unique slug, and an owner.
  • `memberships` — the join between a user and an organization, carrying their role. This is the only source of truth for access.
  • `invitations` — email invitations with a seven-day lifetime, expired daily by a scheduled job.

Roles

Three roles, ranked member < admin < owner:

  • member — read organization data, and leave.
  • admin — everything a member can, plus rename the organization, invite people, revoke invitations, and remove members.
  • owner — everything an admin can, plus change roles, remove admins, and delete the organization.
There is one owner per organization, and the template deliberately ships no ownership transfer. If your product needs it, add it as an explicit, audited operation — it has to demote the old owner, promote the new one, and update the organization record atomically.

The access pattern

Every function that touches an organization-scoped table resolves access through a shared helper rather than checking inline. Queries return nothing to outsiders instead of erroring, so the existence of an organization is never leaked; mutations throw, and state the minimum role the operation requires.

ts
// Queries: null or [] for outsiders — don't reveal the org exists
const found = await getMembership(ctx, args.orgId);
if (!found) return null;

// Mutations: throw, with the minimum role for the operation
await requireMembership(ctx, args.orgId, "admin");

Following this one pattern is what keeps tenant isolation intact. Writing an ad-hoc check inside a function is how isolation bugs get introduced — see Security architecture.

Invitations

An invitation is issued against an email address and can only be accepted by someone authenticating with that same address, checked against the verified identity rather than anything the client sends. Invitations never grant the owner role, and they expire after seven days whether or not anyone acts on them.

Accepting an invitation is the one place where a user gains access to data they could not previously see. Treat any change to that path as security-critical.