Parliament
Chapter management platform for Beta Theta Pi · Alpha Mu Chapter
Parliament is the platform I built from scratch to run the Alpha Mu chapter of Beta Theta Pi. It replaced a scattered mix of spreadsheets, group chats, and paper sign-in sheets with one app that every active member, pledge, and officer uses.
It handles chapter business end to end: legislation and elections, committees, events and attendance, service hours, pledge education, real-time chat, and the chapter's public website. Security is built in rather than bolted on: passkeys, policy-driven two-factor, attack detection, and field-level encryption.
I built and maintain it solo. Right now I'm working on multi-chapter support and on the documentation future chapter leadership will need to take it over when I graduate in May 2027.

157k
lines of Python
2,900+
automated tests
207
versioned releases
560+
commits, solo
Tour the App
Real screens from the current version, running on demo data.

Open votes, as a member sees them. A bylaw amendment needing a two-thirds majority, and a venue vote that runs a runoff automatically if nothing clears a majority.
- —Casting a ballot requires re-entering your password, so an unlocked phone left on a table can't vote for you
- —Every ballot gets a receipt that the member can later verify was counted
- —Three vote modes: percentage thresholds, exact counts, and plurality with automatic runoffs
What It Covers
Ten modules, all in one Django project.
Legislation & voting
Percentage, exact-count and plurality votes with automatic runoffs, anonymous ballots, co-authors, and a verifiable receipt for every vote.
Elections
A configurable officer slating process: custom application forms, eligibility and GPA checks, interviews, and secret-ballot rounds.
Committees
Dashboards, committee-level votes that can be pushed to the whole chapter, documents, minutes, and attendance.
Events & attendance
Calendar with .ics subscriptions, sign-ups with caps and auto-promoting waitlists, attendance with an excuse workflow.
Service hours
Submissions with a custom form builder, approval queues, per-period requirements, and hours credited automatically from service events.
Pledge education
Tasks, quizzes with per-question analysis, points, meeting attendance and absence requests, one page per pledge for the educator.
Recruitment
Rush events and a candidate pipeline from prospect to bid, with permission tiers for private notes.
Real-time chat
WebSocket channels for the chapter and each committee, unread counts, and in-app push notifications.
Public website
The chapter's public site, fully editable by officers: WYSIWYG content, a photo library, a routed contact form, SEO previews.
Admin & operations
Feature flags, page toggles, audit logs, quarantine and emergency lockdown, and a documented “log in as” support tool.
Under the Hood
What's actually running in production, not just the UI.
Architecture
- —Django served by Daphne, one ASGI server handling both HTTP and the WebSocket chat, behind nginx, which accepts traffic only from Cloudflare
- —PostgreSQL behind PgBouncer for connection pooling; Redis for the cache, sessions, and the real-time channel layer
- —Celery workers and a Celery beat scheduler for everything time-based: votes that open and close themselves, event reminders, digest emails, and a weekly weak-password audit
- —Each process runs as its own systemd service, with a nightly backup on a systemd timer
Security & access control
- —WebAuthn passkeys alongside policy-driven TOTP 2FA — the org can require it for admins only, officers and admins, everyone, or set per-user overrides, with a signed “remember this device” cookie
- —Progressive rate limiting on login, password reset, and passkey auth, scoped by both IP and username with escalating lockouts, persisted for admin visibility
- —Middleware that scans every request for injection and XSS patterns, auto-quarantining accounts and blacklisting IPs after repeated attempts — logged, not silent
- —Geo-restriction: a session that logs in from outside the US is flagged and blocked from bulk data exports (member directories, audit logs, service-hour CSVs), matched by resolved URL name so a moved route stays covered
- —Per-request CSP nonces with no unsafe-inline scripts, and explicit no-store cache headers on every dynamic response so a CDN or a phone's back/forward cache can never replay one member's session to someone else
- —An admin “impersonate user” tool with a single documented allowlist of what it bypasses (account setup screens) versus what it never does (quarantine, lockdown, maintenance mode) — enforced by a test that fails the build if any code reads around it
How it stays reliable
- —2,900+ automated tests run in parallel as a pre-push gate. I cut it from almost five minutes to under one, and made the suite silent so a real failure can't hide in noise
- —Rules are enforced by tests rather than by memory: a test fails the build if a route captures a member id as a number, if a submit button renders outside its form, or if code reads the impersonation session key directly
- —Every one of the 207 releases has its own changelog, and a Django system check flags any changelog whose commit or deploy status disagrees with git
- —Feature flags and per-page toggles let risky features ship dark and be switched off without a deploy
Bugs worth remembering
The “remember this device” cookie never worked for pledges
It compared user IDs as integers, but a pledge's ID looks like P-C7JKZY, not a number — the comparison threw and silently fell back to asking for a TOTP code. It failed closed, so no one was ever at risk; it just took months to notice every new member was being asked twice on every login for no visible reason.
Random CSRF failures traced to a phone's own cache
Django doesn't set cache headers on ordinary responses by default, so a CDN — or a phone's back/forward cache — could hand one member's login page, CSRF token included, back to a different session. Fixed by forcing Cache-Control: no-store on every dynamic response, not just error pages.
A committee page reporting “1 member” for every committee
Filtering a queryset before annotating a Count() on that same relation makes the aggregate run over the join the filter already narrowed, not the full relation — distinct=True doesn't help, since it guards against multiplied rows, not a narrowed join. The fix was ordering: annotate before filter.
A shared modal that broke every submit button inside it
Moving a modal's footer into a reusable component rendered its buttons after the caller's closing </form> tag, and a submit button outside its form submits nothing. Three features silently stopped working. The bug lived in no single file, only in how two elements ended up on the rendered page, so the guard test now renders the page and checks that every submit button belongs to a form.
What's Next
- —Multi-chapter support: turning Parliament into a platform that can host more than one chapter. I started by classifying all 150 data models by how each would belong to a chapter, and finding every uniqueness rule that would collide between chapters, before changing any code.
- —Handoff: documentation, an officer guide built into the app, and hardening, so the chapter can keep running it after I graduate.

