A verification gate MAC puts in front of Humanitix ticket links. It checks whether a visitor is a current MAC member (via mac-auth login + the MSA membership roster) and either sends them to Humanitix with a personal discount code pre-applied, or to the normal ticket link.
See SPEC_mac_membership_verify.md for the source of truth and CLAUDE.md for the rules that
are easy to get wrong. This is not a Humanitix replacement and not a payments system.
React + Vite frontend, Express API, Postgres via Drizzle ORM, single container (Dokploy on the Oracle VM). Matches the rest of the MAC Suite.
- 1. Roster import — schema, admin upload route, safety gate, xlsx parsing.
- 2. mac-auth integration — real Better Auth flow: session-cookie sign-in → JWT → Bearer,
EdDSA JWKS verification against
monashcoding/mac-auth(macUserId/roles/teamclaims,mac-suiteaudience). - 3. Auto-apply link discovery — confirmed
?discountcode={code}(§8),composeAutoApplyUrl. - 4. Linking flow — email auto-match, student-ID entry, DB rate limiting, skip, full §7 state machine.
- 5. Code provisioning cron — deterministic codes, CSV export, Discord ping, Triggers A & B.
- 6. Event-specific verify page —
/e/:slugwith interstitial + manual continue. - 7. Generic verify page —
/with active-events list. - 8. Events admin CRUD — create/activate fires Trigger A.
- 9. Manual review queue — attempt-exhausted list + officer manual link.
npm install
cp .env.example .env # point DATABASE_URL at your Postgres
npm run db:generate # generate SQL migrations from the Drizzle schema
npm run db:migrate # apply them
npm run dev # start the API on :3000
npm run dev:web # start the Vite SPA on :5173 (proxies /api → :3000)
npm test # unit tests: parser, safety gate, rate limit, codes (no DB)
npm run build # compile server + build SPA into dist/ (single container)
npm run cron:daily-diff # Trigger B (§9) — wire to a scheduled jobThe frontend lives in web/ (React + Vite) and is served by Express from dist/web/ in
production — one container serving SPA + API (§3).
Integration follows monashcoding/mac-auth: the SPA
POSTs /api/auth/sign-in/social → provider → callback sets a .monashcoding.com session
cookie → SPA GETs /api/auth/token for a JWT and sends it as Authorization: Bearer. The
backend (src/server/auth/mac-auth.ts) verifies it locally against the EdDSA JWKS, mirroring the
service's own examples/verify.ts (issuer AUTH_URL, audience mac-suite, canonical id
macUserId).
Account chooser (§5): mac-auth now sets prompt=select_account on its Google and Microsoft
providers (suite-wide), so the provider account chooser is forced at login — no silent auto-pick
of the wrong account. It only applies during an actual OAuth login; an existing MAC session is
reused untouched. The app additionally shows the signed-in account with a "Not you? Switch
account" control (AccountBar) for switching after the fact.
Single container (SPA + API) + Postgres via Dokploy — see
docs/deploy-dokploy.md. Migrations apply automatically on container
start; auto-deploy triggers on push to main.
Admin-only, gated by a mac-auth exec/admin role claim.
# Upload the MSA Clubs & Societies export (Members_*.xlsx):
curl -X POST http://localhost:3000/api/admin/roster/import \
-H "Authorization: Bearer <mac-auth JWT>" \
-F file=@Members_2026.xlsx
# Current roster visibility:
curl http://localhost:3000/api/admin/roster/summary \
-H "Authorization: Bearer <mac-auth JWT>"Safety gate (§6): an import whose ENROLLED count is zero, or less than half the current
ENROLLED count, is refused with HTTP 409 unless you re-submit with -F override=true. This
stops a wrong-sheet or half-downloaded file from silently gutting the membership base.
Each import replaces the roster snapshot wholesale under a new import_batch_id; prior batches
are kept queryable, never hard-deleted.
Every enrolled member gets a distinct discount code per event (member_event_codes, keyed on
roster_id + event_id) — MAC's member pricing is set ad hoc per event, so a single global code
can't reproduce the right price everywhere. Officers never touch codes by hand; the admin panel
generates them and hands back a CSV.
Auto-listing (primary path). Set HUMANITIX_API_KEY (Humanitix account → Settings → API
Keys) and the admin lists the org's live, upcoming events straight from the Humanitix read-only
Public API — no per-event URL entry. Each row shows whether codes exist yet; Download codes CSV
provisions codes for that event and downloads the file in one click. This is the official
x-api-key API used for reading only (event list + one event's details) — not dashboard
scripting, not order/ticket sync. Uploading that CSV into each event's Promote → Discounts → CSV
upload stays a manual step by design (there is no discount-upload API). Unset the key and the
admin falls back to manual event entry.
The auto-apply link handed to verified members is {event_url}?discountcode={code} (§8).
Student-ID rate limit (§7). Failed student-ID lookups are throttled in the DB (survives page reloads): 20 failed attempts, then a 1h cooldown from the last attempt. The cap only exists to stop scripted ID enumeration — a real member who mistypes effectively never hits it.