Organizations
Pitchbox supports multi-tenant orgs. On a fresh install a default org is seeded and the first user joins as owner. Every project, campaign, draft, run, account, and blocklist entry is scoped to an org through projects.organization_id.
Roles
| Role | Can invite | Can change roles | Can remove members | Notes |
|---|---|---|---|---|
owner | yes | yes | yes | First user |
admin | yes | yes | yes | Same as owner today |
member | no | no | no | Default invite role |
Tenant scoping
The hook handle in web/src/hooks.server.ts runs loadOrganizationForUser on every authenticated request and either:
- Sets
event.locals.org = { id, slug, role }and continues, or - Returns 404 if the user has no membership (not 403 - we don't leak existence).
/invite/* and /api/orgs/* are exempt because a newly registered user with no membership still needs to accept an invite.
Server queries either filter projects.organization_id directly (e.g. listProjects({ organizationId })) or call one of the helpers in @pitchbox/shared/orgs:
projectBelongsToOrg(db, projectId, orgId)campaignBelongsToOrg(db, campaignId, orgId)draftBelongsToOrg(db, draftId, orgId)runBelongsToOrg(db, runId, orgId)
Each returns false for cross-tenant access; the route then returns 404.
Invite flow
Admin generates a link
httpPOST /api/orgs/<slug>/invites Content-Type: application/json { "role": "member", "email": "alice@example.com" }Response:
json{ "token": "…48 hex chars…", "url": "https://.../invite/<token>", "expiresAt": "…ISO8601…" }The invite is valid for 7 days and is single-use (the row is marked
accepted_atonce consumed).Invitee visits
/invite/<token>- Not logged in? Redirected to
/login?next=/invite/<token>. - Logged in? The page server calls
acceptInvite, creates a membership, and redirects to/.
- Not logged in? Redirected to
Programmatic accept
httpPOST /api/orgs/<slug>/invites/<token>/acceptSame effect as visiting the page. Returns
{ organizationId, role }or 404 if the token is invalid/expired/consumed.
Members management
/settings/members (deferred to a follow-up) lists members and lets admins change roles or remove members. Until shipped, manage memberships directly in the memberships table.
Database
organizations id, slug (unique), name
memberships id, organization_id, user_id, role, created_at (unique org+user)
org_invites id, organization_id, token (unique), email, role,
expires_at, created_at, accepted_at, created_by_user_id
projects … organization_id → organizations.idEvery other tenant-scoped table reaches the org through projects.organization_id.