Skip to main content
An organization is the tenancy boundary. Every row that belongs to a customer hangs off organization_id, and every query filters on it. A user reaches an organization through a membership, which carries exactly one role. One user can hold memberships in many organizations; the panel’s switcher is that list.

The public code

Every organization has a public_code shaped org_<32 hex>. It is what appears in the panel URL and what travels in the X-Organization-Id header.
It is opaque and immutable on purpose: renaming an organization must never break a link. And it identifies without authorizing — pasting someone else’s code into the URL resolves no session, because the membership JOIN matches nothing.
The internal UUID never leaves the API on the surfaces the panel uses. Use public_code in URLs and headers.

Creating one

Sign-up creates the first organization. A signed-in user can open more, becoming owner of each:
The session switches to the new organization immediately — nobody creates a workspace and then goes looking for it. Three invariants are established in that same transaction, and they hold for the organization’s whole life:
  • it always has the three system roles (owner, admin, member);
  • it always has an owner;
  • it always has a live and a test environment, each with a publishable key.

Switching

Switching is navigation, not state you have to manage. Membership is verified every time, and switching into an organization you do not belong to answers 404 — never a leak that it exists.

Inviting people

An invitation is created against a role and emailed as a one-time link, valid for 7 days. One pending invitation per address, enforced by a partial unique index — so a double click cannot produce two.
Reading the invitation tells the accept screen whether the address already has an account (existing_account), so it knows whether to ask for a name and password. An existing account joins directly — the invitation went to that address, and possession of the link proves nothing more is needed. The invitation exists even when the email is lost. Revoke and re-invite.

Leaving and removing

Two different actions, deliberately: Leaving is a user administering their own access, so it is not gated on members:write. Removing yourself through the first endpoint answers 403 with a message pointing at the second. Removal takes effect on that person’s very next request: the session’s membership JOIN stops matching.

The last-owner guard

An organization must keep at least one owner. Demoting, removing or leaving as the last owner answers:
409
The guard locks the owner rows before counting, so two concurrent demotions of different owners serialize instead of both reading “2 owners” and leaving the organization ownerless.

Deleting

Owner only (organization:delete), and the exact name must be typed back:
Everything cascades: memberships, roles, keys, invitations, environments, contacts, and the sessions pinned to it. A mismatched name answers 400 confirmation_mismatch.