Zum Inhalt springen
Docs

Befehlspalette

Zu einer Seite oder Ansicht springen oder ein Todo per Name finden.

Änderungen

Care service roles on Better Auth organizations

The repository's architectural decision, rationale and consequences.

Markdown

Status: accepted · Date: 2026-10-31 · Related: ADR-0028 (privilege matrix), ADR-0002 (leaf modules)

Enforcement: packages/api/src/features/team/team.rules.test.ts (table-driven matrix, full-frame cases, member ≡ nurse), packages/api/src/platform/rpc/require-org-role.test.ts (care roles reach the clinical boundary; member is read as nurse), packages/auth/src/invitation-policy.test.ts (only owner/admin hold invitation:create; every care role is a configured role).

A Wundklar Organization is a care service (PRD §1.5). Its Members hold one of five roles — owner/admin (PDL), wound_expert (Wundexpertin ICW), nurse (PFK), aide (PHK) — and every clinical procedure, sync handler and screen asks what that role may do. The template inherited Better Auth's three organization roles (owner/admin/member) with their generic access-control statements; this ADR records how we add the care roles without re-inventing Better Auth's AC and without threading role strings through the service layer.

Decision

  1. Care roles on the Better Auth organization plugin. packages/auth/src/care-roles.ts passes roles = { owner, admin, wound_expert, nurse, aide, member } to organization(). owner and admin keep Better Auth's default statements (so they hold invitation:create); the three care roles and the template member share a defaultAc.newRole({ ac: ["read"] }) role — no write statement in the Better Auth sense. Only owner and admin may invite, change roles or deactivate (invitation policy, packages/auth/src/invitation-policy.ts); every care role is a valid value for the invite-member endpoint's role parameter.

  2. member is read as nurse. The template default member stays a configured role so an older seed still passes, but require-org-role and team.rules.can() normalize it to nurse. The clinical boundary speaks in care-role terms only; nothing writes member any more.

  3. The permission leaf. packages/api/src/features/team/team.rules.ts exports CARE_PERMISSIONS and can(role, permission, policy?), the single answer every seam asks. The matrix of plan §1.5 is a static table; can returns a boolean. The one policy-dependent cell is photo:intimate-full-frame.

  4. Full frame of intimate photos. The D-Team screen is the normative one (coordinator decision, plan §5 question 1): wound expert by default; the PDL may grant the PDL role (owner, admin) through the org policy fullFrameRoles (set in ticket 19); nurse and aide never. can("wound_expert", "photo:intimate-full-frame") is always true; can("owner" | "admin", …, { fullFrameRoles }) is true only when the policy lists the role. The legal export (MD audit, Art. 15) always includes originals and is a separate permission (export:legal) held only by owner/admin.

  5. can() crosses the "no role strings in services" rule, by design. ADR-0011 keeps role strings out of services. The sync dispatcher is the one documented exception: a mutation arrives with the sender's role on the request context, and the dispatcher asks can(role, permission, policy) before routing to the feature handler. Features themselves still never read role strings. The exception is spelled out here so the next reader does not undo it.

Consequences

  • Later tickets gate every clinical seam with the same can() call; adding a permission is a one-line change in the matrix with the test row that pins it.
  • A role rename or a new role (future trainee?) is a change in care-roles.ts and the matrix; nothing else moves.
  • fullFrameRoles is a policy row, not a role attribute: the PDL changes it from the D-Team screen without migrating membership.
  • Older seeds that stored member still work: require-org-role and can() map it to nurse at the edge.

Alternatives rejected

  • Mint a care-role AC with full write statements per permission. Better Auth's AC was built for CRUD on its own entities; mapping clinical permissions (assessment:record, photo:intimate-full-frame) onto member/organization/invitation statements would be a lossy translation. can() reads a readable matrix instead.
  • Hierarchy (admin > wound_expert > nurse > aide). The matrix has no clean inclusion: a wound expert may approve reviews but may not manage the team; a nurse may propose a plan but may not activate it. A flat list avoids ordering bugs.
  • Role strings in each feature's service. Would duplicate the matrix across features and make a product-wide change (say, letting nurses activate plans in a pilot) a sweep. Keeping it in the sync dispatcher is a one-point change.
Auf GitHub bearbeitenDie Dokumentation ist auf Englisch.