<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sravya Mummana</title>
    <description>The latest articles on DEV Community by Sravya Mummana (@sravya_mummana_686b1502e6).</description>
    <link>https://dev.to/sravya_mummana_686b1502e6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4156237%2F574b51c1-1690-4b93-a577-2ce03c9ed2bc.png</url>
      <title>DEV Community: Sravya Mummana</title>
      <link>https://dev.to/sravya_mummana_686b1502e6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sravya_mummana_686b1502e6"/>
    <language>en</language>
    <item>
      <title>CRUD Was the Easy Part: What I Learned Building a Self-Hosted Judging System</title>
      <dc:creator>Sravya Mummana</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:18:37 +0000</pubDate>
      <link>https://dev.to/sravya_mummana_686b1502e6/crud-was-the-easy-part-what-i-learned-building-a-self-hosted-judging-system-1h8c</link>
      <guid>https://dev.to/sravya_mummana_686b1502e6/crud-was-the-easy-part-what-i-learned-building-a-self-hosted-judging-system-1h8c</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Then I asked myself a simple question: &lt;em&gt;what happens if a participant opens the browser console and sends a score directly to the API?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I thought I was building
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architectural decision that changed everything
&lt;/h2&gt;

&lt;p&gt;Early on I made one choice that shaped the rest of the build: &lt;strong&gt;the backend is the absolute authority on every business rule.&lt;/strong&gt; Not the frontend.&lt;/p&gt;

&lt;p&gt;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. &lt;/p&gt;

&lt;p&gt;But if a user crafts a raw HTTP request, the UI state doesn't matter. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8czretermlakmylro5iq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8czretermlakmylro5iq.png" alt=" " width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The practical consequence was moving almost all complexity into Express middleware and PostgreSQL constraints. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can you vote for your own team?&lt;/strong&gt; The server joins the &lt;code&gt;team_members&lt;/code&gt; table and returns &lt;code&gt;403 Forbidden&lt;/code&gt; if it finds your user ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can you submit after the deadline?&lt;/strong&gt; The server checks &lt;code&gt;submissions_close&lt;/code&gt; against &lt;code&gt;Date.now()&lt;/code&gt;. The client's clock is irrelevant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can bypass a rule by closing your browser and opening &lt;code&gt;curl&lt;/code&gt;, it’s not a rule. It’s a suggestion.&lt;/p&gt;




&lt;h2&gt;
  
  
  Surviving Docker: The Graceful Shutdown
&lt;/h2&gt;

&lt;p&gt;Container orchestration looks seamless until you deploy an update. When you run &lt;code&gt;docker compose up -d&lt;/code&gt; to deploy a new backend image, Docker sends a &lt;code&gt;SIGTERM&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;If a judge is midway through a complex transaction to save 15 different rubric scores when that happens, the data gets corrupted.&lt;/p&gt;

&lt;p&gt;To fix this, I had to implement a graceful shutdown sequence in the entry point (&lt;code&gt;backend/src/index.ts&lt;/code&gt;). When Docker sends &lt;code&gt;SIGTERM&lt;/code&gt;, the server stops accepting new connections, finishes processing active requests, safely closes the PostgreSQL connection pool, and &lt;em&gt;then&lt;/em&gt; exits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shutdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Graceful shutdown initiated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Stop accepting new connections, finish inflight requests&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;closeDb&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Safely drain the Postgres pool&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Fallback: If requests hang for &amp;gt;10s, force kill&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forced shutdown after timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGINT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGINT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;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.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Race Condition: Row-Level Locking
&lt;/h2&gt;

&lt;p&gt;"Judges submit scores" sounds simple. But what if a judge has a spotty internet connection, gets impatient, and mashes the "Submit" button three times? &lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;DRAFT&lt;/code&gt; before any of them committed the change to &lt;code&gt;SUBMITTED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Application-level checks aren't enough here. I had to push the concurrency control down to the database using PostgreSQL's &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// backend/src/services/scoring.service.ts&lt;/span&gt;

