Part of #11586 (epic — ruling and V1 scope live there; read it first).
Deliverable
The backend half of the universal org join link, V1:
Data — new platform object (suggested name sys_join_link; naming per sibling conventions in packages/platform-objects/src/identity/): organization_id (lookup sys_organization), token (opaque, ≥128-bit random, URL-safe), role semantics PINNED to member (not authorable in V1 — a constant, not a field, unless the schema layer makes a fixed field cheaper to audit), expires_at (default: creation +7d), revoked_at (nullable), max_uses (nullable), use_count, created_by. Exactly ONE active (non-revoked, non-expired) link per org — enforce at the write endpoint AND with a partial unique index if the driver supports it. Generic CRUD suppressed (managed via endpoints only, like sys_invitation).
Endpoints (mounted beside the better-auth organization surface; exact mount point is implementation detail):
Contract bookkeeping — Clause-② YES (this widens the public API surface): book any new feature-gated inputs in PUBLIC_AUTH_FEATURES (packages/spec/src/kernel/public-auth-features.ts, organization entry) per the bidirectional feature-gate guard; new error codes go wherever the error-code ledger requires. Expect the needs:contract-review chain regardless of dispatch tier (subagent tier is not self-provable).
Tests — authz matrix (stranger / plain member / admin / owner) on all mutating endpoints; expiry / revocation / rotation / exhaustion; verified-email gate on join; an assertion that join goes through the addMember path (not a raw insert); idempotent re-join; token-info leak surface (no org internals for invalid tokens).
Token storage note (PM suggestion, dev judges): retrievable-by-admin plaintext in the org-scoped row is acceptable for V1 (member-grade, expiring, revocable — unlike an OAuth client_secret); if hashed-at-rest with show-once is trivially compatible with the console's "copy link anytime" UX, flag the option in the report rather than deciding silently.
Non-goals (V1, ruled on the epic)
Domain allowlists, approval queue, per-link roles, multiple concurrent links, SCIM interplay.
Consumers to unblock
objectui (console dialog + /join/:token route) and cloud (control-plane org-scoping + E2E) sub-issues carry Blocked-by: pointing here — their cards state what they need: stable endpoint paths + error codes in this card's PR body.
Part of #11586 (epic — ruling and V1 scope live there; read it first).
Deliverable
The backend half of the universal org join link, V1:
Data — new platform object (suggested name
sys_join_link; naming per sibling conventions inpackages/platform-objects/src/identity/):organization_id(lookup sys_organization),token(opaque, ≥128-bit random, URL-safe), role semantics PINNED tomember(not authorable in V1 — a constant, not a field, unless the schema layer makes a fixed field cheaper to audit),expires_at(default: creation +7d),revoked_at(nullable),max_uses(nullable),use_count,created_by. Exactly ONE active (non-revoked, non-expired) link per org — enforce at the write endpoint AND with a partial unique index if the driver supports it. Generic CRUD suppressed (managed via endpoints only, likesys_invitation).Endpoints (mounted beside the better-auth organization surface; exact mount point is implementation detail):
auth.api.addMember— the server-only endpoint measured in the [finding]sys_member"Add Member" action targetsPOST /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 thread) so org hooks, audit, and limits fire — NEVER hand-insertsys_memberrows. Already-a-member ⇒ idempotent success. Distinct error codes for expired / revoked / exhausted / invalid.Contract bookkeeping — Clause-② YES (this widens the public API surface): book any new feature-gated inputs in
PUBLIC_AUTH_FEATURES(packages/spec/src/kernel/public-auth-features.ts,organizationentry) per the bidirectional feature-gate guard; new error codes go wherever the error-code ledger requires. Expect theneeds:contract-reviewchain regardless of dispatch tier (subagent tier is not self-provable).Tests — authz matrix (stranger / plain member / admin / owner) on all mutating endpoints; expiry / revocation / rotation / exhaustion; verified-email gate on join; an assertion that join goes through the addMember path (not a raw insert); idempotent re-join; token-info leak surface (no org internals for invalid tokens).
Token storage note (PM suggestion, dev judges): retrievable-by-admin plaintext in the org-scoped row is acceptable for V1 (member-grade, expiring, revocable — unlike an OAuth client_secret); if hashed-at-rest with show-once is trivially compatible with the console's "copy link anytime" UX, flag the option in the report rather than deciding silently.
Non-goals (V1, ruled on the epic)
Domain allowlists, approval queue, per-link roles, multiple concurrent links, SCIM interplay.
Consumers to unblock
objectui (console dialog +
/join/:tokenroute) and cloud (control-plane org-scoping + E2E) sub-issues carryBlocked-by:pointing here — their cards state what they need: stable endpoint paths + error codes in this card's PR body.