Executive summary
easySPEAK has three parallel legacy permission layers plus a fourth, newer one — the API permissions table — which is the only layer that is data-driven and centrally maintainable. The matrix below documents every feature against every role, and doubles as a migration tracker: each feature is either already covered by the central API model or still guarded by scattered legacy checks.
The practical consequence: "less is more" doesn't require new enforcement machinery. The API layer's 8 grant types are a good model. What's needed is (1) named role presets over the existing officer flags, (2) feature-by-feature API coverage of the legacy-only areas, each an independently testable step, and (3) this matrix maintained as the single registry of who-can-do-what.
Findings worth a decision at the meeting
docs/API_PERMISSIONS.md (and the iOS copy) say VPE = ti_code 20; the enforcing procs (sp_validate_api_permission, sp_get_user_permissions) check ti_code = 2, matching ROLE_VPE=2 in constants.php. The docs are wrong — one-line fix.
~13 permissions grant buddy/mentor access. The per-action gate implements it; the bulk sp_get_user_permissions (which apps use to render UI) does not — buddies/mentors hold rights the UI never shows them.
sp_get_user_permissions returns every same_user=1 action unconditionally (no target-member context), so clients can render actions the per-action gate then denies.
The anyone grant column exists (v2.25) but no seeded permission uses it. Worth confirming intentional — otherwise a clean bill of health.
17 booleans set free-hand per club per officer row. This is the single biggest "less is more" win: named presets per officer title (§ Target model) collapse the combinations without changing enforcement.
At last night's club meeting, open roles were filled around the break the way clubs actually do it: Paul — Treasurer — calling the names, and Kit, who held the assignment permissions and the laptop, entering them. It worked, but only because Kit was in the room. Assignment rights are deliberately narrow today: v2.28 restricted "Assign member to role" to VPE + meeting chair + admin on the API side, and the website hangs it off per-officer booleans (officer_edit_assignment, officer_schedule_speakers). Note also that "meeting chair" means the topic poster who created the meeting, not the member running it as Toastmaster. The requirement: live role-filling should be one step, not two — whoever is running the segment assigns directly, rather than relaying through a permission-holder.
The resilience argument: if only one or two people can work easySPEAK for the club, everything stalls when they're away or out of action. A plausible default is that any committee member (club officer) can assign/agree roles, with the v2.28 narrowing (VPE + meeting chair + admin) as an opt-in restriction for clubs that want it — rather than the current hard-coded narrow default. v2.28 deliberately removed club_member/club_officer from "Assign member to role", so this was a conscious choice; the question is what drove it, and whether it should be per-club configurable. Presets make this cheap: it's one default in the "Club Officer — standard" bundle, not a schema change.
How access control actually works today — four layers
1 · phpBB account level
phpbb_users.user_level: ANONYMOUS(-1) · USER(0) · ADMIN(1) · MOD(2) · TM_ADMIN(10) · DISTRICT_ADMIN(20, scoped via phpbb_tm_district_admin). Admin override appears everywhere.
2 · Officer rows
phpbb_tm_officer: free-text title + ti_code (identity and hierarchy: 1–49 club · 50–59 area · 60–69 division · 70–99 district) + 17 boolean capability flags. Resolved by get_TM_all_authority() into the global $tm_auth.
3 · Membership status
phpbb_tm_users.status → phpbb_tm_status: full_member (member vs guest) + participation flags (can_take_role, can_speak, …). A guest is just data.
4 · API permissions — the destination
phpbb_tm_api_permissions: 68 named actions × 8 grant columns (same-user, club member, club officer, VPE, meeting chair, buddy/mentor, admin, anyone). Every authorised proc validates first. Central, data-driven, testable.
The near-binary reality: counting actual checks across all web pages — binary is_officer 63×, district_officer 32×; the 17 granular flags appear 233× but mostly toggle in-page buttons (is_vpe 50×, edit_users 25×, edit_meeting 21×, schedule_speakers 20×), not page access. Different officer titles only differ through whatever flags a club happened to set.
The Feature × Role matrix (as-is)
Roles from District Director down to guest, as requested. The API column marks coverage by the central permission model — every — there is a future testable migration step.
1 · Meetings & agenda
| Feature | G | M | MC | CO | VPE | DO | AD | API |
|---|---|---|---|---|---|---|---|---|
| Meeting calendar / upcoming & past meetings | ⚙ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
| View meeting / agenda (desktop, mobile, print) | ⚙ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
| Create meeting / bulk creation | — | — | — | f | ✓ | ✓ | ✓ | API |
| Edit meeting, agenda items, slot timings | — | — | ✓ | f | ✓ | ✓ | ✓ | API |
| Close meeting (record outcomes) | — | — | ✓ | f | ✓ | ✓ | ✓ | API |
| Meeting attendance (view / confirm own) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
| Attendance reports / reminders (email, print) | — | — | ✓ | f | ✓ | ✓ | ✓ | — |
| Meeting audit / timezone shift utilities | — | — | — | ✓ | ✓ | ✓ | ✓ | — |
2 · Roles & participation
| Feature | G | M | MC | CO | VPE | DO | AD | API |
|---|---|---|---|---|---|---|---|---|
| Sign up for / decline roles (self); confirm attendance | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
| Accept role (email link — self only) | — | ✓ | — | — | — | — | ✓ | API |
| Assign / unassign others to roles (tightened v2.28) | — | — | ✓ | — | ✓ | — | ✓ | API |
| Record role outcomes | — | — | ✓ | ✓ | ✓ | — | ✓ | API |
| Table Topics speakers (manage / view) | — | V | ✓ | ✓ | ✓ | — | ✓ | API |
| Request a speech slot (self) | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | API |
| Schedule speakers (others) | — | — | — | f | ✓ | ✓ | ✓ | part |
| Speech introductions | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
3 · Education — Pathways, workbooks, evaluations
| Feature | G | M | MC | CO | VPE | DO | AD | API |
|---|---|---|---|---|---|---|---|---|
| My workbooks / Pathways progress (own) | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | API |
| View / manage another member's progress (+ buddy/mentor) | — | — | — | ✓ | ✓ | — | ✓ | API |
| Pathway role-task admin (add/edit/delete) | — | — | — | ✓ | ✓ | — | ✓ | API |
| Evaluations (view forms / submit / view own) | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | API |
| CC/CL manual tracking, club speech log | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
4 · Charts, reports & DCP
| Feature | G | M | MC | CO | VPE | DO | AD | API |
|---|---|---|---|---|---|---|---|---|
| Member charts (10 charts) | ⚙ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
| Club annual review / stats | ⚙ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| DCP tracker (flag-gated) | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | API |
| DCP education progress / level completions | — | — | — | ✓ | — | — | ✓ | API |
| OTM / member survey (flag-gated) | — | ✓ | ✓ | f | ✓ | — | ✓ | — |
| Site history (audit log) | — | — | — | ✓ | ✓ | ✓ | ✓ | — |
5 · Members & club directory
| Feature | G | M | MC | CO | VPE | DO | AD | API |
|---|---|---|---|---|---|---|---|---|
| Member list | ⚙ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| Add member / guest | — | — | — | f | ✓ | ✓ | ✓ | — |
| Edit member records / status | — | — | — | f | ✓ | ✓ | ✓ | — |
| Club data page incl. Club Control Panel (edit) | V | V | V | f | f | ✓ | ✓ | — |
| User profile (own) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
| Find-a-club, maps, neighbouring clubs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
6 · Communication
| Feature | G | M | MC | CO | VPE | DO | AD | API |
|---|---|---|---|---|---|---|---|---|
| Private messages (own) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
| Club forums (view / post) | ⚙ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
| Email agenda to members | — | — | ✓ | f | ✓ | ✓ | ✓ | — |
| Club mass email | — | — | — | f | f | ✓ | ✓ | — |
| District / org email | — | — | — | — | — | ✓ | ✓ | — |
| Push notifications & preferences (own device) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
7 · Club website, portal & media
| Feature | G | M | MC | CO | VPE | DO | AD | API |
|---|---|---|---|---|---|---|---|---|
| Portal pages (view) | ⚙ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | API |
| Portal pages (edit) | — | — | — | ✓ | — | — | ✓ | API |
| Photo albums (view / upload / moderate) | ⚙ | ✓ | ✓ | f | ✓ | — | ✓ | — |
| Knowledge base / help wiki | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
8 · Events, payments & voting
| Feature | G | M | MC | CO | VPE | DO | AD | API |
|---|---|---|---|---|---|---|---|---|
| Event booking / payment (self) | login | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| Dues / paid-until admin | — | — | — | f | — | ✓ | ✓ | — |
| Live voting — cast (flag-gated) | — | ✓ | — | — | — | — | ✓ | API |
| Live voting — manage | — | — | ✓ | ✓ | — | — | ✓ | API |
| District elections & results | — | V | — | — | — | ✓ | ✓ | — |
9 · District / multi-club & 10 · System admin
| Feature | G | M | MC | CO | VPE | DO | AD | API |
|---|---|---|---|---|---|---|---|---|
| Area / Division / District data pages | V | V | V | V | V | ✓ | ✓ | — |
| District directory generation (council) | — | — | — | — | — | ✓ | ✓ | — |
| Translation team tools | — | — | — | ✓ | ✓ | ✓ | ✓ | — |
| Feature flags | — | — | — | — | — | — | ✓ | API |
| User / club / officer / workbook admin (~80 screens) | — | — | — | — | — | scoped | ✓ | — |
| DB utilities, cache, exports, security tooling | — | — | — | — | — | — | ✓ | — |
Reading the API column as a migration tracker: meetings, roles, education, forums, messaging, notifications, portal, DCP and voting are already centralised. The big legacy-only blocks — each a candidate for the next testable steps — are member management, club data / Club Control Panel, email tooling, reports, district features, and the admin panel.
The API permission model — 68 actions, grouped
Grant key: SU same-user · CM club member · CO club officer (any ti_code 1–99) · VPE (ti_code=2) · MC meeting chair · BM buddy/mentor · AD admin. No action grants anyone.
| Group | Actions | Typical grants |
|---|---|---|
| Meetings — view | View current / upcoming / past meetings, agenda, schedule, attendance, locations, templates, managers | CM + CO + VPE + MC + AD |
| Meetings — manage | Create meeting · Edit meeting · Close meeting · Set meeting award | CO + VPE (+ MC for edit/close) + AD |
| Roles & participation | Confirm attendance · Accept role · Assign / Unassign member to role (VPE + MC + AD only, tightened v2.28) · Record role outcomes · Get eligible members · Table Topics manage / view | mixed — deliberately narrow for assignment |
| Speeches | Create / Edit / Delete speech request · View speech request data · View user speech requests | SU + CO + VPE + BM (+ CM for create/view) |
| Pathways & workbooks | Create / Delete user workbook · View member progress · View available role tasks · Edit member-role-pathway link · Pathway role-task add / edit / delete · Link / Unlink role assignment | SU + CO + VPE + BM + AD |
| Evaluations | View evaluation forms · Submit evaluation · View evaluation | CM + CO + VPE + MC (view own: SU) |
| Forums | View metadata / topics / posts · Create topic / post · Edit own post · Rename topic | CM + CO + VPE + AD (edit own: SU) |
| Messaging & notifications | View / Send private messages · Register / Get / Delete push device · Get / Set notification preferences | SU + AD only — strictly personal |
| Portal | View portal page · List portal pages · Edit portal page | view: CM+CO+VPE+AD · edit: CO+AD |
| Charts & DCP | View club charts · View DCP tracker · DCP education progress · DCP level completions | member view; officer detail |
| Live voting | Cast live vote (CM+AD) · Manage live vote (CO+MC+AD) | v2.34 |
| Profile / System | View user profile (SU+CO+VPE+BM+AD) · Feature flags save / update / delete / remove (AD only) |
Enforcement: every authorised stored procedure's first statement is sp_validate_api_permission(action, user, club, other_member, meeting) — short-circuit OR over the grant columns. OAuth scopes (meetings:read, profile:read, …) gate the app before the role matrix applies. Full row-by-row seed data: PATCH v2.23 → v2.34.
Proposed target model — "less is more"
Keep the enforcement mechanics (they work). Collapse the combinations in the wild with named role presets, and let each preset double as a view preset — the "different view and default features per role" ask falls out of the same model, because apps already pre-render UI from sp_get_user_permissions and switch_view.php already toggles officer/member views.
| Preset | Maps to | Default capability bundle |
|---|---|---|
| Guest | status full_member=0 | View what the club makes public; book attendance |
| Member | status full_member=1 | Participate: roles, speeches, workbooks, forums, PMs, votes |
| Meeting Manager | meeting chair (topic poster) — proposed: also the meeting's assigned Toastmaster/TME | Member + edit/close their meeting, assign roles in it (covers live role-filling around the break when no officer is present) |
| Club Officer — standard | Secretary, Treasurer, SAA, VPM, VPPR, IPP | Member + edit meetings/agendas, member admin, club email |
| Club Officer — education | VPE (ti_code=2) | Standard + schedule speakers, pathways/workbook admin |
| Club Officer — full | President (+ configurable) | Standard + edit club settings, change officers |
| District Officer | ti_code 50–99 (AD / DivD / DD) | Officer capabilities across all clubs in their org unit + district features |
| System Admin | user_level 1 / 10 / 20 | Platform administration (scoped for DISTRICT_ADMIN) |
Presets are applied when an officer is appointed; per-club overrides remain possible but become flagged, auditable exceptions. Result: same 8 API grant types, ~7 presets covering ~95% of officer rows, one registry (this matrix). Fewer combinations — not fewer capabilities.
Design note — speech slots as "dibs" placeholders
Allocating speeches ahead of time needs its own careful flow, distinct from role assignment. Proposed lifecycle:
- Claim ("dibs"): a member says they want to speak and gets a placeholder, not a confirmed speech. Fairness rules apply only under contention: 3 slots and 3 requesters — no rules needed, everyone speaks. 6 requesters for 3 slots — recency decides, surfaced as traffic lights next to each requester (e.g. red = spoke at the previous meeting, amber = spoke within the fairness window, green = due a slot). Small clubs will rarely see this; it matters as clubs grow, and seasonally. The data already exists (
last_spoke, speech history) — the traffic light is a display over it, not new machinery. Competition-season wrinkle: contest preparation legitimately means giving the same speech more than once, and often visiting other clubs to hone it — so the rules need a contest-prep exception (or a club toggle to prioritise it), and repeat delivery of a marked contest speech shouldn't count against recency the same way as a new project. Cross-club recency: today each club only sees when someone last spoke there — a member of several clubs can show "never spoke here" while speaking weekly elsewhere. Fine when there's no contention; under contention the traffic light should optionally reflect overall recency. Speech history is stored per user (not per club), so a cross-club view is already queryable within an instance — and becomes universal after the planned 3-instance merge. Needs a visibility decision (does a club see activity at other clubs by default, or just an aggregate "spoke recently elsewhere" flag?). - Confirm: before a deadline, the member fills in the placeholder — picks their path/project and speech title. The agenda updates from "Speaker: (reserved — Paul)" to the real entry.
- Release: if they don't confirm in time, or choose to step back, the slot is released — offered to the next requester or opened for general signup — rather than silently sitting as an empty placeholder on the agenda.
Most of the machinery exists (speech requests, agenda slots, notification preferences); what's missing is the explicit placeholder state, the fairness window, and the confirm/release transitions with reminders. This slots naturally into the API-first migration as its own testable work item.
Migration in testable steps
- Adopt this matrix as the living registry (KB page). Every feature row carries a state:
legacy-only → API-covered → legacy-check-retired. - Fix the cheap discrepancies now — F1 docs fix (1 line); F2 add buddy/mentor to
sp_get_user_permissions; F3 documentsame_userbulk semantics. Three small, independent, testable MRs. - Seed the presets — one new table (
phpbb_tm_officer_preset) + "apply preset" in officer admin. No enforcement change; pure data hygiene, verifiable by before/after flag comparison. - Per legacy-only feature: permission row → stored proc (validating first) → endpoint → point web/app at it. One feature per MR, each testable by direct
CALLon the test DB + curl. Suggested order: member management → club data/Control Panel → email tooling → reports → district → admin panel. - Only then consider retiring legacy layers — by which point every access decision is named, central, and individually tested.