<?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>Forem: Thea</title>
    <description>The latest articles on Forem by Thea (@highflyer910).</description>
    <link>https://forem.com/highflyer910</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2491%2F2e334b3d-5af0-43e5-95c0-e3ededf148f7.png</url>
      <title>Forem: Thea</title>
      <link>https://forem.com/highflyer910</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/highflyer910"/>
    <language>en</language>
    <item>
      <title>Modern Web Guidance: Teaching AI Agents to Stop Coding Like It's 2019</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Thu, 21 May 2026 10:05:14 +0000</pubDate>
      <link>https://forem.com/highflyer910/modern-web-guidance-teaching-ai-agents-to-stop-coding-like-its-2019-508b</link>
      <guid>https://forem.com/highflyer910/modern-web-guidance-teaching-ai-agents-to-stop-coding-like-its-2019-508b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-io-writing-2026-05-19"&gt;Google I/O Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There’s a slightly embarrassing problem that almost everyone using AI coding agents has run into: your agent still codes like it’s 2019.&lt;/p&gt;

&lt;p&gt;Ask it to build a modal, and it reaches for a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with JavaScript click handlers and messy &lt;code&gt;z-index&lt;/code&gt; hacks. Ask it to build a tooltip, and it creates a whole positioning system from scratch. Ask it to add passkey login, and it starts rebuilding things the browser already handles on its own.&lt;/p&gt;

&lt;p&gt;This isn't really the agent's fault. It was trained on the entire history of the web: millions of StackOverflow answers, GitHub repos, and old tutorials from a time when &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; didn't exist, the Popover API wasn't even a thing, and container queries sounded like science fiction. AI models don't automatically know what's current. They know what's common.&lt;/p&gt;

&lt;p&gt;That gap between what's "common" and what's "current" is exactly what  &lt;a href="https://developer.chrome.com/docs/modern-web-guidance" rel="noopener noreferrer"&gt;&lt;strong&gt;Modern Web Guidance&lt;/strong&gt;&lt;/a&gt;, announced at Google I/O 2026, is trying to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Modern Web Guidance?
&lt;/h2&gt;

&lt;p&gt;Modern Web Guidance is basically a set of skills you add to your AI coding agent. Think of it as a rulebook that helps steer the agent away from outdated patterns and toward features browsers can already handle today.&lt;/p&gt;

&lt;p&gt;It's not a linter or another docs site. It works inside your AI agent and helps guide the code it generates.&lt;/p&gt;

&lt;p&gt;It launched in early preview on May 19 with over 100 use cases focused on accessibility, performance, and security. It already works with tools like Antigravity, Claude Code, GitHub Copilot CLI, Gemini CLI, and more, with WebStorm support announced for the near future. Setup is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx modern-web-guidance@latest &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command, and your agent starts using more modern web patterns by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem It's Actually Solving
&lt;/h2&gt;

&lt;p&gt;Before getting into how it works, it's worth talking about why this even matters.&lt;/p&gt;

&lt;p&gt;The web platform has changed a lot over the last few years. We got:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element (and the ability to animate it with native CSS)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Popover API&lt;/strong&gt;: simple, accessible overlays without extra JavaScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS Anchor Positioning&lt;/strong&gt;: UI that stays attached to elements without JS math&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container Queries&lt;/strong&gt;: components that react to their own size, not the whole page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View Transitions API&lt;/strong&gt;: smooth page and UI transitions in just a few lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passkeys / WebAuthn&lt;/strong&gt;: secure login that the browser handles for you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren’t obscure experimental features. They’re widely available, well supported, and better than the patterns they replace. But AI agents still default to the old ones because that’s what they’ve seen the most.&lt;/p&gt;

&lt;p&gt;The result? Codebases built with AI assistance often carry five-year-old technical debt, shipped at today’s speed.&lt;/p&gt;

&lt;p&gt;Modern Web Guidance tries to fix this. It tells the agent: when the user asks for a modal, use &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;. When they ask for a tooltip, use the Popover API and CSS Anchor Positioning. When they ask for a login form, think about passkeys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying It Out
&lt;/h2&gt;

&lt;p&gt;I installed Modern Web Guidance via the CLI and tried a few prompts to see how much it actually changes the output.&lt;/p&gt;

&lt;p&gt;Before (without guidance): asking an agent to "build a modal with smooth open/close animations" usually gives you something like this: a &lt;code&gt;&amp;lt;div class="modal-overlay"&amp;gt;&lt;/code&gt;, &lt;code&gt;display: none&lt;/code&gt; toggled with JavaScript, opacity transitions on a &lt;code&gt;z-index: 9999&lt;/code&gt; container, and a &lt;code&gt;document.addEventListener('keydown')&lt;/code&gt; for Escape handling.&lt;/p&gt;

&lt;p&gt;It works. It’s also the kind of code a senior web developer would flag in a code review and replace.&lt;/p&gt;

&lt;p&gt;After (with guidance): the same prompt produces a &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element, animated with &lt;code&gt;@starting-style&lt;/code&gt; and &lt;code&gt;allow-discrete&lt;/code&gt;. Accessibility? Handled by the browser.&lt;/p&gt;

&lt;p&gt;The output is not just cleaner, it's less code doing more things correctly.&lt;/p&gt;

&lt;p&gt;I ran a similar test with tooltip/popover UI, and the guidance pushed the agent toward the Popover API with CSS Anchor Positioning. That removes a whole class of JavaScript that used to be needed just to keep tooltips attached to their triggers while scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Baseline Integration Is the Clever Part
&lt;/h2&gt;

&lt;p&gt;What makes Modern Web Guidance more interesting than just a list of best practices is its integration with &lt;strong&gt;&lt;a href="https://web.dev/baseline" rel="noopener noreferrer"&gt;Baseline&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Baseline is a web platform initiative that defines clear tiers of browser support: Newly Available (supported in all major browsers in the last ~30 months) and Widely Available. It gives developers a simple way to know if a feature is safe to use.&lt;/p&gt;

&lt;p&gt;Modern Web Guidance ties into this directly. When it suggests a feature, it’s tagged with a Baseline level. If your app needs to support older browsers, it includes the right fallbacks automatically. If you’re building for modern browsers only, it just uses the latest features without hesitation.&lt;/p&gt;

&lt;p&gt;This means the guidance isn’t static. As the web platform changes and features move from Newly Available to Widely Available, the skills get updated. You’re not managing rules, you’re just subscribing to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Honest Critique
&lt;/h2&gt;

&lt;p&gt;Modern Web Guidance is genuinely good, but it's worth being clear-eyed about what it is and what it isn't.&lt;/p&gt;

&lt;p&gt;It’s still early. It ships with over 100 use cases. The web platform has thousands of patterns and APIs, so this will feel limited pretty quickly if you go into less common areas like advanced WebGL, complex Service Worker setups, or niche CSS layouts. The team is accepting GitHub contributions, which makes sense, but it also means the real value of this tool will depend on community input over the next few months, not just what is shipped at I/O.&lt;/p&gt;

&lt;p&gt;It assumes your agent actually follows it. Modern Web Guidance works by adding context to your agent’s session, such as skills, CLAUDE.md files, or similar setups. But how well an agent follows that context vs. ignoring it and falling back to what it learned before can vary a lot. It’s not a strict rule; it’s more like a strong suggestion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.w3.org/community/" rel="noopener noreferrer"&gt;&lt;strong&gt;The web standards community&lt;/strong&gt;&lt;/a&gt; is becoming the arbiter of what “modern” means, and that’s more complicated than it sounds. To be fair, this isn’t just a Google project. The repo lists the Google Chrome team, Microsoft Edge team, and the wider web community as co-maintainers. That matters, as it’s closer to a shared platform effort than a single company product.&lt;/p&gt;

&lt;p&gt;But that also makes things more interesting, not less. When a cross-browser group defines what “modern web best practices” look like and bakes that into guidance used inside AI tools, it quietly shapes how millions of developers write code.&lt;/p&gt;

&lt;p&gt;The guidance is well-intentioned and aligned with standards. But it’s still a curated view of the web, created by a specific set of organizations. It shouldn’t be treated as neutral; it should be understood and questioned like any other opinionated layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Install It?
&lt;/h2&gt;

&lt;p&gt;Yes, no hesitation, if you're building anything that runs in the browser with an AI coding agent.&lt;/p&gt;

&lt;p&gt;The bigger picture is more interesting, though: Modern Web Guidance is an early sign of what happens when the people who build the web platform also shape how AI writes web code. That feedback loop, from spec authors to agent guidance to shipped code, could help the web actually catch up to what it already supports.&lt;/p&gt;

&lt;p&gt;For too long, great web platform features have gone unused because the ecosystem, including AI, keeps reaching for older solutions out of habit. If Modern Web Guidance works as intended, that gap will start closing faster.&lt;/p&gt;

&lt;p&gt;And that’s a meaningful thing to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;You can install it at &lt;a href="https://developer.chrome.com/docs/modern-web-guidance/get-started#installation" rel="noopener noreferrer"&gt;developer.chrome.com/docs/modern-web-guidance&lt;/a&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;# Recommended (auto-updates)&lt;/span&gt;
npx modern-web-guidance@latest &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Claude Code&lt;/span&gt;
/plugin marketplace add GoogleChrome/modern-web-guidance
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;modern-web-guidance@googlechrome
/reload-plugins

&lt;span class="c"&gt;# Gemini CLI&lt;/span&gt;
gemini extensions &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/GoogleChrome/modern-web-guidance &lt;span class="nt"&gt;--auto-update&lt;/span&gt;

&lt;span class="c"&gt;# Antigravity CLI&lt;/span&gt;
agy plugin &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/GoogleChrome/modern-web-guidance

&lt;span class="c"&gt;# GitHub CLI&lt;/span&gt;
gh skill &lt;span class="nb"&gt;install &lt;/span&gt;GoogleChrome/modern-web-guidance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source is open on GitHub at &lt;a href="https://github.com/GoogleChrome/modern-web-guidance-src" rel="noopener noreferrer"&gt;github.com/GoogleChrome/modern-web-guidance-src&lt;/a&gt;, contributions and feedback are welcome while it's in early preview.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Announced at Google I/O 2026 on May 19, 2026, and currently in early preview.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleiochallenge</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Idempotency Keys: What Most Tutorials Don't Tell You</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Wed, 29 Apr 2026 11:19:26 +0000</pubDate>
      <link>https://forem.com/highflyer910/idempotency-keys-what-most-tutorials-dont-tell-you-1ncc</link>
      <guid>https://forem.com/highflyer910/idempotency-keys-what-most-tutorials-dont-tell-you-1ncc</guid>
      <description>&lt;p&gt;Every payment flow has a silent enemy: the network. Requests time out, connections drop, users panic, and click twice. What happens to your system when the same charge arrives more than once?&lt;br&gt;
