Skip to content

feat(auth): multi-org enablement — system-scoped adapter reads + onOrganizationCreated hook#2323

Merged
os-zhuang merged 3 commits into
mainfrom
fix/auth-adapter-system-reads
Jun 25, 2026
Merged

feat(auth): multi-org enablement — system-scoped adapter reads + onOrganizationCreated hook#2323
os-zhuang merged 3 commits into
mainfrom
fix/auth-adapter-system-reads

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Two framework changes that together make cloud multi-org usable. Paired with cloud PR objectstack-ai/cloud#507.

1. System-scoped adapter reads — fixes empty organization.list()

A user who belongs to ≥1 org got an empty GET /api/v1/auth/organization/list (HTTP 200, []), so the org switcher / Organizations page showed nothing — even with correct sys_member rows.

Root cause: better-auth resolves a user's orgs by querying the member model through the ObjectQL adapter. Those find/findOne/count calls carry no caller context. The cloud control-plane org-scope read hook scopes sys_member/sys_organization by the caller's user id, skipping only when context.isSystem — so context-less adapter reads were filtered to zero. (The active-org session hook was unaffected — it reads via the raw control driver, bypassing the engine hook.)

Fix: withSystemReadContext(engine) wraps a data engine so its reads (find/findOne/count) carry context.isSystem: true (merged; caller context wins on other keys). Applied in both createObjectQLAdapterFactory (prod) and the legacy createObjectQLAdapter. Writes untouched. Correct because better-auth already authenticated the session and scopes by its own where-clauses.

2. onOrganizationCreated seam — born-with-environment for user-created orgs

better-auth's org-plugin models (organization/member) don't fire core databaseHooks, so a host stack had no server-side seam to run side effects on org creation. Add an optional onOrganizationCreated config callback, invoked from the org plugin's afterCreateOrganization hook with { organizationId, userId, name, slug }. Failure-isolated (org creation is never rolled back). The cloud control plane (#507) wires it to provision each new org's production environment — upholding its 1-prod-env-per-org "born-with-env" invariant for the 2nd+ org a user creates (the signup hook only covers the first org).

Verification

  • Unit: 124/124 plugin-auth tests pass, incl. new withSystemReadContext coverage (isSystem on reads, context merge, writes untouched).
  • E2E (local cloud control plane): organization.list() returns all of a user's orgs (console org switcher + page render them; switching active org works); creating a 2nd org via createOrganization provisions its os-<id> production env, immediately usable.

🤖 Generated with Claude Code

The better-auth -> ObjectQL adapter issued find/findOne/count without a
caller context. On the cloud control plane, the org-scope read hook keys
off the CALLER's user id to scope sys_member / sys_organization, so
context-less adapter reads were filtered down to zero and
organization.list() returned no orgs for a user who is a real member of
one or more orgs (multi-org switching was unusable).

Wrap the adapter's data engine in withSystemReadContext, marking every
read context.isSystem:true so such hooks pass through. better-auth has
already authenticated the session and scopes results by its own
where-clauses (e.g. member.userId = session.user), so system reads are
correct here. Writes are untouched (org-scope is a read-only hook).

Applied to both createObjectQLAdapterFactory (production) and the legacy
createObjectQLAdapter. Verified end-to-end against the cloud control
plane: organization.list() now returns all of a user's orgs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 25, 2026 11:45am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth.

10 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/implementation-status.mdx (via @objectstack/plugin-auth)
  • content/docs/concepts/packages.mdx (via @objectstack/plugin-auth)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/auth-sso.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/kernel-services.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/packages.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/plugins.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…ffects

better-auth's organization-plugin models (organization/member) do NOT fire
core databaseHooks, so a host stack had no server-side seam to run side
effects when a user creates a 2nd+ organization. Add an optional
onOrganizationCreated config callback, invoked from the org plugin's
afterCreateOrganization hook with { organizationId, userId, name, slug }.
Failure-isolated (org creation is never rolled back).

The cloud control plane uses this to uphold its born-with-environment
invariant: every organization, not just the signup/personal org, is born
with its production environment. Without it a user-created org landed
env-less and unusable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size/m and removed size/s labels Jun 25, 2026
@os-zhuang os-zhuang changed the title fix(auth): run better-auth objectql adapter reads as system reads (unblocks organization.list) feat(auth): multi-org enablement — system-scoped adapter reads + onOrganizationCreated hook Jun 25, 2026
…em-reads

# Conflicts:
#	packages/plugins/plugin-auth/src/objectql-adapter.test.ts
#	packages/plugins/plugin-auth/src/objectql-adapter.ts
@os-zhuang
os-zhuang merged commit 0885fde into main Jun 25, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the fix/auth-adapter-system-reads branch June 25, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant