Skip to content

Let self-host serve several organizations while creation stays off - #81

Open
shuv1337 wants to merge 1 commit into
v2from
feat/selfhost-multi-org
Open

shuv1337 wants to merge 1 commit into
v2from
feat/selfhost-multi-org

Conversation

@shuv1337

@shuv1337 shuv1337 commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Why

Self-host is getting real multi-organization support: only owners of the original (root) organization may create organizations, people join only by invitation, and SSO never adds anyone. Today a self-host database that holds a second organization refuses every sign-in, invitation and SSO login, because each path requires exactly one organization. This PR makes the server tolerate several organizations while creation stays disabled, and records the root organization so a later release can authorize creation against it.

Rollout gate: this release must ship and be verified in production before any organization-creation code (#82 and above) is activated there. A server older than this one locks every account out as soon as a second organization exists (measured below), so the release that tolerates several organizations has to be the one running, and the one any rollback lands on, before a second organization can be created.

What changed

  • registration.ts: removed instanceOrganization. A session needs a membership in any organization. /self-host/register admits into the invitation's own organization. SSO no longer auto-joins a verified, approved-domain address: it signs in only people who already belong to an organization and connected SSO on Security; anyone else gets sso_not_member ("Ask an administrator to invite you.").
  • New root-organization.ts: a Better Auth plugin table selfHostRoot with one row (id = 'root', organizationId unique, FK to organization with on delete restrict). Setup writes it in the same transaction that creates the first organization.
  • Migration private_self_host_migrations / 1_root_organization: records the root only when the database has exactly one organization; zero or several leave it unset (fail closed). Recorded once in its own journal, never replayed. Cloud does not load the plugin or the journal.
  • allowUserToCreateOrganization stays false.
  • Docs: self-host page and README describe invitations-only, SSO for existing members, and the rollback floor.

Schema compatibility

Additive only: one new table and one new journal table. The running server (411a959e) never reads either.

Measured by starting both server versions on the same scratch PGlite data directory:

Database Server Result
one organization, migrated by this branch 411a959e code starts, owner sign-in 200
two organizations 411a959e code owner sign-in 403 "The instance must have exactly one organization"
same two-organization database this stack owner sign-in 200

Verification

bun run check: exit 0.

bun run e2e:self-host --test-name '<titles>' on this branch:

  • self-host-many-organizations.spec.ts (new, legacyStorage fixtures): root recorded by setup; an upgrade records an install's only organization once and a restart does not replay it; several organizations without a root leave it unset; with two organizations members sign in, an invitation admits into the inviting organization only, an existing member accepts into a second organization with its role, someone with no membership cannot sign in, setup stays closed across a restart. 3/3 passed.
  • self-host-sso.spec.ts: 9/9 passed, including new "Self-host SSO admits no one automatically and refuses a verified, approved address without an invitation". The HTTPS and UserInfo scenarios now invite the person and connect SSO before the admitted sign-in.
  • Invitation, onboarding and password login scenarios: 7/7 passed.
  • Negative checks against origin/v2's registration.ts: the several-organization scenario fails at the owner's sign-in (expected 403 to be 200), and the no-auto-join scenario fails because the uninvited address is admitted.

The fork CI (fork-ci.yml) runs check and the security scenarios; it does not read the block below.

self-host-many-organizations.spec.ts
self-host-sso.spec.ts
invitation-redemption.spec.ts
invitation-roles.spec.ts
invitation-reaccept.spec.ts
invitation-security.spec.ts
invitation-dialog.spec.ts
self-host-onboarding.spec.ts
hosted.spec.ts

A self-host database refused every sign-in, invitation and SSO login once it
held a second organization, because each path required exactly one. Sessions
now need a membership in any organization, an invitation admits into its own
organization, and SSO signs in only people who already belong to one: it no
longer adds a verified, approved address to the instance's organization.

Setup records the organization it creates as the instance's root in a new
selfHostRoot table. The private_self_host_migrations step 1_root_organization
records an existing install's root when it has exactly one organization and
leaves it unset otherwise. Organization creation remains disabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
shuv1337 added a commit that referenced this pull request Nov 6, 2026
--rollout pr81 adds two requirements to the general checks: the archive
holds exactly one organization, and private_hosted_migrations and
private_self_host_migrations are exactly the journals
7c49225 writes, with nothing missing,
extra or renamed. The report now counts organizations in every mode and,
for the rollout, lists missing pinned entries and counts unexpected ones.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant