Skip to content

fix(auth): tenancy-mode-aware create-user bind + ADR-0093 integration tests#2887

Merged
os-zhuang merged 1 commit into
mainfrom
claude/tenancy-lifecycle-membership
Jul 13, 2026
Merged

fix(auth): tenancy-mode-aware create-user bind + ADR-0093 integration tests#2887
os-zhuang merged 1 commit into
mainfrom
claude/tenancy-lifecycle-membership

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Follow-up to #2884 (merged). Carries the one real bug found by multi-org runtime verification plus the ADR-0093 integration-test hardening.

The bug (found by verification, fixed here)

The #2884 endpoint bind resolved its target org via resolveDefaultOrgId — which prefers the slug='default' org regardless of how many orgs exist. In a multi-org deployment the bootstrap Default Organization coexists with real tenant orgs, so /admin/create-user would have bound every new user into the default org — violating ADR-0093 D3 ("the framework never guesses in multi mode"). Notably this was a case the old #2882 count-heuristic ("exactly one org") would also have hit whenever only the bootstrap org existed yet.

Fix: the bind now consults the tenancy service (new optional getTenancy on AdminUserEndpointDeps, wired in auth-plugin.ts): single mode → default org; multi mode → defaultOrgId() returns null → no bind. Fallback to the old resolution only when tenancy isn't wired (lean embeddings). Two unit regression tests pin it.

Multi-org runtime verification (how the bug was caught)

Built @objectstack/organizations from the cloud repo (its link: deps pointed at this worktree), linked it into the showcase example, booted with OS_MULTI_ORG_ENABLED=true, and verified on a clean instance:

Check Result
Boot with plugin loaded (no fail-fast, no degraded) Tenancy: multi-tenant, multiOrgEnabled=true, degradedTenancy=false
Admin sign-in ✅ 200
/admin/create-user → membership none (organizationId: null) — with this fix; would have mis-bound without it
Sign-up → membership none (reconciler no-ops in multi mode)
DB truth (sys_member) ✅ only the bootstrap admin owner row; created/signed-up users correctly member-less

(Single-org e2e was verified previously: create-user and sign-up both produce default-org memberships.)

Integration tests (dogfood)

New packages/dogfood/test/membership-reconciler.dogfood.test.ts — drives the real better-auth pipeline via bootStack(showcase):

  1. Sign-up membership via the user.create.after reconciler alone — the invariant e2e (no endpoint bind exists on this path, so the membership proves the hook).
  2. Backfill (D6) — binds a pre-existing member-less user, idempotent on re-run.
  3. Invite-only (D1) — reconciler policy-skip + backfill reason: 'policy', no rows.
  4. Yield rule (D2) — a host-bound membership wins; the reconciler never double-binds.

Harness note: bootStack deliberately keeps autoDefaultOrganization: false (the ADR-0057 org-less single-tenant posture) and no better-auth admin plugin, so the tests mint the default org themselves (system context) and the create-user endpoint path stays covered by its 31 unit tests.

Also exports reconcile-membership + tenancy-service from the package index as the ADR-0093 host API, and adds the @objectstack/plugin-auth workspace dep to dogfood.

Testing

  • dogfood: 46 files / 234 tests pass (incl. the 4 new).
  • plugin-auth: 435 tests pass (incl. 2 new endpoint regressions).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XbLd2KKeVs39EztDYMQ1YU


Generated by Claude Code

… tests

Multi-org runtime verification (enterprise @objectstack/organizations linked
into a live stack) caught a real gap in the #2884 endpoint bind: it resolved
its target org via resolveDefaultOrgId (slug='default' first), so in multi-org
— where the bootstrap default org coexists with tenant orgs — /admin/create-user
would have bound new users into the default org, violating ADR-0093 D3. The
bind now consults the tenancy service (getTenancy endpoint dep): single mode →
default org; multi mode → no bind. Two unit regression tests added.

Verified live on a clean multi-org stack: mode=multi (multiOrgEnabled=true,
degradedTenancy=false), no fail-fast with the plugin loaded, sign-in works,
and BOTH create-user and sign-up leave new users member-less (DB-verified) —
invites / host hooks own membership there. Single-org behavior unchanged.

Also:
- export reconcile-membership + tenancy-service from the package index (the
  ADR-0093 host API: hosts compose the reconciler; embeddings query mode).
- dogfood integration tests (packages/dogfood/test/membership-reconciler.
  dogfood.test.ts) driving the REAL better-auth pipeline: sign-up membership
  via the user.create.after reconciler alone (the invariant e2e), backfill
  bind + idempotency (D6), invite-only refusal (D1), yield-to-host-membership
  (D2). bootStack keeps autoDefaultOrganization:false, so the tests mint the
  default org themselves (system context).
- dogfood gains a workspace dep on @objectstack/plugin-auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XbLd2KKeVs39EztDYMQ1YU
@vercel

vercel Bot commented Jul 13, 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 Jul 13, 2026 3:13pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling size/m labels Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/dogfood, @objectstack/plugin-auth.

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

  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authorization.mdx (via packages/dogfood)
  • content/docs/permissions/delegated-administration.mdx (via packages/dogfood)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/implementation-status.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.

@os-zhuang os-zhuang marked this pull request as ready for review July 13, 2026 15:25
@os-zhuang os-zhuang merged commit da5e686 into main Jul 13, 2026
18 checks passed
@os-zhuang os-zhuang deleted the claude/tenancy-lifecycle-membership branch July 13, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants