<?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: Shivansh Goel</title>
    <description>The latest articles on DEV Community by Shivansh Goel (@tech-aficionado).</description>
    <link>https://dev.to/tech-aficionado</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%2F4043770%2Fe093baf7-9019-45b4-afa3-8bffabba92e8.jpg</url>
      <title>DEV Community: Shivansh Goel</title>
      <link>https://dev.to/tech-aficionado</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tech-aficionado"/>
    <language>en</language>
    <item>
      <title>I Got Doxxed Through My Real Email — So I Built a Free, Self-Hosted Email Aliasing Service on Cloudflare</title>
      <dc:creator>Shivansh Goel</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:37:06 +0000</pubDate>
      <link>https://dev.to/tech-aficionado/i-got-doxxed-through-my-real-email-so-i-built-a-free-self-hosted-email-aliasing-service-on-1h4</link>
      <guid>https://dev.to/tech-aficionado/i-got-doxxed-through-my-real-email-so-i-built-a-free-self-hosted-email-aliasing-service-on-1h4</guid>
      <description>&lt;p&gt;I signed up for a job board I'd never heard of. The listing looked legitimate — "Senior Frontend Dev, Remote, $180K." I used my real Gmail address.&lt;/p&gt;

&lt;p&gt;Two weeks later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;14 "recruiters" I never contacted were emailing me&lt;/li&gt;
&lt;li&gt;3 phishing attempts disguised as interview invitations&lt;/li&gt;
&lt;li&gt;1 data breach notification from HaveIBeenPwned&lt;/li&gt;
&lt;li&gt;My inbox was permanently polluted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My email was sold. Shared. Leaked. And there's no "un-leak" button.&lt;/p&gt;

&lt;p&gt;I couldn't fix the past. But I could make sure it &lt;strong&gt;never happened again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built GhostRelay — a self-hosted email aliasing service that generates unlimited masked addresses, all forwarding to your real inbox. When one gets compromised? Kill it. Your real address stays invisible forever.&lt;/p&gt;

&lt;p&gt;The twist? It runs entirely on &lt;strong&gt;Cloudflare's free tier.&lt;/strong&gt; No server. No monthly bill. No catch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Your Email Is Your Identity (And It's Everywhere)
&lt;/h2&gt;

&lt;p&gt;Think about how many services have your real email right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every SaaS free trial you signed up for&lt;/li&gt;
&lt;li&gt;Every newsletter you subscribed to "just to get the PDF"&lt;/li&gt;
&lt;li&gt;Every conference registration&lt;/li&gt;
&lt;li&gt;Every sketchy WiFi captive portal&lt;/li&gt;
&lt;li&gt;Every online order&lt;/li&gt;
&lt;li&gt;Every forum account from 2014&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is a potential leak point. And when &lt;em&gt;any&lt;/em&gt; of them gets breached, it's YOUR inbox that gets hammered.&lt;/p&gt;

&lt;p&gt;The average person's email has been exposed in &lt;strong&gt;5-7 data breaches&lt;/strong&gt; (check yours at haveibeenpwned.com — I'll wait).&lt;/p&gt;

&lt;p&gt;You can't control whether some random startup stores your email in plaintext. But you CAN control &lt;strong&gt;which email they have.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing GhostRelay
&lt;/h2&gt;

&lt;p&gt;GhostRelay gives every service a &lt;strong&gt;unique, disposable email address&lt;/strong&gt; that forwards to your real inbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You sign up for SketchyJobBoard.com with:
  → jobboard-2025@yourdomain.com

That alias forwards to:
  → your.real@gmail.com

When jobboard-2025@ starts getting spam:
  → Delete the alias. Done. Zero leakage.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You never give out your real email again. Every service gets its own alias. When one is compromised, you know &lt;strong&gt;exactly&lt;/strong&gt; which company leaked it (because only they had that alias).&lt;/p&gt;

&lt;p&gt;It's email compartmentalization. The same principle as using separate passwords — but for your address itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works (The Architecture)
&lt;/h2&gt;

&lt;p&gt;The entire system runs on three free Cloudflare services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender → youralias@yourdomain.com
              │
              ▼
   ┌─────────────────────┐
   │  Cloudflare Email    │  ← Intercepts incoming mail (free)
   │  Worker              │
   └──────────┬──────────┘
              │
              ▼
   ┌─────────────────────┐
   │  Cloudflare KV       │  ← Looks up alias → destination mapping (free)
   │  (Key-Value Store)   │
   └──────────┬──────────┘
              │
              ▼
   ┌─────────────────────┐
   │  Forward to real     │  ← Your Gmail/Outlook/ProtonMail receives it
   │  inbox               │
   └─────────────────────┘

Dashboard (yourdomain.com/admin):
   ┌─────────────────────┐
   │  Cloudflare Pages    │  ← Static dashboard to manage aliases (free)
   └─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No servers. No Docker. No VPS. No AWS. Just Cloudflare's free tier doing what it does best.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Email Worker (Core Logic)
&lt;/h2&gt;

&lt;p&gt;When mail arrives at any address on your domain, this Worker intercepts it:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;email&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;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Extract alias from recipient&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recipient&lt;/span&gt; &lt;span class="o"&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;to&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;alias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Look up in KV store&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALIASES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Unknown alias — reject silently (no bounce = no confirmation to spammers)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;setReject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown recipient&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="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if alias is disabled&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;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;setReject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alias disabled&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="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if alias is expired&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;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;setReject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alias expired&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="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Forward to real destination&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Update stats (non-blocking)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALIASES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`stats:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;alias&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALIASES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`stats:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;alias&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&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="na"&gt;lastReceived&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;lastSender&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="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&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 the entire forwarding engine. ~30 lines. Handles unlimited aliases. Runs at edge globally. Costs nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dashboard
&lt;/h2&gt;

&lt;p&gt;The web interface (hosted on Cloudflare Pages, also free) lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create aliases&lt;/strong&gt; — One click, or type a custom name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-generate random aliases&lt;/strong&gt; — &lt;code&gt;x7k9m2@yourdomain.com&lt;/code&gt; for maximum anonymity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label aliases&lt;/strong&gt; — "Used for: LinkedIn", "Used for: Amazon", "Used for: that sketchy startup"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View stats&lt;/strong&gt; — How many emails each alias has received, last sender, last received date&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable aliases&lt;/strong&gt; — Stop forwarding without deleting (in case you need it again)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete aliases&lt;/strong&gt; — Permanent. Gone. No more mail accepted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set expiration&lt;/strong&gt; — "Auto-disable this alias after 7 days" (perfect for free trials)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reply anonymously&lt;/strong&gt; — Send emails FROM your alias without revealing your real address&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Comparison: GhostRelay vs. Every Paid Alternative
&lt;/h2&gt;

&lt;p&gt;Here's what the market looks like in 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;SimpleLogin&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;addy.io&lt;/strong&gt; (AnonAddy)&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Firefox Relay&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Apple Hide My Email&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;GhostRelay&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;€4/mo (or €9.99 w/ Proton bundle)&lt;/td&gt;
&lt;td&gt;$1-$4/mo&lt;/td&gt;
&lt;td&gt;$0.99/mo (Mozilla VPN bundle)&lt;/td&gt;
&lt;td&gt;Requires iCloud+ ($0.99-$12.99/mo)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Annual cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;€48/yr (standalone)&lt;/td&gt;
&lt;td&gt;$12-$48/yr&lt;/td&gt;
&lt;td&gt;$11.88/yr&lt;/td&gt;
&lt;td&gt;$11.88-$155.88/yr&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free tier aliases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20 (shared domain)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0 (paid only)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Paid tier aliases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited (always free)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom domain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes (required, ~$10/yr)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-hostable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (complex — Docker + Postgres + SMTP server)&lt;/td&gt;
&lt;td&gt;Yes (complex)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes (15 min, no server)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reply from alias&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Alias labeling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-alias stats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Expiring aliases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works if company dies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (hosted) / Maybe (self-host)&lt;/td&gt;
&lt;td&gt;No (hosted) / Maybe (self-host)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes (you own everything)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 min (hosted) / 2-4 hrs (self-host)&lt;/td&gt;
&lt;td&gt;5 min (hosted) / 2-4 hrs (self-host)&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15 minutes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infrastructure required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (hosted) / VPS + Docker + DB (self-host)&lt;/td&gt;
&lt;td&gt;None (hosted) / VPS + Docker (self-host)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Apple device&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None (Cloudflare free tier)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vendor lock-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proton ecosystem&lt;/td&gt;
&lt;td&gt;Independent&lt;/td&gt;
&lt;td&gt;Mozilla ecosystem&lt;/td&gt;
&lt;td&gt;Apple ecosystem&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Key Differentiator
&lt;/h3&gt;

&lt;p&gt;Every alternative falls into one of two camps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Camp 1: Hosted services&lt;/strong&gt; — Easy to set up, but you're paying monthly and trusting a company with your email routing. If they shut down, raise prices, or get acquired by someone shady, your aliases die.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Camp 2: Self-hosted (complex)&lt;/strong&gt; — You own everything, but you need a VPS ($5-20/mo), Docker, PostgreSQL, an SMTP server, DNS configuration, TLS certificates, and ongoing maintenance. Most people give up halfway through setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GhostRelay is Camp 3: Self-hosted (trivial).&lt;/strong&gt; You own everything, but the "server" is Cloudflare's free edge network. No VPS. No Docker. No database management. No TLS certificate renewal. No maintenance. Deploy once, forget it exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use Gmail's "+" Trick?
&lt;/h2&gt;

