easySPEAK — Feature × Role Access Matrix

As-is audit of every feature and who can access it · proposed "less is more" target model · 10 July 2026 · Paul Cowen

Basis: full source audit of easy-speak-rep — v2.33/v2.34 schema, 163 tables, 119+ stored procedures, ~200 web pages

"Define a table/matrix identifying every feature or function and define who has access to it. Reducing the permission combinations should be something to strive for. Less is more." — with the API permission model as the central, testable path forward.

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.

~90%of web page access gates collapse to binary "officer-or-not" (+admin override)
68named API permissions × 8 grant types — the central model already in production
17free-form boolean flags per officer row → 217 possible combinations; ~7 archetypes actually intended
1officer title special-cased anywhere in code: VPE. No "if President…" exists

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

F1 — VPE ti_code discrepancy (docs vs code).

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.

F2 — buddy/mentor grants honoured inconsistently.

~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.

F3 — same_user over-grants in the bulk listing.

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.

F4 — nothing is public by accident.

The anyone grant column exists (v2.25) but no seeded permission uses it. Worth confirming intentional — otherwise a clean bill of health.

F5 — officer flag combinations are unbounded.

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.

Live example (10 July) — role-filling is a two-step process.

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.

Open question for the team — why isn't role assignment a committee-wide default?

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.

G Guest/publicM Club memberMC Meeting chairCO Club officer — any committee title: President, Secretary, Treasurer, VPM, VPPR, SAA, IT/Webmaster… The system doesn't distinguish between them; what each officer can do is set by their row's 17 boolean flags. VPE is the only title the code recognises by identity.VPE VP EducationDO District/Div/Area officer (incl. DD)AD Admin (ADMIN/TM_ADMIN; DISTRICT_ADMIN = scoped)
full access · V view-only · club-configurable (public/feature flag) · f depends on officer's own flags · no access · API centrally covered · part partial

1 · Meetings & agenda

FeatureGMMCCOVPEDOADAPI
Meeting calendar / upcoming & past meetingsAPI
View meeting / agenda (desktop, mobile, print)API
Create meeting / bulk creationfAPI
Edit meeting, agenda items, slot timingsfAPI
Close meeting (record outcomes)fAPI
Meeting attendance (view / confirm own)API
Attendance reports / reminders (email, print)f
Meeting audit / timezone shift utilities

2 · Roles & participation

FeatureGMMCCOVPEDOADAPI
Sign up for / decline roles (self); confirm attendanceAPI
Accept role (email link — self only)API
Assign / unassign others to roles (tightened v2.28)API
Record role outcomesAPI
Table Topics speakers (manage / view)VAPI
Request a speech slot (self)API
Schedule speakers (others)fpart
Speech introductions

3 · Education — Pathways, workbooks, evaluations

FeatureGMMCCOVPEDOADAPI
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

FeatureGMMCCOVPEDOADAPI
Member charts (10 charts)API
Club annual review / stats
DCP tracker (flag-gated)API
DCP education progress / level completionsAPI
OTM / member survey (flag-gated)f
Site history (audit log)

5 · Members & club directory

FeatureGMMCCOVPEDOADAPI
Member list
Add member / guestf
Edit member records / statusf
Club data page incl. Club Control Panel (edit)VVVff
User profile (own)API
Find-a-club, maps, neighbouring clubs

6 · Communication

FeatureGMMCCOVPEDOADAPI
Private messages (own)API
Club forums (view / post)API
Email agenda to membersf
Club mass emailff
District / org email
Push notifications & preferences (own device)API

7 · Club website, portal & media

FeatureGMMCCOVPEDOADAPI
Portal pages (view)API
Portal pages (edit)API
Photo albums (view / upload / moderate)f
Knowledge base / help wiki

8 · Events, payments & voting

FeatureGMMCCOVPEDOADAPI
Event booking / payment (self)login
Dues / paid-until adminf
Live voting — cast (flag-gated)API
Live voting — manageAPI
District elections & resultsV

9 · District / multi-club & 10 · System admin

FeatureGMMCCOVPEDOADAPI
Area / Division / District data pagesVVVVV
District directory generation (council)
Translation team tools
Feature flagsAPI
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.

GroupActionsTypical grants
Meetings — viewView current / upcoming / past meetings, agenda, schedule, attendance, locations, templates, managersCM + CO + VPE + MC + AD
Meetings — manageCreate meeting · Edit meeting · Close meeting · Set meeting awardCO + VPE (+ MC for edit/close) + AD
Roles & participationConfirm attendance · Accept role · Assign / Unassign member to role (VPE + MC + AD only, tightened v2.28) · Record role outcomes · Get eligible members · Table Topics manage / viewmixed — deliberately narrow for assignment
SpeechesCreate / Edit / Delete speech request · View speech request data · View user speech requestsSU + CO + VPE + BM (+ CM for create/view)
Pathways & workbooksCreate / Delete user workbook · View member progress · View available role tasks · Edit member-role-pathway link · Pathway role-task add / edit / delete · Link / Unlink role assignmentSU + CO + VPE + BM + AD
EvaluationsView evaluation forms · Submit evaluation · View evaluationCM + CO + VPE + MC (view own: SU)
ForumsView metadata / topics / posts · Create topic / post · Edit own post · Rename topicCM + CO + VPE + AD (edit own: SU)
Messaging & notificationsView / Send private messages · Register / Get / Delete push device · Get / Set notification preferencesSU + AD only — strictly personal
PortalView portal page · List portal pages · Edit portal pageview: CM+CO+VPE+AD · edit: CO+AD
Charts & DCPView club charts · View DCP tracker · DCP education progress · DCP level completionsmember view; officer detail
Live votingCast live vote (CM+AD) · Manage live vote (CO+MC+AD)v2.34
Profile / SystemView 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.

PresetMaps toDefault capability bundle
Gueststatus full_member=0View what the club makes public; book attendance
Memberstatus full_member=1Participate: roles, speeches, workbooks, forums, PMs, votes
Meeting Managermeeting chair (topic poster) — proposed: also the meeting's assigned Toastmaster/TMEMember + edit/close their meeting, assign roles in it (covers live role-filling around the break when no officer is present)
Club Officer — standardSecretary, Treasurer, SAA, VPM, VPPR, IPPMember + edit meetings/agendas, member admin, club email
Club Officer — educationVPE (ti_code=2)Standard + schedule speakers, pathways/workbook admin
Club Officer — fullPresident (+ configurable)Standard + edit club settings, change officers
District Officerti_code 50–99 (AD / DivD / DD)Officer capabilities across all clubs in their org unit + district features
System Adminuser_level 1 / 10 / 20Platform 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:

  1. 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?).
  2. 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.
  3. 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

  1. Adopt this matrix as the living registry (KB page). Every feature row carries a state: legacy-only → API-covered → legacy-check-retired.
  2. Fix the cheap discrepancies now — F1 docs fix (1 line); F2 add buddy/mentor to sp_get_user_permissions; F3 document same_user bulk semantics. Three small, independent, testable MRs.
  3. 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.
  4. Per legacy-only feature: permission row → stored proc (validating first) → endpoint → point web/app at it. One feature per MR, each testable by direct CALL on the test DB + curl. Suggested order: member management → club data/Control Panel → email tooling → reports → district → admin panel.
  5. Only then consider retiring legacy layers — by which point every access decision is named, central, and individually tested.