Podium is a peer-judging platform for Hack Club hackathons. Attendees select an official event, submit projects, and vote.
- Frontend: SvelteKit (Svelte 5), Tailwind, DaisyUI
- Backend: FastAPI, SQLModel (async PostgreSQL)
- Auth: Magic link email via Loops API
- Cache: Redis (optional — leaderboards cached 30s; app works without it)
- CAPTCHA: Cloudflare Turnstile on unauthenticated endpoints
- Monitoring: Sentry
Core entities:
- User — email, display_name, first_name, is_superadmin
- Event — name, slug, phase, feature_flags_csv, repo_validation, demo_validation, require_address
- Project — name, repo, image_url, demo, owner_id, event_id, points, validation_status, validation_message
- Vote — voter_id, project_id, event_id (unique on voter+project)
M2M relationships via junction tables:
event_attendees— User ↔ Eventproject_collaborators— User ↔ Project
Events move through phases in order: draft → submission → voting → closed.
| Phase | What's allowed |
|---|---|
draft |
Not yet visible to users |
submission |
Users can join and submit projects |
voting |
Submissions closed; users can vote |
closed |
Voting closed; leaderboard visible |
The phase is changed by the event owner via the admin panel UI or PATCH /events/admin/{id}.
Events are grouped into a series — a flagship event plus its satellites (e.g., "Scrapyard 2025"). Only one series is active at a time, controlled by ACTIVE_EVENT_SERIES in backend config. Events with matching feature_flags_csv appear in the event selector.
Key rules:
- One event per user per series — backend enforces; selecting a new event auto-switches
- Events are admin-created — users select from a list, never create events
- All official events are equal — no UI distinction between flagship and satellites
- Past series are read-only — users can view but not edit old projects
Validation has two independent layers:
1. Frontend (instant, non-blocking warnings) — frontend/src/lib/validation.ts
Shows a warning as the user types. Only implemented for github and itch; setting either field to none or custom shows no frontend warning at all. There is no frontend equivalent for custom validators.
2. Backend (background, async) — backend/podium/validators/
Runs after every project create/update; never blocks submission. Results appear as badges (pending → valid | warning).
Validation strategy is configured per-event via repo_validation and demo_validation:
| Setting | Frontend warning | Backend check |
|---|---|---|
github |
Regex check for valid GitHub URL | GitHub public API — repo must exist |
itch |
Regex check for valid itch.io URL | Scrapes itch.io for .game_frame (browser-playable) |
custom |
None | Calls the named module from validators/custom/REGISTRY |
none |
None | Skipped |
Adding a custom backend validator:
- Create
backend/podium/validators/custom/<name>.pyimplementingvalidate_repo(url)and/orvalidate_demo(url), each returningValidationResult. - Register it in
validators/custom/__init__.py:REGISTRY["<name>"] = <module>. - Set the event's
custom_validatorfield to"<name>"and setrepo_validationand/ordemo_validationto"custom".
Events can also set require_address: true to enforce that users have a shipping address on file before submitting — this is a hard block at the API level.
Sign in → Select event → (Address check) → Submit project → Validation → Vote
- User signs in via magic link
- Selects from available official events (
GET /events/official) - If
require_addressis set, must provide shipping address first - Creates or joins a project (join codes allow collaborators)
- Background validation runs; badge appears on project card
- Can vote on other projects
Vote limits scale with project count: 1 vote (< 4 projects), 2 votes (4-19), 3 votes (≥ 20). Users can't vote for their own or collaborated projects.
Backend (backend/podium/):
main.py— FastAPI app, lifespan (Redis init/close)config.py— Dynaconf settings (all env vars withPODIUM_prefix)constants.py— Shared types:EventPhase,RepoValidation,DemoValidation,BAD_AUTH, etc.limiter.py— slowapi rate limiter (user-email based)db/postgres/— SQLModel models and database session helpersrouters/— API endpoints (auth,users,events,projects,admin,superadmin)validators/— Project URL validation:github.py,itch.py,custom/(event-specific); input validation:email.py,turnstile.pycache/— Redis helpers (cache_get,cache_set,cache_delete) with graceful no-op fallback
Frontend (frontend/src/):
hooks.client.ts— Client init, auth validationlib/client/— Generated OpenAPI client (runbun run openapi-tsto regenerate)lib/forms/— Reusable form components (Button, Input, Label, Textarea, FileDropZone)lib/logos/— Event logo components (CampfireFlagship, CampfireSat)lib/user.svelte.ts— User statelib/validation.ts— Project validationroutes/— SvelteKit pages
backend/scripts/manage.py— TUI: create events, manage attendees, toggle superadmin, delete users/events/superadmin— Web UI (requiresis_superadmin): list/create/delete events, edit owner and validation settings, list usersPATCH /events/admin/{id}— API: update any event field (owners and superadmins)- NocoDB — spreadsheet UI for the database (see nocodb.md)
Superadmin status (is_superadmin) is toggled via the TUI Users tab — there is no API for it.