I used to think idempotency keys were one of those "nice-to-have backend things."&lt;br&gt;
You know the kind you tell yourself you'll fix later. That thinking is dangerous.&lt;br&gt;
When it comes to payments, idempotency isn't a feature; it's what prevents duplicate charges and inconsistent state.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Problem: "The Double Tap"
&lt;/h3&gt;

&lt;p&gt;Imagine this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user clicks &lt;strong&gt;"Pay"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The network is slow, or their Wi-Fi drops&lt;/li&gt;
&lt;li&gt;They click &lt;strong&gt;"Pay"&lt;/strong&gt; again&lt;/li&gt;
&lt;li&gt;Your API receives two identical requests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're not handling this correctly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user gets charged twice&lt;/li&gt;
&lt;li&gt;Your database ends up in a weird state&lt;/li&gt;
&lt;li&gt;You lose user trust instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This doesn't require a bug. It's normal behavior in distributed systems.&lt;/p&gt;
&lt;h3&gt;
  
  
  What Is Idempotency?
&lt;/h3&gt;

&lt;p&gt;In simple terms:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;You can send the same request multiple times, but it will be processed only once.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every retry with the same key should return the same result as the first successful request.&lt;/p&gt;
&lt;h4&gt;
  
  
  Important edge case: idempotency ≠ caching
&lt;/h4&gt;

&lt;p&gt;It's tempting to think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If something fails, just return the same failure."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But it's not that simple.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the request &lt;strong&gt;never reached your server logic&lt;/strong&gt;, a retry should proceed normally&lt;/li&gt;
&lt;li&gt;If the request &lt;strong&gt;may have started processing&lt;/strong&gt;, you must rely on stored state, not assumptions&lt;/li&gt;
&lt;li&gt;If an external system (like a payment provider) already processed the charge, a retry must &lt;strong&gt;not&lt;/strong&gt; trigger it again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Idempotency protects against &lt;strong&gt;duplicate processing&lt;/strong&gt;, not against retrying genuinely unprocessed requests.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Key: &lt;code&gt;Idempotency-Key&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To make this work, every payment intent needs a unique identifier.&lt;br&gt;
This key must be generated &lt;strong&gt;once per intent&lt;/strong&gt; (not per click) and reused across retries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ WRONG: New key on every click&lt;/span&gt;
&lt;span class="c1"&gt;// const handlePay = () =&amp;gt; {&lt;/span&gt;
&lt;span class="c1"&gt;//   const key = crypto.randomUUID();&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ RIGHT: One key per payment intent&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// generate when checkout loads&lt;/span&gt;

&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/payments/charge&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;Content-Type&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;application/json&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;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&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="nx"&gt;payload&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;h4&gt;
  
  
  Where does the key live on the client?
&lt;/h4&gt;

&lt;p&gt;"Generated once" means you need to persist it across retries.&lt;br&gt;
Good options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React state or a ref&lt;/strong&gt; (single-page flow)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sessionStorage&lt;/code&gt;&lt;/strong&gt; (if reloads are possible)&lt;/li&gt;
&lt;li&gt;A server-generated key passed into the checkout session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key should be tied to the &lt;strong&gt;intent&lt;/strong&gt;, not the button click.&lt;/p&gt;

&lt;p&gt;🔒 Security note: Idempotency keys can reveal payment patterns (e.g., same key on retry = something went wrong). Always send them over HTTPS, and avoid logging full keys in plaintext in production logs; hash or truncate if needed.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Server Side: Where the Real Work Happens
&lt;/h3&gt;

&lt;p&gt;Sending the header does nothing unless your backend actually uses it.&lt;br&gt;
Here's the core logic:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleChargeRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idempotency-key&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency-Key header is required&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;existing&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;keyStore&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;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 🚨 Same key must always mean the same intent&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;existing&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;currentSignature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractBusinessSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;storedSignature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;businessSignature&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;currentSignature&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;storedSignature&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency key reused with different payment details&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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Process the payment&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;paymentProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Store every deterministic response (success or known failure)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;keyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;businessSignature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;extractBusinessSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Only include immutable business fields - never timestamps, request IDs, or metadata&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractBusinessSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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;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;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerId&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;If the payment provider returns a failure (e.g., 402 or 500), you should store that response just like a success. Otherwise, a retry might reprocess the same key and produce a different result, breaking idempotency.&lt;/p&gt;

&lt;p&gt;The only exception is when the result is unknown (e.g., network timeout or provider unavailable). In those cases, the client should retry with the same key, and your system should check the provider state before deciding what to return.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Danger: Concurrency
&lt;/h3&gt;

&lt;p&gt;Here's where most implementations break.&lt;br&gt;
Two identical requests can arrive &lt;strong&gt;at the same time&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;both check "key not found"&lt;/li&gt;
&lt;li&gt;both proceed&lt;/li&gt;
&lt;li&gt;both charge the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Game over.&lt;/p&gt;
&lt;h4&gt;
  
  
  The fix
&lt;/h4&gt;

&lt;p&gt;You need a &lt;strong&gt;database-level unique constraint&lt;/strong&gt; on the idempotency key.&lt;br&gt;
This is what actually makes sure only one request wins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;idempotency_keys&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;business_signature&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&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;blockquote&gt;
&lt;p&gt;In production, the key should be inserted with a unique constraint &lt;strong&gt;before&lt;/strong&gt; processing the payment to avoid race conditions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If two requests try to insert the same key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one succeeds&lt;/li&gt;
&lt;li&gt;the other fails → and must fetch the stored result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this, your idempotency is not reliable.&lt;/p&gt;

&lt;h4&gt;
  
  
  Don't use SQL?
&lt;/h4&gt;

&lt;p&gt;The unique constraint is ideal, but here are alternatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis: &lt;code&gt;SET key value NX EX ttl&lt;/code&gt; (atomic create-if-not-exists)&lt;/li&gt;
&lt;li&gt;MongoDB: unique index on &lt;code&gt;idempotencyKey&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;DynamoDB: conditional write with &lt;code&gt;attribute_not_exists(idempotencyKey)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever you use, the operation must be atomic. A check-then-insert pattern will always have a race condition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fake Idempotency (Very Common)
&lt;/h3&gt;

&lt;p&gt;A lot of systems look correct, but aren't.&lt;br&gt;
Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checking the key but not storing the response&lt;/li&gt;
&lt;li&gt;storing the key but not handling concurrency&lt;/li&gt;
&lt;li&gt;using in-memory storage in a distributed system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a &lt;strong&gt;false sense of safety&lt;/strong&gt;, which is worse than having none.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key Expiration: The Gotcha Nobody Mentions
&lt;/h3&gt;

&lt;p&gt;Idempotency keys shouldn't live forever.&lt;br&gt;
A typical approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep them for ~24 hours&lt;/li&gt;
&lt;li&gt;clean up old entries&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;prevents unlimited storage growth&lt;/li&gt;
&lt;li&gt;gives you a safe retry window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After expiration, the same key can be treated as new. This is fine only if the original session has ended (for example, the user abandoned checkout).&lt;/p&gt;

&lt;p&gt;⚠️ Be careful: if a key expires while a payment is still processing (rare, but possible), a retry could create a duplicate. Set expiration long enough to cover your processing time and retry window. 24 hours is usually safe for most payment flows.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Full Flow
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CHECKOUT LOADS
      ↓
Generate idempotency key (once)
      ↓
User clicks "Pay"
      ↓
POST /charge (Idempotency-Key)
      ↓
SERVER: Key exists? ── YES → return stored response ✓
      │
      NO
      ↓
Try to insert key (unique constraint)
      │
      ├─ success → process payment → store result
      │
      └─ fail → fetch existing result
      ↓
Return response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  A Quick Note on HTTP Methods
&lt;/h3&gt;

&lt;p&gt;GET requests are &lt;strong&gt;intended&lt;/strong&gt; to be idempotent; calling them multiple times should not change server state.&lt;br&gt;
Idempotency keys are mainly for POST and PATCH - basically, anything that changes state.&lt;/p&gt;
&lt;h3&gt;
  
  
  Idempotency Beyond Payments
&lt;/h3&gt;

&lt;p&gt;This isn't just about payments. The same idea applies to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email sending (avoid duplicates)&lt;/li&gt;
&lt;li&gt;webhook handling (providers retry delivery)&lt;/li&gt;
&lt;li&gt;message queues (at-least-once delivery)&lt;/li&gt;
&lt;li&gt;database writes (deduplication)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anywhere you need &lt;strong&gt;exactly-once behavior on unreliable networks&lt;/strong&gt;, idempotency is your tool.&lt;/p&gt;
&lt;h3&gt;
  
  
  How to Test It (Because Trust, But Verify)
&lt;/h3&gt;

&lt;p&gt;💡 &lt;strong&gt;Pro tip:&lt;/strong&gt; Don't just hope it works, prove it.&lt;br&gt;
In your tests, send the same idempotency key twice:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idempotent charge: second request returns cached result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test_order_123&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="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cus_abc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// First request — processes payment&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res1&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;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/charge&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;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Second request — should return cached result, NOT call payment provider again&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res2&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;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/charge&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;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentProviderMock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledTimes&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="c1"&gt;// 🔑 critical check&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your mock gets called twice, your idempotency is leaking. Fix it before prod.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Checklist
&lt;/h3&gt;

&lt;p&gt;Before shipping a payment flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Key generated &lt;strong&gt;once per intent&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Key persisted across retries&lt;/li&gt;
&lt;li&gt;[ ] Backend checks key &lt;strong&gt;before processing&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Response is stored and reused&lt;/li&gt;
&lt;li&gt;[ ] Payload consistency is validated&lt;/li&gt;
&lt;li&gt;[ ] Database enforces a &lt;strong&gt;unique constraint&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Key store is shared across instances&lt;/li&gt;
&lt;li&gt;[ ] Expiration policy is defined&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one of these is missing, it's not done.&lt;/p&gt;




&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;Client sends the same key. The server enforces a unique constraint. Store every result (success or failure). Return stored result on retry.&lt;br&gt;
Miss any of these? You don't have idempotency. You have hope.&lt;br&gt;
The double tap is coming. Be ready for it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>backend</category>
      <category>api</category>
    </item>
    <item>
      <title>Future Museum of Extinct Things - A Glimpse from 2100</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Sun, 19 Apr 2026 10:18:53 +0000</pubDate>
      <link>https://forem.com/highflyer910/future-museum-of-extinct-things-a-glimpse-from-2100-4cl</link>
      <guid>https://forem.com/highflyer910/future-museum-of-extinct-things-a-glimpse-from-2100-4cl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-04-16"&gt;Weekend Challenge: Earth Day Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;By 2100, most of what you’re about to see is already gone.&lt;br&gt;
