<?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: jasonm4130</title>
    <description>The latest articles on DEV Community by jasonm4130 (@jasonm4130).</description>
    <link>https://dev.to/jasonm4130</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%2F3861583%2Fb9a4c776-b1eb-4195-8aba-b6dedf0b9255.jpeg</url>
      <title>DEV Community: jasonm4130</title>
      <link>https://dev.to/jasonm4130</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jasonm4130"/>
    <language>en</language>
    <item>
      <title>The Axios Breach Started with a Plaintext Token — Here's How I Keep Zero Secrets in My Repos</title>
      <dc:creator>jasonm4130</dc:creator>
      <pubDate>Mon, 06 Apr 2026 00:38:29 +0000</pubDate>
      <link>https://dev.to/jasonm4130/the-axios-breach-started-with-a-plaintext-token-heres-how-i-keep-zero-secrets-in-my-repos-3h49</link>
      <guid>https://dev.to/jasonm4130/the-axios-breach-started-with-a-plaintext-token-heres-how-i-keep-zero-secrets-in-my-repos-3h49</guid>
      <description>&lt;p&gt;Last week, a North Korean state actor compromised the axios npm package — 100 million weekly downloads — and pushed a cross-platform RAT to every machine that ran &lt;code&gt;npm install&lt;/code&gt; during a three-hour window.&lt;/p&gt;

&lt;p&gt;The entire attack chain started with one thing: &lt;strong&gt;a long-lived npm token stored in plaintext on a developer's machine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The attacker social-engineered the maintainer into installing malware. The malware harvested the token. The token was used to publish malicious versions. Even though the project had OIDC trusted publishing configured — the "right" way — the legacy plaintext token sitting alongside it provided a bypass.&lt;/p&gt;

&lt;p&gt;One plaintext secret. 100 million affected installs.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not a one-off
&lt;/h2&gt;

&lt;p&gt;The axios breach fits a pattern that's accelerating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;28.65 million new hardcoded secrets&lt;/strong&gt; were pushed to public GitHub repos in 2025 — up 34% year-over-year (&lt;a href="https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/" rel="noopener noreferrer"&gt;GitGuardian, 2026&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;64% of secrets leaked in 2022 were still active in 2026&lt;/strong&gt; — nobody rotated them&lt;/li&gt;
&lt;li&gt;AI-assisted commits leak secrets at &lt;strong&gt;2x the baseline rate&lt;/strong&gt; (3.2% vs 1.5%)&lt;/li&gt;
&lt;li&gt;Stolen credentials appear in &lt;strong&gt;31% of all breaches&lt;/strong&gt; over the past decade&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Shai-Hulud worm that hit npm in September 2025 was even more direct — it ran TruffleHog on compromised machines specifically to harvest &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, &lt;code&gt;NPM_TOKEN&lt;/code&gt;, and &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; from developers' environments.&lt;/p&gt;

&lt;p&gt;The common thread: secrets that exist in plaintext — in &lt;code&gt;.env&lt;/code&gt; files, in shell history, in CI variables, on disk — are attack surface waiting to be exploited.&lt;/p&gt;

&lt;h2&gt;
  
  
  The typical approach (and why it breaks)
&lt;/h2&gt;

&lt;p&gt;Most projects handle secrets one of these ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;.env&lt;/code&gt; files (gitignored)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env — "don't worry, it's in .gitignore"
&lt;/span&gt;&lt;span class="n"&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;=&lt;span class="n"&gt;v1&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;-&lt;span class="n"&gt;abc123def456&lt;/span&gt;
&lt;span class="n"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;=&lt;span class="n"&gt;wJalrXUtnFEMI&lt;/span&gt;/&lt;span class="n"&gt;K7MDENG&lt;/span&gt;/&lt;span class="n"&gt;bPxRfiCYEXAMPLEKEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The file sits on disk in plaintext. Any malware, any compromised dependency with filesystem access, any &lt;code&gt;postinstall&lt;/code&gt; script can read it.&lt;/li&gt;
&lt;li&gt;Developers copy it between machines via Slack, email, or shared drives.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.gitignore&lt;/code&gt; is a single line of defense. One &lt;code&gt;git add -A&lt;/code&gt; from a tired developer (or an AI assistant) and it's in the history forever.&lt;/li&gt;
&lt;li&gt;New team members ask "can someone send me the .env?" in chat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. CI/CD secrets (GitHub Actions, etc.)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better for CI, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doesn't help local development.&lt;/li&gt;
&lt;li&gt;Secrets are still stored somewhere — now you have two sources of truth.&lt;/li&gt;
&lt;li&gt;Rotating means updating both the secret store and the CI config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Vault/secrets manager&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The enterprise answer. Often the right call for large teams. But for a solo developer or small team managing infrastructure? It's a lot of overhead for "I need my API token when I run &lt;code&gt;terraform plan&lt;/code&gt;."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually do: 1Password CLI + op run
&lt;/h2&gt;

