What if, instead of building a project for a hackathon, you had to build the platform that actually judges the hackathon?
That was the prompt behind DOGFOOD 2026: "Build the platform that will judge you."
Most hackathon projects are quick prototypes with forty npm packages, three mock APIs, and a prayer. For this challenge, I decided to go in the exact opposite direction:
One Python process. One SQLite database. Zero runtime third-party dependencies. Zero outbound network calls.
No Flask. No FastAPI. No Django. No Redis. No PostgreSQL. No CDN.
Just Python's standard library, SQLite, vanilla CSS/JS, and Docker.
Here is the story of how I built Lockdown, the architectural hurdles I ran into, and why extreme constraints made me a significantly better engineer.
- 🌐 Live Demo: lockdown-1-3tuu.onrender.com
- 📺 Video Walkthrough: Watch the demo on YouTube
- 💻 Source Code: github.com/Aryanxp1/Lockdown

Figure 1: The Lockdown homepage — event metrics, a six-stage pipeline rail, and recent submissions.
What is Lockdown?
Lockdown is a self-hosted competition operating system. It handles the complete lifecycle of a hackathon:
It supports:
- Multiple hackathons in a single installation with strict data isolation.
- Participant workspaces with team management and deadline-enforced submissions.
- Blind judge queues with weighted rubric scoring and draft autosave.
- Cross-judge statistical normalization (z-scores) to balance harsh vs. lenient graders.
- Frozen publication snapshots with SHA-256 checksums and cryptographic certificates.
- A synchronous append-only audit trail recording every authentication, refusal, score, and publication.
The entire runtime stack fits in this diagram:
Browser (HTML / CSS / JS)
│
▼
Python ThreadingHTTPServer
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
Auth & Sessions State & Deadlines Judging & Scoring
(HMAC / scrypt) (Window enforcement) (Z-score / Rubrics)
│ │ │
└───────────────────────┼───────────────────────┘
│
▼
SQLite 3 (WAL + RLock)
No external message brokers. No background worker daemons. When a request completes, its database transaction and audit log entry are already committed.
Why Zero Dependencies?
Using FastAPI, SQLAlchemy, and Tailwind would have made several parts faster to write. But giving up frameworks forced me to understand the fundamentals of web architecture:
-
No magic routing: I had to write an HTTP dispatcher on top of Python's
http.server, handle MIME types, enforce body upload caps (2 MiB max), parse query strings, and implement path-traversal guards from scratch. -
Deterministic security: Security headers (
CSP,X-Frame-Options: DENY,nosniff,SameSite=Lax) aren't plugin defaults; they are explicitly applied to every outgoing response. -
True air-gap compliance: Because
app/never importsurllib.requestor any HTTP client, the portal physically cannot leak data or phone home. Running the container on an internal Docker network with no default gateway passes the entire acceptance suite without a hitch.
Architectural Challenge 1: The Multi-Hackathon Isolation Problem
A naive hackathon portal assumes there is only ever one event. But real infrastructure needs to host multiple hackathons simultaneously:
LOCKDOWN PLATFORM
│
┌──────────────────┴──────────────────┐
▼ ▼
Hackathon A (Active) Hackathon B (Embargoed)
├── Teams ├── Teams
├── Projects ├── Projects
└── Judges └── Judges
This introduced a crucial authorization question:
Does an organizer have global superuser access across every competition?
I decided the answer had to be an uncompromising NO. Organizer privileges are strictly scoped per event.
If you are an organizer for Hackathon A, navigating to Hackathon B doesn't just hide admin buttons in the template — the handler itself evaluates events.can_manage() and returns a 403 Forbidden while synchronously appending the unauthorized attempt to the audit log.

