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.