&lt;p&gt;I manage Cloudflare infrastructure for &lt;a href="https://formrecap.com" rel="noopener noreferrer"&gt;FormRecap&lt;/a&gt; (a form abandonment recovery SaaS) using Terraform. That means API tokens, account IDs, zone IDs, R2 storage credentials — a lot of secrets.&lt;/p&gt;

&lt;p&gt;Here's the setup. It's three files, and &lt;strong&gt;zero secrets touch disk or git.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The &lt;code&gt;.env.op&lt;/code&gt; file (committed to git)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.op — safe to commit. Contains zero secrets.&lt;/span&gt;
&lt;span class="c"&gt;# These are 1Password references, not values.&lt;/span&gt;

&lt;span class="c"&gt;# Cloudflare provider&lt;/span&gt;
&lt;span class="nv"&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://Private/Cloudflare Terraform/api_token
&lt;span class="nv"&gt;TF_VAR_cloudflare_api_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://Private/Cloudflare Terraform/api_token

&lt;span class="nv"&gt;CLOUDFLARE_ACCOUNT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://Private/Cloudflare Terraform/account_id
&lt;span class="nv"&gt;TF_VAR_cloudflare_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://Private/Cloudflare Terraform/account_id

&lt;span class="c"&gt;# Zone IDs&lt;/span&gt;
&lt;span class="nv"&gt;TF_VAR_zone_id_formrecap_com&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://Private/Cloudflare Terraform/zone_id_formrecap_com
&lt;span class="nv"&gt;TF_VAR_zone_id_endurebyte_com&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://Private/Cloudflare Terraform/zone_id_endurebyte_com
&lt;span class="c"&gt;# ... more zones&lt;/span&gt;

&lt;span class="c"&gt;# R2 state backend (S3-compatible)&lt;/span&gt;
&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://Private/Cloudflare Terraform/r2_access_key
&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://Private/Cloudflare Terraform/r2_secret_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every value is an &lt;code&gt;op://&lt;/code&gt; URI — a pointer to a field in a 1Password vault. The file itself contains &lt;strong&gt;nothing sensitive&lt;/strong&gt;. It's committed to git, reviewed in PRs, and documents exactly which secrets the project needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: The Makefile (also committed)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;OP&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; op run &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.op &lt;span class="nt"&gt;--&lt;/span&gt;

&lt;span class="nl"&gt;init&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;OP&lt;span class="p"&gt;)&lt;/span&gt; terraform init

&lt;span class="nl"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;OP&lt;span class="p"&gt;)&lt;/span&gt; terraform plan

&lt;span class="nl"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;OP&lt;span class="p"&gt;)&lt;/span&gt; terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;op run&lt;/code&gt; resolves every &lt;code&gt;op://&lt;/code&gt; reference at runtime, injects the real values as environment variables into the child process, and cleans them up when the process exits. The secrets exist only in memory, only for the duration of the command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: One 1Password item
&lt;/h3&gt;

&lt;p&gt;In 1Password, create a single item (I called mine "Cloudflare Terraform") with fields matching each &lt;code&gt;op://&lt;/code&gt; reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api_token:                 → your Cloudflare API token
account_id:                → your Cloudflare account ID
zone_id_formrecap_com:     → the zone ID
r2_access_key:             → R2 S3-compatible access key
r2_secret_key:             → R2 S3-compatible secret key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The entire workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make plan    &lt;span class="c"&gt;# 1Password prompts for biometric auth → secrets injected → plan runs&lt;/span&gt;
make apply   &lt;span class="c"&gt;# same flow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this is better than .env files
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nothing on disk.&lt;/strong&gt; There's no file containing secrets for malware to read. If the axios RAT had been running on my machine, it would have found &lt;code&gt;.env.op&lt;/code&gt; — a file full of &lt;code&gt;op://&lt;/code&gt; URIs that are useless without my 1Password vault and biometric authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Biometric gate.&lt;/strong&gt; Every &lt;code&gt;op run&lt;/code&gt; invocation requires Touch ID (or your OS equivalent). Even if someone has shell access to your machine, they can't exfiltrate secrets without your fingerprint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One source of truth.&lt;/strong&gt; Secrets live in 1Password. Period. No syncing between &lt;code&gt;.env&lt;/code&gt;, CI secrets, and a shared password doc. Rotate the token in 1Password and every &lt;code&gt;make plan&lt;/code&gt; picks it up instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-documenting.&lt;/strong&gt; The &lt;code&gt;.env.op&lt;/code&gt; file serves as a manifest of every secret the project needs, which vault it's in, and what it's called. New team member? Share the 1Password vault. They clone the repo, run &lt;code&gt;make plan&lt;/code&gt;, and it just works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No &lt;code&gt;.gitignore&lt;/code&gt; anxiety.&lt;/strong&gt; The env file is &lt;em&gt;supposed&lt;/em&gt; to be committed. There's nothing to accidentally leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup (5 minutes)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install 1Password CLI&lt;/span&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; 1password/tap/1password-cli

