<?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: SURAJ NANDAN</title>
    <description>The latest articles on DEV Community by SURAJ NANDAN (@suraj_nandan).</description>
    <link>https://dev.to/suraj_nandan</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%2F3859390%2F61fb1878-a099-48c3-a66a-2efc6ed32a99.jpeg</url>
      <title>DEV Community: SURAJ NANDAN</title>
      <link>https://dev.to/suraj_nandan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/suraj_nandan"/>
    <language>en</language>
    <item>
      <title>Team projects: what I try to do, from building GigShield with two teammates</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Sun, 25 Oct 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/team-projects-what-i-try-to-do-from-building-gigshield-with-two-teammates-3eej</link>
      <guid>https://dev.to/suraj_nandan/team-projects-what-i-try-to-do-from-building-gigshield-with-two-teammates-3eej</guid>
      <description>&lt;p&gt;&lt;strong&gt;GigShield&lt;/strong&gt; is a parametric income-insurance platform for delivery riders. It was a &lt;strong&gt;team of three&lt;/strong&gt;, built with Rishabh and Abhinav, so the credit isn't mine alone. I'll say that every time I talk about it.&lt;/p&gt;

&lt;p&gt;This post is about what the project is, which pieces I built, and what I try to do on team projects. Some of those lessons are things I did; some are things I now try to do. I'll be clear about which is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Delivery riders in India are paid weekly and live on thin cash buffers. One day of heavy rain, a hazardous air-quality reading or a platform outage can wipe out a day or two of income. Traditional insurance, with its paperwork, adjusters and reimbursement weeks later, doesn't fit a loss that small and that sudden.&lt;/p&gt;

&lt;p&gt;Our answer was &lt;strong&gt;parametric&lt;/strong&gt; cover: instead of reimbursing a loss after someone proves it, it pays a fixed amount automatically when an objective, external signal crosses a threshold.&lt;/p&gt;

&lt;p&gt;We priced it the way riders think about money, &lt;strong&gt;weekly&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Weekly price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;₹79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;₹129&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;₹179&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each plan has its own daily payout cap and covered hours. The product deliberately covers lost earning hours &lt;em&gt;only&lt;/em&gt;: no health, life, accident or vehicle cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  My part
&lt;/h2&gt;

&lt;p&gt;The role line from the case study, for accuracy: I built the &lt;strong&gt;trigger engine&lt;/strong&gt;, the &lt;strong&gt;payout lifecycle and receipts&lt;/strong&gt;, the &lt;strong&gt;rider dashboard&lt;/strong&gt;, the &lt;strong&gt;admin and fraud-review screens&lt;/strong&gt;, &lt;strong&gt;role-based routing&lt;/strong&gt; and the &lt;strong&gt;3D landing page&lt;/strong&gt;. The liveness check was built together with Rishabh. Rishabh and Abhinav built most of the authentication, the Express server and the help chat.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trigger engine
&lt;/h3&gt;

&lt;p&gt;It watches for seven kinds of disruption: heavy rain, heatwave, AQI spikes, platform outages, curfews, local strikes and zone closures. Rather than a bare yes/no, each trigger gets a &lt;strong&gt;confidence score&lt;/strong&gt;, built from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a base value per trigger type,&lt;/li&gt;
&lt;li&gt;how reliable the weather data source was,&lt;/li&gt;
&lt;li&gt;and the rider's risk profile (higher risk lowers confidence).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The score maps to a label: &lt;strong&gt;Strong, Normal or Weak&lt;/strong&gt;. In sketch form:&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;function&lt;/span&gt; &lt;span class="nf"&gt;triggerConfidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceReliability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;riderRisk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sourceReliability&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;riderRisk&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3&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;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Strong&lt;/span&gt;&lt;span class="dl"&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;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Normal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Weak&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;(That's illustrative. The real thresholds are in the repo. They're reasonable guesses, and I'll explain why that matters below.)&lt;/p&gt;

&lt;p&gt;Weather comes from &lt;strong&gt;Open-Meteo&lt;/strong&gt;, with a 2.5-second timeout and an automatic fallback to &lt;strong&gt;wttr.in&lt;/strong&gt;. That way one flaky API can't quietly stop the engine. Cooldown and de-duplication windows stop a single storm from firing again and again, and every trigger is written to an audit trail that syncs to the backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payouts and fraud
&lt;/h3&gt;

&lt;p&gt;A claim moves through a visible lifecycle, &lt;em&gt;pending verification → verified → processing → settled&lt;/em&gt;, and riders get a downloadable PDF receipt. In this build &lt;strong&gt;payouts are simulated&lt;/strong&gt;; there's no real payment rail connected.&lt;/p&gt;

&lt;p&gt;For fraud, we built a liveness check on risky sessions: the app asks for a random hand gesture, and MediaPipe's hand-landmark model classifies it right in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I try to do on team projects
&lt;/h2&gt;

&lt;p&gt;These are practices I'm working towards, informed by this project. I'm not claiming a perfect record.&lt;/p&gt;

&lt;h3&gt;
  
  
  Split by interface, not by file
&lt;/h3&gt;

&lt;p&gt;Agree the contract between the pieces first (what data crosses the boundary, in what shape), then build each side independently. "You do the frontend, I'll do the backend" fails when nobody has defined what the frontend sends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write the contract down
&lt;/h3&gt;

&lt;p&gt;Even a messy README beats arguing in chat. A written contract can be pointed at; a remembered one gets reinterpreted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merge small, merge often
&lt;/h3&gt;

&lt;p&gt;Integration pain arrives all at once if you wait. Small merges turn one big collision into many tiny ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agree what "done" means
&lt;/h3&gt;

&lt;p&gt;"Works on my machine" can mean three different things to three people. Agree upfront: does it mean deployed? Tested? Reviewed?&lt;/p&gt;

&lt;h3&gt;
  
  
  A trap I watch for
&lt;/h3&gt;

&lt;p&gt;Taking over the hard part because it's faster than explaining it. It's a short-term win and a long-term loss for the team. The teammate who never gets to struggle with the hard part never learns it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd flag as limits
&lt;/h2&gt;

&lt;p&gt;The case study lists these honestly, and they apply here too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger cooldown and de-duplication state lives in the browser's storage&lt;/strong&gt;, which is fine for a demo but belongs on the server in anything real.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payouts and parts of the fraud workflow are simulated.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The thresholds are reasonable guesses.&lt;/strong&gt; Calibrating them properly would need historical weather and earnings data we didn't have.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you're starting a team project
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Agree the contract.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Merge small, merge often.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write down who owns what.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit everyone&lt;/strong&gt;, publicly, by name.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A one-page team contract
&lt;/h2&gt;

&lt;p&gt;Fill this in at the start of any team project. It takes ten minutes and prevents hours of argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PROJECT:           ______________________
GOAL (one line):   ______________________
DEADLINE:          ______________________

OWNERS
  Frontend:        ______   Backend:    ______
  Data / schema:   ______   Deploy:     ______
  Docs / demo:     ______

CONTRACTS
  API shape agreed in: ______ (file / doc link)
  Branch strategy:     main + short-lived feature branches
  Merge rule:          at least one review, merge daily

DEFINITION OF DONE
  [ ] works on the deployed URL  [ ] reviewed  [ ] README updated

IF WE DISAGREE
  Each side writes 3 lines. Pick the option that is easiest to reverse.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A tiny API contract example
&lt;/h2&gt;

&lt;p&gt;Agree on the exact shape before building either side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/claims&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;request&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"riderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"r_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"triggerType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"heavy_rain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"triggeredAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-01T08:00:00Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"claimId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c_456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending_verification"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(duplicate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;within&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cooldown)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"duplicate_trigger"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retryAfterMinutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(This is an illustration of the &lt;em&gt;format&lt;/em&gt;, not GigShield's actual API.) Once both sides agree, the frontend can mock it and the backend can implement it in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling disagreements
&lt;/h2&gt;

&lt;p&gt;Disagreements are normal. Some approaches that help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate the decision from the person.&lt;/strong&gt; Argue about options, not people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer reversible decisions.&lt;/strong&gt; If it's easy to undo, just try it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-box debates.&lt;/strong&gt; Fifteen minutes, then decide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down the decision and the reason&lt;/strong&gt;, so it isn't re-litigated next week.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Teaching while you build
&lt;/h2&gt;

&lt;p&gt;The trap I mentioned, taking over the hard part, has a better alternative:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pair on the first one.&lt;/strong&gt; Do the hard task &lt;em&gt;together&lt;/em&gt;, once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let them drive.&lt;/strong&gt; You narrate; they type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review rather than rewrite.&lt;/strong&gt; Comment on their version instead of replacing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celebrate the learning&lt;/strong&gt;, not just the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's slower for a day and faster for a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crediting people
&lt;/h2&gt;

&lt;p&gt;On a portfolio or a post, a good credit line states the team size, the names, and &lt;em&gt;which part you built&lt;/em&gt;. For GigShield, that's a team of three with Rishabh and Abhinav, and the case study says exactly which parts I built. It's accurate, and it makes your own claims more believable.&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%2Foutc78ks4uqtnudj646g.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%2Foutc78ks4uqtnudj646g.png" alt="Key takeaways from day 8" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's your best tip for team projects that don't implode?&lt;/p&gt;

</description>
      <category>teamwork</category>
      <category>hackathon</category>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Tutorial hell is real, and the way out is boring</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Sat, 24 Oct 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/tutorial-hell-is-real-and-the-way-out-is-boring-28o8</link>
      <guid>https://dev.to/suraj_nandan/tutorial-hell-is-real-and-the-way-out-is-boring-28o8</guid>
      <description>&lt;p&gt;Tutorial hell is a trap most learners fall into at some point. You may know it: a folder of half-finished follow-alongs and the feeling of progress without any actual skill.&lt;/p&gt;

&lt;p&gt;The good news is that the way out isn't clever. The bad news is that it's boring. This post lays out &lt;em&gt;why&lt;/em&gt; tutorials trap people, a concrete way out, and a one-week experiment you can run starting tomorrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What keeps people stuck
&lt;/h2&gt;

&lt;p&gt;Tutorials remove the hardest part of programming: &lt;strong&gt;deciding what to do next.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you follow along, you always know the next step, because someone is telling you. The tutorial author already made every design decision, hit every dead end, and edited the video so it looks smooth. You experience the &lt;em&gt;result&lt;/em&gt; of those decisions without experiencing the decision-making.&lt;/p&gt;