This is my Earth Day project - a museum built from that absence, an archive of what humanity lost during the Anthropocene.&lt;/p&gt;

&lt;p&gt;The premise is simple: curators from the future built this collection, looking back at us.&lt;br&gt;
Every exhibit documents a real environmental loss. Not invented species, but things already gone, or measurably disappearing right now. The Great Barrier Reef. The monarch migration. The vaquita porpoise.&lt;/p&gt;

&lt;p&gt;Visitors can do two things:&lt;br&gt;
&lt;strong&gt;Nominate an exhibit&lt;/strong&gt; - type a word, a phrase, anything.&lt;br&gt;
&lt;em&gt;"Fireflies."&lt;/em&gt; &lt;em&gt;"The sound of a forest."&lt;/em&gt;&lt;br&gt;
The AI archivist grounds that in real, documented science, turning it into a permanent museum card.&lt;br&gt;
No fiction. Every exhibit is real data, real species, real loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask the Curator&lt;/strong&gt; - a floating button opens a conversation with the museum's archivist.&lt;br&gt;
It is 2100. Everything is already gone.&lt;br&gt;
The curator speaks entirely in the past tense and answers from that weight.&lt;br&gt;
You’re not chatting with an assistant.&lt;br&gt;
You’re talking to someone who has already watched it disappear.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://futuremuseum.vercel.app/" rel="noopener noreferrer"&gt;anthropocene-archive.vercel.app&lt;/a&gt;&lt;br&gt;
Try nominating something you're afraid we'll lose. Then ask the Curator what happened to it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/highflyer910" rel="noopener noreferrer"&gt;
        highflyer910
      &lt;/a&gt; / &lt;a href="https://github.com/highflyer910/future-museum" rel="noopener noreferrer"&gt;
        future-museum
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Future Museum of Extinct Things&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;It is the year 2100. You are standing in a digital archive built by those who remembered.&lt;/em&gt;
&lt;em&gt;This is what they chose to preserve.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://futuremuseum.vercel.app/" rel="nofollow noopener noreferrer"&gt;→ Visit the Museum&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/weekend-2026-04-16" rel="nofollow"&gt;DEV Earth Day Challenge 2026&lt;/a&gt; - a contemplative digital museum set in the year 2100, where the exhibits are the species, places, sounds, and sensations that humanity lost during the Anthropocene. Visitors can nominate what &lt;em&gt;they&lt;/em&gt; are afraid we will lose, and an AI curator - powered by Google Gemini - writes a scientifically-grounded permanent exhibit for each one.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What It Is&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The premise: a museum from the future, looking back at us. Every exhibit documents a real environmental loss, not invented, not speculative fiction, but things already gone or measurably disappearing. The Great Barrier Reef. The monarch migration. The sound of a full dawn chorus. Truly dark skies.&lt;/p&gt;
&lt;p&gt;The Gemini integration isn't decorative…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/highflyer910/future-museum" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; vanilla HTML, CSS, and JavaScript — no frameworks, no build step. GSAP for animation. Two Gemini-powered Vercel serverless functions. &lt;br&gt;
I deliberately kept the stack minimal. This project isn’t about complexity - it’s about control over tone, pacing, and interaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The nomination feature&lt;/strong&gt; sends user input to &lt;code&gt;/api/gemini.js&lt;/code&gt;, where Gemini is prompted to translate a vague or emotional phrase into a real, documented environmental phenomenon.&lt;br&gt;
The challenge wasn’t generating text - it was &lt;em&gt;constraining it&lt;/em&gt;.&lt;br&gt;&lt;br&gt;
Without strict instructions, the model drifted into fiction. With too many constraints, it became sterile. The prompt had to balance both: enforce real species, real data, real locations - while still sounding like a human curator, not a report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Curator chat&lt;/strong&gt; lives in &lt;code&gt;/api/curator.js&lt;/code&gt; and is treated as a separate system entirely.&lt;br&gt;
It’s not just a chatbot, it’s a character with rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;year 2100
&lt;/li&gt;
&lt;li&gt;speaks only in the past tense
&lt;/li&gt;
&lt;li&gt;offers no solutions, only memory
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s also context-aware. If a user opens an exhibit and asks a question, the Curator responds from within that specific loss rather than generically.&lt;br&gt;
Both functions run server-side, keeping the API key completely off the client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design-wise&lt;/strong&gt;, everything supports the same idea: quiet loss.&lt;br&gt;
Soil, bark, amber, parchment - materials that age and decay.&lt;br&gt;&lt;br&gt;
A subtle grain overlay, concentric rings that echo tree rings, ripples, or sonar, something searching, or remembering.&lt;br&gt;
The goal wasn’t just to show information.&lt;br&gt;
It was to make it feel like something already gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best use of Google Gemini&lt;/strong&gt;: two integrations, both central to the concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generating scientifically grounded exhibits from visitor input
&lt;/li&gt;
&lt;li&gt;maintaining a consistent character voice from the year 2100&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>gemini</category>
      <category>earthday</category>
    </item>
    <item>
      <title>git blame --emotions: No Solutions. Just Vibes.</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Sat, 04 Apr 2026 12:04:22 +0000</pubDate>
      <link>https://forem.com/highflyer910/git-blame-emotions-no-solutions-just-vibes-3gbc</link>
      <guid>https://forem.com/highflyer910/git-blame-emotions-no-solutions-just-vibes-3gbc</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/aprilfools-2026"&gt;DEV April Fools Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built a tool that does absolutely nothing useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;git blame --emotions&lt;/strong&gt; is a Shakespearean error therapy app. You paste your error message. It gives you a sonnet. No solutions. No Stack Overflow links. No rubber duck. Just iambic pentameter and the gentle acknowledgment that your &lt;code&gt;undefined is not a function&lt;/code&gt; is &lt;em&gt;grieving&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The app lives at the intersection of two deeply important things: the emotional intelligence of Elizabethan poetry, and the complete uselessness of a tool that refuses to help you debug anything. It will not fix your code.&lt;/p&gt;

&lt;p&gt;It also looks like a 1999 Geocities fan site - Comic Sans, pastel chaos, and a visitor counter stuck at 000418.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live site:&lt;/strong&gt; &lt;a href="https://git-blame-emotions.vercel.app/" rel="noopener noreferrer"&gt;git-blame--emotions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what happens when you use it:&lt;br&gt;
Paste an error. Click the button.&lt;br&gt;
You get a Shakespearean sonnet about your suffering.&lt;br&gt;
Your code remains broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus:&lt;/strong&gt; If you paste anything containing &lt;code&gt;418&lt;/code&gt; or &lt;code&gt;teapot&lt;/code&gt;, you get a special Easter egg. I won't spoil it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/highflyer910" rel="noopener noreferrer"&gt;
        highflyer910
      &lt;/a&gt; / &lt;a href="https://github.com/highflyer910/git-blame--emotions" rel="noopener noreferrer"&gt;
        git-blame--emotions
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;git blame --emotions 💔&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;No solutions. Just vibes.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/d1f4dba716690f42d3152316a1985757a70505b898c5d76a6bb1d32af3e0e419/68747470733a2f2f68696768666c7965723931302e736972762e636f6d2f676974626c616d652e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/d1f4dba716690f42d3152316a1985757a70505b898c5d76a6bb1d32af3e0e419/68747470733a2f2f68696768666c7965723931302e736972762e636f6d2f676974626c616d652e706e67" alt="git blame --emotions in action"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A Shakespearean error therapy app powered by Google Gemini AI and a complete disregard for productivity.&lt;/p&gt;
&lt;p&gt;You paste your error message. It writes you a sonnet. The error remains. You feel seen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live site:&lt;/strong&gt; &lt;a href="https://git-blame-emotions.vercel.app" rel="nofollow noopener noreferrer"&gt;git-blame--emotions&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What It Does&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;You paste an error message, or stack trace&lt;/li&gt;
&lt;li&gt;Google Gemini AI reads it and writes a &lt;strong&gt;Shakespearean sonnet&lt;/strong&gt; - 14 lines, ABAB CDCD EFEF GG rhyme scheme, iambic pentameter&lt;/li&gt;
&lt;li&gt;Confetti fires&lt;/li&gt;
&lt;li&gt;You feel emotionally validated&lt;/li&gt;
&lt;li&gt;Your code is still broken&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This tool will not fix your bugs. It will not suggest a solution. It will not link you to Stack Overflow. It will, however, tell you that your &lt;code&gt;NullPointerException&lt;/code&gt; is &lt;em&gt;"a void where love should be"&lt;/em&gt; - and sometimes that's enough.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Bonus:&lt;/strong&gt; Paste anything containing &lt;code&gt;418&lt;/code&gt; or &lt;code&gt;teapot&lt;/code&gt; for a special surprise. RFC 2324 compliant.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tech Stack&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vanilla HTML, CSS, JavaScript&lt;/strong&gt; - zero…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/highflyer910/git-blame--emotions" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Tech stack is intentionally minimal:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pure vanilla HTML, CSS, JavaScript - zero frameworks, zero build tools&lt;/li&gt;
&lt;li&gt;One serverless function &lt;code&gt;api/poem.js&lt;/code&gt; on Vercel&lt;/li&gt;
&lt;li&gt;Google Gemini API for the sonnet generation&lt;/li&gt;
&lt;li&gt;canvas-confetti for emotional release&lt;/li&gt;
&lt;li&gt;Google Fonts: UnifrakturMaguntia + Patrick Hand&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Honestly, this started as a joke. It stayed a joke. I read "build something completely useless," and thought: what if instead of fixing errors, we just... felt them.&lt;/p&gt;

&lt;p&gt;The frontend is a single &lt;code&gt;index.html&lt;/code&gt; + &lt;code&gt;style.css&lt;/code&gt; + &lt;code&gt;script.js&lt;/code&gt;. The Geocities aesthetic isn't laziness; it's a &lt;em&gt;commitment&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The API lives in &lt;code&gt;api/poem.js&lt;/code&gt;, a Vercel serverless function that calls Gemini and keeps the API key server-side.&lt;/p&gt;

&lt;p&gt;Most of the work went into the prompt. I force Gemini to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;write a proper Shakespearean sonnet (ABAB CDCD EFEF GG)&lt;/li&gt;
&lt;li&gt;use iambic pentameter&lt;/li&gt;
&lt;li&gt;treat the error as a character with feelings&lt;/li&gt;
&lt;li&gt;and absolutely refuse to help in any technical way&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 418 Easter egg was non-negotiable. HTTP 418 "I'm a Teapot" gets special treatment: confetti, a wobbling teapot overlay, and a dramatic sonnet that treats the RFC like high tragedy.&lt;/p&gt;

