feat(auth): allow multiple orgs to claim the same email domain#3447
feat(auth): allow multiple orgs to claim the same email domain#3447viktormarinho wants to merge 6 commits into
Conversation
Drops the UNIQUE constraint on organization_domains.domain so several orgs can opt into auto-join from the same corporate domain. The onboarding flow now renders an org picker when more than one match is returned, and /domain-join takes an explicit organizationSlug to disambiguate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsSuggested: Minor ( React with an emoji to override the release type:
Current version:
|
There was a problem hiding this comment.
1 issue found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/migrations/091-organization-domains-allow-multi.ts">
<violation number="1" location="apps/mesh/migrations/091-organization-domains-allow-multi.ts:30">
P2: `down()` is not safely reversible after this migration is used: re-adding `UNIQUE (domain)` will fail when duplicate domains exist.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| .execute(); | ||
| await sql` | ||
| ALTER TABLE organization_domains | ||
| ADD CONSTRAINT organization_domains_domain_key UNIQUE (domain) |
There was a problem hiding this comment.
P2: down() is not safely reversible after this migration is used: re-adding UNIQUE (domain) will fail when duplicate domains exist.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/migrations/091-organization-domains-allow-multi.ts, line 30:
<comment>`down()` is not safely reversible after this migration is used: re-adding `UNIQUE (domain)` will fail when duplicate domains exist.</comment>
<file context>
@@ -0,0 +1,32 @@
+ .execute();
+ await sql`
+ ALTER TABLE organization_domains
+ ADD CONSTRAINT organization_domains_domain_key UNIQUE (domain)
+ `.execute(db);
+}
</file context>
PR #3445 renamed the Claude Code "Haiku" tier label to "Haiku 4.5" and updated agent-models.test and agent-section.test but missed agent-model-popover.test, leaving its getByText("Haiku") lookup failing in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds HTTP integration tests for the onboarding auto-join endpoints, with explicit coverage for the new multi-org behavior: - /domain-lookup returns ALL orgs that claim the same domain (the picker's input), and still surfaces auto-join-disabled rows so the caller decides per row. - /domain-join joins the only eligible match when slug is omitted; returns 409 requiresSelection when 2+ eligible matches exist; honors a caller-picked slug and rejects slugs that don't claim the domain or have auto-join disabled. Stubs auth.api.addMember with a direct member-table insert so the tests can assert membership without wiring the full Better Auth adapter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the previous HTTP-integration tests (heavy on mocks) with a Playwright spec that drives the multi-org onboarding picker against a live dev server. The dev server's embedded postgres listens on a dynamic port written to <home>/services/postgres/state.json. A new fixture (e2e/fixtures/db.ts) discovers that file across the data-dir defaults the CLI and `deco services` commands each use, then opens a real `pg.Client`. The spec seeds two orgs that both claim `acme-e2e-<ts>.test` with auto-join enabled, signs up a fresh user through the actual signup form, flips their `emailVerified` flag + tears down the auto-created org via SQL (the parts the UI cannot reach without OTP/magic-link flows), then drives /onboarding through the real DOM: asserts the multi-org heading, both org cards, two Join buttons, and that clicking one persists the correct `member` row while leaving the other empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/migrations/091-organization-domains-allow-multi.ts">
<violation number="1" location="apps/mesh/migrations/091-organization-domains-allow-multi.ts:30">
P2: `down()` is not safely reversible after this migration is used: re-adding `UNIQUE (domain)` will fail when duplicate domains exist.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Fixes two issues uncovered by running the new spec against a live dev server: 1. The fixture only checked `process.cwd()/.deco` — Playwright runs from apps/mesh, but the dev server (started from repo root) writes state.json into the repo-root .deco. Now walks up four levels from CWD looking for a state.json that exists, before falling back to ~/deco. 2. The teardown tried to DELETE the auto-created org after stripping memberships, but the org has RESTRICT-FK children (seeded connection_aggregations etc.) that block the delete. Removing the user's membership rows is enough on its own — authClient.organization.list() only returns orgs the caller is a member of, so the orphaned org doesn't show up in the redirect logic. Verified: spec passes (1 passed, 4.7s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The e2e workflow boots a real postgres:16 service on :5432 and sets DATABASE_URL — there is no embedded-postgres state.json on CI, so the fixture's discovery walk failed every time. Now the fixture prefers DATABASE_URL when set (CI path) and only falls back to the state.json scan for local dev. Local run still passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
744cca3 to
cf39ef0
Compare
What is this contribution about?
Drops the UNIQUE constraint on
organization_domains.domainso several orgs can opt into auto-join from the same corporate domain — useful when different teams at the same company set up independent orgs. The onboarding flow now renders an org picker when the domain matches more than one org, andPOST /api/auth/custom/domain-jointakes an explicitorganizationSlugto disambiguate (returns 409requiresSelectionwhen omitted with >1 eligible match)./domain-setupno longer rejects when another org has the same domain; the storage-layer 23505 unique-violation handler and the dead "domain race" cleanup in setup are removed.How to Test
bun run --cwd=apps/mesh migrate).a@acme.test), onboard and create org "Acme" with auto-join enabled — claimsacme.test.b@acme.test), onboard and create a second org "Acme Labs" with auto-join enabled (previously blocked) — should succeed and claimacme.testalongside Acme.c@acme.test), hit/onboarding: should see the picker listing both orgs, and joining either one redirects to that org.Migration Notes
091-organization-domains-allow-multidropsorganization_domains_domain_keyand adds a non-uniqueorganization_domains_domain_idxfor the by-domain lookup path. Reversible.Review Checklist
Summary by cubic
Allow multiple organizations to claim the same email domain. Onboarding now shows a picker when more than one org matches, and auto-join requires a chosen org in that case.
New Features
organization_domains.domain; added non-uniqueorganization_domains_domain_idx.GET /api/auth/custom/domain-lookupreturnsorganizations: []with all matches (id, name, slug, logo, autoJoinEnabled), including auto-join-disabled rows.POST /api/auth/custom/domain-joinaccepts optional{ organizationSlug }; joins the only eligible match when omitted, else returns 409requiresSelection; rejects unverified/generic domains and disabled claims./api/auth/custom/domain-setuppermits duplicate claims and redirects existing members to an org that already claims the domain.Content-Type: application/json.Bug Fixes
DATABASE_URLin CI and falls back to local embedded Postgres (walks up CWD for.deco/services/postgres/state.json); teardown removes only membership rows to avoid FK errors.agent-model-popovertest to use the "Haiku 4.5" label to fix CI.Written for commit cf39ef0. Summary will update on new commits. Review in cubic