&lt;p&gt;That's pleasant, and it feels like learning. But the skill you actually need on a real project is the one the tutorial hid from you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which of the ten possible approaches should I try first?&lt;/li&gt;
&lt;li&gt;What do I do when the error message makes no sense?&lt;/li&gt;
&lt;li&gt;How do I know whether this design will hurt me in a week?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't learn those by watching someone else answer them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What helps
&lt;/h2&gt;

&lt;p&gt;Not a clever trick. A &lt;strong&gt;project slightly too big for you&lt;/strong&gt;, and a deadline you made up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Pick something you want to use
&lt;/h3&gt;

&lt;p&gt;Not a todo app. Not a weather clone because every tutorial has one. Something that solves a small problem &lt;em&gt;you&lt;/em&gt; actually have. Motivation is the fuel, and borrowed motivation runs out around the second hard bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Write the smallest version that works
&lt;/h3&gt;

&lt;p&gt;Write down, in one sentence, what the smallest useful version does. Then build only that. If the sentence has the word "and" in it more than once, it's too big.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Get stuck. Search the exact error. Fix it. Repeat.
&lt;/h3&gt;

&lt;p&gt;This is the actual curriculum. It's slow, uncomfortable, and where nearly all the learning happens. Copy the error message exactly, read the top few answers, and try one fix at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "slightly too big" looks like
&lt;/h2&gt;

&lt;p&gt;My college RAG helpdesk, Internal GPT, stretched me in exactly this way. To be useful at all, retrieval, embeddings and chunking all had to work. A helpdesk with no retrieval isn't a helpdesk, so each piece had to be learned, because the project wouldn't work otherwise.&lt;/p&gt;

&lt;p&gt;Notice that the project didn't teach me by being impressive. It taught me by being &lt;em&gt;blocked&lt;/em&gt; until I understood things. That's the property to look for: a project where the next step isn't handed to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic weekly plan
&lt;/h2&gt;

&lt;p&gt;If you're stuck right now, here's a plan that fits around other commitments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mon&lt;/td&gt;
&lt;td&gt;Write a one-sentence spec for the smallest version. List 3 unknowns.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tue&lt;/td&gt;
&lt;td&gt;Research only the first unknown. Build the thinnest slice that touches it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wed&lt;/td&gt;
&lt;td&gt;Get it running end to end, however ugly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thu&lt;/td&gt;
&lt;td&gt;Break it on purpose. Fix what breaks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fri&lt;/td&gt;
&lt;td&gt;Deploy it somewhere with a real URL.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sat&lt;/td&gt;
&lt;td&gt;Write down what you learned in 10 lines.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sun&lt;/td&gt;
&lt;td&gt;Rest. Seriously.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things in that plan are non-negotiable. First, &lt;strong&gt;ship something ugly by Wednesday&lt;/strong&gt;. Second, &lt;strong&gt;deploy by Friday&lt;/strong&gt;. A live URL changes how seriously you take a project.&lt;/p&gt;

&lt;h2&gt;
  
  
  A caveat: tutorials aren't evil
&lt;/h2&gt;

&lt;p&gt;I'm not saying tutorials are useless. They're great for a &lt;em&gt;new tool&lt;/em&gt;, when you need to learn its vocabulary and shape quickly. Documentation quick-starts do the same job.&lt;/p&gt;

&lt;p&gt;The trap is using them as a &lt;strong&gt;substitute for decisions&lt;/strong&gt;. A good rule of thumb: use a tutorial to learn the tool, then close it and make your own decisions with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One-week experiment
&lt;/h2&gt;

&lt;p&gt;Here's the exercise I'd actually recommend.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finish a tutorial you've been meaning to finish.&lt;/li&gt;
&lt;li&gt;Close it.&lt;/li&gt;
&lt;li&gt;Build the same thing again &lt;strong&gt;from memory&lt;/strong&gt;, with &lt;strong&gt;one change&lt;/strong&gt; (a different data source, a new feature, a different layout).&lt;/li&gt;
&lt;li&gt;Notice exactly where you freeze.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those freezes are the real curriculum. Write each one down as a question. That list is your study plan, and it's far more useful than the next tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signs you're making progress
&lt;/h2&gt;

&lt;p&gt;Progress feels different from watching:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You get stuck &lt;em&gt;more&lt;/em&gt; often, but you get unstuck faster.&lt;/li&gt;
&lt;li&gt;You start recognising error messages.&lt;/li&gt;
&lt;li&gt;You can estimate roughly how long something will take.&lt;/li&gt;
&lt;li&gt;You have opinions about how to structure a project.&lt;/li&gt;
&lt;li&gt;You've deleted code you wrote because you found a better way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those are happening, you're out of the hell, whatever your folder of unfinished tutorials says.&lt;/p&gt;

&lt;h2&gt;
  
  
  A project ladder
&lt;/h2&gt;

&lt;p&gt;If you're not sure what to build, climb a ladder. Each rung is slightly too big for the previous one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;The new thing it forces&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;A command-line tool that renames files by pattern&lt;/td&gt;
&lt;td&gt;Reading input, handling errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;A small web page that reads and writes to local storage&lt;/td&gt;
&lt;td&gt;State, the DOM, persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;A tiny API plus a page that uses it&lt;/td&gt;
&lt;td&gt;Requests, validation, deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;The same app with real accounts and a database&lt;/td&gt;
&lt;td&gt;Auth, permissions, schema design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;A collaborative or real-time feature&lt;/td&gt;
&lt;td&gt;Concurrency, sync, failure modes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that nothing on the ladder is a tutorial clone. Each rung is something a real person might use.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose when you're stuck choosing
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write down three annoyances from your own week.&lt;/li&gt;
&lt;li&gt;For each, ask: could a small program help?&lt;/li&gt;
&lt;li&gt;Pick the one you'd actually use tomorrow.&lt;/li&gt;
&lt;li&gt;Write the one-sentence spec.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can't think of anything, ask a friend or family member what repetitive thing they do. Real users produce better projects than invented ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  A "freeze log"
&lt;/h2&gt;

&lt;p&gt;When you rebuild something from memory, keep a log of where you stalled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Freeze 1: couldn't remember how to debounce the search input.
  -&amp;gt; Question: what is debouncing and when is it needed?
Freeze 2: didn't know how to handle a failed fetch.
  -&amp;gt; Question: how do I show an error state?
Freeze 3: unsure where to store the auth token.
  -&amp;gt; Question: cookies vs local storage, trade-offs?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each freeze is a learning target, so study &lt;em&gt;those&lt;/em&gt;, not the next generic course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions people ask
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Isn't it inefficient to struggle?"&lt;/strong&gt; Short struggles are the efficient part. The rule of thumb: if you're stuck for more than 30–45 minutes with no new idea, ask for help or read the docs. Struggle with a time limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What if I don't know enough to start?"&lt;/strong&gt; You never will. Start with the smallest slice and learn as the next block appears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Should I stop watching tutorials entirely?"&lt;/strong&gt; No. Use them like a dictionary: look up what you need, then go back to building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How do I know the project is 'big enough'?"&lt;/strong&gt; If you can finish it in a weekend without learning anything new, it's too small.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you finish
&lt;/h2&gt;

&lt;p&gt;A finished project has three artifacts: the code, a live URL and a short write-up. The write-up is the part that turns a project into evidence of learning. Say what it does, what you struggled with, and what you'd change.&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%2Fvbjjybkeurm5v6jtx0jt.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%2Fvbjjybkeurm5v6jtx0jt.png" alt="Key takeaways from day 7" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's the biggest thing you've built that you weren't "ready" for? What forced you to learn?&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>learning</category>
      <category>career</category>
      <category>motivation</category>
    </item>
    <item>
      <title>Why my portfolio looks like a newspaper (and what that taught me about design systems)</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Fri, 23 Oct 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/why-my-portfolio-looks-like-a-newspaper-and-what-that-taught-me-about-design-systems-46m9</link>
      <guid>https://dev.to/suraj_nandan/why-my-portfolio-looks-like-a-newspaper-and-what-that-taught-me-about-design-systems-46m9</guid>
      <description>&lt;p&gt;Most developer portfolios are a dark gradient with a glowing card. I wanted something people would remember, so mine is a &lt;strong&gt;newspaper&lt;/strong&gt;: &lt;em&gt;The Nandan Review&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Underneath it is a small design system I call &lt;strong&gt;The Press&lt;/strong&gt;. This post is about how it works, why the newspaper idea held together, and what it taught me about design systems in general. If you've ever stared at a blank stylesheet wondering what shade of grey to use, this is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: ink on paper
&lt;/h2&gt;

&lt;p&gt;A newspaper press is mechanical, precise and industrial. It stamps ink onto paper with force and intention. I wanted every interaction on the site to feel the same way: deliberate and a little permanent.&lt;/p&gt;

&lt;p&gt;That gave me a first principle that I could test every decision against:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Everything should feel printed.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What does that rule out?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Glowing or fluorescent colours. Print is muted and natural.&lt;/li&gt;
&lt;li&gt;Rounded, soft borders. Rules in print are hard and sharp.&lt;/li&gt;
&lt;li&gt;Floaty blurred shadows. Print shadows, if they exist, are sharp and dark, cast by a light that never moves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I had that single sentence, dozens of tiny decisions stopped being decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three fonts, three jobs
&lt;/h2&gt;

&lt;p&gt;The second principle is typographic hierarchy: &lt;strong&gt;every font serves one purpose and never overlaps.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Archivo&lt;/strong&gt; (variable) for display headlines. Heavy weight, tight letter-spacing, commands attention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newsreader&lt;/strong&gt; (variable) for body text. Elegant serifs and a comfortable reading rhythm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Space Mono&lt;/strong&gt; for metadata, labels and code. Its monospaced precision tells you "this is a technical detail, not narrative."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The enforcement matters more than the choice. Because each font has exactly one role, the typography becomes invisible. Readers absorb content without ever thinking about the typeface, which is the goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Colour: paper, ink, oxblood
&lt;/h2&gt;

&lt;p&gt;The palette is deliberately tiny: a warm paper tone, a near-black ink, and an oxblood accent. That's it.&lt;/p&gt;

&lt;p&gt;Here's the practical effect. When I add a new component, I never ask "what shade should this be?" The answer is already decided: it's paper, ink or oxblood. In a design system, that's what "tokens" are for. Instead of hard-coding colours, I name them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--paper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c"&gt;/* warm off-white */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c"&gt;/* near-black */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--oxblood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c"&gt;/* the one accent colour */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.press-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--paper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c"&gt;/* hard shadow, no blur */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.press-tag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Space Mono'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;monospace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--oxblood&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;Notice there's no blur radius on that shadow. A hard, offset shadow is the whole "printed" look in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The grid is law, but not a prison
&lt;/h2&gt;

&lt;p&gt;Layouts follow a strict column grid that gives the page a visual rhythm. But it flexes at breakpoints, like a newspaper reformatting for different editions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Desktop:&lt;/strong&gt; a multi-column layout that mirrors a broadsheet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile:&lt;/strong&gt; a single column that reads like a pocket edition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because everything snaps to the same underlying grid, elements align vertically across the page, which creates the quiet order that print designers perfected over centuries.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tiny component library
&lt;/h2&gt;

&lt;p&gt;A design system isn't only tokens. It's a set of reusable pieces that carry those tokens everywhere. The Press has a handful of primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PressSection&lt;/strong&gt;: the paper background with a subtle noise texture overlay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SectionMasthead&lt;/strong&gt;: section headers with an animated rule that draws across the page, like a press stamping down the title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PressTag&lt;/strong&gt; and &lt;strong&gt;PressButton&lt;/strong&gt;: consistent interactive elements with hard borders and monospaced labels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is small, but because every section uses them, the whole site feels like one publication instead of a collage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motion that has a reason
&lt;/h2&gt;

&lt;p&gt;My motion rule: &lt;strong&gt;animation must explain something.&lt;/strong&gt; It isn't decoration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content reveals with a gentle stagger and slight upward motion, like sheets of paper being fed through a press. It guides attention from one thing to the next.&lt;/li&gt;
&lt;li&gt;Section rules draw horizontally, simulating the mechanical action of a press.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an animation doesn't help someone understand order or structure, it doesn't ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a newspaper for a portfolio?
&lt;/h2&gt;

&lt;p&gt;A developer portfolio is fundamentally a &lt;strong&gt;record of work&lt;/strong&gt;. A newspaper is a format people already know how to scan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Projects become front-page stories.&lt;/li&gt;
&lt;li&gt;Certifications become archive entries.&lt;/li&gt;
&lt;li&gt;Skills become classified listings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The metaphor gives structure to content that would otherwise be a pile of cards. It also makes the page feel intentional, because a theme that's applied consistently reads as craft.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd warn you about
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend a strong theme is free.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A strong theme can become a cage.&lt;/strong&gt; Some content just doesn't fit a newspaper layout, and you end up bending it or leaving it out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility needs deliberate checking.&lt;/strong&gt; Beige paper plus grey text is an easy way to fail contrast requirements. Muted palettes need extra attention, not less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Themes age.&lt;/strong&gt; If you make the metaphor the whole identity, changing it later is a rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's not for everyone.&lt;/strong&gt; Many visitors just want the facts fast, and a distinctive layout adds a tiny cost to that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it taught me about design systems
&lt;/h2&gt;

&lt;p&gt;Three lessons I'm confident about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A design system isn't a component library. It's a list of decisions you refuse to remake.&lt;/strong&gt; The components are just where the decisions live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fewer options means faster shipping.&lt;/strong&gt; Three colours and three fonts removed more work than any plugin could.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the rules in a sentence.&lt;/strong&gt; "Everything should feel printed" settled arguments I hadn't even had yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building your own system, start with the sentence, not the Figma file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a mini "Press" system in 15 minutes
&lt;/h2&gt;

&lt;p&gt;Here's a tiny version you can paste into a page to feel the approach. The colour values are &lt;em&gt;examples&lt;/em&gt; for this demo, so choose your own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--paper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;#f4efe6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;#1c1c1c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--oxblood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;#7a1f1f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--paper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Newsreader'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Georgia&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Archivo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;letter-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-0.02em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Space Mono'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;monospace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.8rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--oxblood&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;text-transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;uppercase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.card&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--paper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.rule&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--ink&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Front page&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Local developer ships honest portfolio&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;hr&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rule"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Project&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;A one-paragraph story.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's about all the "style" it takes: three colours, three fonts, a hard shadow and a double rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your contrast
&lt;/h2&gt;

&lt;p&gt;A muted palette needs a deliberate accessibility check. WCAG asks for a contrast ratio of at least &lt;strong&gt;4.5:1&lt;/strong&gt; for normal body text. For the example colours above:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pair&lt;/th&gt;
&lt;th&gt;Contrast ratio&lt;/th&gt;
&lt;th&gt;Passes body-text minimum?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ink on paper&lt;/td&gt;
&lt;td&gt;about 14.9 : 1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oxblood on paper&lt;/td&gt;
&lt;td&gt;about 9.0 : 1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid-grey (#8a8a8a) on paper&lt;/td&gt;
&lt;td&gt;about 3.0 : 1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the classic trap: a soft grey that looks elegant but fails. You can calculate the ratio yourself from relative luminance, or use any browser dev-tools contrast checker.&lt;/p&gt;

&lt;h2&gt;
  
  
  A design-system starter checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] A one-sentence principle ("everything should feel printed")&lt;/li&gt;
&lt;li&gt;[ ] 3–5 colour tokens, named by &lt;em&gt;role&lt;/em&gt;, not by hue&lt;/li&gt;
&lt;li&gt;[ ] Fonts with &lt;strong&gt;one job each&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] A spacing scale (4, 8, 16, 24, 32…)&lt;/li&gt;
&lt;li&gt;[ ] 4–6 primitive components&lt;/li&gt;
&lt;li&gt;[ ] A motion rule ("animation must explain something")&lt;/li&gt;
&lt;li&gt;[ ] A contrast check on every colour pair&lt;/li&gt;
&lt;li&gt;[ ] A mobile layout&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Won't a strict theme limit me?"&lt;/strong&gt; Yes, deliberately. The limit is the feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How do I pick the principle?"&lt;/strong&gt; Look at something physical whose feel you admire (paper, signage, a machine) and describe it in one sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How many tokens is too many?"&lt;/strong&gt; If you can't remember them, there are too many.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Dark mode?"&lt;/strong&gt; A themed system needs a second palette &lt;em&gt;designed&lt;/em&gt;, not just inverted. For a print theme, a dark variant can be "newsprint at night", muted rather than neon.&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%2Fyp7rfxuko77u04ivfd7s.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%2Fyp7rfxuko77u04ivfd7s.png" alt="Key takeaways from day 6" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to see it in action, the site is at surajnandan.in. What's the single sentence that defines &lt;em&gt;your&lt;/em&gt; design taste?&lt;/p&gt;

</description>
      <category>design</category>
      <category>css</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I use AI coding tools without letting them think for me</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Thu, 22 Oct 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/how-i-use-ai-coding-tools-without-letting-them-think-for-me-28i6</link>
      <guid>https://dev.to/suraj_nandan/how-i-use-ai-coding-tools-without-letting-them-think-for-me-28i6</guid>
      <description>&lt;p&gt;I use AI tools a lot. I'm not going to pretend otherwise. My Whiteboard.ai project, for example, was built with Claude Code under written engineering rules and a plan-then-approve loop, and that's stated plainly in its case study.&lt;/p&gt;

&lt;p&gt;But I've had to set rules for myself, because I noticed a specific failure mode: &lt;strong&gt;I can ship something I don't understand.&lt;/strong&gt; That's the real risk of AI tools, and it isn't about quality of output. It's about what happens to &lt;em&gt;me&lt;/em&gt; if the tool does all the thinking.&lt;/p&gt;

&lt;p&gt;Here are the rules, where AI helps, where it quietly hurts, and one lesson from a different angle: building products &lt;em&gt;on top of&lt;/em&gt; models.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;p&gt;Without AI, if I don't understand something, I can't write it. The gap between "I need this" and "I have this" forces learning.&lt;/p&gt;

&lt;p&gt;With AI, that gap can vanish. The tool writes the code; it compiles; it passes a quick manual check; I move on. The code is in my repo, but the &lt;em&gt;understanding&lt;/em&gt; isn't in my head. Weeks later something breaks and I'm debugging code that's effectively someone else's, in a codebase I'm supposed to own.&lt;/p&gt;

&lt;p&gt;So I set rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  My rules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Explain it back
&lt;/h3&gt;

&lt;p&gt;If I can't explain a block of generated code in my own words, I don't commit it. "It works" isn't enough. I need to be able to say &lt;em&gt;why&lt;/em&gt; it works and what would break it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. I choose the architecture
&lt;/h3&gt;

&lt;p&gt;The assistant fills in; it doesn't decide where logic lives. The decision to put CardBridge's rules in Postgres (Day 2) was mine, with reasons I could defend. That's the kind of decision I don't hand over, because the consequences last the whole life of the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Read the diff. Always.
&lt;/h3&gt;

&lt;p&gt;Generated changes can touch files I didn't expect. Reading the diff before accepting is the cheapest quality control there is. If a change surprises me, that's a signal to stop and ask why.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Verify in the running app
&lt;/h3&gt;

&lt;p&gt;"It compiles" isn't "it works". Type checks and unit tests are necessary, not sufficient. For anything user-facing, I run the actual app and use the feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Write the rules down
&lt;/h3&gt;

&lt;p&gt;For Whiteboard.ai, I wrote engineering rules in a file and had the assistant plan first, with me approving the plan before code. A written rule survives a long session in a way that a mental intention doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI genuinely helps me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Boilerplate and repetitive refactors.&lt;/strong&gt; Renaming across forty files, adapting a pattern to a new case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rubber-ducking a bug&lt;/strong&gt; when nobody's awake to ask. Explaining the problem clearly is half the fix, and a responsive duck helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unfamiliar APIs&lt;/strong&gt;, where I need a starting example to adapt rather than a blank page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test scaffolding&lt;/strong&gt;, once I know &lt;em&gt;what&lt;/em&gt; to test.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it quietly hurts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It makes the first draft so easy that I skip the thinking.&lt;/strong&gt; The first design is rarely the best one. When drafting is free, it's tempting to stop at draft one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confident wrong answers.&lt;/strong&gt; A fluent explanation isn't evidence that it's right. I've learned to treat fluency as zero information about correctness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent scope creep.&lt;/strong&gt; Assistants happily "improve" things you didn't ask about.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A lesson from building &lt;em&gt;on top of&lt;/em&gt; models
&lt;/h2&gt;