&lt;p&gt;On mobile, the site politely asks you to rotate your CRT monitor. There is also a vertical label that says &lt;code&gt;EMPTY SPACE FOR YOUR TEARS&lt;/code&gt;. These are features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ☕ Best Ode to Larry Masinter
&lt;/h3&gt;

&lt;p&gt;The visitor counter stops at 000418. The site claims RFC 2324 compliance. The teapot Easter egg turns the most absurd HTTP status code into a Shakespearean tragedy. It felt only right.&lt;/p&gt;

&lt;h3&gt;
  
  
  🤖 Best Google AI Usage
&lt;/h3&gt;

&lt;p&gt;Google Gemini powers every poem. It reads raw error messages and turns them into structured Shakespearean sonnets with dramatic tone and zero usefulness. The 418 path uses a separate prompt to elevate the RFC into full tragedy.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌟 Community Favorite
&lt;/h3&gt;

&lt;p&gt;We’ve all been there at 2am, staring at a stack trace and questioning everything. Sometimes you don’t need a fix. You need someone to say: &lt;em&gt;"thy memory, shattered like my heart."&lt;/em&gt;&lt;br&gt;
That’s this app.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;em&gt;No bugs were fixed during the making of this website.&lt;/em&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>418challenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>DevStretch: The Antiburnout Protocol for Devs Who Forgot They Have Bodies</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Sun, 01 Mar 2026 21:03:41 +0000</pubDate>
      <link>https://forem.com/highflyer910/devstretch-the-antiburnout-protocol-for-devs-who-forgot-they-have-bodies-3am</link>
      <guid>https://forem.com/highflyer910/devstretch-the-antiburnout-protocol-for-devs-who-forgot-they-have-bodies-3am</guid>
      <description>&lt;h2&gt;
  
  
  The Community
&lt;/h2&gt;

&lt;p&gt;Let’s be honest: most of us treat our physical bodies like a deprecated legacy dependency. It’s still running, it’s technically functional, but it hasn't had an update in years, and we’ve been ignoring the &lt;code&gt;STIFF_NECK_WARNING&lt;/code&gt; in the logs for six hours.&lt;/p&gt;

&lt;p&gt;I built this for the community of developers, specifically the ones who:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sit at a 45-degree angle until they merge with their chair.&lt;/li&gt;
&lt;li&gt;Make a "crunchy" sound when they finally stand up at 3 AM.&lt;/li&gt;
&lt;li&gt;Treat "Hydration" as just another cup of coffee.
Burnout isn't just a mental state; it’s a physical bug report. DevStretch is the patch.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://devstretch.vercel.app" rel="noopener noreferrer"&gt;DevStretch&lt;/a&gt;&lt;/strong&gt; is a terminal-themed PWA designed to interrupt your "flow state" before it permanently wrecks your posture.&lt;/p&gt;

&lt;p&gt;It’s an 11-step maintenance protocol. We’re not "stretching"; we’re refactoring our spines. I gave every movement a proper developer rebrand because let’s face it - you’re more likely to "Clear Cache" than "Rest your eyes."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Protocol Name&lt;/th&gt;
&lt;th&gt;System Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Review That Code&lt;/td&gt;
&lt;td&gt;Neck Stretch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Roll Back&lt;/td&gt;
&lt;td&gt;Shoulder Rolls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Prevent Carpal Tunnel PR&lt;/td&gt;
&lt;td&gt;Wrist Stretches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Deploy to Standing Position&lt;/td&gt;
&lt;td&gt;Sit to Stand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Clear Cache&lt;/td&gt;
&lt;td&gt;Eye Break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Refactor Your Spine&lt;/td&gt;
&lt;td&gt;Seated Back Twist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Offline Mode&lt;/td&gt;
&lt;td&gt;Walk Away&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Memory Garbage Collection&lt;/td&gt;
&lt;td&gt;Box Breathing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Extend Your Reach&lt;/td&gt;
&lt;td&gt;Overhead Arm Stretch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Lint Your Posture&lt;/td&gt;
&lt;td&gt;Posture Check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;git commit --water&lt;/td&gt;
&lt;td&gt;Hydration Reminder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The UI is a dark mode terminal aesthetic - phosphor green on near-black, JetBrains Mono font, scanlines, a flickering timer with a blinking cursor, and a startup boot sequence that makes you feel like you’re initializing a mainframe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://devstretch.vercel.app" rel="noopener noreferrer"&gt;devstretch.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Open it on your phone and "Add to Home Screen." It’s a PWA, so it works offline when your Wi-Fi goes down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The project is entirely dependency-free. No React, no Vite, no node_modules folder larger than the project itself. Just clean, modular Vanilla JS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/highflyer910/devstretch" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I chose a deliberately "boring" stack in the best way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web Speech API&lt;/strong&gt;: Provides hands-free voice guidance. No need to look at the screen while you're "Refactoring your spine."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen Wake Lock API&lt;/strong&gt;: This was crucial. It prevents the phone screen from dimming or locking mid-stretch, ensuring the timer doesn't throttle while you're away from the keyboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Notifications API&lt;/strong&gt;: Background stand-up reminders that stay active even if you close the tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Worker&lt;/strong&gt;:Full offline support. If your internet dies, your health protocol shouldn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The "Bug" Log: Notification Hell&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Browser notifications were humbling. I learned the hard way that new Notification() called from the main thread is often silently blocked; the "Senior" move is rerouting everything through the Service Worker via registration.showNotification().&lt;/p&gt;

&lt;p&gt;Even then, OS-level notification layers (Focus Assist on Windows, battery optimization on Android) can swallow notifications entirely. Permission shows as &lt;code&gt;granted&lt;/code&gt;, the Service Worker fires without errors... and nothing appears. Still actively debugging. Sometimes shipping means shipping with a 'Known Issue' 🙃&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deeper platform integration for background notifications&lt;/li&gt;
&lt;li&gt;Custom exercise editor - add your own stretches&lt;/li&gt;
&lt;li&gt;Configurable rest time&lt;/li&gt;
&lt;li&gt;Dedicated wrist and eye exercise sets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;git commit -m "took care of myself today"&lt;/code&gt;&lt;br&gt;
&lt;em&gt;// It's a feature, not a bug.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>showdev</category>
      <category>pwa</category>
    </item>
    <item>
      <title>Smooth Page Transitions with Zero Libraries: The View Transitions API</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Tue, 11 Nov 2025 13:04:58 +0000</pubDate>
      <link>https://forem.com/highflyer910/smooth-page-transitions-with-zero-libraries-the-view-transitions-api-3o0m</link>
      <guid>https://forem.com/highflyer910/smooth-page-transitions-with-zero-libraries-the-view-transitions-api-3o0m</guid>
      <description>&lt;p&gt;I recently started using the View Transitions API in one of my projects, and honestly, it felt like unlocking a hidden superpower for the web. I had to share it.&lt;/p&gt;

&lt;p&gt;Not long ago, this API was just a Chrome experiment. But as of 2025, it’s officially everywhere, supported in Chrome, Edge, Safari, and Firefox, covering over 85% of browsers. It’s now powering everything from e-commerce sites to dashboards and blogs, making transitions feel native and seamless.&lt;/p&gt;

&lt;p&gt;Here’s what I’ve learned using it, and why you’ll probably want to add it to your toolkit too.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Is the View Transitions API?
&lt;/h3&gt;

&lt;p&gt;The View Transitions API animates between two page states, toggling a UI mode, opening a modal, or moving between pages. It works whether you're updating part of a page or navigating between entirely different pages.&lt;/p&gt;

&lt;p&gt;The trick: give an element the same &lt;code&gt;view-transition-name&lt;/code&gt; before and after the update. The browser retains the element’s identity and morphs it, automatically handling position, size, and content blending.&lt;/p&gt;

&lt;p&gt;No libraries. No keyframes. Just native transitions that are easy to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Pattern (Only 3 Steps!)
&lt;/h3&gt;

&lt;p&gt;To apply a transition to an in-page element (like a card changing from a grid to a list view):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Name your element in CSS&lt;/strong&gt;&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;.content.active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;main-content&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;&lt;strong&gt;2. Keep your element in the DOM (toggle its state)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"content card-view active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Card content&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Wrap your state change in &lt;code&gt;startViewTransition()&lt;/code&gt;&lt;/strong&gt;&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&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="c1"&gt;// Toggle state - the element keeps the same view-transition-name&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card-view&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grid-view&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;&lt;strong&gt;That's it! The browser handles the animation.&lt;/strong&gt;&lt;br&gt;
💡 Tip: Avoid using &lt;code&gt;element.innerHTML = '...'&lt;/code&gt; or re-rendering components that replace the DOM completely, which breaks the element’s identity and stops the smooth transition.&lt;br&gt;
Instead, just toggle classes or, if you’re using a framework like React, make sure elements keep stable keys so the DOM node stays the same.&lt;/p&gt;
&lt;h3&gt;
  
  
  See It in Action
&lt;/h3&gt;

&lt;p&gt;I built a small SPA demo to test how smooth the View Transitions API can be. It uses directional animations and custom effects, and honestly, it feels surprisingly close to a native app.&lt;/p&gt;

&lt;p&gt;Pages slide left and right, hero sections move up and down, and the back button even reverses the animation automatically.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://codepen.io/HighFlyer910/pen/WbwebvQ" rel="noopener noreferrer"&gt;View Live Demo on CodePen&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Try it yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click between pages to see forward animations (slides right-to-left)&lt;/li&gt;
&lt;li&gt;Use the back button to see reverse animations (slide left to right)&lt;/li&gt;
&lt;li&gt;Watch the hero section move up and down on its own&lt;/li&gt;
&lt;li&gt;Open DevTools and inspect the &lt;code&gt;::view-transition-*&lt;/code&gt; pseudo-elements to see how the browser does it!&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Advanced Technique #1: Directional Back Navigation
&lt;/h3&gt;

&lt;p&gt;Want your back button to slide the other way? Here’s how to make your SPA navigation feel more like a native app:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The JavaScript&lt;/strong&gt;&lt;br&gt;
We listen for the popstate event (which fires on back/forward navigation) and briefly add a class to the root element to trigger reverse animations.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handlePopState&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&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;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;home&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Add class to trigger reverse animations&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;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;back-transition&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;startViewTransition&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="nf"&gt;renderPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&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="c1"&gt;// Clean up after animation completes&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;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;back-transition&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;renderPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&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;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;popstate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handlePopState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The CSS&lt;/strong&gt;&lt;br&gt;
We target the global transition, which is automatically assigned the name root.&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="c"&gt;/* Forward navigation (default) */&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&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="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease-in&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-to-left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&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="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease-out&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-from-right&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Reverse navigation (back button) */&lt;/span&gt;
&lt;span class="nc"&gt;.back-transition&lt;/span&gt; &lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&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="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease-in&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-to-right&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.back-transition&lt;/span&gt; &lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&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="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease-out&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-from-left&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 creates that iOS/Android feeling where navigation direction matches user intent!&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced Technique #2: Custom Animations for Specific Elements
&lt;/h3&gt;

