<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zarar Ahmed</title>
    <description>The latest articles on DEV Community by Zarar Ahmed (@rucs0).</description>
    <link>https://dev.to/rucs0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036663%2F83a71440-548b-44e1-85a9-72bd1c62608b.png</url>
      <title>DEV Community: Zarar Ahmed</title>
      <link>https://dev.to/rucs0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rucs0"/>
    <language>en</language>
    <item>
      <title>There is not a single image file in my template store</title>
      <dc:creator>Zarar Ahmed</dc:creator>
      <pubDate>Wed, 12 Aug 2026 02:28:11 +0000</pubDate>
      <link>https://dev.to/rucs0/there-is-not-a-single-image-file-in-my-template-store-g96</link>
      <guid>https://dev.to/rucs0/there-is-not-a-single-image-file-in-my-template-store-g96</guid>
      <description>&lt;p&gt;I sell Next.js templates. Every product needs cover art, and the obvious way to do that is to screenshot each template, export a few sizes, and ship the images.&lt;/p&gt;

&lt;p&gt;I didn't do that. There is no &lt;code&gt;public/images&lt;/code&gt; directory in the storefront. No PNG, no JPG, no WebP, no SVG illustration. Every product's cover art is drawn at render time from &lt;strong&gt;two numbers and a string&lt;/strong&gt; in a TypeScript file.&lt;/p&gt;

&lt;p&gt;Here is the entire visual configuration for a product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ledgerly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ledgerly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;hues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;160&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="nx"&gt;motif&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dashboard&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;code&gt;hues&lt;/code&gt; is a pair of oklch hue angles. &lt;code&gt;motif&lt;/code&gt; picks which abstract layout to draw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;The honest answer is that I was avoiding a chore, and it turned into an architecture decision.&lt;/p&gt;

&lt;p&gt;Screenshots rot. Every time I changed a template's hero section, its cover art was subtly wrong. I'd have to re-shoot, re-export, re-optimise, re-upload. With eight products that's a small job I would do badly and late.&lt;/p&gt;

&lt;p&gt;Drawing the art from data removed the chore entirely. Adding a template is now one object in a catalog file, and the art comes with it.&lt;/p&gt;

&lt;p&gt;The side effects turned out to matter more than the thing I was avoiding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing to lay out badly.&lt;/strong&gt; No image means no intrinsic size to guess at, so no layout shift while covers load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing to download.&lt;/strong&gt; The covers cost zero network bytes. They're DOM nodes with inline styles, compressed along with the rest of the HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebranding is a find-and-replace.&lt;/strong&gt; Changing a product's palette is editing two integers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that actually makes it work: oklch
&lt;/h2&gt;

&lt;p&gt;If you do this in HSL, you get a mess. &lt;code&gt;hsl(60 80% 50%)&lt;/code&gt; — yellow — is visually much lighter than &lt;code&gt;hsl(240 80% 50%)&lt;/code&gt; — blue — even though both claim 50% lightness. So a card generated at hue 60 looks washed out next to one at hue 240, and you end up hand-correcting every product. At which point you have not saved yourself anything.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;oklch()&lt;/code&gt; is perceptually uniform: the same lightness value looks like the same lightness to a human eye across every hue. That is the single property that makes generated art viable, because it means one set of lightness and chroma values works for &lt;em&gt;every&lt;/em&gt; product, and only the hue changes.&lt;/p&gt;

&lt;p&gt;The whole color system is one helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`oklch(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`oklch(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; / &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then everything is expressed relative to the product's two hues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h-4 w-12 rounded-full"&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`linear-gradient(90deg, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0.6&lt;/code&gt; lightness and &lt;code&gt;0.2&lt;/code&gt; chroma are tuned once, by eye, on one product. They then look correct on all of them. That is the entire trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motifs, or: don't draw the template, evoke it
&lt;/h2&gt;

&lt;p&gt;The second decision was what to actually draw.&lt;/p&gt;

&lt;p&gt;The temptation is to render a tiny faithful replica of each template. Don't. It's enormous work, it looks like a broken screenshot at small sizes, and it drifts out of date exactly like the screenshots you were trying to avoid.&lt;/p&gt;

&lt;p&gt;Instead each motif is an abstract arrangement of blocks suggesting a &lt;em&gt;kind&lt;/em&gt; of interface. The &lt;code&gt;dashboard&lt;/code&gt; motif is a narrow sidebar, a row of stat tiles, and a bar chart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex min-h-0 flex-1 items-end gap-1 rounded-md bg-white/[0.04] p-2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 rounded-sm"&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those bar heights are hardcoded. They're not data, they're composition: a shape chosen because it reads as "chart" at 300px wide. Random heights look like noise; a designed sequence looks intentional.&lt;/p&gt;

&lt;p&gt;Neutral elements use plain white at low alpha (&lt;code&gt;bg-white/[0.06]&lt;/code&gt;) rather than the brand hue. If everything is tinted, nothing reads as accent. The hues are spent only where they carry meaning: the active nav item, the primary button, the chart.&lt;/p&gt;

&lt;p&gt;Each motif sits inside a shared browser frame which does a lot of work for very little code. It tells you "this is a website" instantly, so the abstract shapes inside are read as an interface rather than as decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is a bad idea
&lt;/h2&gt;

&lt;p&gt;It works here because I'm selling &lt;em&gt;interfaces&lt;/em&gt;, and interfaces are made of rectangles. Abstract rectangles evoke them honestly.&lt;/p&gt;

&lt;p&gt;It would be a terrible approach for a photography portfolio, an illustration marketplace, or anything where the specific visual content is the product. You cannot procedurally generate a photograph, and you shouldn't try.&lt;/p&gt;

&lt;p&gt;There's also a real trade-off I accepted: a generated cover tells you the &lt;em&gt;category&lt;/em&gt;, not the &lt;em&gt;design&lt;/em&gt;. It can't sell the actual template. That's fine for me because every product links to a complete live demo, so the cover only has to get someone to click. If you have no demo, a real screenshot is probably doing more work than a pretty abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If your product art is systematic — categories, tiers, types — consider whether it can be derived from data instead of drawn by hand. You get consistency for free, and adding a product stops involving a design task.&lt;/p&gt;

&lt;p&gt;Start with &lt;code&gt;oklch&lt;/code&gt;. Without perceptual uniformity you'll spend all the time you saved hand-correcting colors.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write this stuff while building &lt;a href="https://nocturneui.com" rel="noopener noreferrer"&gt;Nocturne&lt;/a&gt;, a store of dark, animation-heavy Next.js templates. The free starter is MIT and has the same token system if you want to poke at it: &lt;a href="https://github.com/Rucs0/nocturne-ember" rel="noopener noreferrer"&gt;github.com/Rucs0/nocturne-ember&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>css</category>
      <category>tailwindcss</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a storefront that works with zero API keys, and it made the code better</title>
      <dc:creator>Zarar Ahmed</dc:creator>
      <pubDate>Sun, 19 Jul 2026 15:33:42 +0000</pubDate>
      <link>https://dev.to/rucs0/i-built-a-storefront-that-works-with-zero-api-keys-and-it-made-the-code-better-309i</link>
      <guid>https://dev.to/rucs0/i-built-a-storefront-that-works-with-zero-api-keys-and-it-made-the-code-better-309i</guid>
      <description>&lt;p&gt;I sell Next.js templates. The store takes payments through Stripe, stores orders in Postgres, keeps ZIPs in object storage, and emails download links through Resend.&lt;/p&gt;

&lt;p&gt;It also runs perfectly with none of those configured. Clone it, &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;npm run dev&lt;/code&gt;, and you get a working storefront: you can browse the catalog, "buy" a template, and download a real ZIP file. No &lt;code&gt;.env&lt;/code&gt;. No accounts. No&lt;br&gt;
keys.&lt;/p&gt;

&lt;p&gt;That started as a convenience. It turned into the single best architectural decision in the codebase, and it's the reason I can hand the repo to someone and have them productive in ninety seconds.&lt;/p&gt;
&lt;h3&gt;
  
  
  The rule
&lt;/h3&gt;

&lt;p&gt;Every integration is optional and independently detected. That's the whole idea, and it lives in about eight lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;integrations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3_BUCKET&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/** True when Stripe isn't configured - checkout simulates a purchase. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;demoMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that these are four independent booleans, not one &lt;code&gt;NODE_ENV&lt;/code&gt; check and not a single &lt;code&gt;DEMO=true&lt;/code&gt; flag. That distinction is what makes it useful rather than cute. You can run with a real database and no Stripe. Or real Stripe and no object storage. Each combination is a valid, working configuration, because every consumer degrades on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  What "degrades" actually means
&lt;/h3&gt;

&lt;p&gt;The temptation with a flag like this is to throw a banner up and disable things. That's not degradation, that's a broken site with an apology on it. The rule I settled on: &lt;strong&gt;the user-visible flow must complete.&lt;/strong&gt; Not just "show an&lt;br&gt;
error nicely".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No database.&lt;/strong&gt; The product catalog is a TypeScript file. When a database is connected it mirrors that catalog into a table and lets rows override commerce fields — price, name, active. When it isn't, the catalog &lt;em&gt;is&lt;/em&gt; the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProducts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Template&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;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getDb&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;db&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;listedTemplates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&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;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;listedTemplates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...merge DB overrides onto the static catalog&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;products: falling back to static catalog:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;listedTemplates&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 &lt;code&gt;catch&lt;/code&gt; matters more than the &lt;code&gt;if&lt;/code&gt;. A database that's configured but unreachable degrades to exactly the same place as a database that was never configured. Production has been down to that path during a Postgres restart&lt;br&gt;
and the store stayed browsable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Stripe.&lt;/strong&gt; Checkout signs a short-lived access grant and redirects to the success page, which is the same page a real purchase lands on. You get the same confirmation, the same download button, the same email prompt. Two hours&lt;br&gt;
of validity instead of a permanent entitlement, and every token is stamped &lt;code&gt;demo: true&lt;/code&gt; so nothing downstream can mistake one for a real sale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No object storage.&lt;/strong&gt; This is my favourite one, because the honest answer was harder than the lazy one. The lazy version returns a 404 or a text file named &lt;code&gt;template.zip&lt;/code&gt;. The download is the product; a fake download makes the whole demo worthless.&lt;/p&gt;

&lt;p&gt;So the repo contains a small ZIP writer — about 190 lines, store method, no compression — that generates a &lt;em&gt;valid&lt;/em&gt; archive on the fly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fixed DOS date: 2026-01-01, so archives are byte-stable.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DOS_DATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1980&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a real &lt;code&gt;.zip&lt;/code&gt; that a real unzip program opens, containing a README explaining that storage isn't configured. It is genuinely a working download,&lt;br&gt;
which is the point.&lt;/p&gt;

&lt;p&gt;That writer earned its keep later. Every real download now streams the ZIP through the app and injects a per-buyer license file, using the same code to append a stored entry to an existing archive without recompressing it. I&lt;br&gt;
wouldn't have written a ZIP encoder to solve watermarking. I wrote it to make demo mode honest, and watermarking came free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No email.&lt;/strong&gt; Here the degradation is deliberately &lt;em&gt;not&lt;/em&gt; transparent, and it's worth explaining why.&lt;/p&gt;

&lt;p&gt;The library flow, enter your email and get a magic link to your purchases, sends the link to the address on file. That's the entire security model: having the link is tied to controlling the inbox. With no mail provider, "degrading gracefully" would mean returning the link in the HTTP response, and that would hand anyone the library for any address they can spell.&lt;/p&gt;

&lt;p&gt;So this one fails closed, and it fails &lt;em&gt;identically for every input&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="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;integrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&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="s2"&gt;Library links are temporarily unavailable. Please contact support.&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;503&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 same endpoint also returns an identical response whether or not an address has purchases, so it can't be used to enumerate who my customers are.&lt;/p&gt;

&lt;p&gt;That's the real lesson. Demo mode is not "make everything work anyway." It's "decide, per integration, what the honest fallback is" — and sometimes the&lt;br&gt;
honest fallback is a clean refusal.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Onboarding is &lt;code&gt;npm install&lt;/code&gt;.&lt;/strong&gt; No credential-sharing ritual, no &lt;code&gt;.env.example&lt;/code&gt; scavenger hunt to get a first page render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most development doesn't touch a paid API.&lt;/strong&gt; Building the checkout UI, the success page, the library, the download flow — none of it needs Stripe keys. Test-mode webhooks are for verifying the integration, not for laying out a&lt;br&gt;
button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure paths are the default path.&lt;/strong&gt; This is the one I didn't anticipate. In a normal codebase the "database is down" branch is written once, never executed, and quietly rots. Here it's the branch that runs every time I start&lt;br&gt;
the dev server. It cannot rot, because it's the one I use most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It forced a real answer about the source of truth.&lt;/strong&gt; "What happens with no database?" is unanswerable if the database &lt;em&gt;is&lt;/em&gt; the product data. Making demo mode work meant deciding that code is canonical and the database holds&lt;br&gt;
overrides. That's a better architecture, and I only reached it because the constraint refused to let me be vague.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it costs
&lt;/h3&gt;

&lt;p&gt;Two things, honestly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every integration point needs a branch&lt;/strong&gt;, and each branch is a decision, not a default. Four integrations across roughly 5,500 lines is very manageable.&lt;br&gt;
Twenty would not be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unexercised branches can lie.&lt;/strong&gt; The fallback paths run constantly in development, but the &lt;em&gt;real&lt;/em&gt; paths only run in production. I've shipped a bug where the demo path worked and the real one didn't — the mirror image of the&lt;br&gt;
usual problem, and just as annoying. Demo mode is not a substitute for integration tests against the real thing.&lt;/p&gt;

&lt;p&gt;The tradeoff has been overwhelmingly worth it, but "every integration is optional" is a discipline with a running cost, not a free lunch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bit I'd steal for any project
&lt;/h3&gt;

&lt;p&gt;Skip the storefront specifics — the transferable idea is one sentence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detect each dependency independently, and define an honest fallback for each one, where "honest" sometimes means refusing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not one &lt;code&gt;DEMO&lt;/code&gt; flag. Not &lt;code&gt;NODE_ENV === "development"&lt;/code&gt;. A boolean per integration, and a real decision about what the product does without it. You'll end up with software that runs anywhere, onboards in one command, and —&lt;br&gt;
because the degraded paths are the ones you use daily — degrades correctly when it counts.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I built &lt;a href="https://nocturneui.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=demo-mode" rel="noopener noreferrer"&gt;Nocturne&lt;/a&gt;, a small collection of dark, animation-heavy Next.js templates. The free starter, &lt;a href="https://github.com/Rucs0/nocturne-ember" rel="noopener noreferrer"&gt;Ember&lt;/a&gt;, is MIT on GitHub&lt;br&gt;
and uses the same design system — no email required, though there's a form if you'd rather have the ZIP.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