&lt;p&gt;AI coding tools are one side. The other is building products that call models. My Knot.ai project, a spoken interview-practice app, taught me something I now think of as a rule of thumb:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Instruction is not control.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In early testing, I handed the whole interview to an LLM and asked it nicely to follow rules: adjust difficulty by one level, cover three topic areas. Against a simulated candidate, the interviewer model tunnel-visioned on one topic and escalated difficulty freely. It ignored "adjust by one level" and "cover three areas" the moment they competed with its own instincts.&lt;/p&gt;

&lt;p&gt;The fix was to split the job. The model does what only a model can: understand an answer, judge it, phrase the next question. Everything &lt;em&gt;countable&lt;/em&gt; became a state machine on the server: difficulty is a streak-based ladder clamped to 1–5, topic coverage rotates on a counter. The model supplies judgement and phrasing; the server decides what happens.&lt;/p&gt;

&lt;p&gt;That's the same principle as my AI-tool rules, from the opposite direction. &lt;strong&gt;Don't rely on a prompt to enforce what code can enforce.&lt;/strong&gt; And don't rely on an assistant to enforce good judgement that only you can supply.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test I use
&lt;/h2&gt;

&lt;p&gt;Before I commit anything an AI wrote, I ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Could I rebuild this from scratch if the tool vanished tomorrow?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If yes, I'm using it as a power tool. If no, I'm renting understanding, and the rent comes due at the worst time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm still figuring out
&lt;/h2&gt;

&lt;p&gt;I don't have this solved. A few open questions I wrestle with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much unfamiliar code is "okay to accept" while I'm learning versus building something to ship?&lt;/li&gt;
&lt;li&gt;When does reviewing a diff become a rubber stamp because I'm tired?&lt;/li&gt;
&lt;li&gt;How do I keep my own coding muscles in shape when the tool is always there?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have answers, I genuinely want them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A review checklist for AI-generated changes
&lt;/h2&gt;

&lt;p&gt;Run through this before accepting a diff.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scope.&lt;/strong&gt; Does it touch only files related to the request?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surprises.&lt;/strong&gt; Anything deleted, renamed or reformatted that I didn't ask for?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understanding.&lt;/strong&gt; Can I explain each hunk aloud?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases.&lt;/strong&gt; What happens on empty input, errors, concurrency?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security.&lt;/strong&gt; Any secrets, unsafe string-building, permissive access?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies.&lt;/strong&gt; Did it add a package? Is it needed and trustworthy?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests.&lt;/strong&gt; Does a test fail without this change and pass with it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running app.&lt;/strong&gt; Did I actually use the feature?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prompts that help me &lt;em&gt;understand&lt;/em&gt;, not just copy
&lt;/h2&gt;

&lt;p&gt;How you ask changes what you learn.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Explain this function line by line as if I'm a second-year student."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"What are three ways this could fail in production?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Give me two alternative designs and the trade-offs of each. Don't pick one."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"What would you check before trusting this?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Write the failing test first, then the minimal fix."&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the pattern: these ask the tool to &lt;em&gt;expose reasoning&lt;/em&gt;, which keeps me in the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  A before/after example
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Weak request:&lt;/strong&gt; "Add rate limiting to my API."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better request:&lt;/strong&gt; "I have a Vercel function at api/chat.ts. I want to limit each IP to 20 requests per minute. List two approaches that fit a serverless host, explain the trade-offs, and wait for me to choose before writing code."&lt;/p&gt;

&lt;p&gt;The second one keeps the architecture decision with me and gives me something to learn from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan-then-approve loop
&lt;/h2&gt;

&lt;p&gt;For bigger tasks, I use a loop with written rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;State the goal and constraints.&lt;/li&gt;
&lt;li&gt;The assistant proposes a plan, with no code yet.&lt;/li&gt;
&lt;li&gt;I read it, push back, and approve or change it.&lt;/li&gt;
&lt;li&gt;It implements one step at a time.&lt;/li&gt;
&lt;li&gt;I review each diff before moving on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Written engineering rules (naming, testing, "don't change files outside the plan") make the loop reliable, because rules written once apply to the whole session.&lt;/p&gt;

&lt;h2&gt;
  
  
  A mini-exercise
&lt;/h2&gt;

&lt;p&gt;Pick a function you wrote with AI help last month. Without looking at the code, write down what it does, what inputs it can't handle and how you'd test it. Then compare with the code. The gaps you find are exactly the "understanding debt" this post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signs I'm over-relying on it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I accept changes without opening the files.&lt;/li&gt;
&lt;li&gt;I can't remember how a feature I "built" works.&lt;/li&gt;
&lt;li&gt;I ask the tool &lt;em&gt;what&lt;/em&gt; to build, not &lt;em&gt;how&lt;/em&gt; to build it.&lt;/li&gt;
&lt;li&gt;Debugging starts with pasting the error rather than reading it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If two or more are true, I slow down for a few days and write something from scratch.&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%2Fpyq4ak16mvz83vb3ziaw.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%2Fpyq4ak16mvz83vb3ziaw.png" alt="Key takeaways from day 5" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How do you draw the line between using AI as a tool and letting it replace understanding?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>I tried to figure out how a student earns online. Here's what I ruled out and why</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Wed, 21 Oct 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/i-tried-to-figure-out-how-a-student-earns-online-heres-what-i-ruled-out-and-why-1ecd</link>
      <guid>https://dev.to/suraj_nandan/i-tried-to-figure-out-how-a-student-earns-online-heres-what-i-ruled-out-and-why-1ecd</guid>
      <description>&lt;p&gt;I'm a student and I'd like income from skills I already have. Not a get-rich-quick plan, just something real that helps cover costs and might grow.&lt;/p&gt;

&lt;p&gt;I went through the usual list. This post is my reasoning, written out honestly, including the parts that aren't finished. It's not advice, it's a worked example of how I weighed things. Your answers could be completely different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints I'm working with
&lt;/h2&gt;

&lt;p&gt;Before the options, the limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I'm mid-degree.&lt;/strong&gt; I graduate in 2027, so full-time work isn't realistic yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time is the scarce resource.&lt;/strong&gt; Classes, projects and life already fill the week.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I'd rather build something once than sell hours repeatedly.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Starting capital is close to zero.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any option has to fit inside those four lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options I weighed
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Teaching or tutoring
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For:&lt;/strong&gt; Real value to people, and teaching deepens your own understanding.&lt;br&gt;
&lt;strong&gt;Against:&lt;/strong&gt; It trades hours for money directly. I'm also still learning myself, and I don't want to sell certainty I don't have yet.&lt;br&gt;
&lt;strong&gt;Verdict:&lt;/strong&gt; Good later, not now.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Job hunting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For:&lt;/strong&gt; It's the best long-term path to steady income and learning from experienced engineers.&lt;br&gt;
&lt;strong&gt;Against:&lt;/strong&gt; I'm mid-degree, so most roles aren't a fit yet, and a lot of effort goes into applications.&lt;br&gt;
&lt;strong&gt;Verdict:&lt;/strong&gt; The right move &lt;em&gt;eventually&lt;/em&gt;. I'm building evidence for it (this series is part of that).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fiverr
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For:&lt;/strong&gt; A marketplace with built-in demand.&lt;br&gt;
&lt;strong&gt;Against:&lt;/strong&gt; It's competitive, and a new profile has no reviews. I set up gigs only recently, so it's genuinely too early to judge.&lt;br&gt;
&lt;strong&gt;Verdict:&lt;/strong&gt; Running quietly in the background. I'm not counting on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Content creation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For:&lt;/strong&gt; Compounds over time and builds an audience.&lt;br&gt;
&lt;strong&gt;Against:&lt;/strong&gt; It takes a long time before it pays, if it ever does, and it competes for the same hours as building.&lt;br&gt;
&lt;strong&gt;Verdict:&lt;/strong&gt; I'm writing this series anyway, but for learning and visibility, not as an income plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Selling a template
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For:&lt;/strong&gt; The work already exists. I'd built a design system, &lt;em&gt;The Press&lt;/em&gt;, for my own portfolio. Packaging it as a template costs far less than starting from scratch, and it's something other students need: a portfolio that doesn't look like everyone else's.&lt;br&gt;
&lt;strong&gt;Against:&lt;/strong&gt; Digital product sales are uncertain, and a template needs support and polish.&lt;br&gt;
&lt;strong&gt;Verdict:&lt;/strong&gt; The best fit for my constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deciding factor
&lt;/h2&gt;

&lt;p&gt;The deciding factor wasn't "this will make money." I don't know that. It was this question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What do I already have that other people need, which I can offer without spending months building something new?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question filters hard. Most "side hustle" ideas fail it because they require learning a new skill, finding customers &lt;em&gt;and&lt;/em&gt; doing the work. The template only required packaging.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually did
&lt;/h2&gt;

&lt;p&gt;I built a separate copy of the portfolio as a template, kept the live site untouched, and priced it low at &lt;strong&gt;$19&lt;/strong&gt;. Low enough that the decision to buy is easy, high enough that it isn't a giveaway. I'm treating the outcome as an experiment. If it sells, I'll learn something about what people want; if it doesn't, I'll learn that too, and the cost was mostly time I'd have spent anyway.&lt;/p&gt;

&lt;p&gt;I'll write about the packaging work itself on Day 27, because it was more unglamorous than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I haven't solved
&lt;/h2&gt;

&lt;p&gt;Here's the honest status: &lt;strong&gt;my Gumroad listing is drafted but not published&lt;/strong&gt;, because payouts need bank details that I haven't connected yet. So as of today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The template is built. ✅&lt;/li&gt;
&lt;li&gt;Screenshots and listing copy are ready. ✅&lt;/li&gt;
&lt;li&gt;The listing is &lt;em&gt;not live&lt;/em&gt;. ❌&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the real state of things: built, not shipped. A lot of projects die in exactly that gap, so I'm naming it publicly to make it harder to ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple decision framework you can reuse
&lt;/h2&gt;

&lt;p&gt;If you're choosing, try this table. Score each option from 1 to 5 on each column, then compare totals.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Uses what I already have&lt;/th&gt;
&lt;th&gt;Fits my free hours&lt;/th&gt;
&lt;th&gt;Doesn't trade time 1:1&lt;/th&gt;
&lt;th&gt;Low upfront cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Option A&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Option B&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point isn't the numbers. It's that writing the criteria down stops you from choosing the option that merely &lt;em&gt;sounds&lt;/em&gt; exciting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Picking by "what already exists that I can offer" beats picking by "what's trending." If you've been stuck choosing, list what you've &lt;em&gt;already built&lt;/em&gt; first. You may be closer than you think.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I think about pricing a first product
&lt;/h2&gt;

&lt;p&gt;I don't have sales data, so I won't pretend to. What I can share is the reasoning that led me to a low price.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anchor on the buyer's alternative.&lt;/strong&gt; What would a student do otherwise? Build their own (days of work) or use a generic template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A low price lowers the decision cost.&lt;/strong&gt; If it's cheaper than a lunch, buying doesn't need deliberation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price is also a signal.&lt;/strong&gt; Too low can suggest low quality; a clear, specific listing offsets that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can raise it later.&lt;/strong&gt; You can't easily un-raise it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I priced it at $19 and consider it an experiment. If you sell something, decide &lt;em&gt;in advance&lt;/em&gt; what result would make you change the price, so you aren't tempted to react to one data point.&lt;/p&gt;

&lt;h2&gt;
  
  
  A one-evening validation test
&lt;/h2&gt;

&lt;p&gt;Before building anything big, try to learn whether anyone wants it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the listing page text first: who it's for, what's included, what isn't.&lt;/li&gt;
&lt;li&gt;Show it to five people who fit the audience. Ask: "Would you pay for this? What's missing?"&lt;/li&gt;
&lt;li&gt;Count how many ask &lt;em&gt;when&lt;/em&gt; they can get it, not whether it's nice.&lt;/li&gt;
&lt;li&gt;If fewer than a couple lean in, rethink before building.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's not statistically rigorous. It's cheap, and it catches the worst surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes to avoid
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setting up payouts last.&lt;/strong&gt; My listing is blocked on exactly that. Set up payments and identity verification &lt;em&gt;first&lt;/em&gt;, since they can take days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building for an imaginary buyer.&lt;/strong&gt; Name a real person who'd use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counting hours as value.&lt;/strong&gt; Buyers pay for outcomes, not effort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promising support you can't give.&lt;/strong&gt; State what's included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping the licence.&lt;/strong&gt; Be explicit about what buyers may and may not do.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A comparison of the five options
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Upfront effort&lt;/th&gt;
&lt;th&gt;Ongoing effort&lt;/th&gt;
&lt;th&gt;Income ceiling&lt;/th&gt;
&lt;th&gt;Fit right now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tutoring&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High (hours)&lt;/td&gt;
&lt;td&gt;Limited by hours&lt;/td&gt;
&lt;td&gt;Later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;High (search)&lt;/td&gt;
&lt;td&gt;Full time&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;After graduation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fiverr&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Running quietly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Uncertain&lt;/td&gt;
&lt;td&gt;Doing for learning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Template&lt;/td&gt;
&lt;td&gt;Medium (done)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Uncertain&lt;/td&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A caution about "passive income"
&lt;/h2&gt;

&lt;p&gt;Be wary of anything marketed as effortless. A product still needs a listing, an audience, support and updates. "Passive" usually means "the effort comes in a different shape". I'd rather describe it as &lt;em&gt;leveraged&lt;/em&gt; effort: work done once that can be sold more than once.&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%2Fjjt9ln8f9mjqv95k0wtz.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%2Fjjt9ln8f9mjqv95k0wtz.png" alt="Key takeaways from day 4" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What did you try first, and did it work? If you've sold a digital product, I'd love to hear what surprised you.&lt;/p&gt;

</description>
      <category>career</category>
      <category>freelance</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>My site's deploys silently failed for two weeks. I didn't notice</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Tue, 20 Oct 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/my-sites-deploys-silently-failed-for-two-weeks-i-didnt-notice-2o3f</link>
      <guid>https://dev.to/suraj_nandan/my-sites-deploys-silently-failed-for-two-weeks-i-didnt-notice-2o3f</guid>
      <description>&lt;p&gt;This one is embarrassing, which is exactly why it's worth writing.&lt;/p&gt;

&lt;p&gt;My portfolio is hosted on Vercel. For roughly &lt;strong&gt;two weeks, from about September 10 to September 24&lt;/strong&gt;, every deploy failed, and the live site just kept serving an older version. I kept pushing changes. I kept wondering why nothing showed up. The site looked fine, because the &lt;em&gt;old&lt;/em&gt; site looked fine.&lt;/p&gt;

&lt;p&gt;Here's the full post-mortem: what broke, why I didn't notice, how I fixed it, and the one rule I took away.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a "silent" failure looks like
&lt;/h2&gt;

&lt;p&gt;Nothing screamed at me. My local build passed. My &lt;code&gt;git push&lt;/code&gt; succeeded. The live URL loaded without errors. The only thing wrong was that none of my newer commits were live, and that's a hard thing to notice when you're the only person looking at the site and you're not comparing versions.&lt;/p&gt;

&lt;p&gt;That's the nasty thing about a frozen deploy: the failure mode looks exactly like success until you specifically check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause
&lt;/h2&gt;

&lt;p&gt;My build runs a &lt;strong&gt;prerender step&lt;/strong&gt; after the main build. It uses Puppeteer to render pages to static HTML so they're quick to load and easy for crawlers to read. On my laptop, Puppeteer downloads a copy of Chrome when you install it.&lt;/p&gt;

&lt;p&gt;On Vercel, npm enforces an &lt;strong&gt;allow-scripts policy&lt;/strong&gt;: dependency &lt;em&gt;postinstall&lt;/em&gt; scripts are skipped unless explicitly allowed. Puppeteer's Chrome download is a postinstall script. So on Vercel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;npm install&lt;/code&gt; ran, but skipped the Chrome download.&lt;/li&gt;
&lt;li&gt;The prerender step tried to launch a browser that didn't exist.&lt;/li&gt;
&lt;li&gt;It crashed.&lt;/li&gt;
&lt;li&gt;The crash returned a non-zero exit code.&lt;/li&gt;
&lt;li&gt;A non-zero exit code from the build command failed the &lt;strong&gt;entire deploy&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So a nice-to-have optimization step was able to block the thing that actually matters: shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, in two parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part 1: a browser that works on Vercel.&lt;/strong&gt; On Linux (which is what Vercel runs), the prerender script falls back to &lt;code&gt;@sparticuz/chromium&lt;/code&gt;, a build of Chromium made for serverless environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2: never let the optimisation break the build.&lt;/strong&gt; If no browser launches at all, the script now &lt;strong&gt;skips prerendering and exits 0&lt;/strong&gt;. The deploy ships; the pages just aren't pre-rendered.&lt;/p&gt;

&lt;p&gt;In pseudo-code the shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/prerender.mjs (simplified)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;launchBrowser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// tries puppeteer, then @sparticuz/chromium&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No browser available, skipping prerender:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="c1"&gt;// IMPORTANT: do not fail the deploy&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...render pages...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule I wrote at the top of that file afterwards: &lt;strong&gt;a post-build nicety must never be able to block a deploy.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't notice for two weeks
&lt;/h2&gt;

&lt;p&gt;Being honest about my own process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I treated a successful &lt;code&gt;git push&lt;/code&gt; as proof the site was updated. It isn't. A push proves the code reached GitHub, nothing more.&lt;/li&gt;
&lt;li&gt;I didn't look at the deployment dashboard, which would have shown a red "Failed" next to every recent commit.&lt;/li&gt;
&lt;li&gt;I had no automated check comparing "what I think is live" with "what is live".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are clever failures. They're habit failures. The bug was small; the blind spot was mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now
&lt;/h2&gt;

&lt;p&gt;After a deploy, I verify the live site directly instead of trusting the pipeline. A few quick checks with &lt;code&gt;curl&lt;/code&gt; against the production domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Is the chat API alive? (returns ready: true)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.surajnandan.in/api/chat

&lt;span class="c"&gt;# 2. Is the RSS feed being served?&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://www.surajnandan.in/rss.xml | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1

&lt;span class="c"&gt;# 3. Are generated social images being served?&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://www.surajnandan.in/og/blog/&amp;lt;slug&amp;gt;.png | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these take more than a few seconds, and between them they confirm that the &lt;em&gt;new&lt;/em&gt; build is serving, not the old one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist you can steal
&lt;/h2&gt;

&lt;p&gt;If you deploy anything to a hosted platform, ask these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Which of my build steps are essential and which are optional?&lt;/strong&gt; Optional steps should fail soft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does my build rely on a postinstall script?&lt;/strong&gt; Hosted platforms may skip them. Check the install logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do I know a deploy actually went out?&lt;/strong&gt; Have an answer that isn't "the push worked."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I get notified when a deploy fails?&lt;/strong&gt; If not, turn it on today. Email or Slack, anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the live site expose a version?&lt;/strong&gt; Even a build timestamp in the footer lets you spot a frozen deploy at a glance.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest lesson
&lt;/h2&gt;

&lt;p&gt;The bug wasn't clever. The failure was that I trusted a green &lt;code&gt;git push&lt;/code&gt; as proof of a live site, and I didn't build a habit of checking.&lt;/p&gt;

&lt;p&gt;Two weeks is a long time for a site to quietly freeze. It happened to a portfolio, where the cost was a few unseen updates. If the same blind spot existed on something a business depended on, the cost would be much higher. I'd rather learn this lesson on my own site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the failure on your machine
&lt;/h2&gt;

&lt;p&gt;You can simulate the Vercel environment locally. The key is to install dependencies &lt;em&gt;without&lt;/em&gt; running postinstall scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; node_modules
npm ci &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;      &lt;span class="c"&gt;# skips scripts like Puppeteer's Chrome download&lt;/span&gt;
npm run build                &lt;span class="c"&gt;# watch the prerender step fail&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit code: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your build prints a non-zero exit code here, a hosted platform would have failed the deploy for the same reason. Fix it, re-run the same three commands, and confirm &lt;code&gt;exit code: 0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rough timeline
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;What I believed&lt;/th&gt;
&lt;th&gt;What was true&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;About Sep 10&lt;/td&gt;
&lt;td&gt;"Deploys are working"&lt;/td&gt;
&lt;td&gt;Builds had started failing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 10 to 24&lt;/td&gt;
&lt;td&gt;"My changes are live"&lt;/td&gt;
&lt;td&gt;The live site was frozen on an older build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;About Sep 24&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;The fallback and fail-soft rule landed, and deploys resumed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The questions I asked afterwards
&lt;/h2&gt;

