<?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: David Bartalos</title>
    <description>The latest articles on DEV Community by David Bartalos (@dbartalos).</description>
    <link>https://dev.to/dbartalos</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%2F3936644%2F0fa9b784-f271-4f90-b49c-b91333078f03.png</url>
      <title>DEV Community: David Bartalos</title>
      <link>https://dev.to/dbartalos</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dbartalos"/>
    <language>en</language>
    <item>
      <title>The Hosting Rejection Tour: Render, AWS EC2, Oracle, and How I Ended Up on a €3.29/mo VPS</title>
      <dc:creator>David Bartalos</dc:creator>
      <pubDate>Tue, 19 May 2026 12:17:00 +0000</pubDate>
      <link>https://dev.to/dbartalos/the-hosting-rejection-tour-render-aws-ec2-oracle-and-how-i-ended-up-on-a-eu329mo-vps-lp2</link>
      <guid>https://dev.to/dbartalos/the-hosting-rejection-tour-render-aws-ec2-oracle-and-how-i-ended-up-on-a-eu329mo-vps-lp2</guid>
      <description>&lt;p&gt;Before the stack worked, three hosting options failed. Each one looked perfect on paper. Each one had a specific, concrete reason it couldn't work. This is the rejection tour.&lt;/p&gt;

&lt;p&gt;The constraint: Medusa v2 is stateful. It needs PostgreSQL, Redis for BullMQ, and enough RAM to run the event bus and workflow engine without getting killed. That rules out serverless. (Medusa can technically start without Redis — it falls back to an in-process event bus — but that means losing job queuing, workflow retries, and reliable event delivery. Fine for a quick local demo; not something you want processing real orders.) It needs persistent storage, a real process manager, and a consistent IP for outbound webhook validation. Every "just deploy it for free" option assumes your workload is stateless. This one isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: Render
&lt;/h2&gt;

&lt;p&gt;Render's free tier looked fine for a low-traffic shop. 512MB RAM, easy deploys from GitHub — cold starts on the free tier, always-on on the paid plan. The plan was to pair it with Neon for PostgreSQL (generous free tier, serverless scaling) and Upstash for Redis (free tier covers BullMQ at low volume). Three managed services, total cost: $0.&lt;/p&gt;

&lt;p&gt;I got Medusa running, ran the setup scripts, and then installed the Prodigi print fulfillment plugin — which loads product mappings and prefetches shipping zones at startup.&lt;/p&gt;

&lt;p&gt;OOM kill on boot. 512MB wasn't enough.&lt;/p&gt;

&lt;p&gt;The obvious fix is to upgrade the Render service. Their starter plan is $7/mo. That's just the API — Neon and Upstash stay free at low volume, but now I'm at $7/mo for a single service with no room to grow, and I still hadn't solved staging. Render's starter plan is $7/mo &lt;em&gt;per service&lt;/em&gt;. Two environments means two API instances: $14/mo before touching the databases.&lt;/p&gt;

&lt;p&gt;I could have stripped the fulfillment plugin to fit inside 512MB. But without it there are no print orders — Prodigi integration is how open-edition prints get produced and shipped. Dropping it to hit a RAM limit means the shop sells originals only, which wasn't the brief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: AWS EC2
&lt;/h2&gt;

&lt;p&gt;After Render, AWS EC2 looked like the right move. A &lt;code&gt;t4g.micro&lt;/code&gt; in &lt;code&gt;eu-west-2&lt;/code&gt; (London): 1 GiB RAM, 2 vCPU ARM64 Graviton2, always-on, full SSH access, swap configurable — all within the AWS free tier for the first 12 months. Double the memory of Render's paid plan, at no cost. I built out the full setup: CloudFormation stack, Nginx reverse proxy, Let's Encrypt TLS, systemd service, GitHub Actions CD, health check cron.&lt;/p&gt;

&lt;p&gt;It ran. There were a couple of ARM64 quirks — a &lt;code&gt;ts-node&lt;/code&gt; source-map crash under Bun that needed &lt;code&gt;TS_NODE_SKIP_SOURCE_MAP_SUPPORT=1&lt;/code&gt;, and Node.js auto-limiting the V8 heap to ~512MB on a 1 GiB instance, fixed with &lt;code&gt;NODE_OPTIONS=--max-old-space-size=1024&lt;/code&gt;. But Medusa came up, Prodigi loaded, the health check passed.&lt;/p&gt;

&lt;p&gt;Then I ran a checkout flow under load. OOM kill.&lt;/p&gt;