&lt;p&gt;Every tech blog suggests this: &lt;code&gt;yourname+service@gmail.com&lt;/code&gt; still delivers to &lt;code&gt;yourname@gmail.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's why it's useless:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Why It Fails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trivially strippable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every spammer knows to remove everything after &lt;code&gt;+&lt;/code&gt;. Zero protection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reveals your real email&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;shivansh+amazon@gmail.com&lt;/code&gt; still exposes &lt;code&gt;shivansh@gmail.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Many sites reject it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sign-up forms often block &lt;code&gt;+&lt;/code&gt; in email fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Can't disable it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You can't "turn off" a + alias. It forwards forever.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No stats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No way to know how many emails a specific + address received&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;+&lt;/code&gt; trick is security theater. It makes you &lt;em&gt;feel&lt;/em&gt; protected while providing zero actual protection.&lt;/p&gt;

&lt;p&gt;GhostRelay gives you &lt;strong&gt;completely unrelated addresses&lt;/strong&gt;. &lt;code&gt;x7k9m2@yourdomain.com&lt;/code&gt; cannot be reverse-engineered to your real email by anyone — not the service, not a hacker, not a data broker.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Usage Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The "One Alias Per Service" Strategy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;linkedin-2025@mydomain.com      → For LinkedIn
amazon-orders@mydomain.com      → For Amazon
github-notif@mydomain.com       → For GitHub notifications
sketchy-trial@mydomain.com      → For that SaaS free trial (expires in 7 days)
conference-reg@mydomain.com     → For event registrations
newsletter-xyz@mydomain.com     → For that newsletter you might unsubscribe from
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;sketchy-trial@&lt;/code&gt; starts getting spam 3 months later? You know &lt;strong&gt;exactly&lt;/strong&gt; who sold your data. Delete the alias. Problem solved.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Temporal Alias" Strategy
&lt;/h3&gt;

&lt;p&gt;For things you only need briefly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wifi-airport-july@mydomain.com  → Airport captive portal (expires in 24 hours)
trial-figma@mydomain.com        → Figma trial (expires in 14 days)
signup-event@mydomain.com       → One-time event (expires after event date)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set an expiration. Forget about it. The alias auto-disables itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Identity Separation" Strategy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;work-freelance@mydomain.com     → All freelance inquiries
personal-dating@mydomain.com    → Dating apps (privacy critical)
finance-banking@mydomain.com    → Banks and financial services
health-medical@mydomain.com     → Healthcare providers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your dating app gets breached, your banking email is untouched. Complete compartmentalization.&lt;/p&gt;




&lt;h2&gt;
  
  
  Self-Hosting Guide (15 Minutes, I Timed It)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What You Need
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A Cloudflare account (free)&lt;/li&gt;
&lt;li&gt;A custom domain (~$10/year from any registrar)&lt;/li&gt;
&lt;li&gt;15 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step-by-Step
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Add your domain to Cloudflare&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Transfer your domain's nameservers to Cloudflare (or buy one directly from Cloudflare Registrar — cheapest option, no markup).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Enable Email Routing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Cloudflare dashboard → Email → Email Routing → Enable. This tells Cloudflare to handle all email for your domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Create the KV Namespace&lt;/strong&gt;&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;# Install Wrangler (Cloudflare CLI)&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; wrangler

&lt;span class="c"&gt;# Login&lt;/span&gt;
wrangler login

&lt;span class="c"&gt;# Create KV namespace for aliases&lt;/span&gt;
wrangler kv:namespace create &lt;span class="s2"&gt;"ALIASES"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Deploy the Email Worker&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Tech-aficionado/GhostRelay---Open-Source.git
&lt;span class="nb"&gt;cd &lt;/span&gt;GhostRelay---Open-Source

&lt;span class="c"&gt;# Update wrangler.toml with your KV namespace ID&lt;/span&gt;
&lt;span class="c"&gt;# Deploy&lt;/span&gt;
wrangler deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Deploy the Dashboard&lt;/strong&gt;&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;cd &lt;/span&gt;dashboard
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run build

&lt;span class="c"&gt;# Deploy to Cloudflare Pages&lt;/span&gt;
wrangler pages deploy dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Set a catch-all email route&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Cloudflare dashboard → Email → Routing → Catch-all → Send to Worker → Select your GhostRelay worker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Done.&lt;/strong&gt; Every email to &lt;code&gt;anything@yourdomain.com&lt;/code&gt; now hits your Worker. Create aliases through the dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Breakdown: The Math That Makes Paid Services Embarrassing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;What You Get&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;3-Year Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Email Routing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Email interception + forwarding&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Workers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100K requests/day free tier&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare KV&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100K reads/day, 1K writes/day&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Pages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dashboard hosting&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom domain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your brand, your rules&lt;/td&gt;
&lt;td&gt;~$0.83/mo&lt;/td&gt;
&lt;td&gt;~$30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TOTAL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unlimited aliases, full analytics, full control&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.83/mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$30&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compare to 3 years of SimpleLogin Premium: &lt;strong&gt;€144&lt;/strong&gt;&lt;br&gt;
Compare to 3 years of addy.io Pro: &lt;strong&gt;$144&lt;/strong&gt;&lt;br&gt;
Compare to 3 years of iCloud+: &lt;strong&gt;$35.64-$467.64&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You save $100+ over 3 years AND own your infrastructure. The domain pays for itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Model
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What GhostRelay Protects Against
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data broker harvesting&lt;/strong&gt; — They get a disposable alias, not your real address&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential stuffing&lt;/strong&gt; — Even if a password leaks, the email is unique per service, making cross-site attacks harder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Targeted phishing&lt;/strong&gt; — If someone obtains a leaked alias, they can't determine your real inbox&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spam avalanches&lt;/strong&gt; — Kill one alias, all spam from that leak stops instantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corporate data mining&lt;/strong&gt; — Services can't cross-reference your activity across platforms via email&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What GhostRelay Does NOT Protect Against
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Man-in-the-middle on Cloudflare&lt;/strong&gt; — Cloudflare processes your email in transit (same as any email provider). For truly sensitive communication, use PGP/GPG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your destination inbox being compromised&lt;/strong&gt; — If someone hacks your Gmail, aliases won't help. Use 2FA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal requests&lt;/strong&gt; — Cloudflare complies with valid legal orders. This is a privacy tool, not a crime tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Trust Model
&lt;/h3&gt;

