Skip to content

[finding] sys_member "Add Member" action targets POST /organization/add-member, which better-auth 1.7.0-rc.2 never mounts (server-only) — on multi-org there is NO remaining UI path to attach an existing user to an org #9941

Description

@baozhoutao

Symptom

The sys_member list-toolbar action Add Member (add_member) always fails with HTTP 404. Its target route POST /api/v1/auth/organization/add-member is never mounted.

Route-mount probe against a deployed 17.0.0 stack (no credentials — contrast with a sibling route that IS mounted):

POST /api/v1/auth/organization/add-member -> 404 (route not mounted)
POST /api/v1/auth/organization/remove-member -> 400 (mounted; body validation fired)

Root cause

better-auth 1.7.0-rc.2 declares addMemberwithout an HTTP path — it is a server-only endpoint (auth.api.addMember), while every sibling member route keeps its path (better-auth/dist/plugins/organization/routes/crud-members.mjs):

constaddMember=(option)=>{ ... returncreateAuthEndpoint({method: "POST", ... })// no path → server-onlyconstremoveMember=(options)=>createAuthEndpoint("/organization/remove-member",{ ... })// mountedconstupdateMemberRole=(option)=>createAuthEndpoint("/organization/update-member-role",{ ... })

So POST /organization/add-member does not exist over HTTP, yet three places in the platform still claim it does (all current on main @ d7c42405):

  1. packages/platform-objects/src/identity/sys-member.object.ts:54 — the add_member action: target: '/api/v1/auth/organization/add-member';
  2. packages/plugins/plugin-auth/src/adopt-membership.ts:72 — doc contract: "POST /organization/add-member pre-checks and refuses first (… a 400)";
  3. by omission: packages/plugins/plugin-auth/src/auth-route-ledger.ts correctly does NOT list organization/add-member (only add-team-member) — the ledger and the action metadata contradict each other. The ledger even has a 'server-only' disposition arm that nothing uses for this endpoint.

Why this is a hard blocker (not a cosmetic 404)

On a multi-org posture there is no path left, in any UI, to attach an existing user to an organization:

  • POST /api/v1/auth/admin/create-userbindUserToSoleOrganizationreconcileMembership resolves the target org via tenancy.defaultOrgId(), which returns null when the posture enforces the org wall (by design — the framework must not guess an org). Outcome no-target-org, no sys_member row.
  • The freshly created user signs in with zero memberships and is greeted by the "create a workspace" bootstrap prompt (they then either mint a stray org or cancel into a dead-end shell).
  • The one advertised remediation — sys_member.object.ts's own comment: "The add_member toolbar action covers the admin 'attach an existing user directly without sending an invitation' flow" — 404s as above.
  • Generic CRUD on sys_member is suppressed (managedBy: "better-auth", ADR-0010 full lock), so an admin cannot hand-create the row either.
  • The invite flow requires an email round-trip, which phone-number-only users (created via the phoneNumber plugin path of create-user) cannot complete.

Expected

Either plugin-auth exposes an admin-guarded HTTP route that wraps auth.api.addMember (and the route ledger lists it), or the add_member action metadata is retargeted to whatever route is the sanctioned "attach existing user" entry — plus the stale adopt-membership.ts claim updated. As shipped, action metadata, route ledger, and better-auth reality are three different stories.

Versions

  • @objectstack/*17.0.0 (GA); reproduced on main @ d7c424055 (2026-08-19) by inspection — same better-auth pin (1.7.0-rc.2), same action target, same ledger omission.
  • better-auth1.7.0-rc.2.

What we did NOT do

No app-side workaround: no patched action metadata, no direct sys_member writes shipped. The app-side issue is parked as blocked on this one.

Part of steedos-labs/os-project-titanwind-ehr#1615

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions