Skip to content
Documentation
Accounts and data

Workspaces, invitations and permissions

Understand tenant ownership, roles, switching and lifecycle rules before extending product data.

View Markdown

One organization-based model

Each workspace is an organization row with memberships. Ordinary signup creates one personal workspace automatically; a valid invited signup joins only the invited workspace. Users can belong to multiple workspaces. There is no separate single-player data model or SINGLE_PLAYER_MODE switch.

Workspaces own product data and subscriptions. Paying for workspace A does not pay for B. Switching changes which workspace the dashboard displays, while account settings and onboarding remain tied to the person. A surviving account retains a membership through guarded fallback behavior when necessary.

Selected workspace and explicit targets

The sidebar switcher remains usable with one workspace and offers creation. getWorkspace() reads the selected workspace from the validated forma-workspace HTTP-only cookie, falling back to an available membership. getWorkspace(id) verifies an explicit target against current memberships.

Every workspace mutation carries its intended ID so a stale tab does not silently write to a newly selected workspace. The server then validates that ID. In the notes action, getWorkspace(org_id) checks access and SQL independently enforces the role.

Role matrix

OperationOwnerAdminMember
Read permitted workspace dataYesYesYes
Rename workspace / manage logoYesYesNo
Invite Admin or MemberYesYesNo
Manage ordinary teammates/invitationsYesWithin server role rulesNo
Manage billing and API keysYesYesNo
Transfer ownershipYesNoNo
Delete workspaceYes, with guardsNoNo
LeaveTransfer first if teammates remainYesYes

Exactly one Owner exists. Owner transfer is a specific operation, not a normal role dropdown option. The database remains authoritative if the UI is stale or a request is forged.

Invitations

Owners/admins invite an email address as Member by default or explicitly as Admin. One pending invitation exists per workspace/email. Invitations expire after seven days. Resending retains the offered role, rotates the secret and expiry, and invalidates the old link. Merely opening a link does not consume it.

Team filters show Active, Pending, Expired, Revoked and All states. A send failure can leave a valid saved invitation with an actionable delivery status. Inspect the row before retrying; a later Resend is a new attempt with a new secret. Provider acceptance is not inbox receipt.

For new users, the validated recipient flow prevents a duplicate personal workspace. Existing users keep their other workspaces when accepting. Reject wrong-email, expired, revoked and forged context. Do not accept a role or invitation identity from editable Auth user metadata.

The existing abuse policy limits explicit workspace creation to five/account/hour, invitations to 30/workspace/hour and repeated recipient sends to a 60-second cooldown. These are not subscription or seat limits. Change the private policy through a new versioned migration, not a second UI-only constant.

Transfer ownership

The current Owner selects an accepted, verified teammate. A pending invitation or unverified account cannot receive ownership. Confirmation makes that teammate Owner and the previous owner Admin atomically. Verify both role views after reload; a successful button click alone does not establish the invariant.

The lifecycle code coordinates locks before organization row locks. Preserve that ordering when adding data cleanup or lifecycle hooks. Competing owner departures and invitation acceptance need transactional verification rather than separate client checks.

Leave and fallback

An owner with teammates must transfer before leaving. A solo owner's Leave action opens the password-and-name-confirmed workspace deletion flow, including any pending invitations. Admins/members can leave under the server rules.

If leaving, removal or workspace deletion would leave a surviving account without membership, a personal fallback is created atomically. Account deletion is different and never creates a replacement workspace. Removing or demoting a user revokes their workspace API keys and outstanding issued invitations; rejoining does not revive old credentials.

Delete with billing protection

Workspace deletion preserves accounts and other workspaces. It removes workspace-owned records and its database-backed logo. Pending Checkout or a nonterminal subscription blocks deletion. Cancel, wait for the paid period to end, refresh billing, and only then retry the explicit deletion flow.

Account deletion also checks any workspaces it would delete. Transferred/shared workspaces and their subscriptions remain. Never remove the billing trigger to make deletion appear successful. Subscriptions and deletion describes the financial-state boundary.

Extend workspace data

New tables should carry org_id, a foreign key, appropriate cleanup behavior, RLS and explicit grants. Ordinary dashboard queries use the signed-in user's client and an explicit workspace filter. API/MCP access is a separate grant: existing workspace-only credentials do not automatically authorize new customer data.

The workspace-notes recipe demonstrates create/list, role checks, stale-form targeting and retry IDs. If hiding team controls for a simpler product, preserve organizations and automatic membership. Hiding a route does not disable its server actions; remove a capability explicitly if the product should not permit it.

Verify lifecycle behavior

Test cross-workspace reads/writes, Member denial, owner transfer, last-membership fallback and stale tabs. Exercise received invitations for both new and existing accounts. Hosted SQL tests validate transactional boundaries; browser tests validate forms and navigation. Real inbox receipt and acceptance remain separate evidence and were not performed for this manual.

Template 0.0.1 · September 2026 source snapshot