&lt;span class="c1"&gt;// Lock the evaluation row exclusively for this transaction&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existingEvals&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`SELECT id, status FROM evaluations 
   WHERE submission_id = $1 AND judge_user_id = $2 
   FOR UPDATE`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;submissionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;judgeUserId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingEvals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;existingEvals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMITTED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConflictError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Evaluation already submitted and locked.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;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 &lt;code&gt;SUBMITTED&lt;/code&gt;. When the second request is finally handed the lock, it reads the new &lt;code&gt;SUBMITTED&lt;/code&gt; status and safely rejects the request with a &lt;code&gt;409 Conflict&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Data integrity preserved.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Scoring Problem That Wasn't About Math
&lt;/h2&gt;

&lt;p&gt;This is the section I wish someone had explained to me before I started.&lt;/p&gt;

&lt;p&gt;A hackathon rubric might have criteria with different scales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Technical Complexity&lt;/strong&gt;: scored 1 to 10&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation&lt;/strong&gt;: scored 1 to 5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1p5m73e6ei3hxv3r5im.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1p5m73e6ei3hxv3r5im.png" alt=" " width="800" height="675"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;Min-Max Normalization&lt;/strong&gt;. Before the server applies any weights, it mathematically squashes every score into a standardized &lt;code&gt;0.0&lt;/code&gt; to &lt;code&gt;1.0&lt;/code&gt; ratio.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min_score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw_score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min_score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
             &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max_score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min_score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weighted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But getting the math right was only half the battle. The harder problem was data modeling: &lt;strong&gt;what is the score of a project that hasn't been evaluated yet?&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Missing ≠ zero
&lt;/h3&gt;

&lt;p&gt;In early tests, my SQL aggregation returned &lt;code&gt;0&lt;/code&gt; 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. &lt;/p&gt;

&lt;p&gt;The aggregation function now distinguishes three states explicitly:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;submittedCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;subEvals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;aggregateScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;submittedCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Assigned, but no submitted evaluations yet&lt;/span&gt;
  &lt;span class="nx"&gt;aggregateScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
  &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NO_SCORE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And &lt;code&gt;DRAFT&lt;/code&gt; evaluations — where a judge has started entering scores but hasn't finalized — are excluded entirely via SQL (&lt;code&gt;WHERE status = 'SUBMITTED'&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A scoring engine can be mathematically flawless but still destroy a hackathon if it treats "missing data" as "bad data."&lt;/p&gt;


&lt;h2&gt;
  
  
  Physical Immutability: The Audit Log
&lt;/h2&gt;

&lt;p&gt;To ensure absolute trust in the judging process, the platform records audit events for domain mutations — evaluation submissions, assignment changes, score updates. &lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;The answer was taking the power away from the Node.js application entirely. I wrote a PL/pgSQL trigger directly into the database migration.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;prevent_audit_modification&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Audit events are immutable and cannot be updated or deleted'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;trg_prevent_audit_update&lt;/span&gt;
  &lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;audit_events&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;prevent_audit_modification&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If anyone—even the database administrator or the application backend—attempts to run an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; query on the &lt;code&gt;audit_events&lt;/code&gt; table, the database violently rejects it. The log is physically append-only. &lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I would design first next time
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model the state machines before any UI.&lt;/strong&gt; Evaluation states (&lt;code&gt;DRAFT → SUBMITTED&lt;/code&gt;, no revert), hackathon lifecycle (&lt;code&gt;DRAFT → ACTIVE → COMPLETED&lt;/code&gt;), and transition rules should be documented before the first React component.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the OpenAPI spec as the first artifact.&lt;/strong&gt; Mine was written after the routes existed, which meant I was documenting behavior rather than designing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build adversarial authorization tests before feature tests.&lt;/strong&gt; The first test for any protected endpoint should be: "what happens when the wrong role calls this?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define what "no data" means for every aggregate.&lt;/strong&gt; The zero-score bug would have been caught immediately if I had asked: "what should this endpoint return when there are no evaluations?"&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  The Reality of Systems Engineering
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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. &lt;/p&gt;

&lt;p&gt;CRUD is easy. Enforcing the rules when nobody is looking at the screen is where the real engineering happens.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;Built with TypeScript, Express, React, PostgreSQL, and Docker during DOGFOOD 2026. Check out the &lt;a href="https://sravyahackthon.netlify.app" rel="noopener noreferrer"&gt;live read-only demo here&lt;/a&gt; or view the source code below.&lt;/em&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/SravyaMummana2006" rel="noopener noreferrer"&gt;
        SravyaMummana2006
      &lt;/a&gt; / &lt;a href="https://github.com/SravyaMummana2006/dogfood" rel="noopener noreferrer"&gt;
        dogfood
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      DOGFOOD 2026 — Self-hosted Hackathon Submission &amp;amp; Judging Platform
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DOGFOOD 2026&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;1. One-Minute Overview&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;What the Platform Is&lt;/h3&gt;
&lt;/div&gt;

&lt;p&gt;DOGFOOD 2026 is an API-first hackathon submission, judging, and verification portal. It serves three primary user personas: &lt;strong&gt;Organizers&lt;/strong&gt; who configure hackathons and rubrics, &lt;strong&gt;Judges&lt;/strong&gt; who evaluate project submissions, and &lt;strong&gt;Participants&lt;/strong&gt; who register teams, submit projects, and engage in community voting.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;What Problem It Solves&lt;/h3&gt;

&lt;/div&gt;

&lt;p&gt;Hackathon evaluation platforms often suffer from security and integrity flaws:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client-side scoring math that can be manipulated in browser developer tools.&lt;/li&gt;
&lt;li&gt;Vulnerabilities where judges can view peer scores and succumb to herd bias.&lt;/li&gt;
&lt;li&gt;Unconstrained database access where draft or missing scores unfairly penalize teams with zero values.&lt;/li&gt;
&lt;li&gt;Lack of cryptographic verification for issued winning certificates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DOGFOOD 2026 resolves these issues by enforcing &lt;strong&gt;strict server-side authority&lt;/strong&gt;, parameterized direct database queries, deterministic min-max score normalization, immutable…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/SravyaMummana2006/dogfood" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>webdev</category>
      <category>node</category>
      <category>postgres</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