&lt;p&gt;You can animate different parts of the page independently using named transitions.&lt;br&gt;
For example, while your main content slides horizontally, a hero section can move vertically or fade, and the browser syncs it all perfectly for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hero Section with Vertical Slide&lt;/strong&gt;&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;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;hero&lt;/span&gt;&lt;span class="o"&gt;)&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="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease-in&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-down-out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;hero&lt;/span&gt;&lt;span class="o"&gt;)&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="m"&gt;0.3s&lt;/span&gt; &lt;span class="m"&gt;0.1s&lt;/span&gt; &lt;span class="n"&gt;ease-out&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-up-in&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;slide-down-out&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;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;40px&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;slide-up-in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;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;40px&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 result? A smooth motion where each element moves on its own but still feels perfectly connected, just like a well-crafted native app.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Use It (and When Not To)
&lt;/h3&gt;

&lt;p&gt;Great for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Toggling states (like dark mode or list ↔ grid views)&lt;/li&gt;
&lt;li&gt;Page navigation in SPAs&lt;/li&gt;
&lt;li&gt;Opening modals or dialogs&lt;/li&gt;
&lt;li&gt;Simple UI transitions that need a touch of polish
Skip it for:&lt;/li&gt;
&lt;li&gt;Complex, multi-step animations (GSAP or Framer Motion are better here)&lt;/li&gt;
&lt;li&gt;Situations that need precise timing across many elements&lt;/li&gt;
&lt;li&gt;Critical interactions where you need guaranteed fallback behavior
&lt;strong&gt;Accessibility Tip:&lt;/strong&gt;
Always respect &lt;code&gt;prefers-reduced-motion&lt;/code&gt;, as some users are sensitive to motion, and fast transitions can make them uncomfortable.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
  &lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;/* Instantly complete the animation */&lt;/span&gt;
    &lt;span class="nl"&gt;animation-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.01ms&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="nb"&gt;none&lt;/span&gt; &lt;span class="cp"&gt;!important&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;&lt;strong&gt;Minimal Fallback (Just in Case)&lt;/strong&gt;&lt;br&gt;
While browser support is excellent, a tiny fallback never hurts:&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;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;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updateDOM&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;updateDOM&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// instant update&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use CSS &lt;code&gt;@supports&lt;/code&gt; to provide a subtle &lt;code&gt;transition&lt;/code&gt; fallback for older browsers:&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;@supports&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt; &lt;span class="m"&gt;0.2s&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;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;The View Transitions API is a surprisingly powerful tool that can turn clunky page changes into smooth, polished experiences, with minimal code.&lt;/p&gt;

&lt;p&gt;Directional transitions and custom element animations let your web app feel closer to a native app, all with just vanilla CSS and JS.&lt;/p&gt;

&lt;p&gt;So next time you update a view, just give it a &lt;code&gt;view-transition-name&lt;/code&gt;.&lt;br&gt;
It’s that simple, and the page instantly feels smoother.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>animation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Single-tenant vs Multi-tenant: What I Wish I Knew When I Started</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Thu, 16 Oct 2025 10:45:52 +0000</pubDate>
      <link>https://forem.com/highflyer910/single-tenant-vs-multi-tenant-what-i-wish-i-knew-when-i-started-1hem</link>
      <guid>https://forem.com/highflyer910/single-tenant-vs-multi-tenant-what-i-wish-i-knew-when-i-started-1hem</guid>
      <description>&lt;p&gt;I should be coding right now, but instead, I went down the single-tenant vs multi-tenant rabbit hole. Then I got lost in videos, articles, and posts that all said different things.&lt;/p&gt;

&lt;p&gt;One person says multi-tenant is the only way to scale. Another says single-tenant is the only way to be secure. And someone always warns: “If you choose wrong, your SaaS will fail.”&lt;/p&gt;

&lt;p&gt;It’s really not that serious. I just wanted a simple explanation without all the technical words and drama. So here’s the version I wish I had read earlier.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Tenant (Apartments):&lt;/strong&gt; Cheaper, easier to update, and perfect when you’re starting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-Tenant (Houses):&lt;/strong&gt; Private, secure, and flexible, but expensive and hard to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid (Schema-per-Tenant):&lt;/strong&gt; Each tenant has its own schema in the same database. It’s more complex but gives better separation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; Multi-tenant apps must have strong data separation. One missing &lt;code&gt;WHERE&lt;/code&gt; clause can leak data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building your first SaaS, multi-tenant is usually the better choice.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Analogy (Houses vs. Apartments)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Single-Tenant = Everyone Has Their Own House&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each customer has a private house, their own driveway, kitchen, and mailbox. In SaaS, that means a separate app and database for each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Very secure:&lt;/strong&gt; If one house is broken into, others are safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to customize:&lt;/strong&gt; You can change things for one client without affecting others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No noisy neighbors:&lt;/strong&gt; No performance issues between users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expensive:&lt;/strong&gt; More infrastructure means higher costs (servers, databases, monitoring)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard to update:&lt;/strong&gt; You must update each app separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More maintenance:&lt;/strong&gt; fixing one problem often means fixing it many times.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When you work with enterprise clients who specifically ask for it.&lt;/li&gt;
&lt;li&gt;If you’re in a highly regulated industry (healthcare, finance, etc.) that needs strong compliance (HIPAA, SOC 2, and so on).&lt;/li&gt;
&lt;li&gt;When your users want custom deployments or on-premise hosting.&lt;/li&gt;
&lt;li&gt;If you actually have funding or a big budget to handle the extra infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Multi-Tenant = One Big Apartment Building&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everyone lives in the same building, but each apartment has a lock. In SaaS, it means one app and one database, but data is separated by tenant IDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Much cheaper:&lt;/strong&gt; Resources are shared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier to update:&lt;/strong&gt; One update affects all tenants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple to add new tenants:&lt;/strong&gt; New signups are just new rows in your database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security must be perfect:&lt;/strong&gt; one small mistake can expose another tenant’s data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "noisy neighbor" problem:&lt;/strong&gt;: A heavy user can slow down others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harder to customize:&lt;/strong&gt; Individual tenant customizations are more complex&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More complex queries:&lt;/strong&gt; Every query needs tenant filtering logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're bootstrapping or indie hacking&lt;/li&gt;
&lt;li&gt;You want to launch and iterate fast&lt;/li&gt;
&lt;li&gt;Your users have similar needs (same features, pricing tiers)&lt;/li&gt;
&lt;li&gt;You're using modern frameworks with built-in tenant isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Schema-per-Tenant = Separate Floors in the Same Building&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One database server, but each tenant gets their own schema (namespace).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each tenant has tables like &lt;code&gt;tenant_123.users&lt;/code&gt;, &lt;code&gt;tenant_123.orders&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Your app switches schemas based on who's logged in&lt;/li&gt;
&lt;li&gt;Better separation than row-level filtering, cheaper than separate databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better isolation than pure multi-tenant&lt;/li&gt;
&lt;li&gt;Easier to backup/restore individual tenants&lt;/li&gt;
&lt;li&gt;Still cheaper than full single-tenant&lt;/li&gt;
&lt;li&gt;Can migrate specific tenants to their own database later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More complex than simple multi-tenant&lt;/li&gt;
&lt;li&gt;Some ORMs don't handle this well&lt;/li&gt;
&lt;li&gt;Database connection pooling gets trickier&lt;/li&gt;
&lt;li&gt;Migration scripts need to run on all schemas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Popular tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Django:&lt;/strong&gt; django-tenants (formerly django-tenant-schemas)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js:&lt;/strong&gt; Knex.js with schema switching, or Prisma with multi-schema support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby on Rails:&lt;/strong&gt; Apartment gem&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - &lt;strong&gt;Postgres:&lt;/strong&gt; Built-in schema support
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Security (The Scary Part)
&lt;/h3&gt;

&lt;p&gt;Multi-tenant security means more than just adding a &lt;code&gt;tenant_id&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three Levels of Protection:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; Use row-level security or tenant views.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application:&lt;/strong&gt; Always filter queries by tenant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API:&lt;/strong&gt; Make sure tokens connect users to the right tenant.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Common mistakes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting &lt;code&gt;WHERE tenant_id = ?&lt;/code&gt; → Instant data leak&lt;/li&gt;
&lt;li&gt;Wrong JWT token → Can access another tenant's data&lt;/li&gt;
&lt;li&gt;Admin panels that bypass tenant filters → Privacy nightmare&lt;/li&gt;
&lt;li&gt;Sharing caches across tenants → Information disclosure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tools That Help:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supabase:&lt;/strong&gt; Built-in RLS, auth hooks, and tenant isolation patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prisma:&lt;/strong&gt; Row-level security middleware and tenant filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgREST:&lt;/strong&gt; API that enforces Postgres RLS automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Django Tenants:&lt;/strong&gt; Mature library for schema-per-tenant in Django&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale:&lt;/strong&gt; Database branching makes tenant testing easier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security Best Practices:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use Postgres RLS as your backup layer&lt;/li&gt;
&lt;li&gt;Choose an ORM that enforces tenant filtering by default&lt;/li&gt;
&lt;li&gt;Include tenant_id in all JWT tokens&lt;/li&gt;
&lt;li&gt;Create separate admin roles with explicit permissions&lt;/li&gt;
&lt;li&gt;Test with multiple tenants in every environment&lt;/li&gt;
&lt;li&gt;Log all cross-tenant access attempts&lt;/li&gt;
&lt;li&gt;Use database-level constraints where possible&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Why I chose multi-tenant
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I'm bootstrapping.&lt;/strong&gt; Single-tenant would be too expensive&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My users don't need custom setups.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I want to move fast.&lt;/strong&gt; One update for all tenants&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can always switch later&lt;/strong&gt; Big clients could get single-tenant setups.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  What Should You Do?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Go multi-tenant if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're bootstrapping or self-funded.&lt;/li&gt;
&lt;li&gt;You want to launch fast.&lt;/li&gt;
&lt;li&gt;Your users have similar needs.&lt;/li&gt;
&lt;li&gt;You don’t deal with heavy security rules.&lt;/li&gt;
&lt;li&gt;Infrastructure costs matter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Go Schema-per-Tenant If:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want better isolation than pure multi-tenant&lt;/li&gt;
&lt;li&gt;You plan to offer data export/backup per tenant&lt;/li&gt;
&lt;li&gt;You might need to migrate large clients later&lt;/li&gt;
&lt;li&gt;Your users have moderately different needs&lt;/li&gt;
&lt;li&gt;You're okay with more complex migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Go single-tenant if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You work with enterprise clients.&lt;/li&gt;
&lt;li&gt;You have funding for infrastructure.&lt;/li&gt;
&lt;li&gt;You need strict data separation.&lt;/li&gt;
&lt;li&gt;You offer custom setups.&lt;/li&gt;
&lt;li&gt;Clients demand it (and pay for it)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;I spent too much time reading about this when I should have been coding.&lt;/p&gt;