&lt;p&gt;A post-mortem is only useful if it produces better questions. These are the ones I now ask about any pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What's the cheapest signal that would have told me this broke?&lt;/strong&gt; A deploy-failed email would have cost nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which steps are essential, which are cosmetic?&lt;/strong&gt; Mark them in the script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's different between my machine and the host?&lt;/strong&gt; Node version, installed browsers, environment variables, install policies, filesystem permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If this step fails, what's the safe default?&lt;/strong&gt; For prerendering, the safe default is "ship without it".&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Turn on failure notifications today
&lt;/h2&gt;

&lt;p&gt;Most hosts can notify you on a failed deploy. It takes a minute, and it removes the need to remember to check. Look for deployment notification settings in your project dashboard and route them to email or chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tiny fail-soft pattern for any optional step
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# in package.json scripts&lt;/span&gt;
&lt;span class="s2"&gt;"build"&lt;/span&gt;: &lt;span class="s2"&gt;"vite build &amp;amp;&amp;amp; npm run postbuild || echo 'postbuild failed (non-fatal)'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;||&lt;/code&gt; means the build still exits 0 if the optional step fails, while printing a clear warning. Use it only for steps that are genuinely optional, and make sure the warning is visible in logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this taught me about "green" signals
&lt;/h2&gt;

&lt;p&gt;A green tick answers one narrow question: "did this command succeed?" It does not answer "is the thing I care about true?" The more important the outcome, the more it deserves a direct check of the outcome itself (the live site) instead of an indirect signal (the pipeline).&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%2F27vnhvbsbb6crjipxkwe.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%2F27vnhvbsbb6crjipxkwe.png" alt="Key takeaways from day 3" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's the dumbest reason a deploy has failed for you? Mine is above. Tell me yours in the comments.&lt;/p&gt;

</description>
      <category>vercel</category>
      <category>devops</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I put my business rules in the database instead of React, here's what it bought me</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Mon, 19 Oct 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/i-put-my-business-rules-in-the-database-instead-of-react-heres-what-it-bought-me-3492</link>
      <guid>https://dev.to/suraj_nandan/i-put-my-business-rules-in-the-database-instead-of-react-heres-what-it-bought-me-3492</guid>
      <description>&lt;p&gt;When I built &lt;strong&gt;CardBridge&lt;/strong&gt;, the scary question was simple: &lt;em&gt;what stops a user from skipping a step in the escrow flow?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the rules live in React, the honest answer is "nothing, if they open the network tab." Anyone can call your API directly. Anyone can replay a request. The frontend is a suggestion, not a guard.&lt;/p&gt;

&lt;p&gt;This post is about the decision that shaped the whole project: moving the rules into Postgres. Here's how it works, what it bought me, and what it cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem CardBridge solves
&lt;/h2&gt;

&lt;p&gt;You find a great discount on something, but it only applies to a card you don't own. Someone else owns that card and would buy it for you for a small fee. The idea is easy. The hard part is &lt;strong&gt;trust&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The requester needs to know the product will actually arrive.&lt;/li&gt;
&lt;li&gt;The cardholder needs to know they'll be paid.&lt;/li&gt;
&lt;li&gt;The platform needs an auditable record of every step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it a trust problem far more than a UI problem. And trust problems are exactly where "the frontend checks it" falls apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision: the database is the referee
&lt;/h2&gt;

&lt;p&gt;A transaction moves through these states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MATCHED → FUNDED → PURCHASED → SHIPPED → DELIVERED → RELEASED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;plus three exits: &lt;code&gt;CANCELLED&lt;/code&gt;, &lt;code&gt;DISPUTED&lt;/code&gt; and &lt;code&gt;REFUNDED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of letting the client update a &lt;code&gt;status&lt;/code&gt; column, every change goes through one function, &lt;code&gt;transition_transaction&lt;/code&gt;. A &lt;code&gt;BEFORE UPDATE&lt;/code&gt; trigger validates the change against three things: a map of legal transitions, the caller's role, and the transaction's invariants. If anything is off, Postgres refuses.&lt;/p&gt;

&lt;p&gt;Here's the idea in simplified form. This is a sketch, not the production schema:&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="c1"&gt;-- Which (from -&amp;gt; to) moves are legal, and who may make them&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;legal_transitions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;from_status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;to_status&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;allowed_role&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed_role&lt;/span&gt;&lt;span class="p"&gt;)&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;function&lt;/span&gt; &lt;span class="n"&gt;guard_transition&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;if&lt;/span&gt; &lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;-- not a status change&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;legal_transitions&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;from_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;to_status&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;allowed_role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_caller_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&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;'illegal transition % -&amp;gt; % for this caller'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&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;transactions_guard&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;on&lt;/span&gt; &lt;span class="n"&gt;transactions&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;guard_transition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;AFTER UPDATE&lt;/code&gt; trigger then does the bookkeeping: it writes an &lt;strong&gt;immutable event log&lt;/strong&gt; row for the transition, keeps the original request's status in sync, and credits the user's tier.&lt;/p&gt;

&lt;p&gt;The result: a buggy or malicious client simply cannot skip a step. It's not that the client is &lt;em&gt;asked&lt;/em&gt; not to. The database says no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Row Level Security on every table
&lt;/h2&gt;

&lt;p&gt;The second half of the story is access control. Only Supabase's public &lt;strong&gt;anon key&lt;/strong&gt; ships to the browser. That key is not a secret, so safety can't come from hiding it. It has to come from &lt;strong&gt;Row Level Security (RLS)&lt;/strong&gt; on every table, so each user can only read and change the transactions they're part of.&lt;/p&gt;

&lt;p&gt;A few extra guards sit on top:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A trigger stops people from promoting their own profile privileges.&lt;/li&gt;
&lt;li&gt;Public-safe &lt;strong&gt;views&lt;/strong&gt; expose only what the browse feed needs.&lt;/li&gt;
&lt;li&gt;Matching has database-level guards: you can't accept your own request, and the escrow amount is computed on insert instead of trusted from the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters. If the client sends the amount, the client controls the amount. If the database computes it, nobody can lie about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeouts on two clocks
&lt;/h2&gt;

&lt;p&gt;Abandoned transactions shouldn't sit in limbo forever. A configurable timeout policy runs in two places: when a transaction page loads, and on a scheduled Edge Function (&lt;code&gt;escrow-timeout-sweep&lt;/code&gt;). Disputed transactions are skipped by every automatic action, so a dispute genuinely freezes escrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it bought me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The frontend can be wrong and nothing breaks.&lt;/strong&gt; I can ship a UI bug without shipping a money bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every state change is auditable.&lt;/strong&gt; The event log is written by a trigger, not by code someone has to remember to call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I stopped worrying about "what if someone calls the API directly".&lt;/strong&gt; They can. The database still decides.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;p&gt;I want to be as honest about the costs as the benefits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debugging triggers is slower&lt;/strong&gt; than debugging a function in the same language as the rest of the app. There's no breakpoint to set; there's an error string to interpret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres has sharp edges.&lt;/strong&gt; You can't use a new enum value in the same transaction that adds it. So adding &lt;code&gt;MATCHED&lt;/code&gt; and &lt;code&gt;SHIPPED&lt;/code&gt; needed its own migration that has to commit &lt;em&gt;before&lt;/em&gt; the state machine's migration runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RLS policies can recurse.&lt;/strong&gt; The SELECT and UPDATE policies on requests and transactions referenced each other and looped. I broke the cycle with &lt;code&gt;SECURITY DEFINER&lt;/code&gt; helper functions. (More on RLS in a later post.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The whole thing ended up as fourteen ordered migrations&lt;/strong&gt;, including the fixes. Versioned SQL is good discipline, but it's also more ceremony than editing a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no automated test suite yet.&lt;/strong&gt; I'll come back to this on Day 20 with a concrete plan, because it's the biggest gap in the project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A note on what's real
&lt;/h2&gt;

&lt;p&gt;To be clear about the prototype: the escrow and KYC &lt;em&gt;providers&lt;/em&gt; are stubbed. The lifecycle, the limits and the state machine are real; the money and identity checks don't touch a live payment or identity provider yet. I'd rather say that here than let the word "escrow" imply more than it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  My takeaway
&lt;/h2&gt;

&lt;p&gt;Put rules where they can't be bypassed. Put &lt;em&gt;presentation&lt;/em&gt; where it's cheap to change.&lt;/p&gt;

&lt;p&gt;That sounds obvious written down, but it took building a project where money moves for me to feel it. The frontend is for the user's convenience. The database is for everybody's protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself: a 20-minute version
&lt;/h2&gt;

&lt;p&gt;You don't need my whole project to feel the idea. Here's a tiny, runnable version with orders instead of escrow. Paste it into any Postgres database.&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;table&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;     &lt;span class="nb"&gt;serial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;'PENDING'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- The only legal moves&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;legal_moves&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_status&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;legal_moves&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PENDING'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'PAID'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PAID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'SHIPPED'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SHIPPED'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'DELIVERED'&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;function&lt;/span&gt; &lt;span class="n"&gt;guard_order&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;if&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;distinct&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
     &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;legal_moves&lt;/span&gt;
                     &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;from_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;to_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&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;'illegal move % -&amp;gt; %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&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;orders_guard&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;on&lt;/span&gt; &lt;span class="n"&gt;orders&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;guard_order&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;-- PENDING&lt;/span&gt;