Figure 2: The public gallery — track filter pills, instant search, and paginated project cards.
Architectural Challenge 2: Deadlines & Append-Only Submissions
In many apps, submitting a project is just a database UPDATE. But in competitions, submissions are legal records:
- What happens if a team submits 5 minutes before the deadline, and then edits their project 1 minute after the deadline?
- Can a team claim their submission was altered by an organizer?
To solve this:
-
Server-enforced deadlines: Deadlines are evaluated inside the request handler. Disabling a submit button in JavaScript is a UX convenience, not security. If a raw
curl POSTarrives one second past the cutoff, it is rejected with403 submission_window_closed. -
Versioned history: Every project update appends a new record to
project_versions. Old descriptions and links are never overwritten in place.

Figure 3: The Organizer Desk — stage controls, live submission metrics, and judge assignment overview.
Architectural Challenge 3: Blind Judging & The Calibration Math
Judging was by far my favorite subsystem to design.
In most hackathons, different judges grade on vastly different scales. One judge might give an average score of 92 with narrow spread, while another judge gives an average of 65 and grades ruthlessly. If you simply calculate arithmetic means, a team's final rank depends heavily on whether they drew a lenient or harsh reviewer.
To fix this, Lockdown implements cross-judge z-score normalization:
z = (raw_score - judge_mean) / judge_stddev
normalized = clamp(75.0 + 12.0 * z, 0, 100)
This rescales all judges toward a standardized distribution with a target mean of 75.0 and standard deviation of 12.0.
And in the fallbacks bullet points right below it:
markdown
- Fewer than 3 completed reviews →
insufficient_sample - Variance / std dev < 2.0 →
zero_variance

Figure 4: Judge calibration roster — per-judge mean, variance, and severity classification.
Explicit Fallbacks (No Fabricated Precision)
What happens if a judge only scored two projects? Or what if a judge gave every single project an identical score of 80 ($\sigma = 0$)?
In those cases, statistical normalization breaks down. Instead of generating artificial numbers or throwing a ZeroDivisionError, Lockdown defines explicit fallback flags:
- Fewer than 3 completed reviews $\rightarrow$
insufficient_sample - Variance $\sigma < 2.0$ $\rightarrow$
zero_variance
Both fall back cleanly to uncalibrated raw scores, and the project is flagged as provisional on the organizer's scoreboard until more reviews arrive.

Figure 5: Evaluation form — weighted criteria, draft autosave via fetch, and 1–5 keyboard shortcuts.
Architectural Challenge 4: Frozen Results vs. Live Queries
Here is a trap many developers fall into:
Building the leaderboard as a live
SELECT ... ORDER BY score DESCquery.
Why is that a disaster? Because if an organizer edits a score or deletes a test account two weeks after the event ends, the public leaderboard silently mutates! Historical rankings get rewritten without anyone noticing.
In Lockdown, publishing is a frozen snapshot:
Internal Review Data
│
▼
Calculate Final Standings (Scoring Engine)
│
▼
Serialize to JSON (`rows_json`)
│
▼
Compute SHA-256 Digest
│
▼
Insert into `result_publications` (revision_no + 1, is_current = 1)
│
▼
Issue Cryptographic Certificates & Commit Audit Row
Once published, the public /results endpoint reads strictly from the active snapshot row. Subsequent edits do not touch historical revisions.

Figure 6: Published standings leaderboard — verifiable scores, rank badges, and track awards.
Architectural Challenge 5: An Append-Only Audit Ledger
Competition software needs to answer forensic questions: Who changed this score? When was this review submitted? Who published the revision?
Every critical mutation in Lockdown is paired with a synchronous call to audit.record() inside the active SQLite transaction:
audit.record(
event_id=event_id,
action="results.publish",
actor_id=user["id"],
target_id=publication_id,
details={"revision": revision_no, "checksum": checksum}
)
Because it executes in the same transaction as the state change, an audit event cannot exist without the action, and an action cannot succeed without creating an audit entry.