&lt;p&gt;Big SaaS companies like Notion, Vercel, Shopify, and Zoom also started simple and are still multi-tenant. Slack started multi-tenant, now hybrid.&lt;/p&gt;

&lt;p&gt;The real problem isn’t “choosing wrong”, it’s never launching at all.&lt;/p&gt;

&lt;p&gt;Many companies eventually use both: multi-tenant for most users and single-tenant for large clients like Stripe or GitHub.&lt;/p&gt;

&lt;p&gt;If you’re still deciding, start simple. Build, launch, and learn. You can always improve later.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>beginners</category>
      <category>saas</category>
    </item>
    <item>
      <title>I Thought My Backups Were Safe - Until I Tried Restoring One</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Mon, 06 Oct 2025 13:27:40 +0000</pubDate>
      <link>https://forem.com/highflyer910/i-thought-my-backups-were-safe-until-i-tried-restoring-one-387i</link>
      <guid>https://forem.com/highflyer910/i-thought-my-backups-were-safe-until-i-tried-restoring-one-387i</guid>
      <description>&lt;p&gt;Imagine this: you wake up one morning, open your app, and something's wrong. Your database is gone. Your files are corrupted. Or a simple bug deleted everything clean.&lt;/p&gt;

&lt;p&gt;You breathe a little easier because you have backups, right? Except… when you try to restore them, they don't work.&lt;br&gt;
They're empty. Or the format is broken. Or restoring takes forever.&lt;/p&gt;

&lt;p&gt;That's the painful truth I learned recently:&lt;br&gt;
&lt;strong&gt;A backup is worthless until you've restored it.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  My Wake-Up Call
&lt;/h2&gt;

&lt;p&gt;Last month, I was refactoring my side project's database schema.&lt;br&gt;
Something went wrong during migration, and I corrupted my SQLite database.&lt;/p&gt;

&lt;p&gt;No problem, I thought, I will just restore my weekly backup.&lt;br&gt;
But when I tried, the backup file was also corrupted.&lt;br&gt;
Later I found out I had been copying the database while the app was still running, catching it middle of writing.&lt;br&gt;
So my “backup system” was creating broken files for weeks, and I didn’t know because I never tested restoring one.&lt;/p&gt;

&lt;p&gt;I was lucky it was only dev data, but it scared me.&lt;br&gt;
If that had been production?&lt;br&gt;
If that had been production, I could have lost real users’ data, their work, and my project’s reputation.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Learned About Backups While Building Side Projects
&lt;/h2&gt;

&lt;p&gt;Before this, I thought backups were just a checklist: make a copy, sleep peacefully.&lt;br&gt;
But a backup is not magic protection.&lt;br&gt;
It’s just a copy of important things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧠 &lt;strong&gt;Your codebase&lt;/strong&gt; - GitHub or GitLab usually handle this&lt;/li&gt;
&lt;li&gt;🗄️ &lt;strong&gt;Your database&lt;/strong&gt; - Postgres, MySQL, Supabase, Firebase, SQLite…
&lt;/li&gt;
&lt;li&gt;🖼️ &lt;strong&gt;User files&lt;/strong&gt; - images, uploads, documents
&lt;/li&gt;
&lt;li&gt;🔐 &lt;strong&gt;Configs &amp;amp; secrets&lt;/strong&gt; - the &lt;code&gt;.env&lt;/code&gt; file, API keys, deployment settings &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real problem:&lt;br&gt;
You don't know if those backups are good until you test restoring them.&lt;br&gt;
Otherwise, it’s like having an emergency plan locked in a drawer.&lt;/p&gt;
&lt;h2&gt;
  
  
  How Bigger Teams Handle It
&lt;/h2&gt;

&lt;p&gt;I checked how bigger companies do backups.&lt;br&gt;
&lt;strong&gt;GitLab&lt;/strong&gt;, for example, runs restore tests every day and tracks success rates.&lt;br&gt;
&lt;strong&gt;Basecamp&lt;/strong&gt; even does “disaster tests,” pretending their main datacenter disappears.&lt;/p&gt;

&lt;p&gt;Usually, their process looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make backups automatically (nightly dumps, snapshots, etc)
&lt;/li&gt;
&lt;li&gt;Restore them into a test environment (not production)
&lt;/li&gt;
&lt;li&gt;Check if everything works: database starts, files open, users can log in&lt;/li&gt;
&lt;li&gt;Get alerts when something fails&lt;/li&gt;
&lt;li&gt;Simulate disasters to see how fast they can recover
Then I realized:
I don't need a datacenter, just a smaller version that fits my side project setup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And actually, many platforms like Supabase, Neon, and Vercel Postgres now have &lt;strong&gt;point-in-time recovery (PITR)&lt;/strong&gt; built in. Sometimes even free. So before you create your own backup scripts, check your dashboard first.&lt;/p&gt;
&lt;h3&gt;
  
  
  🧠 What to Back Up
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Backup Method&lt;/th&gt;
&lt;th&gt;Test Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Push to GitHub/GitLab (with 2FA!)&lt;/td&gt;
&lt;td&gt;Clone repo to a fresh folder: &lt;code&gt;git clone &amp;lt;repo&amp;gt; test-restore &amp;amp;&amp;amp; cd test-restore &amp;amp;&amp;amp; npm install &amp;amp;&amp;amp; npm run dev&lt;/code&gt;
&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;• SQLite: &lt;code&gt;sqlite3 mydb.db ".backup backup.db"&lt;/code&gt;&lt;br&gt;• Postgres: &lt;code&gt;pg_dump mydb &amp;gt; backup.sql&lt;/code&gt;&lt;br&gt;• Managed DBs: Use built-in exports or PITR&lt;/td&gt;
&lt;td&gt;Restore to test DB: &lt;code&gt;sqlite3 test.db &amp;lt; backup.sql&lt;/code&gt; then run a query to verify data. For Supabase/Neon, use their one-click export + local restore.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Files&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Store in &lt;strong&gt;Backblaze B2&lt;/strong&gt;, &lt;strong&gt;S3&lt;/strong&gt;, or &lt;strong&gt;Google Cloud Storage&lt;/strong&gt; (all offer free tiers). Avoid consumer Dropbox for production data.&lt;/td&gt;
&lt;td&gt;Download 3–5 random files monthly and verify they open. Bonus: enable &lt;strong&gt;object lock&lt;/strong&gt; or &lt;strong&gt;versioning&lt;/strong&gt; to prevent accidental deletion.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Configs &amp;amp; Secrets&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Store in a &lt;strong&gt;password manager&lt;/strong&gt; (1Password, Bitwarden) or &lt;strong&gt;encrypted offline vault&lt;/strong&gt; (VeraCrypt). &lt;strong&gt;Never in Git—even private repos!&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Run project locally using only saved configs. Use GitHub Secrets or &lt;code&gt;.env.vault&lt;/code&gt; for CI/CD, not raw &lt;code&gt;.env&lt;/code&gt; files.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;⚠️ &lt;em&gt;Important SQLite Tip:&lt;/em&gt;&lt;br&gt;
** Copying a live &lt;code&gt;.db&lt;/code&gt; file can damage it. Use &lt;code&gt;.backup&lt;/code&gt; or &lt;code&gt;VACUUM INTO&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; sqlite3 mydb.db &lt;span class="s2"&gt;"VACUUM INTO backup.db"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a clean, safe copy even while your app is running.&lt;br&gt;
💡 &lt;em&gt;Pro tip:&lt;/em&gt; Automate your test restore with a small script:&lt;br&gt;
For example:&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;#!/bin/bash&lt;/span&gt;
   &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;  &lt;span class="c"&gt;# Exit on any error&lt;/span&gt;
   &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; test.db
   sqlite3 test.db &lt;span class="s2"&gt;".read latest_backup.sql"&lt;/span&gt;
   sqlite3 test.db &lt;span class="s2"&gt;"SELECT count(*) FROM users;"&lt;/span&gt;
   &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ Restore test passed!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can even run it weekly on GitHub Actions, if it fails, you’ll get an alert.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⏰ When to Test
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Monthly (15–30 minutes)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restore one random backup locally
&lt;/li&gt;
&lt;li&gt;Download a few random user files&lt;/li&gt;
&lt;li&gt;Write down anything strange or failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quarterly (1–2 hours)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full local restore: DB + files + app&lt;/li&gt;
&lt;li&gt;Time it - how long does recovery take?&lt;/li&gt;
&lt;li&gt;Update your notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Yearly (half day)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pretend your laptop died
&lt;/li&gt;
&lt;li&gt;Can you restore &lt;em&gt;everything&lt;/em&gt; from your backups and docs?
&lt;/li&gt;
&lt;li&gt;It’s a great test for your memory and process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ &lt;em&gt;Privacy reminder:&lt;/em&gt; &lt;br&gt;
&lt;code&gt;If your app stores user data, make sure backups are encrypted at rest.&lt;br&gt;
Most cloud providers do this by default.&lt;br&gt;
And never keep plain emails or passwords in backups.&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;It’s easy to think backups are only for big companies.&lt;br&gt;
But if you're building side projects or working solo, losing data can kill your project one day. Users won’t wait while you say, &lt;em&gt;“Oops, I thought I had backups.”&lt;/em&gt;&lt;br&gt;
Now, backups aren’t about peace of mind when I make them;&lt;br&gt;
They’re about peace of mind when I &lt;strong&gt;restore&lt;/strong&gt; them.&lt;br&gt;
Because backups aren’t about paranoia, they’re about love for your future self. ❤️&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backup</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Passkeys Without the Pain: A Frontend Dev’s Guide</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Thu, 25 Sep 2025 11:08:56 +0000</pubDate>
      <link>https://forem.com/highflyer910/passkeys-without-the-pain-a-frontend-devs-guide-276k</link>
      <guid>https://forem.com/highflyer910/passkeys-without-the-pain-a-frontend-devs-guide-276k</guid>
      <description>&lt;p&gt;I was building my SaaS side project when I realized I needed to add passkeys. Everyone talks about better security and user experience, but I had no idea where to start. Would I need to rebuild my entire auth system? How complicated is this stuff?&lt;/p&gt;

&lt;p&gt;Turns out, it’s way simpler than I thought. You don’t need to break your existing login flow to add passkeys. Let me walk you through how I added them in a single weekend, kept everything working smoothly, and got ready for when users start signing up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Passkeys, Anyway?
&lt;/h2&gt;