&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PAID'&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;-- ok&lt;/span&gt;
&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DELIVERED'&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- ERROR: illegal move PAID -&amp;gt; DELIVERED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the last line and watch the database refuse. That refusal is the whole idea. Now imagine that every client, in every language, in every future version of your app, hits the same wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exercises
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add a &lt;code&gt;CANCELLED&lt;/code&gt; state reachable from &lt;code&gt;PENDING&lt;/code&gt; and &lt;code&gt;PAID&lt;/code&gt; only.&lt;/li&gt;
&lt;li&gt;Add an &lt;code&gt;order_events&lt;/code&gt; table and an &lt;code&gt;AFTER UPDATE&lt;/code&gt; trigger that logs every change.&lt;/li&gt;
&lt;li&gt;Make &lt;code&gt;order_events&lt;/code&gt; append-only by revoking &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; on it.&lt;/li&gt;
&lt;li&gt;Add a role check: only an admin may move an order to &lt;code&gt;CANCELLED&lt;/code&gt; after &lt;code&gt;PAID&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When &lt;em&gt;not&lt;/em&gt; to put logic in the database
&lt;/h2&gt;

&lt;p&gt;I don't want to oversell this. It isn't always the right call.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rules that change weekly.&lt;/strong&gt; Business rules that product teams tweak constantly are painful to ship as migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy computation.&lt;/strong&gt; The database is a shared, scarce resource. Don't make it do work an app server can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams without SQL skill.&lt;/strong&gt; If nobody can read the triggers, nobody can maintain them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic that needs outside calls.&lt;/strong&gt; Calling payment providers or email services from inside a trigger is a recipe for pain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prototypes you'll throw away.&lt;/strong&gt; The ceremony costs more than it saves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good compromise: put the &lt;strong&gt;invariants&lt;/strong&gt; (things that must &lt;em&gt;never&lt;/em&gt; be violated) in the database, and keep the &lt;strong&gt;policies&lt;/strong&gt; (things that may change) in application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions I get asked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Isn't that vendor lock-in?"&lt;/strong&gt; Triggers and RLS are standard PostgreSQL, not a vendor feature. Supabase makes them convenient, but the logic would move with the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How do you version it?"&lt;/strong&gt; As ordered migrations. CardBridge's schema is fourteen of them, including the fixes, so the database can be rebuilt from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What about the UX when Postgres rejects something?"&lt;/strong&gt; Catch the error in the RPC layer and translate it into a friendly message. The database refuses; the frontend explains.&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%2Fmoddsljoff0vs0ikuh3z.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%2Fmoddsljoff0vs0ikuh3z.png" alt="Key takeaways from day 2" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you moved logic into the database before? Did you regret it, or would you do it again? I'd especially like to hear from anyone who's tested trigger-based logic well, because that's my next problem.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>supabase</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I'm a B.Tech student in a small Indian city, and I'm going to write honestly for 30 days</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Sun, 18 Oct 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/im-a-btech-student-in-a-small-indian-city-and-im-going-to-write-honestly-for-30-days-3pba</link>
      <guid>https://dev.to/suraj_nandan/im-a-btech-student-in-a-small-indian-city-and-im-going-to-write-honestly-for-30-days-3pba</guid>
      <description>&lt;p&gt;Most developer blogs I read sound like their authors have it all figured out. I don't, so I'm not going to write like that.&lt;/p&gt;

&lt;p&gt;This is Day 1 of a 30-day series. One post a day, every day, for the next month. Before the first technical post lands tomorrow, I owe you an introduction, a list of the rules I'm holding myself to, and a map of where this is going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who I am
&lt;/h2&gt;

&lt;p&gt;I'm Suraj. I live in &lt;strong&gt;Kishanganj, India&lt;/strong&gt;, and I'm studying B.Tech in Computer Science at SRM (2023–2027). I build full-stack and AI projects, mostly on my own, sometimes in a team. Here's what's actually in my portfolio right now, with no rounding up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CardBridge&lt;/strong&gt;, a card-discount marketplace where the escrow rules live inside Postgres instead of the frontend. Built solo over about two months.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whiteboard.ai&lt;/strong&gt;, a real-time collaborative system-design whiteboard where an AI reviews your architecture diagram. It runs locally against a dev database. It is &lt;em&gt;not&lt;/em&gt; deployed, and I'll say that every time it comes up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knot.ai&lt;/strong&gt;, a spoken interview-practice app with five AI interviewer personas, built for a hackathon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hospitality.ai&lt;/strong&gt;, an insurance-aware care navigator, built for a healthcare challenge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job Scheduler&lt;/strong&gt;, a distributed background-job platform built on Postgres alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GigShield&lt;/strong&gt;, parametric income insurance for delivery riders. This one was a team of three, so the credit is shared, and I'll name my teammates when I write about it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This portfolio&lt;/strong&gt;, which looks like a newspaper (&lt;em&gt;The Nandan Review&lt;/em&gt;) and has its own design system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That list looks impressive written down. Reading it back, I notice it doesn't say how many times each project broke, how many decisions I'd reverse, or which ones have no tests. That's the part I want to write about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 30 days of this
&lt;/h2&gt;

&lt;p&gt;Three honest reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Writing exposes what I only half-understand.&lt;/strong&gt; I can build a Postgres state machine, but explaining &lt;em&gt;why&lt;/em&gt; it beats checks in the frontend, in plain words, forces a clarity that building alone doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A public record of how I think is a better résumé than a list of buzzwords.&lt;/strong&gt; Anyone can write "Postgres, React, Node" on a page. Fewer people can show a decision, the alternatives they rejected, and the thing that went wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. I'd like to meet other people who write in public.&lt;/strong&gt; Wherever you are, if you're learning in the open too, I'd like to read your work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules I'm setting for myself
&lt;/h2&gt;

&lt;p&gt;I'm writing these down on Day 1 so you can hold me to them on Day 30.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Only write what I actually did or actually believe.&lt;/strong&gt; No invented metrics. No "I 10x'd my productivity" claims. If a number appears in a post, it comes from something I can point to in a repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say what didn't work.&lt;/strong&gt; My case studies each end with a "limits" section. Whiteboard.ai's AI review eval is written but has never been run against a real API key, so its catch rate is unmeasured. I'd rather you read that sentence from me than discover it yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep every post readable in one sitting.&lt;/strong&gt; Most will take somewhere between 4 and 9 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit teammates.&lt;/strong&gt; Team projects will say so, with names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opinions are labelled as opinions.&lt;/strong&gt; When I'm giving advice rather than reporting what I did, I'll say so.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's coming
&lt;/h2&gt;

&lt;p&gt;Here's the rough shape of the month, so you know what you're signing up for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weeks 1–2: the building side.&lt;/strong&gt; Putting business rules in the database. The deploy that silently failed for two weeks. How I decide between a rewrite and a refactor. What a design system actually is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weeks 2–3: AI and systems.&lt;/strong&gt; How a RAG helpdesk answers only from documents. How I use AI coding tools without outsourcing my thinking. What went wrong building Whiteboard.ai and Knot.ai. Six data structures I implemented by hand for a job scheduler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weeks 3–4: the human side.&lt;/strong&gt; Money, motivation, impostor syndrome, writing, reading other people's code, and what I'm honestly &lt;em&gt;not&lt;/em&gt; good at yet.&lt;/p&gt;

&lt;p&gt;Not every post will be a tutorial. Some will be about decisions, some about doubt. If you only want code, you'll find plenty. If you want to know what a student actually thinks halfway through a CS degree, there's some of that too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small ask
&lt;/h2&gt;

&lt;p&gt;If you're a student or early developer working from somewhere that isn't a tech hub, say hi in the comments. Tell me one thing you're stuck on this week. It can be technical ("my RLS policy returns nothing") or not ("I don't know what to build next"). I'll read every comment, and the best questions will turn into posts later in the series.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small glossary for the series
&lt;/h2&gt;

&lt;p&gt;Some terms will come up repeatedly. If any are new, here's the plain-English version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State machine:&lt;/strong&gt; a model where something can be in exactly one &lt;em&gt;state&lt;/em&gt; at a time (say, &lt;code&gt;FUNDED&lt;/code&gt;), with a fixed list of allowed moves between states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row Level Security (RLS):&lt;/strong&gt; a database feature where &lt;em&gt;the database itself&lt;/em&gt; decides which rows a given user can read or change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG (Retrieval-Augmented Generation):&lt;/strong&gt; give an AI model the relevant pieces of your own documents at question time, so it answers from them instead of from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding:&lt;/strong&gt; a list of numbers that represents the meaning of a piece of text, so similar meanings sit close together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parametric insurance:&lt;/strong&gt; cover that pays automatically when a measurable event (like heavy rain) crosses a threshold, with no claim to argue over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent:&lt;/strong&gt; safe to run twice. Doing it again has the same effect as doing it once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-ahead log (WAL):&lt;/strong&gt; write the change to durable storage &lt;em&gt;first&lt;/em&gt;, then treat it as done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless function:&lt;/strong&gt; a small piece of code the host runs on demand, so you don't manage a server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to follow along
&lt;/h2&gt;

&lt;p&gt;You don't have to read in order. Each post stands alone, and later posts link back to earlier ones where it helps. A few ways to use the series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binge it&lt;/strong&gt; in a weekend using the reading map on Day 29.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick by interest:&lt;/strong&gt; database and security (Days 2, 17, 20), AI (Days 5, 11, 12), career and mindset (Days 4, 9, 14, 26).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skim the takeaways card&lt;/strong&gt; at the end of each post first, then decide whether to read the whole thing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd love your help with
&lt;/h2&gt;

&lt;p&gt;I'm learning in public, which means I'll get things wrong. The most useful comments are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Corrections.&lt;/strong&gt; If a claim is off, tell me and I'll fix the post and note what changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counter-examples.&lt;/strong&gt; "That didn't work for me because…" is gold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions.&lt;/strong&gt; If something wasn't clear, it's my writing's fault, not yours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also, a quick disclosure about how these were made: the posts are written from my own project notes and case studies, and I use AI tools in my workflow (more on that on Day 5). Every technical claim is something I can point to in a repository.&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%2Fszrvd82t2c5vozmn5dre.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%2Fszrvd82t2c5vozmn5dre.png" alt="Key takeaways from day 1" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tomorrow: why I put my escrow rules in the database instead of in React, and what it cost me.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;See you then.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>learning</category>
    </item>
    <item>
      <title>How I built a live halftone portrait in React with just Canvas 2D</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Sat, 17 Oct 2026 04:12:03 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/how-i-built-a-live-halftone-portrait-in-react-with-just-canvas-2d-5ejg</link>
      <guid>https://dev.to/suraj_nandan/how-i-built-a-live-halftone-portrait-in-react-with-just-canvas-2d-5ejg</guid>
      <description>&lt;p&gt;My portfolio is set like a newspaper, so my photo couldn't just be a photo. I wanted it to look like a printed halftone: a grid of dots, bigger where the picture is dark and smaller where it's light. And I wanted it to be alive, so the dots swell and get pushed away when you move the cursor over them.&lt;/p&gt;