&lt;p&gt;The 1 GiB ceiling wasn't enough headroom when Prodigi, Medusa, and an active checkout were all competing for memory at once. And the infrastructure complexity — CloudFormation, Nginx config, cert renewal timers, CloudWatch, a manual cutover checklist — was a real ongoing cost for a solo project. After the free year, the bill would be ~$7.90/mo anyway. At that price you can just buy a VPS that has the memory you need and none of the ceremony.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 3: Oracle Always Free
&lt;/h2&gt;

&lt;p&gt;Oracle's Always Free tier is, on paper, absurd. The A1 Flex shape gives you up to 4 OCPU and 24 GB RAM at no cost, permanently. ARM64, which is what Hetzner's cheapest VPS uses anyway. UK London region available. I signed up.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The selected shape is not available in this Availability Domain. Please try a different Availability Domain or shape."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tried every Availability Domain in UK London. Same message. I tried Frankfurt. Same message. I tried Amsterdam. Same.&lt;/p&gt;

&lt;p&gt;This isn't a new problem. The Oracle community forum has threads going back two years with hundreds of people reporting A1 capacity unavailable in every European region. Oracle adds capacity occasionally, and it disappears within hours as people snap it up. The forum advice is to script retries and keep trying. I tried that for a few weeks.&lt;/p&gt;

&lt;p&gt;The Always Free tier only works if you can actually provision the instance. If capacity is gone, it's gone indefinitely. For a project you want to actually ship, "keep retrying and hope" isn't a deployment strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked: Hetzner CAX11
&lt;/h2&gt;

&lt;p&gt;Hetzner's CAX11 is €3.29/mo. ARM64, 2 vCPU, 4 GB RAM, 40 GB NVMe SSD, 20 TB/month egress included. I signed up, got the server provisioned in about 30 seconds, and had Medusa running the same afternoon.&lt;/p&gt;

&lt;p&gt;The setup that's been stable since:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two systemd services, one box.&lt;/strong&gt; Dev and production run as separate Medusa instances — different ports, different &lt;code&gt;.env&lt;/code&gt; files, different PostgreSQL databases. Caddy sits in front and routes &lt;code&gt;api-dev.nadiapoe.co.uk&lt;/code&gt; and &lt;code&gt;api.nadiapoe.co.uk&lt;/code&gt; to the right one. Each service restarts automatically on failure. No Docker, no orchestration — just two &lt;code&gt;medusa.service&lt;/code&gt; unit files and a Caddy config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploys via GitHub Actions.&lt;/strong&gt; On push to &lt;code&gt;main&lt;/code&gt;, a workflow SSHes in, pulls the latest code, runs &lt;code&gt;bun install&lt;/code&gt; and &lt;code&gt;bun run build&lt;/code&gt;, and restarts the appropriate service. Under 60 seconds end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backups to Cloudflare R2.&lt;/strong&gt; A nightly cron runs &lt;code&gt;pg_dump&lt;/code&gt; and uploads the compressed archive to a dedicated R2 bucket (&lt;code&gt;nadiapoe-backups&lt;/code&gt;). R2 is already in the stack for media — adding a backup bucket costs nothing extra. Retention is 7 daily dumps on-server, 30 in R2. If the VPS burns down, restoring is &lt;code&gt;pg_restore&lt;/code&gt; and a &lt;code&gt;git pull&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The total monthly cost: €3.29. Both environments. All services. Backups included.&lt;/p&gt;

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

&lt;p&gt;"Free" hosting for stateful backend workloads usually means one of two things: a RAM ceiling that kills anything real, or a capacity queue where "available" means "available when someone else cancels." For a stateless frontend or a simple API, free tiers are great — Cloudflare Pages handles the storefront for nothing. But the moment you have a process that needs to stay alive, own persistent state, and run at startup, a cheap paid VPS is more reliable than a free tier with asterisks.&lt;/p&gt;

&lt;p&gt;Hetzner CAX11 at €3.29/mo is less than most people spend on a coffee a month. It's not free. It's better than free.&lt;/p&gt;

&lt;p&gt;One genuine acknowledgement before moving on: the free tiers from Cloudflare, Resend, Neon, and Upstash make it possible to build and prove a business before it earns a penny. They lower the bar for anyone who wants to try something without betting money on it first. That matters, and it's worth saying.&lt;/p&gt;