&lt;p&gt;Passkeys are a secure, passwordless login method using biometrics (like your fingerprint or face scan) or device-based authentication. They’re basically the user-friendly names for the FIDO2 and WebAuthn standards. &lt;/p&gt;

&lt;p&gt;Here’s the gist: when you create a passkey, your device makes a unique cryptographic key pair. The private key stays locked on your device (protected by your fingerprint or PIN), while the public key goes to the server. During login, your device signs a challenge with the private key, and the server checks it with the public key.&lt;/p&gt;

&lt;p&gt;No passwords. No phishing. No credential stuffing (that’s when attackers reuse leaked passwords across multiple sites, hoping one works).&lt;/p&gt;

&lt;p&gt;Think of them as a futuristic “Login with Google” button, minus the third-party dependency and with top-notch security. Plus, they can sync across devices through providers like Apple iCloud Keychain, Google Password Manager, and 1Password. Cross-device sync is a game-changer.&lt;br&gt;
Important note: the PIN or biometric you use doesn’t sync. That’s just the local way your device unlocks its stored keys. The actual passkey itself is what gets synced through your Apple/Google/1Password account.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Simplest Way to Add Passkeys (That Actually Works)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Add One Button
&lt;/h3&gt;

&lt;p&gt;I added a single passkey button to my login form—no database chaos, no 3 a.m. panic sessions.&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="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Only show if browser supports passkeys */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;supportsPasskeys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handlePasskeyLogin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;w-full border-2 border-dashed border-gray-300 p-3 rounded-lg hover:border-gray-500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="err"&gt;🔐&lt;/span&gt; &lt;span class="nx"&gt;Try&lt;/span&gt; &lt;span class="nx"&gt;Passkey&lt;/span&gt; &lt;span class="nx"&gt;Login&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Let Users Opt In (But Encourage It)
&lt;/h3&gt;

