Repository navigation
Let self-host serve several organizations while creation stays off - #81
Merged
Merged
Conversation
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>
This was referenced Nov 5, 2026
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>
shuv1337
added a commit
that referenced
this pull request
Nov 9, 2026
… SSO admission on the image (#85) * Let self-host serve several organizations while creation stays off 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> * Let owners of the self-host root organization create organizations Better Auth's create endpoint now asks whether the signed-in user owns the root organization that setup recorded. Admins, members, people outside the root, owners of other organizations, anonymous callers and bearer keys are refused, and an install with no root record refuses everyone. The creator becomes the new organization's owner. Co-authored-by: Cursor <cursoragent@cursor.com> * Run the cross-organization isolation scenarios on self-host Self-host can now hold several organizations, so the scenarios proving that apps, accounts, connections, groups, grants, dashboard batches and PAT MCP calls stay inside their organization run there too. Self-host cannot remove organizations, so those scenarios remove theirs only on Cloud; each self-host scenario's server is discarded. The batch scenario's own organization is created by the root owner, who invites the member as an administrator. Co-authored-by: Cursor <cursoragent@cursor.com> * Offer organization creation in the self-host switcher to root owners A signed-in user's GET /api/auth/self-host/capabilities answers whether they may create organizations, from the same root-owner check the create endpoint applies. The self-host switcher shows Create organization only when it says so; the server still refuses everyone else. Co-authored-by: Cursor <cursoragent@cursor.com> * Prove self-host setup races, setup rollback, forged creators and the switcher's unknown answer Five callers racing to set up a fresh instance leave one owner, organization and root record. A setup that fails at its last write leaves no rows and a retry succeeds. Creation ignores a body naming the owner and refuses the owner's own API key and MCP OAuth token, each shown valid where it was issued. The switcher offers no creation while the capability answer is pending or failed, and recovers on reload. Co-authored-by: Cursor <cursoragent@cursor.com> * Prove SSO admission against the self-host image The released-image suite gains two scenarios that run the native image with SSO configured against the synthetic OpenID provider. A sidecar owns the container network and forwards the provider's HTTPS address, so the image trusts it through NODE_EXTRA_CA_CERTS. A verified, approved address without a membership, an unverified address and an unapproved domain are refused with their codes and no session or membership. An invited member who connected SSO signs in, and is refused once removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add a fail-closed offline gate for the self-host root record scripts/inspect-self-host-root.ts loads an executor-host export archive into an in-memory PGlite and compares it with the identity the operator recorded before the upgrade: one organization ID and its full set of owners. It resolves PGlite from the repository, refuses any version but the product's 0.5.8 before running it, and requires Postgres 18. It passes only when the archive has exactly one root record, id root, naming that organization, the organization's owners are exactly the recorded set, and the journal records 1 root_organization. It exits 1 when the gate fails, 2 for an invalid invocation or baseline and 3 when the archive or PGlite cannot be used, and prints a fixed report without rows, SQL errors or stack traces. e2e:root-inspection runs it as a separate process over synthetic PGlite archives for each pass, failure, baseline and error case. Co-authored-by: Cursor <cursoragent@cursor.com> * Add the #81 rollout profile to the self-host root gate --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> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
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
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: removedinstanceOrganization. A session needs a membership in any organization./self-host/registeradmits 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 getssso_not_member("Ask an administrator to invite you.").root-organization.ts: a Better Auth plugin tableselfHostRootwith one row (id = 'root',organizationIdunique, FK toorganizationwithon delete restrict). Setup writes it in the same transaction that creates the first organization.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.allowUserToCreateOrganizationstaysfalse.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:
411a959ecode200411a959ecode403"The instance must have exactly one organization"200Verification
bun run check: exit 0.bun run e2e:self-host --test-name '<titles>'on this branch:self-host-many-organizations.spec.ts(new,legacyStoragefixtures): 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.origin/v2'sregistration.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) runscheckand the security scenarios; it does not read the block below.