&lt;p&gt;Live shop: &lt;a href="https://nadiapoe.co.uk" rel="noopener noreferrer"&gt;nadiapoe.co.uk&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>medusa</category>
      <category>selfhosted</category>
      <category>hetzner</category>
    </item>
    <item>
      <title>Blazingly Fast Ecommerce Stack for Less Than a Coffee a Month — No Marketplace, No Platform Cut</title>
      <dc:creator>David Bartalos</dc:creator>
      <pubDate>Mon, 18 May 2026 06:30:00 +0000</pubDate>
      <link>https://dev.to/dbartalos/blazingly-fast-ecommerce-stack-for-less-than-a-coffee-a-month-no-marketplace-no-platform-cut-59dn</link>
      <guid>https://dev.to/dbartalos/blazingly-fast-ecommerce-stack-for-less-than-a-coffee-a-month-no-marketplace-no-platform-cut-59dn</guid>
      <description>&lt;p&gt;If you've ever looked at a marketplace's fee page and felt your eye twitch, this post is for you.&lt;/p&gt;

&lt;p&gt;The major selling platforms take their cut from every angle — transaction fees, listing fees, monthly subscriptions, payment processing. The percentages vary but the direction doesn't: a meaningful slice of every sale goes to infrastructure you don't own or control. And that's before the visibility problem: on a marketplace of millions of listings, the algorithm decides whether your work gets seen at all.&lt;/p&gt;

&lt;p&gt;My girlfriend had been listing her work on one of the big marketplaces for a while — barely any traffic, zero sales. The fees were almost beside the point. I saw the disappointment and floated the idea: her own site, her own corner of the internet — and I'd build it.&lt;/p&gt;

&lt;p&gt;I'm a software engineer. I like a challenge. So I set out to give it a shot. The result runs at &lt;strong&gt;€3.29/mo&lt;/strong&gt; for all backend infrastructure. The site is live at &lt;a href="https://nadiapoe.co.uk" rel="noopener noreferrer"&gt;nadiapoe.co.uk&lt;/a&gt;. Here's the stack and the decisions behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Artwork loads instantly.&lt;/strong&gt; Watercolours are the product. A 2-second LCP would send visitors away before they saw a painting. No image-CDN that stamps a watermark on the hero image, no spinner while the page wakes a cold serverless function.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No third-party watermarks.&lt;/strong&gt; Image-CDN convenience — Cloudinary, imgix — isn't worth a logo in the corner of the hero. This is an artist's portfolio. The paintings deserve respect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real commerce, not a payment link.&lt;/strong&gt; Multi-currency (GBP / EUR / USD / AUD), international fulfillment for prints, originals shipped from the UK. A Stripe payment link in the Instagram bio wasn't going to cut it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No tracking, no cookie banner.&lt;/strong&gt; The site doesn't follow visitors. No analytics cookies, no third-party pixels, no consent popup to dismiss before you can see a painting. Purchase data goes only as far as it needs to: card details to Stripe, a shipping address — originals are shipped by us from the UK, prints via a print-on-demand provider for international orders. Nothing retained beyond what's needed to get the order out the door.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Indie budget.&lt;/strong&gt; One artist, no team, no investor. Anything more than ~£10/month total infrastructure is a recurring tax on the creative work. That ceiling shaped every hosting decision in the stack.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack, piece by piece
&lt;/h2&gt;

&lt;p&gt;The infrastructure splits cleanly across two providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloudflare (free tier)
&lt;/h3&gt;

&lt;p&gt;Everything the visitor touches runs on Cloudflare. CDN and WAF sit in front of everything — Bot Fight Mode, rate limiting, R2 hotlink protection. Behind them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; hosts the Astro 6 storefront. Static by default, per-route SSR where needed (checkout callbacks, contact form, structured data feeds). The painting pages are pure HTML — the entire collection ships zero JS until the cart is opened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare R2&lt;/strong&gt; stores all images and process videos. Zero egress fees, served from a custom domain (&lt;code&gt;media.nadiapoe.co.uk&lt;/code&gt;). Videos are pre-encoded to four variants locally with ffmpeg (720p desktop, 480p mobile, JPEG poster, 64×64 thumb) and uploaded via wrangler. No URL transforms, no image-service quotas to exhaust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare D1&lt;/strong&gt; is the edge SQL database — one table, one purpose: like counts. The like button stores your choice in localStorage and increments a counter in D1. No cookies, no tracking, no consent popup. You see the count; nothing sees you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hetzner VPS — €3.29/mo
&lt;/h3&gt;

&lt;p&gt;Everything commerce-related runs on a single Hetzner CAX11: 2 vCPU ARM64, 4 GB RAM, 40 GB NVMe, 20 TB/month egress. On it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medusa v2&lt;/strong&gt; — the commerce backend. Dev and prod as separate systemd services, both reverse-proxied by Caddy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; — orders, products, customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; — BullMQ event bus and workflow engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Render, AWS, and Oracle Always Free each failed for a different specific reason. Hetzner just works.&lt;/p&gt;

