4.3 KiB
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)mapsowner/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
{
"version": 1,
"updatedAt": "<serverTimestamp>",
"updatedByUid": "<uid>",
"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:
presidentis immutable (always full authority); never written or editable.- Unknown rank ids and unknown capability keys are rejected.
- Capability keys are constrained to
DEFAULT_CLUB_CAPABILITIESids.
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 whoserole == 'admin'. Validate the written shape (version, allowed rank ids, boolean capability values only). Reject writes that would alterpresident.
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
- Add
fetchRankPermissions(clubId)andsaveRankPermissions(clubId, callerUid, matrix)tolib/club-data.ts, mirroring the authorization pattern ofupdateClubAccessSettings(owner/admin check +updateDoc). - In
ranks-roles/page.tsx: load persisted permissions inloadData, and implementhandleSavePermissionsto callsaveRankPermissions, then re-read to confirm the write survived before showing success. - Flip
<ClubPermissionMatrix persistenceAvailable={true} />and passonSavePermissions/onResetDefaults/isSaving. - Add tests: persistence round-trip (save → reload → values survive), owner/admin
authorization success, member/unauthenticated rejection,
presidentimmutability, 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.