&lt;p&gt;It turned out to be about 150 lines of React and plain Canvas 2D, with no dependencies. Here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: turn the image into a grid of samples
&lt;/h2&gt;

&lt;p&gt;A halftone is just a low-resolution version of the picture drawn with dots. So the first job is to shrink the image to roughly one pixel per dot and read the pixels back.&lt;/p&gt;

&lt;p&gt;I draw the image onto a tiny offscreen canvas (84 columns wide by default, with the rows worked out from the aspect ratio), and then call &lt;code&gt;getImageData&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;columns&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;width&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;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&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;octx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&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="na"&gt;willReadFrequently&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// cover-fit the image into the sample grid&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;octx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;octx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getImageData&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Letting the browser do the downscaling is the trick: &lt;code&gt;drawImage&lt;/code&gt; averages the source pixels for me, so each sample is already the average colour of its patch of the photo.&lt;/p&gt;

&lt;p&gt;For every pixel I store its position (normalised 0 to 1), its colour, and its darkness, which I get from luminance:&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;lum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2126&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.7152&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.0722&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;darkness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`rgb(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&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="s2"&gt;)`&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I only build this once per resize, not every frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: draw a dot per sample
&lt;/h2&gt;

&lt;p&gt;Each frame I clear the canvas and draw one circle per sample. The radius is what makes it look like a halftone: darker samples get a bigger dot.&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;cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;darkness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.62&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;0.85&lt;/code&gt; exponent is a small tone curve. Without it the midtones came out too light. &lt;code&gt;cell * 0.62&lt;/code&gt; stops neighbouring dots from touching in the darkest areas.&lt;/p&gt;

&lt;p&gt;To make the portrait fade into the page instead of ending in a hard rectangle, I multiply the radius by an elliptical vignette and use &lt;code&gt;smoothstep&lt;/code&gt; so the edge falls off gently:&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;smoothstep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;t&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;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;smoothstep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;eDist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;eDist&lt;/code&gt; is the sample's distance from the centre of an ellipse, where 1 is the edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: make it breathe
&lt;/h2&gt;

&lt;p&gt;Even with no cursor I wanted a little life in it. A tiny sine wave, offset by each dot's position, nudges the radius by about 5%, so it ripples slowly:&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="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: react to the cursor
&lt;/h2&gt;

&lt;p&gt;This is the part people notice. I keep a target position for the pointer and ease the "real" position toward it, so the effect trails the cursor with a bit of weight:&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="nx"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each dot inside an influence radius, two things happen: the dot grows, and it gets pushed away from the cursor, both scaled by a smooth falloff:&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;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;influenceRadius&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;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;smoothstep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;influenceRadius&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.05&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                     &lt;span class="c1"&gt;// swell&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;push&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// shove away from the cursor&lt;/span&gt;
  &lt;span class="nx"&gt;ox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;oy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;push&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;Using &lt;code&gt;smoothstep&lt;/code&gt; rather than a linear falloff is what makes it feel like ink spreading rather than a hard circle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it behave
&lt;/h2&gt;

&lt;p&gt;A canvas animation that runs forever is rude. Three small guards keep this one polite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pause when off-screen.&lt;/strong&gt; An &lt;code&gt;IntersectionObserver&lt;/code&gt; stops the animation loop when the canvas isn't visible and restarts it when it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respect reduced motion.&lt;/strong&gt; If &lt;code&gt;prefers-reduced-motion&lt;/code&gt; is set, it draws a single static frame and never starts the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the pixel ratio.&lt;/strong&gt; &lt;code&gt;devicePixelRatio&lt;/code&gt; is clamped to 2, so very dense screens don't draw four times the pixels for no visible gain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also resize with a &lt;code&gt;ResizeObserver&lt;/code&gt; and rebuild the samples then, and I decode the image with &lt;code&gt;img.decode()&lt;/code&gt; before drawing so there's no half-loaded flash.&lt;/p&gt;

&lt;p&gt;Finally, the canvas is &lt;code&gt;aria-hidden&lt;/code&gt;, since it's decoration. The page's real heading carries the meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd improve
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It draws one &lt;code&gt;arc&lt;/code&gt; per dot.&lt;/strong&gt; For about 7,000 dots that's fine, but a much denser grid would be better as a single batched path or a WebGL shader. I draw dots individually because each one has its own colour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large photos cost something at load.&lt;/strong&gt; The sampling step reads back image data once, which is quick for a small grid, but it's worth resizing the source photo to something sensible first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Want the whole thing?
&lt;/h2&gt;

&lt;p&gt;The halftone portrait is one part of a bigger project: a newspaper-style portfolio with a typewriter intro, a command palette, case studies and a blog. I turned it into a template, with a live demo you can poke at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://newspaper-template-zeta.vercel.app" rel="noopener noreferrer"&gt;https://newspaper-template-zeta.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Template (paid): &lt;a href="https://surajnandan.gumroad.com/l/press-portfolio" rel="noopener noreferrer"&gt;https://surajnandan.gumroad.com/l/press-portfolio&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The halftone component above is self-contained, so you're welcome to copy the ideas straight into your own project.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building GigShield: Instant Protection for Gig Workers with a First Risk Engine</title>
      <dc:creator>SURAJ NANDAN</dc:creator>
      <pubDate>Fri, 03 Apr 2026 12:15:06 +0000</pubDate>
      <link>https://dev.to/suraj_nandan/building-gigshield-instant-protection-for-gig-workers-with-a-first-risk-engine-2a3e</link>
      <guid>https://dev.to/suraj_nandan/building-gigshield-instant-protection-for-gig-workers-with-a-first-risk-engine-2a3e</guid>
      <description>&lt;p&gt;Gig workers lose income for reasons they cannot control: heavy rain, platform outages, curfews, poor air quality, and local disruptions. Most traditional claim systems are slow, manual, and stressful at the exact moment people need help.&lt;/p&gt;

&lt;p&gt;I built GigShield to explore a different approach: parametric protection that can trigger payouts automatically when predefined external conditions are met, with fraud controls and verification built into the flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product idea
&lt;/h2&gt;

&lt;p&gt;GigShield is designed around one simple promise: if covered disruption conditions happen, workers should know quickly whether they are eligible and how much they will receive.&lt;/p&gt;

&lt;p&gt;The app includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tiered weekly plans with clear coverage limits&lt;/li&gt;
&lt;li&gt;Trigger-based payout logic (weather, platform, and social disruptions)&lt;/li&gt;
&lt;li&gt;A transparent decision flow with explicit reason codes&lt;/li&gt;
&lt;li&gt;Fraud checks before settlement for risky claims&lt;/li&gt;
&lt;li&gt;A full payout lifecycle from event detection to receipt&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2F2dfxg32ns1qxu7lzvndg.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.amazonaws.com%2Fuploads%2Farticles%2F2dfxg32ns1qxu7lzvndg.png" alt=" " width="799" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this was interesting to build
&lt;/h2&gt;

&lt;p&gt;Most demos stop at UI. I wanted this one to model real decision mechanics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Policy domains and exclusions&lt;/li&gt;
&lt;li&gt;Coverage-hour checks&lt;/li&gt;
&lt;li&gt;Daily caps by plan&lt;/li&gt;
&lt;li&gt;Trigger confidence and cooldown windows&lt;/li&gt;
&lt;li&gt;Verification gates for high-risk claims&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This made the project less like a landing page and more like a small rules-driven system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture in practice
&lt;/h2&gt;

&lt;p&gt;The stack is React + Vite + Tailwind, with utility modules that isolate policy, payout, fraud, observability, and integrations concerns. Routes are separated by persona and workflow: onboarding, dashboard, payout, history, fraud explainability, and admin operations.&lt;/p&gt;

&lt;p&gt;A key design choice was frontend-first state with local persistence. That made iteration fast and let me simulate full flows end-to-end without waiting on backend dependencies. Supabase auth and schema scaffolding are present, but most operational logic currently runs client-side for demo velocity.&lt;/p&gt;

&lt;p&gt;Trade-off: speed of development vs tamper resistance and production-grade trust boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fraud and trust layer
&lt;/h2&gt;

&lt;p&gt;The most important part was preventing blind auto-payouts. GigShield includes several controls:&lt;/p&gt;

&lt;p&gt;Risk scoring that classifies users into low, medium, high risk&lt;br&gt;
Conditional verification for high-risk payout attempts&lt;br&gt;
Device fingerprinting for repeated-attempt detection&lt;br&gt;
Velocity limits to block rapid abuse loops&lt;br&gt;
Geo-consistency checks against expected service location&lt;br&gt;
Liveness challenge flow with selfie + gesture prompts&lt;br&gt;
Even in a demo environment, this created realistic friction only where needed.&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.amazonaws.com%2Fuploads%2Farticles%2Fdo3fuuef2avj4j80tmc5.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.amazonaws.com%2Fuploads%2Farticles%2Fdo3fuuef2avj4j80tmc5.png" alt=" " width="800" height="675"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Payout lifecycle design
&lt;/h2&gt;

&lt;p&gt;Instead of a single “success/fail” event, payouts move through explicit states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pending verification&lt;/li&gt;
&lt;li&gt;Verified&lt;/li&gt;
&lt;li&gt;Processing&lt;/li&gt;
&lt;li&gt;Settled or failed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That state machine made it easier to debug behavior, generate history views, and provide clear UI feedback. It also allowed reason-code driven outcomes such as coverage inactive, cap reached, policy excluded, or verification required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tested
&lt;/h2&gt;

&lt;p&gt;I added tests around the pieces most likely to break business trust:&lt;/p&gt;

&lt;p&gt;Risk score to risk level mapping&lt;br&gt;
Plan cap and payout capping behavior&lt;br&gt;
Security checks like velocity and geo guardrails&lt;br&gt;
End-to-end payout flow transitions under different scenarios&lt;br&gt;
This gave me confidence that “looks correct” in UI also means “behaves correctly” in logic.&lt;/p&gt;

</description>
      <category>guildwire</category>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
