The first version of my hackathon platform had a login screen, a form to create events, and a page where judges could type in scores. It looked like it worked.
Then I asked myself a simple question: what happens if a participant opens the browser console and sends a score directly to the API?
The answer was: it would succeed. The server would accept it. There was no check. The "authorization" was a React component that hid a button.
That was the moment the project changed from a collection of screens into an actual engineering problem. Here is how I built the DOGFOOD 2026 judging platform—and the senior-level traps I had to engineer my way out of.
What I thought I was building
The DOGFOOD 2026 hackathon challenge asks you to build a self-hosted hackathon management platform — the kind of tool that organizers use to register teams, assign judges, collect scores, and publish results.
The initial mental model was pure CRUD: create a hackathon, register users, submit projects, enter scores, show a leaderboard. I had React for the frontend, Express and TypeScript for the backend, PostgreSQL for storage, and Docker Compose to tie it all together. Straightforward.
What I didn't anticipate was that the interesting engineering would have almost nothing to do with rendering pages, and almost everything to do with making sure the system's rules couldn't be accidentally — or deliberately — bypassed.
The architectural decision that changed everything
Early on I made one choice that shaped the rest of the build: the backend is the absolute authority on every business rule. Not the frontend.
This sounds obvious in retrospect, but in a rush, it’s tempting to handle authorization by simply hiding UI elements. If you aren't a judge, don't render the "Submit" button.
But if a user crafts a raw HTTP request, the UI state doesn't matter.
The practical consequence was moving almost all complexity into Express middleware and PostgreSQL constraints.
-
Can you vote for your own team? The server joins the
team_memberstable and returns403 Forbiddenif it finds your user ID. -
Can you submit after the deadline? The server checks
submissions_closeagainstDate.now(). The client's clock is irrelevant.
If you can bypass a rule by closing your browser and opening curl, it’s not a rule. It’s a suggestion.
Surviving Docker: The Graceful Shutdown
Container orchestration looks seamless until you deploy an update. When you run docker compose up -d to deploy a new backend image, Docker sends a SIGTERM signal to the old container. By default, Node.js doesn't care. It will immediately terminate, aggressively severing active database connections and killing inflight API requests.
If a judge is midway through a complex transaction to save 15 different rubric scores when that happens, the data gets corrupted.
To fix this, I had to implement a graceful shutdown sequence in the entry point (backend/src/index.ts). When Docker sends SIGTERM, the server stops accepting new connections, finishes processing active requests, safely closes the PostgreSQL connection pool, and then exits.
const shutdown = async (signal: string): Promise<void> => {
logger.info({ signal }, 'Graceful shutdown initiated');
// Stop accepting new connections, finish inflight requests
server.close(async () => {
await closeDb(); // Safely drain the Postgres pool
logger.info('Server closed');
process.exit(0);
});
// Fallback: If requests hang for >10s, force kill
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
This is the difference between a prototype and production. A prototype hopes the server never goes down. Production expects it to go down and plans the funeral.
The Race Condition: Row-Level Locking
"Judges submit scores" sounds simple. But what if a judge has a spotty internet connection, gets impatient, and mashes the "Submit" button three times?
If the server processes those three requests concurrently, it might insert duplicate scores, corrupt the aggregate average, or bypass the "evaluations cannot be edited once submitted" rule because all three requests read the status as DRAFT before any of them committed the change to SUBMITTED.
Application-level checks aren't enough here. I had to push the concurrency control down to the database using PostgreSQL's SELECT ... FOR UPDATE.
// backend/src/services/scoring.service.ts
// Lock the evaluation row exclusively for this transaction
const { rows: existingEvals } = await client.query(
`SELECT id, status FROM evaluations
WHERE submission_id = $1 AND judge_user_id = $2
FOR UPDATE`,
[submissionId, judgeUserId]
);
if (existingEvals.length > 0 && existingEvals[0].status === 'SUBMITTED') {
throw new ConflictError('Evaluation already submitted and locked.');
}
When those three concurrent requests hit the database, Postgres queues them. The first request grabs the lock, completes the transaction, and marks the status as SUBMITTED. When the second request is finally handed the lock, it reads the new SUBMITTED status and safely rejects the request with a 409 Conflict.
Data integrity preserved.
The Scoring Problem That Wasn't About Math
This is the section I wish someone had explained to me before I started.
A hackathon rubric might have criteria with different scales:
- Technical Complexity: scored 1 to 10
- Presentation: scored 1 to 5
If a judge gives 8/10 for complexity and 5/5 for presentation, you can't just add them. The raw sum (13) implies complexity mattered twice as much, because its range is twice as wide. That's a scale artifact, not an intentional weighting.
The fix is Min-Max Normalization. Before the server applies any weights, it mathematically squashes every score into a standardized 0.0 to 1.0 ratio.
let normalized = 0;
if (criterion.max_score > criterion.min_score) {
normalized = (input.raw_score - criterion.min_score)
/ (criterion.max_score - criterion.min_score);
}
const weighted = normalized * criterion.weight;
But getting the math right was only half the battle. The harder problem was data modeling: what is the score of a project that hasn't been evaluated yet?
Missing ≠ zero
In early tests, my SQL aggregation returned 0 for unreviewed projects. That's mathematically valid if you treat the absence of evaluations as "all zeros." But it's semantically wrong. Zero implies the project was judged and failed spectacularly.
The aggregation function now distinguishes three states explicitly:
if (submittedCount > 0) {
const sum = subEvals.reduce((a, b) => a + b, 0);
aggregateScore = Number((sum / submittedCount).toFixed(4));
} else {
// Assigned, but no submitted evaluations yet
aggregateScore = null;
status = 'NO_SCORE';
}
And DRAFT evaluations — where a judge has started entering scores but hasn't finalized — are excluded entirely via SQL (WHERE status = 'SUBMITTED').
A scoring engine can be mathematically flawless but still destroy a hackathon if it treats "missing data" as "bad data."
Physical Immutability: The Audit Log
To ensure absolute trust in the judging process, the platform records audit events for domain mutations — evaluation submissions, assignment changes, score updates.
But how do you prove the audit log itself hasn't been tampered with? If my Express app can insert records, what stops a compromised endpoint from updating or deleting them?
The answer was taking the power away from the Node.js application entirely. I wrote a PL/pgSQL trigger directly into the database migration.
CREATE OR REPLACE FUNCTION prevent_audit_modification()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Audit events are immutable and cannot be updated or deleted';
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_prevent_audit_update
BEFORE UPDATE OR DELETE ON audit_events
FOR EACH ROW
EXECUTE FUNCTION prevent_audit_modification();
If anyone—even the database administrator or the application backend—attempts to run an UPDATE or DELETE query on the audit_events table, the database violently rejects it. The log is physically append-only.
Furthermore, the audit insert runs inside the same transaction block as the domain logic. If the audit insert fails, the score update rolls back. You cannot change the leaderboard without leaving a permanent, immutable trace.
What I would design first next time
I want to be honest about how this project developed. It was not designed on a whiteboard and then implemented cleanly. The architecture evolved through failures and edge cases. If I started over, I'd change the order of operations:
-
Model the state machines before any UI. Evaluation states (
DRAFT → SUBMITTED, no revert), hackathon lifecycle (DRAFT → ACTIVE → COMPLETED), and transition rules should be documented before the first React component. - Write the OpenAPI spec as the first artifact. Mine was written after the routes existed, which meant I was documenting behavior rather than designing it.
- Build adversarial authorization tests before feature tests. The first test for any protected endpoint should be: "what happens when the wrong role calls this?"
- Define what "no data" means for every aggregate. The zero-score bug would have been caught immediately if I had asked: "what should this endpoint return when there are no evaluations?"
The Reality of Systems Engineering
Building a hackathon platform isn't about rendering a nice gallery of projects. It's about designing a system that hostile actors can't manipulate, that survives network interruptions, and that guarantees mathematical fairness.
The dashboard looks great. The login page works. But the thing I am actually proud of is invisible to the user: row-level locks, graceful container terminations, normalized weighting algorithms, and database-level immutability.
CRUD is easy. Enforcing the rules when nobody is looking at the screen is where the real engineering happens.
Built with TypeScript, Express, React, PostgreSQL, and Docker during DOGFOOD 2026. Check out the live read-only demo here or view the source code below.
SravyaMummana2006
/
dogfood
DOGFOOD 2026 — Self-hosted Hackathon Submission & Judging Platform
DOGFOOD 2026
A self-hosted, offline-capable hackathon submission, judging, and verification platform engineered for server-side authorization, deterministic score normalization, relational auditability, and cryptographic record verification.
1. One-Minute Overview
What the Platform Is
DOGFOOD 2026 is an API-first hackathon submission, judging, and verification portal. It serves three primary user personas: Organizers who configure hackathons and rubrics, Judges who evaluate project submissions, and Participants who register teams, submit projects, and engage in community voting.
What Problem It Solves
Hackathon evaluation platforms often suffer from security and integrity flaws:
- Client-side scoring math that can be manipulated in browser developer tools.
- Vulnerabilities where judges can view peer scores and succumb to herd bias.
- Unconstrained database access where draft or missing scores unfairly penalize teams with zero values.
- Lack of cryptographic verification for issued winning certificates.
DOGFOOD 2026 resolves these issues by enforcing strict server-side authority, parameterized direct database queries, deterministic min-max score normalization, immutable…


Top comments (0)