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):
packages/platform-objects/src/identity/sys-member.object.ts:54 — the add_member action: target: '/api/v1/auth/organization/add-member';packages/plugins/plugin-auth/src/adopt-membership.ts:72 — doc contract: "POST /organization/add-member pre-checks and refuses first (… a 400)";- 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-user → bindUserToSoleOrganization → reconcileMembership 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
Symptom
The
sys_memberlist-toolbar action Add Member (add_member) always fails with HTTP 404. Its target routePOST /api/v1/auth/organization/add-memberis never mounted.Route-mount probe against a deployed 17.0.0 stack (no credentials — contrast with a sibling route that IS mounted):
Root cause
better-auth
1.7.0-rc.2declaresaddMemberwithout 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):So
POST /organization/add-memberdoes not exist over HTTP, yet three places in the platform still claim it does (all current onmain@d7c42405):packages/platform-objects/src/identity/sys-member.object.ts:54— theadd_memberaction:target: '/api/v1/auth/organization/add-member';packages/plugins/plugin-auth/src/adopt-membership.ts:72— doc contract: "POST /organization/add-memberpre-checks and refuses first (… a 400)";packages/plugins/plugin-auth/src/auth-route-ledger.tscorrectly does NOT listorganization/add-member(onlyadd-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-user→bindUserToSoleOrganization→reconcileMembershipresolves the target org viatenancy.defaultOrgId(), which returnsnullwhen the posture enforces the org wall (by design — the framework must not guess an org). Outcomeno-target-org, nosys_memberrow.sys_member.object.ts's own comment: "Theadd_membertoolbar action covers the admin 'attach an existing user directly without sending an invitation' flow" — 404s as above.sys_memberis suppressed (managedBy: "better-auth", ADR-0010 full lock), so an admin cannot hand-create the row either.phoneNumberplugin path ofcreate-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 theadd_memberaction metadata is retargeted to whatever route is the sanctioned "attach existing user" entry — plus the staleadopt-membership.tsclaim updated. As shipped, action metadata, route ledger, and better-auth reality are three different stories.Versions
@objectstack/*17.0.0 (GA); reproduced onmain@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_memberwrites shipped. The app-side issue is parked as blocked on this one.Part of steedos-labs/os-project-titanwind-ehr#1615