<?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>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>
