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:

CheckResult
Boot with plugin loaded (no fail-fast, no degraded)Tenancy: multi-tenant, multiOrgEnabled=true, degradedTenancy=false
Admin sign-in✅ 200
/admin/create-user → membershipnone (organizationId: null) — with this fix; would have mis-bound without it
Sign-up → membershipnone (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

vercelBot commented Jul 13, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 13, 2026 3:13pm

Request Review

@github-actionsgithub-actionsBot 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 mainJul 13, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/tenancy-lifecycle-membership branch July 13, 2026 15:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude