member-removal-design.md
Rama main · réplica de lectura del repositorio original.
# Former-member lifecycle
**Status:** implemented in `apps/web` on 2026-08-06; production verification and deployment are separate gates.
This document is the canonical member-removal contract. A room name is a durable
ledger participant, not an account or invitation. “Former” changes future
participation; it never deletes history and cannot revoke a shared room link.
## Invariants
- The active roster is used for new payers, new split participants, identity
claims, push, active counts, and social preview art.
- The ledger directory contains active and Former members and is used for old
expenses, shares, reactions, settlements, exports, balances, and recap history.
- An active member may become Former only when their live room-currency balance
is exactly `0n`. There is no tolerance and no reference-count predicate.
- At least one active member must remain.
- Member, expense, share, reaction, settlement, attribution, and audit rows are
never deleted by this lifecycle.
- The eligibility read and transition use the room advisory lock shared by every
money and roster write.
## Transition to Former
`DELETE /api/rooms/:slug/members/:memberId` is an idempotent soft transition:
1. lock and reload the room;
2. reject the last active member;
3. compute the authoritative live balance and require exact zero;
4. set `removedAt`;
5. delete that member’s push subscriptions;
6. append an immutable `member_removed` audit event;
7. publish only after commit.
The UI calls this “Former”, explains that room-link access is unchanged, and
offers a short Undo plus a durable Reactivate action.
## Behavior while Former
- The member is omitted from untouched EQUAL defaults and every selector for
new activity.
- A saved expense edit may preserve a Former member only in the role already on
that expense: old payer as payer, old share holder as participant. Roles do
not escalate.
- Historical edits, expense restore, or settlement deletion may reopen the
member’s balance. The Former row then reappears in balances and settlement
until it is exactly zero again; membership is not silently restored.
- Historical names and avatars remain labeled Former in expenses, payments,
reactions, exports, and the People directory.
- A supplied Former or rotated token is a failed identity claim
(`MEMBER_TOKEN_INVALID`), never anonymous attribution. A missing token remains
anonymous link-holder access where that route permits it.
## Restore and reactivation
- Settings restore reuses the same member ID, clears `removedAt`, rotates the
token, and returns no identity secret to the restorer.
- Confirmed same-name reactivation reuses the same ID, rotates the token, and
returns the new proof only to that caller.
- Active case-insensitive name collisions are rejected under the room lock.
- Personas and palettes remain reserved across the full ledger directory, so a
restored identity does not duplicate a visual identity allocated in between.
## Draft and offline recovery
An identity transition must not erase typed money. Open expense and settlement
sessions stay logically mounted behind JoinGate and resume after identity
selection. If an active draft member becomes Former, their exact role and typed
amount/weight remain visible so the recorder can remove or replace it.
Offline expense creates rejected for member lifecycle changes become durable
blocked records. They:
- retain endpoint, room, `addedAt`, client key, complete body, and token;
- never retry automatically or get evicted by the queue cap;
- block only newer writes in the same room;
- allow payer/participant remapping to distinct active IDs without changing
exact amounts or weights;
- replace the stale token only on explicit Retry;
- leave storage only after success or explicit Discard.
Committed client-key retries are resolved before token validation, so a lost
success response remains success even if the original token rotated meanwhile.
## Access and residual defenses
Former is not a ban. Anyone with the reusable room link can still read the room,
choose another active identity, or add a distinct name. Real eviction needs a
different authorization model.
Prisma ledger relations still cascade at the database level so deleting an
entire Room can clean up its graph. Application code has no member hard-delete
path; lifecycle tests assert the member and every ledger relation survive.
Changing those relations to `Restrict` requires a dedicated migration and room-
deletion review, not an incidental schema edit.
`removedAt` records the current Former transition. After multiple
remove/reactivate cycles it cannot reconstruct every past inactive interval;
latecomer suggestions therefore use the available interval conservatively.