Skip to content

Allow Multiple Accounts for Different Orgs - #220

Merged
renatodellosso merged 48 commits into
mainfrom
nick/allow-multiple-accounts
Apr 17, 2026
Merged

renatodellosso merged 48 commits into
mainfrom
nick/allow-multiple-accounts

Conversation

@Indy1131

@Indy1131 Indy1131 commented Apr 13, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #73

  • Created playwright test to model functionality
  • Changed schema to integrate new functionality
  • Updated BetterAuth's hooks to accomodate change

Checklist:

  • I have mentioned a ticket above
  • My changes meet the acceptance criteria of said ticket
  • I have self-reviewed my changes
  • I have requested a review from the appropriate team member(s)
  • I have fixed all merge conflicts
  • I have ensured CI checks pass

@netlify

netlify Bot commented Apr 13, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for servicestart ready!

Name Link
🔨 Latest commit 59766d8
🔍 Latest deploy log https://app.netlify.com/projects/servicestart/deploys/69e2691df3383b00082e1049
😎 Deploy Preview https://deploy-preview-220--servicestart.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment thread lib/clientAuthUtils.ts Fixed
Comment thread lib/auth.ts Fixed
Comment thread lib/authUtils.ts Fixed
@greptile-apps

greptile-apps Bot commented Apr 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR scopes user identity per-organization so the same email address can hold accounts across multiple orgs. It replaces the global findByEmail lookup with findByEmailAndOrganization, wires BetterAuth's adapter overrides (createUserOverride, findUserByEmailOverride) into a before hook, adds organization_id to the users table, and ships a new E2E test that verifies cross-org signup.

Several previously-flagged issues remain open (missing UNIQUE (email, organization_id) DB constraint, dead-code after hook path guard, ROOT_DOMAIN defaulting to lvh.me, userOrganizations absent from the schema export). The new migration also has no UPDATE statement to backfill organization_id for existing rows, which will silently break sign-in for any pre-existing user after the migration is applied.

Confidence Score: 3/5

Not safe to merge until the migration data-gap is addressed and previously flagged issues are resolved.

A P1 migration issue is introduced in this PR (no backfill for existing users, breaking sign-in post-upgrade). Multiple P1 findings from previous review rounds are still open: missing UNIQUE constraint, dead-code after-hook, wrong ROOT_DOMAIN default, missing schema export.

drizzle/20260417115347_broad_iron_patriot/migration.sql, lib/utils.ts, lib/auth.ts, lib/schema.ts

Important Files Changed

Filename Overview
drizzle/20260417115347_broad_iron_patriot/migration.sql Creates user_organizations table and adds organization_id to users. Missing UNIQUE constraint (previously flagged) and missing data migration for existing users with NULL org_id.
lib/auth.ts Adds multi-tenant before hook with adapter overrides. after hook dead-code path guard unresolved.
lib/schema.ts userOrganizations still absent from the schema export; otherwise schema changes are correct.
lib/authUtils.ts New createUserOverride/findUserByEmailOverride logic is correct for new users.
lib/services/UserService.ts Org-scoped lookup added; import { and } misplaced after function definition.
tests/e2e/multipleAccounts.spec.ts Valid E2E test; name splitting via hardcoded offsets is fragile.
lib/utils.ts ROOT_DOMAIN defaults to lvh.me — previously flagged, unresolved.

Reviews (8): Last reviewed commit: "Generate new migrations" | Re-trigger Greptile

Comment thread app/login/page.tsx Outdated
Comment on lines +4 to +6
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_organizations_pkey" PRIMARY KEY("user_id","organization_id")
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Composite UNIQUE constraint missing from migration

The schema defines (table) => [sql\UNIQUE (email, organization_id)`]on theuserstable, but this migration only drops the oldusers_email_keyconstraint without adding the replacement composite one. Drizzle does not auto-generateALTER TABLE ADD CONSTRAINT` for raw-SQL constraints in migrations.

As a result, the database has no enforcement for the (email, org) uniqueness invariant. The application-level findByEmailAndOrganization check avoids duplicates most of the time, but concurrent sign-ups with the same credentials can still produce duplicate rows.

Add to the migration:

ALTER TABLE "users" ADD CONSTRAINT "users_email_organization_id_key" UNIQUE ("email", "organization_id");

Comment thread lib/auth.ts Outdated
Comment thread lib/schema.ts
Comment thread app/login/page.tsx Outdated
Comment thread lib/auth.ts Fixed
Indy1131 and others added 3 commits April 13, 2026 00:05
…caping or encoding'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…ariable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Indy1131 <Indy1131@users.noreply.github.com>

@renatodellosso renatodellosso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall flow looks good, but the PR needs some cleanup.

Comment thread app/login/page.tsx Outdated
Comment thread app/signup/page.tsx Outdated
Comment thread lib/services/UserService.ts Outdated
Comment thread lib/auth.ts Outdated
Comment thread lib/auth.ts Outdated
Comment thread lib/authClient.ts Outdated
Comment thread lib/clientAuthUtils.ts Outdated
const rootDomain =
typeof process !== "undefined" && process.env.E2E_TENANT_DOMAIN
? process.env.E2E_TENANT_DOMAIN
: "lvh.me"; // Default to new domain

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, what is "lvh.me"?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a domain that always resolves to the localhost. I was using it for the e2e testing for the different subdomains for each organization (a.lvh.me and b.lvh.me)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok.

Comment thread lib/schema.ts
Comment thread tests/e2e/multipleAccounts.spec.ts
Comment thread app/api/organization/resolve-id/route.ts Outdated
Co-authored-by: Indy1131 <Indy1131@users.noreply.github.com>
Comment thread scripts/setupJunoEmail.ts Outdated
Comment thread scripts/setupJuno.ts
Comment thread scripts/setupOrg.ts
Comment thread scripts/waitForDb.ts Outdated

@renatodellosso renatodellosso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Indy1131 Once you restore the old migrations, we can merge this PR.

Co-authored-by: renatodellosso <renatodellosso@users.noreply.github.com>
Comment on lines +8 to +9
ALTER TABLE "users" DROP CONSTRAINT "users_email_key";--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "organization_id" text;--> statement-breakpoint

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No data migration for pre-existing users

After ADD COLUMN "organization_id" text, every row already in users will have organization_id = NULL. The new findByEmailAndOrganization query filters with eq(users.organizationId, organizationId), which never matches NULL, so any existing user who tries to sign in will be silently treated as non-existent and either receive a credentials error or generate a duplicate account.

Add a UPDATE statement to assign existing users to their organization before the column is used, e.g.:

UPDATE "users"
SET "organization_id" = (
  SELECT "id" FROM "organizations" ORDER BY "created_at" LIMIT 1
)
WHERE "organization_id" IS NULL;

Adjust the sub-query to match however existing users should be mapped (by slug, by the default org, etc.).

@renatodellosso
renatodellosso merged commit cb6133b into main Apr 17, 2026
24 of 25 checks passed

This branch had an error being deployed

1 failed deployment
Preview — 59766d81 Deployed Apr 17, 2026 by renatodellosso via drop-db #233
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.

Allow Multiple Accounts for Different Orgs

3 participants