&lt;p&gt;You trust:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt; — To not read your email content in transit (they have no business incentive to, and their reputation depends on not doing so)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your destination provider&lt;/strong&gt; — Gmail/Outlook/ProtonMail processes your final mail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yourself&lt;/strong&gt; — You control the Worker code, the KV data, the domain&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You do NOT trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any third-party SaaS email aliasing company&lt;/li&gt;
&lt;li&gt;Any database you don't control&lt;/li&gt;
&lt;li&gt;Any service that could disappear or raise prices&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What if Cloudflare cancels the free tier?&lt;/strong&gt;&lt;br&gt;
A: The Worker code is portable. Deploy to AWS Lambda, Deno Deploy, or any edge runtime in an hour. Your aliases are exportable as JSON. You're not locked in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I use this with ProtonMail as my destination?&lt;/strong&gt;&lt;br&gt;
A: Yes. The destination can be any email address. ProtonMail, Tutanota, Gmail, Outlook — anything that receives email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What about sending/replying FROM an alias?&lt;/strong&gt;&lt;br&gt;
A: Supported. Cloudflare Email Workers can route outbound mail too. Reply to an email and it appears to come from the alias, not your real address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How many aliases can I create?&lt;/strong&gt;&lt;br&gt;
A: Unlimited. KV's free tier handles 1,000 writes/day. Unless you're creating 1,000 aliases per day (you're not), you'll never hit the limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does this work with catch-all?&lt;/strong&gt;&lt;br&gt;
A: Yes. You can either create explicit aliases OR enable catch-all mode where ANY address at your domain forwards to you. Catch-all is simpler but means you can't "disable" a specific alias without adding deny rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Source
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Tech-aficionado/GhostRelay---Open-Source" rel="noopener noreferrer"&gt;github.com/Tech-aficionado/GhostRelay---Open-Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; JavaScript (49.2%) + CSS (27.8%) + HTML (23.0%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; 100% Cloudflare free tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup time:&lt;/strong&gt; 15 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ongoing maintenance:&lt;/strong&gt; Zero&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Your email is the master key to your digital life. Every password reset, every account recovery, every two-factor backup — it all routes through your inbox.&lt;/p&gt;

&lt;p&gt;And you've been giving that master key's address to every random service with a sign-up form.&lt;/p&gt;

&lt;p&gt;GhostRelay doesn't fix the past. Your email is already in a dozen breach databases. But it fixes the future. From today, no service ever gets your real address again.&lt;/p&gt;

&lt;p&gt;Every interaction gets its own disposable alias. Every leak is contained. Every spam wave is killable with one click.&lt;/p&gt;

&lt;p&gt;Your real inbox becomes a fortress that nobody can find — because nobody has the address.&lt;/p&gt;

&lt;p&gt;Deploy GhostRelay. Give out aliases. Sleep better.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Check haveibeenpwned.com right now. How many breaches is your email in? Drop the number in the comments (no judgment — mine was 11 before I built this). Then deploy GhostRelay so that number never grows again.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://github.com/Tech-aficionado" rel="noopener noreferrer"&gt;Shivansh Goel&lt;/a&gt; — AI &amp;amp; Full Stack Developer who believes privacy shouldn't cost a monthly subscription.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>anonymous</category>
    </item>
    <item>
      <title>I Rebuilt the 90s Tamagotchi for the Browser — And Accidentally Learned More About State Machines Than Any Tutorial Taught Me</title>
      <dc:creator>Shivansh Goel</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:29:18 +0000</pubDate>
      <link>https://dev.to/tech-aficionado/i-rebuilt-the-90s-tamagotchi-for-the-browser-and-accidentally-learned-more-about-state-machines-254j</link>
      <guid>https://dev.to/tech-aficionado/i-rebuilt-the-90s-tamagotchi-for-the-browser-and-accidentally-learned-more-about-state-machines-254j</guid>
      <description>&lt;p&gt;In 1996, Bandai sold 82 million Tamagotchis. Kids carried egg-shaped plastic keychains everywhere, frantically pressing three buttons to feed, clean, and play with a pixelated blob that would literally die if you ignored it during math class.&lt;/p&gt;

&lt;p&gt;It was the first time millions of people felt genuine emotional attachment to a piece of software.&lt;/p&gt;

&lt;p&gt;30 years later, I rebuilt that entire experience — in the browser, with TypeScript, zero dependencies, completely open source.&lt;/p&gt;

&lt;p&gt;No app store. No download. No install. Just open a tab and adopt your pet.&lt;/p&gt;

&lt;p&gt;And in the process, I learned more about state machines, game loops, and emotional design than any computer science course ever taught me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Build a Virtual Pet in 2025?
&lt;/h2&gt;

&lt;p&gt;Three reasons:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Nostalgia Is a Distribution Hack
&lt;/h3&gt;

&lt;p&gt;People share things that trigger childhood memories. It's not rational — it's emotional. A browser-based Tamagotchi hits a nerve that no todo app or dashboard ever will.&lt;/p&gt;

&lt;p&gt;When I shared an early prototype, the response wasn't "cool tech stack." It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"OH MY GOD I used to cry when mine died in second grade"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That emotional reaction is worth more than any Product Hunt launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Game State Machines Are Criminally Underrated
&lt;/h3&gt;

&lt;p&gt;Every tutorial teaches state machines with traffic lights or toggle buttons. Boring. Useless. Forgettable.&lt;/p&gt;

&lt;p&gt;A virtual pet has &lt;strong&gt;dozens of interconnected states&lt;/strong&gt;, real-time decay, evolution paths, conditional transitions, and edge cases that force you to actually &lt;em&gt;think&lt;/em&gt; about state architecture. After building this, implementing complex UI flows in production apps felt trivial.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Not Everything Needs to Be a SaaS
&lt;/h3&gt;

&lt;p&gt;The indie dev world is obsessed with "revenue-generating side projects." Sometimes you should build something purely because it makes people smile. The best projects are the ones you'd use even if nobody else existed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Meet Your New Pet
&lt;/h2&gt;

&lt;p&gt;When you open Tamagochi, you get an egg. It hatches. A tiny pixelated creature appears. It has needs. Meet them, and it thrives. Ignore them, and... well, &lt;code&gt;gameOver&lt;/code&gt; is a valid state.&lt;/p&gt;

&lt;p&gt;Your pet has four core needs that decay in real-time:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PetState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;hunger&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="c1"&gt;// 0 = full, 100 = starving&lt;/span&gt;
  &lt;span class="nl"&gt;happiness&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="c1"&gt;// 100 = ecstatic, 0 = depressed&lt;/span&gt;
  &lt;span class="nl"&gt;energy&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="c1"&gt;// 100 = wired, 0 = exhausted&lt;/span&gt;
  &lt;span class="nl"&gt;hygiene&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="c1"&gt;// 100 = squeaky clean, 0 = filthy&lt;/span&gt;
  &lt;span class="nl"&gt;age&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="c1"&gt;// in "pet days"&lt;/span&gt;
  &lt;span class="nl"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;egg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;baby&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;teen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adult&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;senior&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;alive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;happy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;neutral&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;angry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sleepy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sick&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;Every few seconds, stats decay. Your pet gets hungrier, sadder, dirtier, more tired. The clock never stops.&lt;/p&gt;

&lt;p&gt;You interact through simple actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feed&lt;/strong&gt; — Reduces hunger (but too much food makes them sick)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Play&lt;/strong&gt; — Increases happiness (but costs energy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean&lt;/strong&gt; — Restores hygiene&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sleep&lt;/strong&gt; — Restores energy (but hunger still increases while sleeping)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medicine&lt;/strong&gt; — Cures sickness (only available when sick)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple inputs. Complex emergent behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Game Loop: Real-Time Decay
&lt;/h2&gt;

&lt;p&gt;The core of any virtual pet is the &lt;strong&gt;tick&lt;/strong&gt; — a periodic function that decays stats and evaluates state transitions:&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;TICK_INTERVAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Every 3 seconds&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DECAY_RATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Gets hungrier&lt;/span&gt;
  &lt;span class="na"&gt;happiness&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.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Gets sadder&lt;/span&gt;
  &lt;span class="na"&gt;energy&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.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// Gets more tired&lt;/span&gt;
  &lt;span class="na"&gt;hygiene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Gets dirtier&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gameTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PetState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PetState&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pet&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;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hunger&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;DECAY_RATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hunger&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;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;happiness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;happiness&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;DECAY_RATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;happiness&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;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;DECAY_RATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&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;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;hygiene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hygiene&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;DECAY_RATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hygiene&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;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Evaluate mood based on current stats&lt;/span&gt;
  &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateMood&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Check for stage evolution&lt;/span&gt;
  &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluateEvolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Check death conditions&lt;/span&gt;
  &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkVitals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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;min&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;max&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="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&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;min&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="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&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;This runs every 3 seconds. In a 10-minute session, your pet goes through ~200 state transitions. Leave the tab open for an hour? That's 1,200 ticks. Your pet's entire life flashes before its eyes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The State Machine: Evolution &amp;amp; Life Stages
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. Your pet doesn't just exist — it &lt;strong&gt;evolves&lt;/strong&gt; through life stages, and &lt;em&gt;how well you care for it&lt;/em&gt; determines which path it takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    [Egg]
                      │
                (hatch: 5 ticks)
                      │
                    [Baby]
                      │
                 (age &amp;gt; 20)
                      │
              ┌───────┴───────┐
              │               │
         [Happy Child]   [Sad Child]
         (avg mood &amp;gt; 6)  (avg mood ≤ 6)
              │               │
         (age &amp;gt; 50)      (age &amp;gt; 50)
              │               │
         [Cool Teen]    [Rebel Teen]
              │               │
         (age &amp;gt; 100)    (age &amp;gt; 100)
              │               │
        [Majestic Adult] [Grumpy Adult]
              │               │
         (age &amp;gt; 200)    (age &amp;gt; 200)
              │               │
        [Wise Senior]   [Cranky Senior]

  ─── Any Stage ───
        │
  (hunger ≥ 100) OR (happiness ≤ 0 for 30+ ticks)
        │
      [Dead] 💀
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The branching creates &lt;strong&gt;replayability&lt;/strong&gt;. You can't see all the evolution paths in one playthrough. Neglect creates different (sadder) variants. Perfect care unlocks rare stages.&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;evaluateEvolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PetState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PetStage&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;avgMood&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAverageMoodScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;moodHistory&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;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;baby&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;avgMood&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;happy_child&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sad_child&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;happy_child&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&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;cool_teen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sad_child&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&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;rebel_teen&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="c1"&gt;// ... and so on for each branch&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&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;h2&gt;
  
  
  The Tab-Close Problem (Hardest Bug I Shipped)
&lt;/h2&gt;

&lt;p&gt;Here's a question that broke my brain for two days:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when someone closes the tab for 8 hours?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Option A: Pet freezes while tab is closed. &lt;strong&gt;Problem:&lt;/strong&gt; This removes all tension. Just close the tab whenever things get hard.&lt;/p&gt;

&lt;p&gt;Option B: Pet dies instantly if you leave for too long. &lt;strong&gt;Problem:&lt;/strong&gt; Cruel. Nobody will come back if death is guaranteed.&lt;/p&gt;

&lt;p&gt;Option C: &lt;strong&gt;Catch-up calculation.&lt;/strong&gt; When the tab regains focus, calculate what &lt;em&gt;would have&lt;/em&gt; happened during the absence, with a mercy cap.&lt;/p&gt;

&lt;p&gt;I went with Option C:&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;calculateCatchUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PetState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastTickTime&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;PetState&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastTickTime&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;missedTicks&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;TICK_INTERVAL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Mercy cap: max 100 ticks of catch-up (5 minutes of real-time damage)&lt;/span&gt;
  &lt;span class="c1"&gt;// Even if you were gone for 8 hours, your pet only suffers 5 minutes&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cappedTicks&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missedTicks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&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;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;for &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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cappedTicks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gameTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Stop if pet died during catch-up&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// If pet would have died, give a "revival" chance on first return&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alive&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;missedTicks&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hunger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Critical but alive&lt;/span&gt;
    &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;happiness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Show: "Your pet almost didn't make it! It missed you."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// On tab focus&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;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibilityState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visible&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastTick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lastTickTime&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;lastTick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateCatchUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastTick&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nf"&gt;renderPet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&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;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sick&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="nf"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your pet almost didn't make it! Quick, take care of it!&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="p"&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;The mercy cap is crucial. Without it, any absence longer than a few hours = guaranteed death = nobody plays more than once. With it, you feel urgency without hopelessness.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CSS-Only Rendering (Zero Canvas, Zero WebGL)
&lt;/h2&gt;

&lt;p&gt;Here's the constraint I set for myself: &lt;strong&gt;no &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, no WebGL, no sprite images.&lt;/strong&gt; The entire pet is rendered with pure CSS.&lt;/p&gt;

&lt;p&gt;Why? Because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It keeps the bundle tiny (~15KB total)&lt;/li&gt;
&lt;li&gt;It works everywhere — even browsers that block canvas fingerprinting&lt;/li&gt;
&lt;li&gt;CSS animations are GPU-accelerated for free&lt;/li&gt;
&lt;li&gt;It's a fun challenge&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Pixel Art with Box Shadows
&lt;/h3&gt;

&lt;p&gt;The pet is drawn using a single &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with a massive &lt;code&gt;box-shadow&lt;/code&gt; declaration — each shadow is one "pixel":&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="nc"&gt;.pet-sprite&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&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="nb"&gt;transparent&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="c"&gt;/* Row 1 - head */&lt;/span&gt;
    &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#3d3d3d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#3d3d3d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#3d3d3d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c"&gt;/* Row 2 */&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="m"&gt;#3d3d3d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#3d3d3d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;20px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#3d3d3d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c"&gt;/* Row 3 - eyes */&lt;/span&gt;
    &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#3d3d3d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c"&gt;/* left eye */&lt;/span&gt;
    &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;/* right eye */&lt;/span&gt;
    &lt;span class="m"&gt;20px&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;#3d3d3d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c"&gt;/* ... more rows for body, legs, etc. */&lt;/span&gt;
  &lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;image-rendering&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pixelated&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;Different emotional states swap to different box-shadow sets:&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;getSpriteForMood&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;happy&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="nx"&gt;sprites&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;happy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Eyes as ^_^ arcs&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sad&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="nx"&gt;sprites&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sad&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// Eyes as ;_; with tear pixel&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;angry&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="nx"&gt;sprites&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Furrowed brow pixels&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sleepy&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="nx"&gt;sprites&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sleepy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Closed eye line, Z pixels&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sick&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="nx"&gt;sprites&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sick&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// Green-tinted, swirl on head&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sprites&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;neutral&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Default dot eyes&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;h3&gt;
  
  
  Animations with @keyframes
&lt;/h3&gt;

&lt;p&gt;The idle bounce — that tiny up-and-down hop that makes the pet feel &lt;em&gt;alive&lt;/em&gt; — is pure CSS:&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="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;idle-bounce&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%,&lt;/span&gt; &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;50&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-3px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;happy-jump&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%,&lt;/span&gt; &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;25&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-8px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-5deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;75&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-8px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;sad-droop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%,&lt;/span&gt; &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;50&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;sleep-breathe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%,&lt;/span&gt; &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;50&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1.05&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.pet&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-mood&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"happy"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.pet-sprite&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;happy-jump&lt;/span&gt; &lt;span class="m"&gt;0.6s&lt;/span&gt; &lt;span class="n"&gt;ease-in-out&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.pet&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-mood&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"sad"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.pet-sprite&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sad-droop&lt;/span&gt; &lt;span class="m"&gt;2s&lt;/span&gt; &lt;span class="n"&gt;ease-in-out&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.pet&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-mood&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"sleepy"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.pet-sprite&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sleep-breathe&lt;/span&gt; &lt;span class="m"&gt;3s&lt;/span&gt; &lt;span class="n"&gt;ease-in-out&lt;/span&gt; &lt;span class="n"&gt;infinite&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;The key insight: &lt;strong&gt;animation speed communicates emotion.&lt;/strong&gt; Happy = fast bounces. Sad = slow droops. Sleepy = glacial breathing. Players read these cues subconsciously.&lt;/p&gt;




&lt;h2&gt;
  
  
  Emotional Design: Making People Care About a Div
&lt;/h2&gt;

&lt;p&gt;The hardest part of this project wasn't the code. It was making players &lt;strong&gt;actually care&lt;/strong&gt; about a collection of CSS pixels.&lt;/p&gt;

&lt;p&gt;Here's what I learned about emotional design:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Pet Should Look at You
&lt;/h3&gt;

&lt;p&gt;When idle, the pet's "eyes" (two pixel dots) slowly drift toward the mouse cursor. It's a 5-line JavaScript addition that makes people say &lt;em&gt;"aww, it's looking at me!"&lt;/em&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mousemove&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;petRect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;petElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&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;petCenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;petRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;petRect&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;petRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;petRect&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="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="c1"&gt;// Shift eye pixels 1px toward cursor (clamped)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;petCenter&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.01&lt;/span&gt;&lt;span class="p"&gt;,&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="mi"&gt;1&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;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;petCenter&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.01&lt;/span&gt;&lt;span class="p"&gt;,&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;eyeLeft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translate(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;eyeRight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translate(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px)`&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;h3&gt;
  
  
  2. Reactions Should Be Immediate and Exaggerated
&lt;/h3&gt;

&lt;p&gt;When you feed the pet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It doesn't just update a number&lt;/li&gt;
&lt;li&gt;It does a little jump&lt;/li&gt;
&lt;li&gt;A tiny heart particle appears&lt;/li&gt;
&lt;li&gt;The eyes squish into happy arcs for 1 second&lt;/li&gt;
&lt;li&gt;Then it returns to normal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This takes 200ms but makes the interaction feel &lt;strong&gt;rewarding&lt;/strong&gt;. Without it, feeding feels like clicking a button. With it, feeding feels like caring for something alive.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Death Should Hurt (A Little)
&lt;/h3&gt;

&lt;p&gt;When the pet dies, the screen doesn't just show "Game Over." It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fades the pet sprite to grayscale slowly (2 seconds)&lt;/li&gt;
&lt;li&gt;Shows small "..." above the pet before it disappears&lt;/li&gt;
&lt;li&gt;Plays a single, quiet tone (if sound is enabled)&lt;/li&gt;
&lt;li&gt;Shows the pet's age, how long you kept it alive, and its best mood&lt;/li&gt;
&lt;li&gt;Offers "Adopt again" — not "Restart" (language matters)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to traumatize anyone. It's to make them &lt;strong&gt;want to do better next time.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Name Your Pet
&lt;/h3&gt;

&lt;p&gt;The first thing you do is name your pet. This is deliberate. Naming creates attachment. "My pet died" hits different than "Sprinkles died."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned (That Transfers to Real Engineering)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. State Machines Are the Answer to 80% of UI Bugs
&lt;/h3&gt;

&lt;p&gt;Every time you've seen a UI in an "impossible state" — a loading spinner AND an error message, a form that's both submitted and editable — that's a state machine problem.&lt;/p&gt;

&lt;p&gt;After building Tamagochi with explicit states and valid transitions, I started seeing production UI bugs differently. The fix is almost always: "define your states explicitly and make invalid transitions impossible."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: boolean soup&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Bug: what if isLoading AND isError are both true?&lt;/span&gt;

&lt;span class="c1"&gt;// Good: state machine&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Impossible to be loading AND error simultaneously&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Time-Based Systems Are Deceptively Hard
&lt;/h3&gt;

&lt;p&gt;"Decay a number every 3 seconds" sounds trivial. But then you deal with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tab becoming inactive (browsers throttle &lt;code&gt;setInterval&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Device going to sleep&lt;/li&gt;
&lt;li&gt;Timezone changes&lt;/li&gt;
&lt;li&gt;Daylight saving time jumps&lt;/li&gt;
&lt;li&gt;Users manipulating system clock&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real-time games that persist across sessions need to think about time as &lt;strong&gt;unreliable input&lt;/strong&gt;, not a constant.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The 10% of Polish Takes 90% of the Time
&lt;/h3&gt;

&lt;p&gt;The core game loop took one weekend. Making it &lt;em&gt;feel good&lt;/em&gt; took three more weeks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tuning decay rates so the game is engaging but not stressful&lt;/li&gt;
&lt;li&gt;Getting animations to feel organic, not mechanical&lt;/li&gt;
&lt;li&gt;Making the death sequence emotional but not manipulative&lt;/li&gt;
&lt;li&gt;Balancing difficulty so casual players survive but invested players are challenged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ratio (10% function, 90% feel) is the same in every product. The feature works in a day. Making users &lt;em&gt;love&lt;/em&gt; it takes a month.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Feature List
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real-time stat decay&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 life stages with evolution branching&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 emotional states with unique sprites&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS-only pixel art rendering&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle animations per mood&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eye-tracking toward cursor&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab-close catch-up calculation&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feeding, playing, cleaning, sleeping&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overfeeding sickness mechanic&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Death sequence with stats&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LocalStorage persistence&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dark mode&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile responsive (touch actions)&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sound effects&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple pet species&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Achievement system&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiplayer pet park&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PWA with push notifications&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Try It Now
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Play Online
&lt;/h3&gt;

&lt;p&gt;Deploy your own with one click, or clone and run locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Tech-aficionado/Tamagochi---Open-Source-Game.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Tamagochi---Open-Source-Game
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;localhost:3000&lt;/code&gt;. Name your pet. Try to keep it alive longer than I did my first time (I didn't last 20 minutes — I was testing the death sequence, I swear).&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy Your Own
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://vercel.com/new/clone?repository-url=https://github.com/Tech-aficionado/Tamagochi---Open-Source-Game" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvercel.com%2Fbutton" alt="Deploy with Vercel" width="103" height="32"&gt;&lt;/a&gt;&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple species&lt;/strong&gt; — Choose between a cat, dog, dragon, robot, or alien (each with different stat decay rates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Achievement system&lt;/strong&gt; — "Kept a pet alive for 30 days", "Evolved to Majestic Adult", "Fed 1000 times"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiplayer pet park&lt;/strong&gt; — Visit friends' pets in a shared space&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom skins&lt;/strong&gt; — Design your own pixel pet with an in-app editor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PWA + push notifications&lt;/strong&gt; — Install on your phone, get "Your pet is hungry!" alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breeding&lt;/strong&gt; — Two maxed-out adults can create a baby with inherited traits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seasonal events&lt;/strong&gt; — Halloween costumes, winter snow, birthday celebrations&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Source
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Tech-aficionado/Tamagochi---Open-Source-Game" rel="noopener noreferrer"&gt;github.com/Tech-aficionado/Tamagochi---Open-Source-Game&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT — Adopt it, fork it, breed it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; TypeScript (70.7%) + CSS (27.2%) + HTML (1.9%) + JavaScript (0.2%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies:&lt;/strong&gt; Zero. Pure TypeScript + CSS. No frameworks. No libraries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle size:&lt;/strong&gt; ~15KB total&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;We spend all day building tools for productivity. Task managers. Dashboards. Analytics. Optimization engines. Everything is about &lt;em&gt;output&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes the most impressive engineering goes into things that have no purpose other than making someone smile for 5 minutes.&lt;/p&gt;

&lt;p&gt;A virtual pet won't get you a promotion. It won't generate MRR. It won't impress a VC.&lt;/p&gt;

&lt;p&gt;But if you can make someone feel a genuine emotional connection to 200 bytes of CSS box-shadows — that's a kind of engineering that matters.&lt;/p&gt;

&lt;p&gt;Build something useless. Build it beautifully. You'll learn more than any tutorial will teach you.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Quick poll: What was YOUR first virtual pet? Tamagotchi? Neopets? Nintendogs? A rock you put googly eyes on? Drop it in the comments — I want to see what generation everyone belongs to.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://github.com/Tech-aficionado" rel="noopener noreferrer"&gt;Shivansh Goel&lt;/a&gt; — AI &amp;amp; Full Stack Developer who believes the best code makes people feel something.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>ai</category>
      <category>opensource</category>
      <category>gamedev</category>
      <category>web3</category>
    </item>
    <item>
      <title>I Built a Self-Hostable URL Shortener Because Bitly Now Charges $10/Month for Basic Links</title>
      <dc:creator>Shivansh Goel</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:25:40 +0000</pubDate>
      <link>https://dev.to/tech-aficionado/i-built-a-self-hostable-url-shortener-because-bitly-now-charges-10month-for-basic-links-dj4</link>
      <guid>https://dev.to/tech-aficionado/i-built-a-self-hostable-url-shortener-because-bitly-now-charges-10month-for-basic-links-dj4</guid>
      <description>&lt;p&gt;In 2023, Bitly's free plan gave you 100 links per month with full analytics.&lt;/p&gt;

&lt;p&gt;In 2025, they slashed it to &lt;strong&gt;5 links per month&lt;/strong&gt;. Five. And they started showing full-screen interstitial ads before redirecting — even on links you created years ago.&lt;/p&gt;

&lt;p&gt;In 2026, their cheapest paid plan is &lt;strong&gt;$10/month&lt;/strong&gt; — just to shorten links without ads.&lt;/p&gt;

&lt;p&gt;I was paying $120/year to turn long URLs into short ones. A problem that takes 3 lines of code to solve.&lt;/p&gt;

&lt;p&gt;So I solved it myself. For free. Forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing ZipLink
&lt;/h2&gt;

&lt;p&gt;ZipLink is a fast, self-hostable URL shortener built with Next.js and Firebase. Deploy it once, use it forever. No monthly fees. No link limits. No ads. No "upgrade to unlock analytics."&lt;/p&gt;

&lt;p&gt;Your links. Your data. Your domain. Your rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yourdomain.com/abc123 → https://some-really-long-url.com/path/to/thing?utm=whatever
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the product. Except you own it completely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The State of URL Shorteners in 2026 (It's Embarrassing)
&lt;/h2&gt;

&lt;p&gt;Let me show you what the "industry leaders" are charging for what is essentially a database lookup:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Comparison Table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Bitly&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Rebrandly&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Short.io&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Dub.co&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;TinyURL Pro&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;ZipLink&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$10-$300/mo&lt;/td&gt;
&lt;td&gt;$13-$349/mo&lt;/td&gt;
&lt;td&gt;$19-$149/mo&lt;/td&gt;
&lt;td&gt;$24-$299/mo&lt;/td&gt;
&lt;td&gt;$9.99/mo&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Annual cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$96-$2,388/yr&lt;/td&gt;
&lt;td&gt;$156-$4,188/yr&lt;/td&gt;
&lt;td&gt;$228-$1,788/yr&lt;/td&gt;
&lt;td&gt;$288-$3,588/yr&lt;/td&gt;
&lt;td&gt;$119.88/yr&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free tier links&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5/month&lt;/td&gt;
&lt;td&gt;10/month&lt;/td&gt;
&lt;td&gt;1,000 total&lt;/td&gt;
&lt;td&gt;25/month&lt;/td&gt;
&lt;td&gt;Unlimited (no analytics)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom domain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Free (1 domain)&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes (yours)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Click analytics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Free (basic)&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Full, free&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Link expiration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Password protection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enterprise only&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Paid only&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Full, free&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;QR codes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2 free, then paid&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-hostable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (complex)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes (simple)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data ownership&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Their servers&lt;/td&gt;
&lt;td&gt;Their servers&lt;/td&gt;
&lt;td&gt;Their servers&lt;/td&gt;
&lt;td&gt;Their servers&lt;/td&gt;
&lt;td&gt;Their servers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Your Firebase&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ads/interstitials&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (free tier)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (free tier)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Never&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Links disappear if you stop paying&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No — you own them&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the killer. With every paid service, &lt;strong&gt;if you cancel your subscription, your links die.&lt;/strong&gt; Years of shared URLs, printed QR codes, embedded links in emails — all broken. They're holding your links hostage.&lt;/p&gt;

&lt;p&gt;ZipLink? Your links live as long as your Firebase project exists (which is free-tier forever for this use case).&lt;/p&gt;




&lt;h2&gt;
  
  
  What ZipLink Actually Does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instant shortening&lt;/strong&gt; — Paste a URL, get a short link in &amp;lt;100ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom slugs&lt;/strong&gt; — Choose your own back-half: &lt;code&gt;yourdomain.com/my-cool-link&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-generated slugs&lt;/strong&gt; — Random 6-character codes when you don't care&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click analytics&lt;/strong&gt; — Total clicks, clicks over time, referrers, countries, devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link expiration&lt;/strong&gt; — Set links to auto-disable after a date or click count&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password protection&lt;/strong&gt; — Require a password to access the destination&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QR code generation&lt;/strong&gt; — Every link gets a downloadable QR code automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk shortening&lt;/strong&gt; — Paste 100 URLs, get 100 short links&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API&lt;/strong&gt; — Full REST API for programmatic link creation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; — Clean UI to manage all your links&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What It Doesn't Do (And Why)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No team collaboration&lt;/strong&gt; — It's a personal tool. Fork it and add auth if you need teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No A/B testing&lt;/strong&gt; — Keep it simple. Use proper A/B tools for that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No "link-in-bio" pages&lt;/strong&gt; — That's a different product. Focus.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Technical Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Next.js 14 (App Router)&lt;/td&gt;
&lt;td&gt;SSR for fast link resolution, React for dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Firebase Firestore&lt;/td&gt;
&lt;td&gt;Free tier handles ~50K reads/day, zero ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Firebase Auth&lt;/td&gt;
&lt;td&gt;Google sign-in for dashboard access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;Free tier, global edge network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any registrar&lt;/td&gt;
&lt;td&gt;Point your domain, done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Analytics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Custom (Firestore)&lt;/td&gt;
&lt;td&gt;No third-party tracking scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How Link Resolution Works
&lt;/h3&gt;

&lt;p&gt;When someone hits &lt;code&gt;yourdomain.com/abc123&lt;/code&gt;, the redirect happens at the edge in ~50ms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/[slug]/route.ts — Next.js route handler&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/firebase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getDoc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateDoc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;increment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/firestore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Look up the short link&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;linkRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;links&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&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;linkSnap&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;getDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;linkRef&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;linkSnap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/404&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;linkSnap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if link is expired&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;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/expired&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="c1"&gt;// Check if click limit reached&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;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxClicks&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clicks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxClicks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/expired&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="c1"&gt;// Record the click (non-blocking)&lt;/span&gt;
  &lt;span class="nf"&gt;updateDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;linkRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;clicks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;increment&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="na"&gt;lastClickedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Record analytics&lt;/span&gt;
  &lt;span class="nf"&gt;recordClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;referrer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;referer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseUserAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Redirect&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;301&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;h3&gt;
  
  
  The Analytics Pipeline
&lt;/h3&gt;

&lt;p&gt;Every click is recorded with:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ClickEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referrer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tablet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;os&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are stored in a Firestore subcollection and aggregated on-read for the dashboard. For the free tier's 50K daily reads, this handles ~5,000 link clicks per day with full analytics — more than enough for personal use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Breakdown: Why It's Actually Free
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Free Tier Limit&lt;/th&gt;
&lt;th&gt;ZipLink Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Firebase Firestore&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50K reads, 20K writes/day&lt;/td&gt;
&lt;td&gt;~5K reads, ~1K writes for moderate use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Firebase Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unlimited users&lt;/td&gt;
&lt;td&gt;1 (you)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel Hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100GB bandwidth/month&lt;/td&gt;
&lt;td&gt;&amp;lt;1GB for a URL shortener&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;~$10/year (only real cost)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Total: $0.83/month&lt;/strong&gt; (just the domain, amortized). Compare that to $10-$300/month for Bitly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Self-Hosting Guide (15 Minutes)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub account&lt;/li&gt;
&lt;li&gt;A Firebase account (free)&lt;/li&gt;
&lt;li&gt;A Vercel account (free)&lt;/li&gt;
&lt;li&gt;A custom domain ($10/year)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Steps
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Clone the repo&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Tech-aficionado/ZipLink---Open-Source.git
&lt;span class="nb"&gt;cd &lt;/span&gt;ZipLink---Open-Source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create a Firebase project&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to console.firebase.google.com&lt;/li&gt;
&lt;li&gt;Create new project → Enable Firestore → Enable Authentication (Google)&lt;/li&gt;
&lt;li&gt;Copy your config keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Set environment variables&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456
NEXT_PUBLIC_FIREBASE_APP_ID=your-app-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Deploy to Vercel&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vercel deploy &lt;span class="nt"&gt;--prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Connect your domain&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In Vercel dashboard → Settings → Domains → Add your domain&lt;/li&gt;
&lt;li&gt;Update DNS records at your registrar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Done.&lt;/strong&gt; You now own a URL shortener that will outlast every SaaS company charging you monthly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "But What If Firebase Shuts Down?" Argument
&lt;/h2&gt;

&lt;p&gt;Fair concern. Here's why it doesn't matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Firebase's free tier has existed for 10+ years&lt;/strong&gt; — Google isn't killing it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data is exportable&lt;/strong&gt; — One command to dump all links as JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The code is framework-agnostic&lt;/strong&gt; — Swap Firestore for Supabase, PlanetScale, or even a JSON file in 30 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have the source code&lt;/strong&gt; — Worst case, migrate to any database. Your slugs and destinations are just key-value pairs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compare this to Bitly: if Bitly shuts down (or you stop paying), you have &lt;strong&gt;zero recourse&lt;/strong&gt;. Your links are gone. Your QR codes on printed materials now point to nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Shorten GitHub links for tweets: &lt;code&gt;yoursite.com/my-repo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create memorable links for documentation: &lt;code&gt;yoursite.com/docs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Track which README links get the most clicks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Content Creators
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Clean links for video descriptions: &lt;code&gt;yoursite.com/latest-video&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Track click-through rates from different platforms&lt;/li&gt;
&lt;li&gt;Password-protect exclusive content links&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Indie Hackers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Product launch links with expiration: &lt;code&gt;yoursite.com/ph-launch&lt;/code&gt; (expires after launch week)&lt;/li&gt;
&lt;li&gt;Track which marketing channels drive traffic&lt;/li&gt;
&lt;li&gt;Branded links that build domain authority&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Job Seekers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio link: &lt;code&gt;yoursite.com/portfolio&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Resume link with click tracking: &lt;code&gt;yoursite.com/resume&lt;/code&gt; (know when recruiters view it)&lt;/li&gt;
&lt;li&gt;Project-specific links: &lt;code&gt;yoursite.com/my-saas&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Not Just Use &lt;code&gt;yoursite.com/redirect&lt;/code&gt;? (The "DIY" Argument)
&lt;/h2&gt;

&lt;p&gt;You could set up Nginx redirects or a simple Express server. But you lose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt; — Who's clicking? From where? When?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic management&lt;/strong&gt; — Adding a new link means editing server config and redeploying&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiration&lt;/strong&gt; — No way to auto-expire links without custom cron jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; — No visual overview of all your links&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QR codes&lt;/strong&gt; — Manual generation every time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password protection&lt;/strong&gt; — Custom middleware for every protected link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ZipLink gives you all of this in a deployable package. It's the difference between "I could build a house" and "here's a house."&lt;/p&gt;




&lt;h2&gt;
  
  
  What's on the Roadmap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge caching&lt;/strong&gt; — Cache popular links at Vercel's edge for &amp;lt;10ms redirects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link grouping&lt;/strong&gt; — Organize links into campaigns/folders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook on click&lt;/strong&gt; — Trigger automations when specific links are clicked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import from Bitly&lt;/strong&gt; — Migrate your existing Bitly links with one CSV upload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI tool&lt;/strong&gt; — &lt;code&gt;ziplink shorten https://long-url.com&lt;/code&gt; from your terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser extension&lt;/strong&gt; — Right-click any page → "Shorten with ZipLink"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack bot&lt;/strong&gt; — Share a link in Slack, auto-shorten with ZipLink&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Source
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Tech-aficionado/ZipLink---Open-Source" rel="noopener noreferrer"&gt;github.com/Tech-aficionado/ZipLink---Open-Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; TypeScript (95.7%) + CSS (4.1%) + JavaScript (0.2%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy time:&lt;/strong&gt; 15 minutes from zero to production&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;URL shortening is a solved problem. It's a database lookup. A key-value store. It's literally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INPUT: slug
OUTPUT: redirect(destination)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And yet companies are charging $10-$300/month for this. With link limits. With ads. With analytics paywalls. With the implicit threat of "stop paying and your links die."&lt;/p&gt;

&lt;p&gt;That's not a service. That's a hostage situation.&lt;/p&gt;

&lt;p&gt;ZipLink is the exit strategy. Deploy once. Own forever. Pay nothing.&lt;/p&gt;

&lt;p&gt;Your links should outlive your subscriptions.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;How much are YOU paying per year just to shorten URLs? Do the math and drop it in the comments. I bet it's more embarrassing than you think.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://github.com/Tech-aficionado" rel="noopener noreferrer"&gt;Shivansh Goel&lt;/a&gt; — AI &amp;amp; Full Stack Developer who refuses to pay rent on solved problems.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a Git-Based Decision Tracker — Branch Your Life Choices, Commit Predictions, and Let Time Score Your Judgment</title>
      <dc:creator>Shivansh Goel</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:17:20 +0000</pubDate>
      <link>https://dev.to/tech-aficionado/i-built-a-git-based-decision-tracker-branch-your-life-choices-commit-predictions-and-let-time-295i</link>
      <guid>https://dev.to/tech-aficionado/i-built-a-git-based-decision-tracker-branch-your-life-choices-commit-predictions-and-let-time-295i</guid>
      <description>&lt;p&gt;Every developer knows the power of &lt;code&gt;git branch&lt;/code&gt;. You experiment without fear, revert mistakes, and merge only what works.&lt;/p&gt;

&lt;p&gt;But what if you could do the same thing with &lt;strong&gt;life decisions&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;That's exactly what I built. And it's called &lt;strong&gt;Forkcast&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;We all face major crossroads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should I take this job offer or stay?&lt;/li&gt;
&lt;li&gt;Should I learn Rust or Go next?&lt;/li&gt;
&lt;li&gt;Should I co-found this startup or keep freelancing?&lt;/li&gt;
&lt;li&gt;Should I move cities for that opportunity?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We make these calls based on gut feeling, maybe a pros/cons list, and then... we &lt;strong&gt;never look back&lt;/strong&gt; to evaluate how good our judgment actually was.&lt;/p&gt;

&lt;p&gt;Think about it. Do you &lt;em&gt;actually&lt;/em&gt; know if you're a good decision-maker? Or do you just assume you are because you can't remember the bad calls?&lt;/p&gt;

&lt;p&gt;There's no version control for decisions. No diff. No blame. No history. No accountability.&lt;/p&gt;

&lt;p&gt;Until now.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing Forkcast
&lt;/h2&gt;

&lt;p&gt;Forkcast is a decision-tracking app that uses Git metaphors to help you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Branch&lt;/strong&gt; — Create a fork when you face a major choice (two or more paths)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit&lt;/strong&gt; — Log your prediction with a confidence level (&lt;em&gt;"I'm 80% sure taking this job is the right call"&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge&lt;/strong&gt; — Pick the path you actually take&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; — Come back months later and honestly rate how it went. Let time be the judge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Over time, you build a &lt;strong&gt;track record of your own judgment&lt;/strong&gt;. You discover patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I'm consistently overconfident about career moves"&lt;/li&gt;
&lt;li&gt;"My technical bets are actually solid — 75% accuracy"&lt;/li&gt;
&lt;li&gt;"I should stop trusting my gut on financial decisions"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a personal prediction market — with the UX of a Git client.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Git Metaphors?
&lt;/h2&gt;

&lt;p&gt;Because developers already think in branches. The mental model is instant:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Git Concept&lt;/th&gt;
&lt;th&gt;Forkcast Equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git branch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a decision fork&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git commit -m "prediction"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Log what you think will happen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git merge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pick the path you're taking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Review your decision history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git blame&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;See which "you" made that call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git diff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compare what you predicted vs. what happened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git revert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Acknowledge a bad decision and course-correct&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The metaphor isn't forced — it genuinely maps 1:1 to how decisions work. You explore options (branches), commit to one (merge), and live with the consequences (production).&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Actually Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Creating a Decision Fork
&lt;/h3&gt;

&lt;p&gt;When you hit a crossroads, you create a new fork:&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;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accept Google offer vs. stay at current startup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-09-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// When I need to decide by&lt;/span&gt;
  &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="p"&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;branch-google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept Google Offer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;L4 SWE role, Mountain View, $320k TC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;predictedOutcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stable career growth, learn at scale, but less ownership&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;branch-stay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stay at Startup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Early engineer, equity potential, wearing many hats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;predictedOutcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Higher risk, higher potential reward, more learning&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="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// open → committed → scored&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Committing Your Prediction
&lt;/h3&gt;

&lt;p&gt;Before you decide, you log what you &lt;em&gt;think&lt;/em&gt; will happen for each path:&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;prediction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;decisionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chosenBranch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;branch-stay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// "I'm 72% sure staying is the right call"&lt;/span&gt;
  &lt;span class="na"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    The startup has product-market fit, we're growing 20% MoM.
    If we hit Series A in 6 months, my equity is worth more than 
    2 years of Google salary. Risk is manageable.
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;predictedScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// On a 1-10 scale, I expect this to be an 8/10 decision&lt;/span&gt;
  &lt;span class="na"&gt;committedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;This is the magic moment. You're writing down &lt;strong&gt;exactly what you believe&lt;/strong&gt; so Future You can't rewrite history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merging — The Decision Is Made
&lt;/h3&gt;

&lt;p&gt;You pick your path. The fork is merged. No going back.&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;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;committed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mergedBranch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;branch-stay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mergedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scoring — Time Is the Judge
&lt;/h3&gt;

&lt;p&gt;3 months, 6 months, a year later — you come back and score:&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;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;decisionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;actualScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Honest rating: 6/10&lt;/span&gt;
  &lt;span class="na"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    Startup didn't hit Series A. Pivoted twice. Learning was incredible 
    but financially it was neutral. Google would have been more stable.
    My confidence of 72% was too high — should have been 50/50.
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scoredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;calibrationDelta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// I was 12% overconfident&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have &lt;strong&gt;data on your own judgment&lt;/strong&gt;. Not a feeling. Data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dashboard — Your Decision History
&lt;/h2&gt;

&lt;p&gt;Over time, Forkcast builds a visual picture of your decision-making:&lt;/p&gt;

&lt;h3&gt;
  
  
  Calibration Chart
&lt;/h3&gt;

&lt;p&gt;A graph showing your predicted confidence vs. actual outcomes. Perfect calibration = the diagonal line. If you say "80% confident" and you're right 80% of the time, you're well-calibrated.&lt;/p&gt;

&lt;p&gt;Most people discover they're &lt;strong&gt;overconfident by 15-25%&lt;/strong&gt;. Knowing this is incredibly valuable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision Timeline
&lt;/h3&gt;

&lt;p&gt;A git-log-style timeline showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open forks (decisions you haven't made yet)&lt;/li&gt;
&lt;li&gt;Committed decisions (waiting to be scored)&lt;/li&gt;
&lt;li&gt;Scored decisions (color-coded: green = good call, yellow = okay, red = regret)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pattern Detection
&lt;/h3&gt;

&lt;p&gt;After 10+ scored decisions, Forkcast identifies patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"You score highest on technical decisions (avg: 7.8/10)"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Career decisions average 5.2/10 — consider seeking more external input"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"You consistently underestimate switching costs"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Technical Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Next.js 14 (App Router) + TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Styling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailwind CSS + Framer Motion for animations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zustand (local-first, persists to localStorage)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IndexedDB via Dexie.js (all data stays on device)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Charts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recharts for calibration visualizations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vercel (static, no server needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why Local-First?
&lt;/h3&gt;

&lt;p&gt;Your life decisions are &lt;strong&gt;deeply personal&lt;/strong&gt;. They shouldn't sit on someone else's server. Forkcast stores everything in your browser's IndexedDB:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero server costs&lt;/li&gt;
&lt;li&gt;Zero data breaches&lt;/li&gt;
&lt;li&gt;Works offline&lt;/li&gt;
&lt;li&gt;Export as JSON anytime&lt;/li&gt;
&lt;li&gt;Optional: sync to your own cloud via CRDTs (planned)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dexie.js schema&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Dexie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ForkcastDB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;version&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="nf"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;++id, title, status, createdAt, mergedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;++id, decisionId, confidence, committedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outcomes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;++id, decisionId, actualScore, scoredAt&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;h3&gt;
  
  
  The Git-Style UI
&lt;/h3&gt;

&lt;p&gt;The interface mimics familiar Git tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│  FORKCAST — Your Decision History               │
├─────────────────────────────────────────────────┤
│                                                 │
│  ● ─── Accept Google Offer (abandoned)          │
│  │                                              │
│  ├─┐  "Accept Google vs. Stay at Startup"       │
│  │ │   Confidence: 72% → Actual: 6/10          │
│  │ │   Calibration: -12% (overconfident)        │
│  │ │                                            │
│  ● ─── Stay at Startup (merged) ✓              │
│  │                                              │
│  │                                              │
│  ● ─── Learn Rust (active)                      │
│  │                                              │
│  ├─┐  "Rust vs. Go for systems work"           │
│  │ │   Confidence: 65%                          │
│  │ │   Status: COMMITTED — awaiting score       │
│  │ │                                            │
│  ● ─── Learn Go (abandoned)                     │
│                                                 │
└─────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Psychology Behind It
&lt;/h2&gt;

&lt;p&gt;Forkcast is built on three cognitive science principles:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Hindsight Bias Elimination
&lt;/h3&gt;

&lt;p&gt;Humans are terrible at remembering what they &lt;em&gt;actually&lt;/em&gt; believed before an outcome. Once we know the result, we convince ourselves "I knew it all along."&lt;/p&gt;

&lt;p&gt;By writing predictions down &lt;strong&gt;before deciding&lt;/strong&gt;, you can't gaslight yourself later.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Calibration Training
&lt;/h3&gt;

&lt;p&gt;Research shows that prediction calibration is a &lt;strong&gt;trainable skill&lt;/strong&gt;. Weather forecasters, for example, become excellently calibrated because they get fast, frequent feedback.&lt;/p&gt;

&lt;p&gt;Forkcast gives you that feedback loop for life decisions — which normally have zero feedback mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Mere Measurement Effect
&lt;/h3&gt;

&lt;p&gt;The act of formally writing down a prediction changes how carefully you think about it. You can't just type "80% confident" without actually asking yourself &lt;em&gt;"Am I really 80% sure?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The tool makes you think harder just by existing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Is This For?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Forkcast is for you if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're at a career crossroads and want to think more clearly&lt;/li&gt;
&lt;li&gt;You're a founder making high-stakes bets weekly&lt;/li&gt;
&lt;li&gt;You're interested in rationality, calibration, or decision science&lt;/li&gt;
&lt;li&gt;You want to look back in 5 years with data, not just fuzzy memories&lt;/li&gt;
&lt;li&gt;You're a developer who appreciates the git metaphor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;It's probably not for you if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You prefer to live spontaneously without reflection&lt;/li&gt;
&lt;li&gt;You're not comfortable being honest with yourself about outcomes&lt;/li&gt;
&lt;li&gt;You think decisions can't be scored (they can — it's called regret)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Try It
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Tech-aficionado/Forkcast---Open-Source-Git.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Forkcast---Open-Source-Git
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;localhost:3000&lt;/code&gt;. Create your first fork. Commit a prediction. Set a reminder to come back and score it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy Your Own
&lt;/h3&gt;

&lt;p&gt;One-click deploy to Vercel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vercel.com/new/clone?repository-url=https://github.com/Tech-aficionado/Forkcast---Open-Source-Git" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvercel.com%2Fbutton" alt="Deploy with Vercel" width="103" height="32"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your data stays in YOUR browser. The deployment is just a static site.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's on the Roadmap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decision templates&lt;/strong&gt; — Pre-built frameworks for career, financial, technical, and relationship decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reminder system&lt;/strong&gt; — "Hey, you made a decision 6 months ago. Time to score it."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export to Markdown&lt;/strong&gt; — Generate a personal "decision journal" you can keep in Obsidian/Notion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CRDT sync&lt;/strong&gt; — Optionally sync across devices without a central server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI reflection&lt;/strong&gt; — Plug in an LLM to challenge your reasoning before you commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team mode&lt;/strong&gt; — Make group decisions with separate prediction commits from each member&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Source
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Tech-aficionado/Forkcast---Open-Source-Git" rel="noopener noreferrer"&gt;github.com/Tech-aficionado/Forkcast---Open-Source-Git&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; TypeScript (96.9%) + CSS (3.0%) + JavaScript (0.1%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Philosophy:&lt;/strong&gt; Local-first, privacy-respecting, no accounts required&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;You &lt;code&gt;git log&lt;/code&gt; your code changes. You &lt;code&gt;git blame&lt;/code&gt; bad commits. You &lt;code&gt;git revert&lt;/code&gt; mistakes.&lt;/p&gt;

&lt;p&gt;But for the decisions that actually shape your life — career moves, city changes, relationship choices, financial bets — you have &lt;strong&gt;zero version history&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You're essentially running &lt;code&gt;rm -rf&lt;/code&gt; on your decision-making data every time you forget what you originally believed.&lt;/p&gt;

&lt;p&gt;Forkcast fixes that. It gives you a commit history for your judgment.&lt;/p&gt;

&lt;p&gt;And after a year of using it? You won't just &lt;em&gt;think&lt;/em&gt; you're a good decision-maker. You'll &lt;strong&gt;know&lt;/strong&gt; — with data.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's a decision you made in the last year that you wish you'd tracked? Drop it in the comments. I'll share mine.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://github.com/Tech-aficionado" rel="noopener noreferrer"&gt;Shivansh Goel&lt;/a&gt; — AI &amp;amp; Full Stack Developer building tools that make humans think more clearly.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a React Component That Turns Any Portfolio Into a 3D Minecraft World</title>
      <dc:creator>Shivansh Goel</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:08:17 +0000</pubDate>
      <link>https://dev.to/tech-aficionado/i-built-a-react-component-that-turns-any-portfolio-into-a-3d-minecraft-world-196p</link>
      <guid>https://dev.to/tech-aficionado/i-built-a-react-component-that-turns-any-portfolio-into-a-3d-minecraft-world-196p</guid>
      <description>&lt;h1&gt;
  
  
  I Built a React Component That Turns Any Portfolio Into a 3D Minecraft World
&lt;/h1&gt;

&lt;p&gt;Every developer portfolio looks the same. Dark mode. Sans-serif font. &lt;em&gt;"Hi, I'm X, a passionate developer."&lt;/em&gt; Scroll. Skills section with floating icons. Scroll. Projects in a grid. Scroll. Contact form nobody will fill.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;I've reviewed hundreds of developer portfolios — as a peer, during hackathons, while hiring. They all blur into one. The content might differ, but the &lt;em&gt;experience&lt;/em&gt; is identical.&lt;/p&gt;

&lt;p&gt;So I asked myself a stupid question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if your portfolio was a playable Minecraft world instead?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And then I actually built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing CraftFolio
&lt;/h2&gt;

&lt;p&gt;CraftFolio is an open-source React component library that transforms your developer portfolio into a live, interactive 3D voxel environment.&lt;/p&gt;

&lt;p&gt;Your projects become structures you can walk up to. Your skills become minable ores. Your experience becomes terrain you traverse. No two portfolios look alike because the world is generated from &lt;em&gt;your&lt;/em&gt; data.&lt;/p&gt;

&lt;p&gt;One component. One config object. A portfolio nobody forgets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CraftFolio&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;craftfolio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CraftFolio&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Shivansh Goel"&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Full Stack Developer"&lt;/span&gt;
      &lt;span class="na"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TypeScript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;React&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Python&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FastAPI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Three.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forkcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com/...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;diamond&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GhostRelay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com/...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;obsidian&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ZipLink&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com/...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emerald&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="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"overworld"&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;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 the entire integration. Drop it in, customize the config, deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;Let me give you a number: &lt;strong&gt;4.7 million.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's how many repositories come up when you search "developer portfolio" on GitHub. Nearly five million variations of the same three-column layout with a hero section.&lt;/p&gt;

&lt;p&gt;When a recruiter reviews 200 portfolios in a week, yours needs to be the one they remember at dinner.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Honey, I saw this kid's portfolio today — it was literally Minecraft."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's the unfair advantage CraftFolio gives you.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Actually Get
&lt;/h2&gt;

&lt;p&gt;When you drop CraftFolio into your React app, it renders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 3D voxel world&lt;/strong&gt; — real-time Three.js rendering with a Minecraft aesthetic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walkable terrain&lt;/strong&gt; — procedurally generated from your data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project structures&lt;/strong&gt; — your repos become buildings you can approach and click&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skill ores&lt;/strong&gt; — your tech stack appears as glowing blocks scattered through the world&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day/night cycle&lt;/strong&gt; — dynamic lighting that shifts over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard navigation&lt;/strong&gt; — WASD movement, mouse to look, click to interact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile touch controls&lt;/strong&gt; — virtual joystick + tap interaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive fallback&lt;/strong&gt; — gracefully degrades to a styled 2D view on very old devices&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Technical Deep-Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CraftFolio/
├── src/
│   ├── components/
│   │   ├── VoxelWorld.tsx        — Three.js scene, camera, renderer
│   │   ├── Terrain.tsx           — Procedural world generation
│   │   ├── PlayerControls.tsx    — First-person camera + WASD movement
│   │   ├── ProjectStructure.tsx  — Interactive project buildings
│   │   ├── SkillOre.tsx          — Floating/glowing skill blocks
│   │   └── HUD.tsx               — Overlay UI (name, minimap, controls hint)
│   ├── shaders/
│   │   ├── voxel.vert            — Vertex shader for block geometry
│   │   └── voxel.frag            — Fragment shader with pixelated lighting
│   ├── systems/
│   │   ├── WorldGenerator.ts     — Perlin noise terrain + structure placement
│   │   ├── PhysicsEngine.ts      — Simple AABB collision detection
│   │   └── InteractionManager.ts — Raycasting for click detection
│   ├── config/
│   │   └── themes.ts             — Overworld, Nether, End color palettes
│   └── index.ts                  — Public API export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Performance: 10,000+ Voxels at 60fps
&lt;/h3&gt;

&lt;p&gt;The naive approach to rendering voxels is one mesh per block. At 10,000 blocks, that's 10,000 draw calls per frame. Your GPU melts.&lt;/p&gt;

&lt;p&gt;CraftFolio uses &lt;strong&gt;instanced rendering&lt;/strong&gt; — a single geometry is drawn thousands of times in one draw call, with per-instance transforms stored in a buffer:&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;blockGeometry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BoxGeometry&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="mi"&gt;1&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blockMaterial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MeshLambertMaterial&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;textureAtlas&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;instancedMesh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InstancedMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;blockGeometry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;blockMaterial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;MAX_BLOCKS&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Set position for each block instance&lt;/span&gt;
&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Matrix4&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&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="nx"&gt;block&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="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;instancedMesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMatrixAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// One draw call for ALL blocks&lt;/span&gt;
&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instancedMesh&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additional optimizations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frustum culling&lt;/strong&gt; — don't render blocks behind the camera&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Face culling&lt;/strong&gt; — don't render faces between two adjacent solid blocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LOD (Level of Detail)&lt;/strong&gt; — distant blocks render as flat colored planes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Texture atlas&lt;/strong&gt; — all block textures in one spritesheet = one texture bind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: Smooth 60fps on desktop, 30fps+ on mid-range mobile.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terrain Generation with Perlin Noise
&lt;/h3&gt;

&lt;p&gt;The world isn't random — it uses layered Perlin noise to create natural-looking hills and valleys:&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;generateTerrain&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="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&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;BlockData&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;blocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BlockData&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &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;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;x&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &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;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="o"&gt;++&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;baseHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perlin2D&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.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&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;hills&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perlin2D&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.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&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;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perlin2D&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.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;height&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hills&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;for &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;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;y&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;blocks&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="nx"&gt;x&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="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&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="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grass&lt;/span&gt;&lt;span class="dl"&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;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dirt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stone&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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blocks&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;Project structures are placed at terrain peaks — naturally visible from anywhere in the world.&lt;/p&gt;

&lt;h3&gt;
  
  
  The React + Three.js Marriage
&lt;/h3&gt;

&lt;p&gt;Reconciling React's declarative model with Three.js's imperative scene graph is painful. Here's how CraftFolio solves it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The scene lives in a ref&lt;/strong&gt; — Three.js objects are created once and mutated, not re-rendered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React controls state&lt;/strong&gt; — UI, config changes, and user data flow through React state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bridge hook syncs them&lt;/strong&gt; — &lt;code&gt;useWorldSync()&lt;/code&gt; watches React state and imperatively updates the scene
&lt;/li&gt;
&lt;/ol&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;useWorldSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CraftFolioConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sceneRef&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RefObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Scene&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sceneRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;rebuildProjectStructures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sceneRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;rebuildSkillOres&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sceneRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;applyTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sceneRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&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;This avoids the "React re-renders destroy my 3D scene" problem while keeping the developer API fully declarative.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Hard Lessons I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Mobile Performance Is a Different Beast
&lt;/h3&gt;

&lt;p&gt;On desktop with a discrete GPU, you can afford expensive shaders. On a 2020 mid-range Android phone? You're fighting for every millisecond.&lt;/p&gt;

&lt;p&gt;I had to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce world size from 100x100 to 40x40 on mobile&lt;/li&gt;
&lt;li&gt;Disable shadows on low-end devices&lt;/li&gt;
&lt;li&gt;Implement a quality auto-scaler that drops render resolution if FPS dips below 24&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Accessibility in a 3D World Is Hard (But Possible)
&lt;/h3&gt;

&lt;p&gt;A recruiter using a screen reader can't "walk through" a 3D world. So CraftFolio includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;aria-live&lt;/code&gt; region that narrates: &lt;em&gt;"You are near the Forkcast project. Press Enter to view details."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Keyboard-only navigation mode (arrow keys + tab)&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;reduceMotion&lt;/code&gt; mode that shows a 2D card layout instead (respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;All interactive elements in the tab order with proper labels&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Bundle Size Wars
&lt;/h3&gt;

&lt;p&gt;Three.js is ~150KB gzipped. For a portfolio component, that's heavy. My approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three.js is a &lt;strong&gt;peer dependency&lt;/strong&gt; — if you're already using it, no extra cost&lt;/li&gt;
&lt;li&gt;Tree-shakeable imports — only pulling in what's needed&lt;/li&gt;
&lt;li&gt;CraftFolio-specific code is ~45KB gzipped&lt;/li&gt;
&lt;li&gt;Lazy loading — the 3D world only initializes when entering the viewport&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Themes: Choose Your Dimension
&lt;/h2&gt;

&lt;p&gt;CraftFolio ships with three themes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Vibe&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overworld&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Green grass, blue sky, warm lighting&lt;/td&gt;
&lt;td&gt;Friendly, approachable portfolios&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nether&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dark red, lava lakes, dramatic shadows&lt;/td&gt;
&lt;td&gt;Edgy/gaming-focused developers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;End&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Purple void, floating islands, ethereal glow&lt;/td&gt;
&lt;td&gt;Creative/artistic portfolios&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CraftFolio&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"nether"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also pass a custom theme object for full control over colors, skybox, and block textures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Is This For?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use CraftFolio if you're:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A junior dev who needs to &lt;strong&gt;stand out&lt;/strong&gt; in a brutal job market&lt;/li&gt;
&lt;li&gt;An experienced dev who wants something &lt;strong&gt;memorable&lt;/strong&gt; for their personal brand&lt;/li&gt;
&lt;li&gt;A game dev who wants their portfolio to demonstrate their passion&lt;/li&gt;
&lt;li&gt;A content creator who needs a unique landing page&lt;/li&gt;
&lt;li&gt;Anyone tired of the same Tailwind/Next.js portfolio template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't use CraftFolio if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your target audience is strictly corporate/enterprise&lt;/li&gt;
&lt;li&gt;You need perfect Lighthouse accessibility scores&lt;/li&gt;
&lt;li&gt;You're applying to a company that blocks WebGL&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Get Started in 60 Seconds
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;craftfolio three @types/three
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CraftFolio&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;craftfolio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Portfolio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CraftFolio&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Your Name"&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Your Title"&lt;/span&gt;
      &lt;span class="na"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;React&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Node.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Python&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Cool Project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A brief description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com/you/project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;diamond&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="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"overworld"&lt;/span&gt;
      &lt;span class="na"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"wasd"&lt;/span&gt;
      &lt;span class="na"&gt;onProjectClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;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;h3&gt;
  
  
  Deploy
&lt;/h3&gt;

&lt;p&gt;Works with any React hosting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vercel deploy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;netlify deploy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;GitHub Pages&lt;/li&gt;
&lt;li&gt;Any static host&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's on the Roadmap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiplayer mode&lt;/strong&gt; — Visitors see each other as pixelated avatars&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom block textures&lt;/strong&gt; — Upload your own pixel art&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NPM CLI scaffolder&lt;/strong&gt; — &lt;code&gt;npx create-craftfolio&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weather system&lt;/strong&gt; — Rain, snow, or sunshine based on location&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sound effects&lt;/strong&gt; — Footsteps, ambient music, interaction clicks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics integration&lt;/strong&gt; — Track which projects visitors interact with most&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Source
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Tech-aficionado/CraftFolio---Open-Sourced-Minecraft-Portfolio-Library" rel="noopener noreferrer"&gt;github.com/Tech-aficionado/CraftFolio---Open-Sourced-Minecraft-Portfolio-Library&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; TypeScript (83.8%) + HTML (8.0%) + CSS (5.7%) + JavaScript (2.5%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle:&lt;/strong&gt; ~45KB gzipped (Three.js peer dependency)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The best portfolio isn't the one with the most impressive project list. It's the one the recruiter &lt;strong&gt;remembers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Five million portfolios on GitHub. Five million dark-mode hero sections. Five million "passionate developer" taglines.&lt;/p&gt;

&lt;p&gt;Or... one Minecraft world that someone actually &lt;em&gt;played&lt;/em&gt; for two minutes.&lt;/p&gt;

&lt;p&gt;Which one do you think gets the callback?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Star it if you'd use it. Fork it if you'd improve it. And drop a comment with YOUR most creative portfolio idea — I want to see what this community comes up with.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://github.com/Tech-aficionado" rel="noopener noreferrer"&gt;Shivansh Goel&lt;/a&gt; — AI &amp;amp; Full Stack Developer building intelligent web platforms.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>react</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
