CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

feat: multi-workspace support — one login, several separate businesses #4041

Merged⚡ AI-generatedXSccantynz wants to mergeclaude/admin-workspace-account-limit-gm2wzamainopened Jul 3, 202611/16 tasks
ccantynzcommented Jul 3, 2026

Originally written by @ccantynz-alt on GitHub.
Imported from https://github.com/ccantynz-alt/AlecRae.com/pull/90


Description

Fixes the reported bug: "in the admin section, I can only create one workspace account." Root cause was two-layered:

  1. Frontend bug — the Workspace → Team tab only kept res.data[0] from GET /v1/organizations and permanently hid the "create organization" form once one existed.
  2. Deeper architectural gapusers.accountId was a hard 1:1 (one login = exactly one tenant/account), so even fixing the UI wouldn't let one login manage more than one separate business.

Craig confirmed he wanted the real fix — genuine multi-tenant workspaces (an agency managing several client businesses under one login) — which is a data-model change, so this was scoped, planned, and built as one PR. See CLAUDE.md Known Issue #63 and the Product Decisions Log for the full write-up.

What changed

  • New workspace_members table (packages/db/src/schema/workspace-members.ts, migration 0004_legal_lady_bullseye.sql with a backfill for existing users) — decouples identity (users) from per-workspace role, so one identity can belong to several workspaces (accounts) with a different role in each.
  • refresh_tokens gains an accountId column so token rotation stays scoped to the active workspace instead of silently reverting to the identity's home account.
  • New GET/POST /v1/workspaces (list / create) and POST /v1/auth/switch-workspace (mints a fresh token pair after verifying membership — role is never trusted from the request).
  • organizations.ts member/invite endpoints now operate on workspace_members instead of users.accountId; invitation-accept no longer re-parents an existing identity into a new account (which would have destroyed their access to other workspaces) — it grants membership instead.
  • Login, register, Google OAuth, passkey, and SSO all resolve role from workspace_members, with a self-healing fallback to the legacy users.role for rows that predate this table.
  • GET /v1/auth/me and the DELETE /v1/account owner-check now reflect the active workspace, not the identity's home account.
  • Frontend: new WorkspaceSwitcher in the dashboard sidebar (list / switch / create workspaces), and the Team tab bug is fixed.
  • Unrelated pre-existing build break fixed in ai-triage/page.tsx (exactOptionalPropertyTypes violation) that was blocking bun run build entirely — bundled in so CI can go green.

⚠️ Deployment note

Run bun run db:migrate on the box before/during deploy — this ships a new table and column.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactor (no functional changes)
  • Documentation update
  • Infrastructure / CI change

Testing

  • New and existing tests pass locally (bun run test — 147/147 in apps/api)
  • Lint and type-check pass (bunx tsc --noEmit clean in apps/api/apps/web/packages/db; bun run lint clean except pre-existing, unrelated errors in dns-providers/* and agent.ts)
  • bun run build passes for apps/web (69 static pages) and packages/db
  • scripts/check-schema-drift.sh passes (migration fully captures the schema change)
  • Manual testing performed — not run against a live Postgres instance in this environment; recommend a smoke test on staging (create workspace → switch → invite a member → switch back) before/after the box deploy

Checklist

  • My code follows the project style guidelines
  • I have performed a self-review of my code
  • New and existing tests pass locally (bun run test)
  • Lint and type-check pass
  • I have updated documentation (CLAUDE.md Known Issue #63 + Product Decisions Log)

Breaking changes

Not breaking for existing single-workspace users — behavior is identical until someone creates or joins a second workspace. The migration backfills a workspace_members row for every existing user so nothing regresses.

Screenshots

N/A (backend + a new sidebar dropdown; not screenshot-tested against a live environment in this session)


Generated by Claude Code

Cross-repo impact

See what breaks downstream if this PR merges.

Analyze →
⮌ Merged

This pull request was merged into main.

c comment · e edit title · m merge · a approve · r request changes · ? shortcuts