Proposal · Alpha School vendor portal · 8 Nov 2026

Approver matrix

Add category owners on top of the existing campus approvers: for example, one person approves lunch bills for every campus, with a backup. Everything else keeps going to the campus's approvers, as today.

Doesn't change today's routing

The approver rules table already has a category column, and everything that reads it filters to rows with no category. Category rows are invisible to current routing.

One row at a time

Add one, delete one, with a change history. No bulk replace. Removing a campus's last approver needs an explicit override.

Switched on in steps

Phase 1: the page, the API, the history. Then a shadow mode that records "would have gone to X". Only then does routing use it.

Where a bill's category comes from

Bills are assigned at submit, before anyone has coded an account, so the category can't come from the bill's own coding. In order:

  1. The account this vendor's bills were last booked to in QuickBooks (from the mirror; free and instant)
  2. Else the vendor's default category, set by the billing team
  3. Else no category: the campus approvers

Open questions

  • The real category list, and which QuickBooks accounts go in each (from finance).
  • The existing Approvers page replaces a campus's list in one go: should it write to the same change history?
  • When this is switched on, the assign-approver step checks category owners first. How would you like that done?
1 · The page

Approval routing, for the billing team

A clickable mockup. Try the tabs, and "Test a bill". Every person, category and account is an example.

Alpha School vendor portal billing-admin@alpha.school (example)

Approval routing

Who approves each invoice. A category owner gets the bill first, for the campuses they cover. Everything else goes to the campus's approvers.

73 campuses2 campuses with no approver3 categories with an owner

One person who approves a kind of spend across campuses, so a lunch bill doesn't wait on each campus's coordinator. The backup gets the bill when the owner says "Not mine" or is away; if neither can, it goes to the campus approvers.

CategoryCampusesOwnerBackupActions
LunchAll campusesRiley Example
riley@alpha.school (example)
Sam Example
SecurityAll campusesDrew Example
drew@alpha.school (example)
None
TransportationAustin K-8, MiamiParker Example
parker@alpha.school (example)
None
UtilitiesAll campusesNo owner: goes to campus approvers
2 · The spreadsheet

The sheet finance fills in

One row per person per campus or category. The billing team uploads it on the Import tab and applies the changes one at a time. Export gives back the same file.

3 · The JSON API · billing team only · staff sign-in or MCP token

One row at a time

Read the whole matrix in one call; change it one row per call. Every add and delete is recorded with who and when. There is no call that replaces everything: the spreadsheet import sends these same single-row calls, after you've seen each one.

Add one row
POST /api/portal/staff/approver-matrix/campus-approvers
{ "realmId": "9130350000000003",
  "email": "morgan@alpha.school",
  "name": "Morgan Example" }

201 Created
{ "id": "par_4k2…", "campus": "Park City",
  "email": "morgan@alpha.school",
  "addedBy": "billing-admin@alpha.school",
  "addedAt": "2026-11-08T20:31:04Z",
  "newPortalUser": true }

409 if that person already approves that campus
Delete one row
DELETE /api/portal/staff/approver-matrix/campus-approvers/par_9x1…

409 Conflict
{ "message": "Casey Example is Miami's only approver.
   Removing them leaves Miami's bills with no one.",
  "lastApprover": true }

DELETE …/campus-approvers/par_9x1…?leaveCampusEmpty=true

200 OK
{ "removed": "par_9x1…", "campusNowEmpty": true }
One row per callNo bulk or replace call exists. A mistake touches one row, and the history shows it.
Last approver is protectedRemoving a campus's only approver, or a category's only owner, is refused unless you say so explicitly.
Bills already waitingA change affects new bills. Bills already waiting keep their approvers unless reassigned.