&lt;span class="c"&gt;# 2. Enable CLI integration in 1Password desktop app&lt;/span&gt;
&lt;span class="c"&gt;#    Settings → Developer → "Integrate with 1Password CLI"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Create your secrets item in 1Password&lt;/span&gt;
&lt;span class="c"&gt;#    (manually, or via CLI)&lt;/span&gt;
op item create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Cloudflare Terraform"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vault&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Private"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'api_token=your-token-here'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'account_id=your-account-id'&lt;/span&gt;

&lt;span class="c"&gt;# 4. Create .env.op referencing those fields&lt;/span&gt;
&lt;span class="c"&gt;# 5. Wrap your commands with: op run --env-file=.env.op -- &amp;lt;command&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works with any tool, not just Terraform:&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;# Node.js&lt;/span&gt;
op run &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.op &lt;span class="nt"&gt;--&lt;/span&gt; node server.js

&lt;span class="c"&gt;# Docker&lt;/span&gt;
op run &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.op &lt;span class="nt"&gt;--&lt;/span&gt; docker compose up

&lt;span class="c"&gt;# Any CLI that reads environment variables&lt;/span&gt;
op run &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.op &lt;span class="nt"&gt;--&lt;/span&gt; wrangler deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What about CI/CD?
&lt;/h2&gt;

&lt;p&gt;For GitHub Actions, 1Password has a &lt;a href="https://github.com/1Password/load-secrets-action" rel="noopener noreferrer"&gt;dedicated action&lt;/a&gt; that uses a service account (no biometric needed in CI) and the same &lt;code&gt;op://&lt;/code&gt; references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1password/load-secrets-action@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;export-env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;OP_SERVICE_ACCOUNT_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;op://Private/Cloudflare Terraform/api_token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;OP_SERVICE_ACCOUNT_TOKEN&lt;/code&gt; in GitHub secrets. Everything else resolves from 1Password. When you rotate a credential, you update it in one place.&lt;/p&gt;

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

&lt;p&gt;The axios maintainer did a lot of things right. The project had OIDC trusted publishing. It had 2FA. It had a responsible disclosure process. But a single long-lived plaintext token — stored on disk, harvestable by malware — made all of that irrelevant.&lt;/p&gt;

&lt;p&gt;The defense isn't "be more careful with &lt;code&gt;.env&lt;/code&gt; files." The defense is &lt;strong&gt;not having secrets in files at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;op://&lt;/code&gt; references are inert data. They're like a map to a vault — useless without the vault key (your biometric). The actual secret never touches the filesystem, never appears in shell history, never gets committed to git, and never persists after the process exits.&lt;/p&gt;

&lt;p&gt;For a solo developer managing production infrastructure — or a maintainer of a package with 100 million weekly installs — that difference is the whole ballgame.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://formrecap.com" rel="noopener noreferrer"&gt;FormRecap&lt;/a&gt;, a form abandonment recovery tool that runs entirely on Cloudflare's developer platform. The Terraform setup described in this article manages all of FormRecap's infrastructure — DNS, D1 databases, KV namespaces, R2 buckets, Pages projects, and AI Gateway — across 8 domains with zero plaintext secrets.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If your forms lose 70% of visitors before they hit submit, &lt;a href="https://formrecap.com" rel="noopener noreferrer"&gt;give it a try&lt;/a&gt; — free tier, one script tag, no backend changes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>terraform</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>I built a form abandonment recovery SaaS entirely on Cloudflare's developer platform</title>
      <dc:creator>jasonm4130</dc:creator>
      <pubDate>Sat, 04 Apr 2026 22:12:22 +0000</pubDate>
      <link>https://dev.to/jasonm4130/i-built-a-form-abandonment-recovery-saas-entirely-on-cloudflares-developer-platform-gh9</link>
      <guid>https://dev.to/jasonm4130/i-built-a-form-abandonment-recovery-saas-entirely-on-cloudflares-developer-platform-gh9</guid>
      <description>&lt;p&gt;81% of people who start filling out a web form abandon it before submitting (&lt;a href="https://www.prnewswire.com/news-releases/most-people-dont-finish-online-forms-citing-security-concerns-and-form-length-300631039.html" rel="noopener noreferrer"&gt;The Manifest, 2018&lt;/a&gt;). For ecommerce, cart abandonment recovery emails convert about 10% of those back into customers. But for lead gen forms, contact forms, quote request forms — there's been no equivalent.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;FormRecap&lt;/strong&gt; over an Easter long weekend to fix this. It's a 2.7KB JavaScript snippet that detects form abandonment and sends recovery emails with magic links that restore the visitor's exact form state.&lt;/p&gt;