&lt;p&gt;One gap in Medusa v2 worth knowing: there's no official Resend provider. Order confirmation emails skip the notification module entirely and call the Resend SDK directly from an &lt;code&gt;order.placed&lt;/code&gt; subscriber.&lt;/p&gt;

&lt;h3&gt;
  
  
  Third-party services
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Stripe&lt;/strong&gt; handles payments across four currencies (GBP / EUR / USD / AUD). Cloudflare injects &lt;code&gt;cf.country&lt;/code&gt; into a &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag at the edge; the storefront reads it to pick the matching Medusa region and present prices in the local currency. User override persists in localStorage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resend&lt;/strong&gt; handles transactional email — 3,000 emails/month free, then $1/1,000.&lt;/p&gt;

&lt;h3&gt;
  
  
  The interactive layer
&lt;/h3&gt;

&lt;p&gt;Svelte islands handle the cart drawer, quantity controls, painting gallery, and region selector. Nano-stores (&lt;code&gt;cartStore&lt;/code&gt;, &lt;code&gt;cartUpdating&lt;/code&gt;, &lt;code&gt;regionStore&lt;/code&gt;) keep islands in sync without a framework router.&lt;/p&gt;

&lt;p&gt;The shop grid itself is static Astro HTML — no Svelte involved. The paintings are the product; a visitor should see the full collection immediately, not wait on an inventory check before anything renders. So every card defaults to "available," then a plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag calls the Medusa API in the background and patches &lt;code&gt;data-status&lt;/code&gt; on each card to flip sold originals to a faded state. No layout shift, no spinner, no framework overhead for a read-only grid.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One Astro 6 gotcha that cost an afternoon:&lt;/em&gt; server-side secrets must come from &lt;code&gt;import { env } from 'cloudflare:workers'&lt;/code&gt;. The old &lt;code&gt;Astro.locals.runtime.env&lt;/code&gt; was removed and &lt;code&gt;import.meta.env&lt;/code&gt; silently bakes &lt;code&gt;undefined&lt;/code&gt; for server-only vars. No build error, no runtime warning — just missing data in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually costs
&lt;/h2&gt;

&lt;p&gt;Backend hosting: &lt;strong&gt;€3.29/mo&lt;/strong&gt; — both staging and production Medusa instances, PostgreSQL, Redis, Caddy, automated nightly backups to Cloudflare R2. Frontend, CDN, edge SQL, object storage, WAF, analytics: free tier. The metered costs are Stripe (1.5–2.9% per transaction — unavoidable regardless of platform, but at least there's no &lt;em&gt;extra&lt;/em&gt; platform cut on top) and Resend (3,000 emails/month free, then $1/1,000).&lt;/p&gt;

&lt;p&gt;Compare that to a typical marketplace taking 6–7% on a £150 original watercolour — that's £9–10 per sale, forever, to infrastructure you don't own. At even modest volume the self-hosted setup pays for itself inside the first month.&lt;/p&gt;

&lt;p&gt;Will this survive a traffic spike? Honestly, no idea — this shop has never been Slashdotted, and a 2 vCPU ARM box with 4 GB RAM is not going to win any load test. But if it ever buckles under the weight of people trying to buy original watercolours, upgrading the VPS will be the easiest problem on the list that day.&lt;/p&gt;

&lt;h2&gt;
  
  
  One snippet worth stealing
&lt;/h2&gt;

&lt;p&gt;This pattern only works because the HTML is served through a Cloudflare Worker — but if you're already on Cloudflare Pages, you have that for free.&lt;/p&gt;

&lt;p&gt;The full CSP is set per-request with a fresh nonce, injected into every &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag at the edge. No build-time hash dance, no list of inline script hashes to maintain. From &lt;code&gt;client/src/middleware.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineMiddleware&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="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;response&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;next&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;HTMLRewriter&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonceBytes&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;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&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;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;nonceBytes&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;rewritten&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HTMLRewriter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&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;element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nonce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rewritten&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="nx"&gt;headers&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;Content-Security-Policy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;buildCsp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rewritten&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rewritten&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloudflare's own bot-fight challenge scripts get whitelisted automatically — they read the nonce from the CSP header and stamp themselves with it. On a static-only setup you're stuck with hashes, and those break the moment Cloudflare rotates a challenge token. The Worker approach sidesteps that entirely.&lt;/p&gt;

&lt;p&gt;The site is &lt;a href="https://nadiapoe.co.uk" rel="noopener noreferrer"&gt;nadiapoe.co.uk&lt;/a&gt; if you want to see the result.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>svelte</category>
      <category>cloudflare</category>
      <category>medusa</category>
    </item>
  </channel>
</rss>
