feat(server): organizations tenant layer — migration + session (PR1/4)#69
Merged
Conversation
Introduce an organizations table as the tenant layer above projects, the unit of isolation (and future anchor for metering/billing). Adds nullable organization_id FKs to admin_users, projects, and admin_invites, plus a per-org require_2fa policy column. The migration backfills a single 'Default' org and assigns every existing admin and project to it (seeding require_2fa from the install-wide setting), so a single-org self-host install behaves exactly as before. Columns are left nullable here; the NOT NULL backstop lands in a later migration once every writer is org-aware. AdminUser/validateSession now carry organizationId so downstream authz can scope owners to their own org. No behaviour change yet.
Isonimus
added a commit
that referenced
this pull request
Jun 21, 2026
- ADR-0005: organizations are the tenant boundary; owner re-scoped from global to org-scoped; 404-not-403 on cross-org; per-org 2FA; the nullable->backfill->NOT NULL rollout; deferred public signup. Indexed in the ADR README. - ROADMAP: flip the multi-tenancy section from planned to SHIPPED (migrations 013-014, PRs #69-#72), keeping public signup + metering/billing + org-mgmt UI as explicit follow-ups. - OpenAPI: note that all /admin responses are org-scoped (cross-org -> 404). Lints clean. - CHANGELOG: Internal entry for the organizations layer.
Isonimus
added a commit
that referenced
this pull request
Jun 21, 2026
- ADR-0005: organizations are the tenant boundary; owner re-scoped from global to org-scoped; 404-not-403 on cross-org; per-org 2FA; the nullable->backfill->NOT NULL rollout; deferred public signup. Indexed in the ADR README. - ROADMAP: flip the multi-tenancy section from planned to SHIPPED (migrations 013-014, PRs #69-#72), keeping public signup + metering/billing + org-mgmt UI as explicit follow-ups. - OpenAPI: note that all /admin responses are org-scoped (cross-org -> 404). Lints clean. - CHANGELOG: Internal entry for the organizations layer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The hosted box is effectively single-tenant today: data is scoped by
project_id, but there is no company entity above projects and the adminownerrole is a global superuser. Safe for self-host, unsafe for a multi-customer box (Company A's owner could read Company B's data; no billing root). This is PR1 of 4 introducing anorganizationstenant layer. See [ADR-0005 / ROADMAP] (landing in PR4).What (inert foundation — no behaviour change)
organizationstable (with per-orgrequire_2fa); nullableorganization_idFKs onadmin_users,projects,admin_invites.'Default'org (seedingrequire_2fafrom the install-wide setting), so self-host behaves exactly as before. A fresh empty DB gets no org here — the bootstrap script (PR3) creates the first org + owner together.organization_idstays nullable; the NOT NULL backstop lands in PR2 once every writer is org-aware.AdminUser/validateSessionnow carryorganizationIdfor the authz re-scoping in PR2.organization_idis off the/v1data hot path — a project API key already resolves to aproject_id. Orgs are an admin/billing concern only.Verification
Defaultorg, all admins + projects assigned.type-check+lintclean.Follow-ups
PR2 authz re-scoping (owner → org-scoped) + per-org 2FA + NOT NULL backstop + isolation tests · PR3 org-aware bootstrap · PR4 docs.
🤖 Generated with Claude Code