&lt;p&gt;The entire product — API, database, session tracking, email dispatch, billing, and dashboard — runs on Cloudflare's developer platform. Here's how.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Snippet (2.7KB IIFE)
  ↓ sendBeacon / fetch
Cloudflare Worker (Hono API)
  ↓
Durable Object (per-form session tracking)
  ↓ alarm fires after abandonment delay
Queue → Workflow → Resend (recovery email)
  ↓
Visitor clicks magic link → form restored
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The stack:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workers&lt;/td&gt;
&lt;td&gt;API + SPA serving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D1&lt;/td&gt;
&lt;td&gt;All relational data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable Objects&lt;/td&gt;
&lt;td&gt;Per-form session state + abandonment timing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queues + Workflows&lt;/td&gt;
&lt;td&gt;Async recovery email pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV&lt;/td&gt;
&lt;td&gt;Config cache, session tokens, rate limiting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics Engine&lt;/td&gt;
&lt;td&gt;High-volume event metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The snippet
&lt;/h2&gt;

&lt;p&gt;The client-side snippet is the most constrained part of the system. It runs on customer websites, so it has to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tiny&lt;/strong&gt; — 2.7KB gzipped, zero dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatible&lt;/strong&gt; — ES2015 target (no optional chaining, no nullish coalescing, no async/await)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy-first&lt;/strong&gt; — excludes passwords, credit cards, SSNs, and 31 other sensitive field patterns by default, plus value-level regex for credit card and SSN formats&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-blocking&lt;/strong&gt; — uses &lt;code&gt;sendBeacon&lt;/code&gt; with &lt;code&gt;text/plain&lt;/code&gt; content type to stay CORS-safelisted (no preflight requests)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It discovers forms via &lt;code&gt;querySelectorAll&lt;/code&gt; with a body-level &lt;code&gt;MutationObserver&lt;/code&gt; for SPAs, tracks field events (focus, blur, change, input), and detects abandonment through &lt;code&gt;visibilitychange&lt;/code&gt;, &lt;code&gt;pagehide&lt;/code&gt;, &lt;code&gt;beforeunload&lt;/code&gt;, and SPA navigation (pushState/replaceState/popstate).&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-customer encryption
&lt;/h2&gt;

&lt;p&gt;Since the snippet captures form field data, security couldn't be an afterthought. Every customer's data is encrypted with its own key:&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="nx"&gt;customerKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HKDF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;masterSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;formrecap-field-enc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Field data is encrypted with AES-GCM-256 using that derived key. Email addresses use HMAC blind indexes for searchable lookups without storing plaintext. All signature and hash comparisons use timing-safe comparison to prevent timing attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Durable Objects for session tracking&lt;/strong&gt; turned out to be the perfect fit. Each form session gets its own DO instance that accumulates events, detects the email field, triggers the abandonment alarm, and persists encrypted snapshots to D1. The alarm API means abandonment detection is just "set alarm for N seconds after last activity" — no polling, no cron.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CORS-safelisted sendBeacon trick&lt;/strong&gt; saved a lot of complexity. By using &lt;code&gt;text/plain&lt;/code&gt; as the content type, the browser treats it as a "simple request" and skips the preflight OPTIONS request entirely. The Worker parses the JSON body server-side regardless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart Placement&lt;/strong&gt; handles the latency trade-off automatically. API routes that hit D1 run near the database, while static assets serve from the nearest edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure cost
&lt;/h2&gt;

&lt;p&gt;On the Workers paid plan ($5/mo), the included limits cover roughly 1,000 free-tier customers. The main variable cost is Resend for recovery emails. At early scale, total cost is about a coffee per month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://formrecap.com/demo" rel="noopener noreferrer"&gt;formrecap.com/demo&lt;/a&gt; — fill out a form, leave the page, and watch the recovery flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://docs.formrecap.com" rel="noopener noreferrer"&gt;docs.formrecap.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier:&lt;/strong&gt; 500 sessions/month on one site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm building this solo and in public. Feedback on the architecture, the snippet's privacy approach, or the product itself is very welcome.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>jamstack</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
