# Backend Contract Proposal — Club Rank Permission Matrix (WEB-W1) Status: **Proposed / not implemented.** This documents the backend work required before the "Operational Permission Matrix" on `app/clubs/[clubId]/manage/ranks-roles/page.tsx` can be made editable. Until this ships, the matrix is rendered read-only (`persistenceAvailable={false}`). ## Problem `handleSavePermissions` previously waited ~400 ms and showed a "Saved" state without writing anything (confirmed in source prior to this change). There is no Firestore document, API route, or security rule that stores or enforces a per-rank capability matrix. The enforced club authorization model is **role based**, derived solely from the administrative role on the member document: - `lib/club-data.ts` → `getPortalPermissions(role)` maps `owner`/`admin` → `canManageSettings`, `canInviteMembers`, `canApproveApplications`, `canPlanRides`; `member` → all false. - Display rank (`President`, `Road Captain`, …) is cosmetic and explicitly does **not** grant software authority. Persisting the matrix without enforcement would mislead club owners into believing a toggle restricts members when it does nothing. That is prohibited by the project rules (no unenforced permission models, no false success). ## Why this was not implemented in the website task - Enforcement requires **Firestore security rule** changes (out of scope / frozen). - A new persistence model must not conflict with the existing Flutter club role model (`owner`/`admin`/`member`). Rank → capability is a different axis and needs a product decision on precedence. ## Proposed contract (for a future, authorized backend change) ### 1. Storage Document: `clubs/{clubId}/settings/rankPermissions` ```jsonc { "version": 1, "updatedAt": "", "updatedByUid": "", "ranks": { "road_captain": { "planRides": true, "manageApplications": false, "postAnnouncements": true, "moderateWall": true, "manageRoster": false, "editProfile": false } // ... one entry per rank id in DEFAULT_CLUB_RANKS except `president` } } ``` Invariants: - `president` is immutable (always full authority); never written or editable. - Unknown rank ids and unknown capability keys are rejected. - Capability keys are constrained to `DEFAULT_CLUB_CAPABILITIES` ids. ### 2. Security rules (requires authorized change to the frozen rules) - Read: any active club member. - Write: only `clubs/{clubId}.ownerUid == request.auth.uid`, or an active member whose `role == 'admin'`. Validate the written shape (version, allowed rank ids, boolean capability values only). Reject writes that would alter `president`. ### 3. Enforcement precedence (product decision required) Rank capabilities may only **narrow**, never widen, what the administrative role already denies. Administrative `role` remains the hard security boundary enforced in rules; rank capabilities are a soft, owner-configurable delegation layer that the Flutter and web clients honor for UI affordances. The Flutter club model must adopt the same precedence so web and mobile agree. ### 4. Website wiring once the above exists 1. Add `fetchRankPermissions(clubId)` and `saveRankPermissions(clubId, callerUid, matrix)` to `lib/club-data.ts`, mirroring the authorization pattern of `updateClubAccessSettings` (owner/admin check + `updateDoc`). 2. In `ranks-roles/page.tsx`: load persisted permissions in `loadData`, and implement `handleSavePermissions` to call `saveRankPermissions`, then re-read to confirm the write survived before showing success. 3. Flip `` and pass `onSavePermissions` / `onResetDefaults` / `isSaving`. 4. Add tests: persistence round-trip (save → reload → values survive), owner/admin authorization success, member/unauthenticated rejection, `president` immutability, and rejection of unknown rank/capability keys. ## Current truthful behavior (shipped in this branch) - Matrix renders as a read-only reference of the standard governance model. - No Save/Reset controls; a "View-only reference" indicator and an availability notice explain that access is enforced by Owner/Admin roles and that per-rank customization is planned. - No save is attempted and no success is reported.