Figure 7: The synchronous append-only audit trail.
The UI Redesign: From "1990s Paper Archive" to High-Contrast Cyber Dark
Early in the project, the UI felt like an old editorial newspaper: cream backgrounds, serif fonts, and thin grey borders. It looked more like a law library than a modern hackathon platform.
I completely overhauled the interface using pure Vanilla CSS:
-
Canvas: Pitch black (
#050508) with layered slate cards (#0e0e13). -
Structure: Crisp, prominent
2.5px solid #4a4e5agrey borders across all project cards, stat containers, and navigation bars. -
Accents: High-visibility crimson glow (
#ef4444) for interactive hovers and status indicators. -
Micro-UX: 1–5 keyboard shortcuts for rapid judge scoring and a floating account switcher (
PORTAL_FAST_LOGIN=1) so organizers and judges can demo roles instantly without typing passwords.

Figure 8: Judge workspace — assignment queue, review progress bar, and evaluated submissions.

Figure 9: Participant workspace — team management, deadline state, and verifiable achievement certificate.
The Bug That Humbled Me: .gitignore vs. Deployment
The hardest bug wasn't the z-score math or the HTTP parser. It was a deployment facepalm.
Everything worked smoothly on my laptop. Then, I tested a clean deployment inside Docker from a fresh repository checkout:
docker compose up
The container immediately crashed on boot.
Why? Because .dogfood.toml (which configures the acceptance checker and port mappings) had been matched by an overly broad .gitignore pattern. It existed on my local machine, but git had never tracked it!
Local machine: Everything works ✅
Fresh Git clone: Missing configuration file ❌
The fix took thirty seconds, but the takeaway stuck with me:
Your laptop is never the deployment artifact. Your git repository is.
Acceptance Testing: Verifying the System
To prove that the platform meets the DOGFOOD 2026 specification, the codebase includes an HTTP-based automated test runner (run.py):
docker compose exec portal python3 run.py .dogfood.toml
Output:
DOGFOOD 2026 acceptance report
portal: http://localhost:8081
claimed: T1 T2
fixtures: fixtures.json
T1 gallery is public ................. PASS
T1 project from fixtures shown ....... PASS
T1 closed event refuses submissions .. PASS
T2 judge sees own scores ............. PASS
T2 judge cannot see peer scores ...... PASS
T2 participant blocked ............... PASS
T2 csv export works .................. PASS
claimed T1 T2, verified T1 T2 (7/7 probes passed)
In addition, standard unittest suites test schema migrations and multi-event database isolation.
Key Takeaways
Building a competition platform with zero dependencies taught me three core lessons:
- Constraints breed clarity: When you can't install third-party libraries for every minor requirement, you stop over-engineering and focus on data flow and state transitions.
- Security belongs in handlers, not CSS: Hiding a button in HTML is not authorization. Every mutation must validate caller permissions, event scope, and timeline windows at the database/API boundary.
- Immutability is peace of mind: When results are published as frozen snapshots and reviews are append-only revisions, you eliminate an entire class of subtle data corruption bugs.
Try It Out
Lockdown is fully open source under the MIT License:
- 🌐 Live Demo: https://lockdown-1-3tuu.onrender.com/
- 📺 YouTube Walkthrough: Watch on YouTube
- 💻 GitHub Repository: https://github.com/Aryanxp1/Lockdown
- ⚙️ Tech Stack: Python 3.12 (Standard Library), SQLite 3 (WAL mode), Vanilla CSS/JS, Docker.
To launch a complete competition instance with 48 seeded projects and demo accounts in under 10 seconds:
git clone https://github.com/Aryanxp1/Lockdown.git
cd Lockdown
docker compose up
Open http://localhost:8081 and explore!
Thanks to the DOGFOOD 2026 team for an awesome competition challenge that gave me an excuse to build real infrastructure from first principles.

Top comments (2)
The z-score calibration with explicit variance fallbacks is the detail most hackathon scoring tools skip. Standard deviation normalization on small judge panels usually blows up the moment one reviewer scores every project 85, so falling back to raw uncalibrated scores under zero variance prevents phantom ranking shifts.
On the concurrency side, pairing WAL with an application-level RLock is smart for ThreadingHTTPServer. The default 5-second busy_timeout in Python's sqlite3 module can still throw database-is-locked errors under sudden deadline submission bursts if two threads try to begin an immediate write transaction simultaneously.
crazyy ideaa