# Care service roles on Better Auth organizations

The repository's architectural decision, rationale and consequences.

# Care service roles on Better Auth organizations

**Status:** accepted · **Date:** 2026-10-31 · **Related:** [ADR-0028](0028-immutable-clinical-records-and-privilege-matrix.md) (privilege matrix), [ADR-0002](0002-no-barrels-on-server-capable-modules.md) (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 [#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](0011-backend-feature-shape.md) 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 [#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 [#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.