&lt;p&gt;I don’t force passkeys on signup, but I nudge users after a few password logins: "Use a passkey. Skip the password."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Use a Service (You've Got Features to Ship)
&lt;/h3&gt;

&lt;p&gt;I could’ve spent months wrestling with WebAuthn docs, but I’m pre-revenue and need to ship. After some research, I picked Clerk; their Next.js integration is smooth, and the passkey setup is painless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other solid options:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stytch&lt;/strong&gt;:  Great for startups, generous free tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth0&lt;/strong&gt;: The enterprise choice, now with much better passkey UX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corbado&lt;/strong&gt;: Super fast setup, great for MVPs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descope&lt;/strong&gt;: Most flexible for custom flows, has a visual workflow builder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase Auth&lt;/strong&gt;: Passkeys now supported if you’re already on Supabase.
Integration took me ~2 hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;p&gt;❌ &lt;strong&gt;Forcing passkeys on everyone&lt;/strong&gt;: Don't. Make it optional but encouraged.&lt;br&gt;
❌ &lt;strong&gt;Overhauling the database&lt;/strong&gt;: Just add a table for passkey data (public key, credential ID, device info). Existing users won't notice.&lt;br&gt;
❌ &lt;strong&gt;Learning WebAuthn from scratch&lt;/strong&gt;: Use a service and ship faster.&lt;br&gt;
❌ &lt;strong&gt;Ignoring backup scenarios&lt;/strong&gt;: Plan for when users lose devices.&lt;br&gt;
⚠️ &lt;strong&gt;Keep in mind&lt;/strong&gt;: Passkeys require HTTPS (localhost works for testing). Custom local domains need proper HTTPS, or WebAuthn will throw errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Service Reliability &amp;amp; Vendor Lock-in
&lt;/h3&gt;

&lt;p&gt;If your passkey provider goes down, most have 99.9% uptime, but keep email/password as a backup. Export user data regularly so you can switch providers if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  User Management Features
&lt;/h3&gt;

&lt;p&gt;Users should manage passkeys like passwords. I added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;View registered devices&lt;/strong&gt;: "iPhone 17, MacBook Pro, Windows Desktop"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove old passkeys&lt;/strong&gt;: For lost/replaced devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passkey naming&lt;/strong&gt;: "Work Laptop" vs "Personal Phone"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage analytics&lt;/strong&gt;: Show last login times per device&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why You Should Add Passkeys Now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User expectation&lt;/strong&gt;: Feels modern, missing it feels outdated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitive edge&lt;/strong&gt;: Still ahead of many apps, but not for long.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security story&lt;/strong&gt;: Great for investors and enterprise clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support costs&lt;/strong&gt;: Less password reset chaos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better conversion&lt;/strong&gt;: Easy login = better retention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future compliance&lt;/strong&gt;: Some industries will require strong authentication.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your Playbook for Adding Passkeys
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start small&lt;/strong&gt;: Add the login button and basic flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a service&lt;/strong&gt;: Stytch for startups, Clerk for full-stack, Auth0 for enterprise, Corbado for speed. Don’t reinvent the wheel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it optional but encourage it&lt;/strong&gt;: Nudges &amp;gt; forcing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test everywhere&lt;/strong&gt;: Mobile, desktop, cross-device sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan for problems&lt;/strong&gt;: Lost devices, account recovery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track adoption&lt;/strong&gt;: See what works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship it fast&lt;/strong&gt;: This isn’t just a nice-to-have anymore.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;It took me one weekend to add passkeys, and now my app is ready for users. Better security, smoother UX, and fewer support headaches.&lt;/p&gt;

&lt;p&gt;Building something cool? I’d love to hear what you’re working on. If you’ve added passkeys to your project, drop a comment with your experience. I’m always learning too.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The Multi-Tab Logout Problem Nobody Warned You About</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Mon, 25 Aug 2025 11:14:42 +0000</pubDate>
      <link>https://forem.com/highflyer910/the-multi-tab-logout-problem-nobody-warned-you-about-2jil</link>
      <guid>https://forem.com/highflyer910/the-multi-tab-logout-problem-nobody-warned-you-about-2jil</guid>
      <description>&lt;p&gt;Picture this: you’re using your favorite web app. You have three tabs open — one with reports, one editing a document, and one checking settings.&lt;/p&gt;

&lt;p&gt;In the settings tab, you click Logout.&lt;br&gt;
You think you’re done… but when you switch back to the other tabs, surprise! They still look logged in. You can click buttons, type in forms, and maybe even see private data.&lt;/p&gt;

&lt;p&gt;This is the multi-tab session problem. And it’s more common than you think.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why This Problem Happens
&lt;/h2&gt;

&lt;p&gt;Browsers don't automatically tell every tab that you've logged out in another one.&lt;br&gt;
Yes, cookies are shared across tabs, but your app’s JavaScript in each tab doesn’t know what happened until it talks to the server again.&lt;/p&gt;

&lt;p&gt;So you end with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Tab A&lt;/strong&gt; -&amp;gt; You're officially logged out (server knows, UI knows)&lt;br&gt;
&lt;strong&gt;- Tab B&lt;/strong&gt; -&amp;gt; Your UI doesn't know what happened and hasn't updated yet&lt;/p&gt;

&lt;p&gt;Result: a weird, broken state that can confuse users or even expose private data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real-Life Example
&lt;/h2&gt;

&lt;p&gt;Imagine a design tool with a subscription:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Tab 1&lt;/strong&gt;: You’re using a premium feature.&lt;br&gt;
&lt;strong&gt;- Tab 2&lt;/strong&gt;: You cancel your subscription in settings.&lt;br&gt;
&lt;strong&gt;- What happens?&lt;/strong&gt; Tab 1 still lets you use the premium feature until refresh.&lt;/p&gt;

&lt;p&gt;Bad for business (free features) and bad for the user (sudden “Access Denied” when saving). Everyone loses.&lt;/p&gt;
&lt;h2&gt;
  
  
  How Developers Fix It
&lt;/h2&gt;

&lt;p&gt;The trick is simple: make your tabs talk to each other.&lt;br&gt;
&lt;strong&gt;Step 1&lt;/strong&gt;: When something important changes (like logging out), store that change in &lt;code&gt;localStorage&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;Step 2&lt;/strong&gt;: Add a listener in every tab that watches for these changes:&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="nb"&gt;window&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;storage&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;event&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="c1"&gt;// Check for our specific key and that it was set to 'loggedOut'&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authStatus&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newValue&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loggedOut&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;// Could reload, redirect, or update state&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&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;Now, if one tab logs out and sets &lt;code&gt;authStatus = 'loggedOut'&lt;/code&gt;, the other tabs instantly know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach&lt;/strong&gt;: instead of always reloading (which can be annoying), you can show a message like: &lt;strong&gt;"You’ve been logged out"&lt;/strong&gt; → redirect to login page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to Remember
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This only works for the same domain (yourapp.com).&lt;/li&gt;
&lt;li&gt;The storage event fires only in other tabs, not the one that made the change.&lt;/li&gt;
&lt;li&gt;For some apps, you may also want to sync session data between tabs, but that’s extra work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you’re building a web app with accounts, don’t forget the multi-tab case. Users won’t thank you when it works… but they’ll notice when it doesn’t.&lt;/p&gt;

&lt;p&gt;Because the only thing worse than a broken logout is working inside a ghost session that died 20 minutes ago. 👻&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>frontend</category>
      <category>programming</category>
    </item>
    <item>
      <title>Creating Text Shadows in CSS: Simple to Advanced Techniques</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Fri, 15 Aug 2025 13:55:06 +0000</pubDate>
      <link>https://forem.com/highflyer910/creating-text-shadows-in-css-simple-to-advanced-techniques-59j2</link>
      <guid>https://forem.com/highflyer910/creating-text-shadows-in-css-simple-to-advanced-techniques-59j2</guid>
      <description>&lt;p&gt;Ever wanted to add beautiful shadows to your text? CSS offers different methods depending on how complex you want your shadows to be. Let's explore both simple and advanced techniques!&lt;/p&gt;

&lt;h2&gt;
  
  
  The Simple Way: &lt;code&gt;text-shadow&lt;/code&gt; (Best for Most Cases)
&lt;/h2&gt;

&lt;p&gt;For basic shadows, use the built-in &lt;code&gt;text-shadow&lt;/code&gt; property:&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="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0.3&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;Why this is awesome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Super easy to write&lt;/li&gt;
&lt;li&gt;Works in all modern browsers&lt;/li&gt;
&lt;li&gt;Best performance&lt;/li&gt;
&lt;li&gt;Perfect for standard shadows&lt;/li&gt;
&lt;li&gt;Supports multiple shadows in one rule (e.g., add a glow with &lt;code&gt;-2px -2px 4px rgba(255,255,255,0.3)&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When You Need Fancy Shadows: The &lt;code&gt;data-text&lt;/code&gt; Technique
&lt;/h2&gt;

&lt;p&gt;Sometimes you want special effects like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradient shadows that match gradient text&lt;/li&gt;
&lt;li&gt;Multiple shadow layers for depth&lt;/li&gt;
&lt;li&gt;Advanced visual effects&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Problem With Duplicating Text
&lt;/h2&gt;

&lt;p&gt;When creating complex shadows, you might duplicate text in HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;
  I ♥ coding
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"shadow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;I ♥ coding&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works but creates messy code that's hard to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Cleaner Way: Using &lt;code&gt;data-text&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here's a better way using a custom HTML attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;data-text=&lt;/span&gt;&lt;span class="s"&gt;"I ♥ coding"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;I ♥ coding&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your 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="nt"&gt;h1&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;relative&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="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;135deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#667eea&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#764ba2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-background-clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-text-fill-color&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;color&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="c"&gt;/* If -webkit-text-fill-color fails */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data-text&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;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&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="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;135deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
             &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;126&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
             &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;162&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-background-clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-text-fill-color&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;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translate&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="m"&gt;3px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;attr(data-text)&lt;/code&gt; means you only write the text once in HTML. If you later change the text, you don’t have to edit multiple places.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;data-text&lt;/code&gt; attribute: Stores your text in HTML&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;attr(data-text)&lt;/code&gt;: CSS grabs the text from the attribute&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;::before&lt;/code&gt;: Creates a shadow layer behind the real text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;transform&lt;/code&gt;: Moves the shadow slightly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;filter: blur()&lt;/code&gt;: Makes the shadow soft&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pro Tip: Use &lt;code&gt;filter: blur()&lt;/code&gt; sparingly. It’s rendered on the GPU but can cause jank on lower-end devices. Avoid animating elements with &lt;code&gt;blur&lt;/code&gt; or &lt;code&gt;backdrop-filter&lt;/code&gt;.&lt;br&gt;
Also, &lt;code&gt;backdrop-filter&lt;/code&gt; has limited support in some older browsers, especially Firefox on Android.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding a Second Shadow Layer
&lt;/h2&gt;

&lt;p&gt;For extra depth, add another layer with &lt;code&gt;::after&lt;/code&gt;:&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="nt"&gt;h1&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data-text&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;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-2&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="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;135deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
             &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;126&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
             &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;162&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-background-clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-text-fill-color&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;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translate&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="m"&gt;5px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&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="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.6&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;Note: Stacking pseudo-elements like this adds a bit of rendering complexity, so reserve it for cases where &lt;code&gt;text-shadow&lt;/code&gt; isn’t enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important Things to Know
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Browser Support&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works in Chrome, Firefox, Edge, and Safari&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-webkit-&lt;/code&gt; prefixes (like &lt;code&gt;-webkit-background-clip&lt;/code&gt;) are mainly for older browsers&lt;/li&gt;
&lt;li&gt;Always test your designs in different browsers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance Tips&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid animating these effects (can slow down your page)&lt;/li&gt;
&lt;li&gt;Too many &lt;code&gt;filter: blur&lt;/code&gt; effects may lag on low-end devices&lt;/li&gt;
&lt;li&gt;For simple shadows, &lt;code&gt;text-shadow&lt;/code&gt; is always faster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;These effects are visual only and won't affect screen readers&lt;/li&gt;
&lt;li&gt;Make sure your text has enough contrast with the background&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Alternative Methods
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;drop-shadow&lt;/code&gt; Filter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For some effects, you can try:&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;.filter-shadow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;drop-shadow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0.3&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;&lt;code&gt;drop-shadow&lt;/code&gt; works better than &lt;code&gt;text-shadow&lt;/code&gt; for see-through text or images, because it keeps the shadow looking clean around transparent parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple &lt;code&gt;text-shadow&lt;/code&gt; Layers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For simple gradient-like effects:&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;.multi-shadow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
    &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;126&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;162&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.3&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;
  
  
  When to Use Each Method
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Best Technique&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;Simple shadows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text-shadow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Easy to write, best performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple solid layers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Multiple text-shadow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Good balance of simplicity and effect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradient shadows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;data-text&lt;/code&gt; method&lt;/td&gt;
&lt;td&gt;Only way to achieve gradient shadows behind gradient text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance critical&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text-shadow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No pseudo-elements or complex rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex animations&lt;/td&gt;
&lt;td&gt;&lt;code&gt;filter: drop-shadow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Can be GPU accelerated; works well with transforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;All methods&lt;/td&gt;
&lt;td&gt;Ensure high contrast (4.5:1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Check out all these shadow techniques with working examples and copy-paste code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://codepen.io/HighFlyer/pen/qEOpVxL?editors=1010" rel="noopener noreferrer"&gt;Demo on CodePen →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Text shadows can take your design from plain to wow. Start with the simple &lt;code&gt;text-shadow&lt;/code&gt; for most cases, and level up to the &lt;code&gt;data-text&lt;/code&gt; technique when you need those fancy gradient effects. &lt;br&gt;
Happy coding!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>tutorial</category>
      <category>frontend</category>
    </item>
    <item>
      <title>SphereConnect: Creating Team Vibes for Axero's Intranet Challenge</title>
      <dc:creator>Thea</dc:creator>
      <pubDate>Tue, 22 Jul 2025 12:16:36 +0000</pubDate>
      <link>https://forem.com/highflyer910/sphereconnect-creating-team-vibes-for-axeros-intranet-challenge-41jb</link>
      <guid>https://forem.com/highflyer910/sphereconnect-creating-team-vibes-for-axeros-intranet-challenge-41jb</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend/axero"&gt;Frontend Challenge: Office Edition sponsored by Axero, Holistic Webdev: Office Space&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Meet SphereConnect! It's an intranet dashboard I created for Axero's "Holistic Webdev: Office Space" challenge, and honestly, I had way too much fun building it. Think of it as your team's digital meeting place where working together feels natural instead of forced.&lt;/p&gt;

&lt;p&gt;Why SphereConnect? Because I wanted something that showed how it brings people together in one connected space, like a digital circle where ideas flow freely and everyone stays connected. Perfect match for what Axero had in mind!&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Explore SphereConnect live!&lt;br&gt;&lt;br&gt;
🌐 &lt;strong&gt;Live Demo&lt;/strong&gt;: &lt;a href="https://sphere-connect.vercel.app/" rel="noopener noreferrer"&gt;SphereConnect&lt;/a&gt;&lt;br&gt;&lt;br&gt;
📂 &lt;strong&gt;GitHub Repo&lt;/strong&gt;: &lt;a href="https://github.com/highflyer910/sphereconnect" rel="noopener noreferrer"&gt;github.com/highflyer910/sphereconnect&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

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

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

&lt;p&gt;&lt;strong&gt;Mobile View&lt;/strong&gt;: A responsive 1-column layout, ensuring usability on the go.&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxevjnfqaokbwaom46cw7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxevjnfqaokbwaom46cw7.png" alt="Mobile View" width="800" height="1670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Development Adventure
&lt;/h2&gt;

&lt;p&gt;Building SphereConnect was like putting together a fun puzzle - each piece had to work perfectly with the others while making teams' lives easier. Let me walk you through how it all came together!&lt;/p&gt;

&lt;h3&gt;
  
  
  The name
&lt;/h3&gt;

&lt;p&gt;Choosing SphereConnect was pretty fun. I kept thinking about how the best workspaces feel like this connected space where everyone understands each other and ideas flow naturally. Plus, it sounds modern and friendly, exactly what I was going for!&lt;/p&gt;

&lt;h3&gt;
  
  
  Process
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Big Picture&lt;/strong&gt;: I read Axero's requirements and got excited about creating something that would make people want to use their intranet. I planned out widgets for Announcements, Events, Team Spotlight, New Hires, Quick Resources, Tasks, and even a dual-mode Chat system. Oh, and that Welcome section with live weather? That was my "let's make this personal" moment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;My Toolkit&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vite-React&lt;/strong&gt; (because life's too short for slow builds:))&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Material-UI (MUI)&lt;/strong&gt; (consistent and beautiful)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lucide-React&lt;/strong&gt; (clean icons)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WeatherAPI.com&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel Analytics&lt;/strong&gt; (gotta know if people use this thing!)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Making It Pretty&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Theme toggle was a must-have since some people prefer light mode, others prefer dark mode&lt;/li&gt;
&lt;li&gt;Added small hover effects and smooth animations because details matter&lt;/li&gt;
&lt;li&gt;Used a widget-based layout that feels familiar but not boring&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Development&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Welcome Section&lt;/strong&gt;: Greets you with real weather and time (with a simple "allow location" button for privacy-conscious users)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat Widget&lt;/strong&gt;: Team chat for those "quick question" moments, plus an AI Assistant (though it's fake AI for now)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick Resources&lt;/strong&gt;: One-click access to all the tools you use - Time Tracker, Launch Pad, you name it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal Touch&lt;/strong&gt;: Resources that change based on who's logged in, including some of my dev.to posts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tasks Section&lt;/strong&gt;: Create, track, and remember your tasks (thanks, localStorage!)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team Spotlight&lt;/strong&gt;: A slideshow that makes everyone feel special&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New Hire Welcome&lt;/strong&gt;: Because starting somewhere new shouldn't feel scary&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Made sure it works well with screen readers and keyboard navigation&lt;/li&gt;
&lt;li&gt;Added alt text for avatars and icons.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Responsiveness&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Responsive grid that goes from 3 columns to 2 to 1 as screens get smaller&lt;/li&gt;
&lt;li&gt;Tested on my phone, tablet, laptop, and it works everywhere!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testing&lt;/strong&gt;: Clicked every button, tried every feature, and tested it on many different browsers.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What This Taught Me
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vite is amazing&lt;/strong&gt;: The development speed is so much faster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MUI Theming is Powerful&lt;/strong&gt;: Once you learn it, you can make everything look exactly how you want&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility Matters&lt;/strong&gt;: Making sure everyone can use your site is important&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive Design takes time&lt;/strong&gt;: Getting those screen size changes just right took longer than expected, but it was worth it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Future Plans
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real Navigation&lt;/strong&gt;: Those navbar links need React Router to work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actual AI&lt;/strong&gt;: Time to replace the fake responses with a real language model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Languages&lt;/strong&gt;: Adding language options because good design shouldn't be limited by language&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Thanks to Axero
&lt;/h3&gt;

&lt;p&gt;Huge thanks to Axero for creating such an inspiring challenge! It made me think beyond just "another dashboard" and consider what makes teams work well together. Plus, I got to build something I'm genuinely proud of, which is always great.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Legal Stuff&lt;/strong&gt;: MIT License for the open-source community, and Axero gets to showcase this work while I keep ownership. Fair deal all around!&lt;/p&gt;

&lt;p&gt;Thanks for taking the time to check out SphereConnect! Building it was fun, and I hope exploring it brings a little joy to your day.✨&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>css</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
