<?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: Luqman Hakim</title>
    <description>The latest articles on DEV Community by Luqman Hakim (@infinitezone).</description>
    <link>https://dev.to/infinitezone</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%2F1144586%2Fb09a33b2-2366-487a-85d0-2e4568c0f7fb.jpeg</url>
      <title>DEV Community: Luqman Hakim</title>
      <link>https://dev.to/infinitezone</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/infinitezone"/>
    <language>en</language>
    <item>
      <title>SonicJS Auth Gotchas on Cloudflare: Signup, Credentials, and RBAC - Part 3</title>
      <dc:creator>Luqman Hakim</dc:creator>
      <pubDate>Sun, 26 Jul 2026 11:33:04 +0000</pubDate>
      <link>https://dev.to/infinitezone/sonicjs-auth-gotchas-on-cloudflare-signup-credentials-and-rbac-10ki</link>
      <guid>https://dev.to/infinitezone/sonicjs-auth-gotchas-on-cloudflare-signup-credentials-and-rbac-10ki</guid>
      <description>&lt;p&gt;Someone tries to log in and you get one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Credential account not found&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Login works, but &lt;strong&gt;You do not have permission to access this area&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Random people registering themselves as editors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post is the production auth/ops guide for SonicJS on Workers — the stuff that isn’t in the “hello deploy” tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Turn off public self-registration
&lt;/h2&gt;

&lt;p&gt;For an internal CMS, open signup is a footgun. Disable Better Auth sign-up in app config:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSonicJSApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;registerCollections&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sonicjs-cms/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SonicJSConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sonicjs-cms/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;blogPostsCollection&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./collections/blog-posts.collection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;registerCollections&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;blogPostsCollection&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;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SonicJSConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;register&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="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extendBetterAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&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;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;emailAndPassword&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="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailAndPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;disableSignUp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;createSonicJSApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Admins create accounts in &lt;strong&gt;Admin → Users → Create New User&lt;/strong&gt; (&lt;code&gt;/admin/users/new&lt;/code&gt;). That path should create both:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the profile row (&lt;code&gt;auth_user&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the Better Auth credential row (&lt;code&gt;auth_account&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;…so the new user can log in immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Two password stores (this is the big one)
&lt;/h2&gt;

&lt;p&gt;SonicJS login (Better Auth) reads credentials from &lt;strong&gt;&lt;code&gt;auth_account&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Some UI/profile flows only update &lt;strong&gt;&lt;code&gt;auth_user.password_hash&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If those diverge, you get:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Credential account not found&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;even though the user “exists” in the admin list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: reset password with a script that updates both
&lt;/h3&gt;

&lt;p&gt;Prefer a CLI over the admin password form when login is broken:&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="nv"&gt;ADMIN_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@example.com &lt;span class="nv"&gt;ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'your-new-password'&lt;/span&gt; npm run set-password:prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That script should upsert the Better Auth credential row &lt;strong&gt;and&lt;/strong&gt; keep the profile hash in sync.&lt;/p&gt;

&lt;p&gt;For brand-new environments, seed once with env vars — never commit passwords:&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="nv"&gt;ADMIN_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@example.com &lt;span class="nv"&gt;ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'your-secure-password'&lt;/span&gt; npm run seed:prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A solid seed uses Wrangler’s platform proxy against production D1:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPlatformProxy&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wrangler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useProduction&lt;/span&gt; &lt;span class="o"&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;argv&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;--remote&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="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;dispose&lt;/span&gt; &lt;span class="p"&gt;}&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;getPlatformProxy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;useProduction&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;remoteBindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;useProduction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// env.DB is your D1 binding — insert auth_user + auth_account, assign RBAC, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements for credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass &lt;code&gt;ADMIN_EMAIL&lt;/code&gt; / &lt;code&gt;ADMIN_PASSWORD&lt;/code&gt; via env&lt;/li&gt;
&lt;li&gt;Enforce min password length&lt;/li&gt;
&lt;li&gt;Exit early if the admin already exists (idempotent seed)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. RBAC ≠ &lt;code&gt;auth_user.role&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Even after login succeeds, &lt;code&gt;/admin&lt;/code&gt; may show:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You do not have permission to access this area&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Classic cause: the user has a string role on &lt;code&gt;auth_user&lt;/code&gt; (e.g. &lt;code&gt;editor&lt;/code&gt;), but &lt;strong&gt;no RBAC role assignment&lt;/strong&gt; that grants &lt;code&gt;portal:access&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Older admin UI flows sometimes wrote only &lt;code&gt;auth_user.role&lt;/code&gt;. Portal access is enforced via RBAC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: promote the user
&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;# default: editor with portal access&lt;/span&gt;
&lt;span class="nv"&gt;ADMIN_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user@example.com npm run promote-user:prod

&lt;span class="c"&gt;# full admin&lt;/span&gt;
&lt;span class="nv"&gt;ADMIN_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user@example.com &lt;span class="nv"&gt;ROLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin npm run promote-user:prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After you patch/upgrade core so “Create New User” assigns RBAC automatically, &lt;strong&gt;new&lt;/strong&gt; users are fine — but existing accounts may still need a one-time promote.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Ops cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anyone can register&lt;/td&gt;
&lt;td&gt;Sign-up still enabled&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;disableSignUp: true&lt;/code&gt; in &lt;code&gt;extendBetterAuth&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Credential account not found”&lt;/td&gt;
&lt;td&gt;Missing/outdated &lt;code&gt;auth_account&lt;/code&gt; row&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;set-password:prod&lt;/code&gt; for that email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login OK, no portal access&lt;/td&gt;
&lt;td&gt;Missing RBAC / &lt;code&gt;portal:access&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;promote-user:prod&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed failed: no DB binding&lt;/td&gt;
&lt;td&gt;Wrong env / migrations&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;wrangler.toml&lt;/code&gt; production D1 + &lt;code&gt;remote = true&lt;/code&gt;; run migrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth weird after redeploy&lt;/td&gt;
&lt;td&gt;Secrets missing/rotated badly&lt;/td&gt;
&lt;td&gt;Re-put &lt;code&gt;BETTER_AUTH_SECRET&lt;/code&gt; / &lt;code&gt;JWT_SECRET&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  5. Beta packages and patches (optional honesty)
&lt;/h2&gt;

&lt;p&gt;SonicJS moves fast (&lt;code&gt;@sonicjs-cms/core&lt;/code&gt; betas). If you hit a framework bug in user-create or login UI, &lt;code&gt;patch-package&lt;/code&gt; can unblock production while you wait for upstream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postinstall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"patch-package"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep patches small and documented. Prefer upstream upgrades when the fix lands — patches are a bridge, not a lifestyle.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Hardening checklist for a private CMS
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Public signup disabled&lt;/li&gt;
&lt;li&gt;[ ] First admin seeded via env (not hardcoded)&lt;/li&gt;
&lt;li&gt;[ ] Create-user flow writes &lt;code&gt;auth_user&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;auth_account&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Editors/admins have RBAC with &lt;code&gt;portal:access&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Password resets go through a script that updates both stores&lt;/li&gt;
&lt;li&gt;[ ] Production secrets set via &lt;code&gt;wrangler secret put&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Collections that should be public API opt in with &lt;code&gt;access.public: ['read']&lt;/code&gt; (from Part 1) — everything else stays private&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Series wrap-up
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/infinitezone/build-an-edge-native-headless-cms-on-cloudflare-workers-sonicjs-d1-r2-1n5j"&gt;Architecture&lt;/a&gt;&lt;/strong&gt; — Workers + D1 + R2, schema-as-code collections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/infinitezone/deploy-a-sonicjs-cms-to-cloudflare-workers-with-github-actions-1gie"&gt;Deploy + CI/CD&lt;/a&gt;&lt;/strong&gt; — secrets, migrate-then-deploy, GitHub Actions + &lt;code&gt;/health&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This post&lt;/strong&gt; — signup lock, credential rows, RBAC&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s a complete path from “edge CMS idea” to “team can log into &lt;code&gt;/admin&lt;/code&gt; without Slack-debugging auth.”&lt;/p&gt;

&lt;p&gt;If you ship something similar, start with SonicJS’s docs, then treat auth as a first-class production concern — not a day-two chore.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Further reading:&lt;/em&gt; &lt;a href="https://sonicjs.com" rel="noopener noreferrer"&gt;SonicJS&lt;/a&gt; · &lt;a href="https://www.better-auth.com/" rel="noopener noreferrer"&gt;Better Auth&lt;/a&gt; · &lt;a href="https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/" rel="noopener noreferrer"&gt;Workers CI with GitHub Actions&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>cms</category>
      <category>authentication</category>
      <category>sonicjs</category>
    </item>
    <item>
      <title>Deploy a SonicJS CMS to Cloudflare Workers with GitHub Actions - Part 2</title>
      <dc:creator>Luqman Hakim</dc:creator>
      <pubDate>Sun, 26 Jul 2026 11:30:25 +0000</pubDate>
      <link>https://dev.to/infinitezone/deploy-a-sonicjs-cms-to-cloudflare-workers-with-github-actions-1gie</link>
      <guid>https://dev.to/infinitezone/deploy-a-sonicjs-cms-to-cloudflare-workers-with-github-actions-1gie</guid>
      <description>&lt;p&gt;This post covers production bindings, secrets, migrate-before-deploy, and a GitHub Actions workflow that typechecks, migrates D1, deploys the Worker, and smoke-checks &lt;code&gt;/health&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A SonicJS project that already runs locally with &lt;code&gt;wrangler dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cloudflare account + Wrangler logged in once (&lt;code&gt;npx wrangler login&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;D1 database and R2 bucket created (or create them now)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler d1 create my-cms-db
npx wrangler r2 bucket create my-cms-media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the returned &lt;code&gt;database_id&lt;/code&gt; into &lt;code&gt;wrangler.toml&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production env in wrangler.toml
&lt;/h2&gt;

&lt;p&gt;Keep a default (local) section and a dedicated &lt;code&gt;[env.production]&lt;/code&gt; block. Same bindings, production vars:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms"&lt;/span&gt;
&lt;span class="py"&gt;main&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src/index.ts"&lt;/span&gt;
&lt;span class="py"&gt;compatibility_date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2024-09-23"&lt;/span&gt;
&lt;span class="py"&gt;compatibility_flags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"nodejs_compat"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[[d1_databases]]&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DB"&lt;/span&gt;
&lt;span class="py"&gt;database_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms-db"&lt;/span&gt;
&lt;span class="py"&gt;database_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_DATABASE_ID"&lt;/span&gt;
&lt;span class="py"&gt;migrations_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"./node_modules/@sonicjs-cms/core/migrations"&lt;/span&gt;

&lt;span class="nn"&gt;[[r2_buckets]]&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MEDIA_BUCKET"&lt;/span&gt;
&lt;span class="py"&gt;bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms-media"&lt;/span&gt;

&lt;span class="nn"&gt;[vars]&lt;/span&gt;
&lt;span class="py"&gt;ENVIRONMENT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"development"&lt;/span&gt;
&lt;span class="py"&gt;CORS_ORIGINS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://localhost:8787"&lt;/span&gt;
&lt;span class="py"&gt;BUCKET_NAME&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms-media"&lt;/span&gt;

&lt;span class="nn"&gt;[env.production]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms"&lt;/span&gt;
&lt;span class="py"&gt;vars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;ENVIRONMENT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;CORS_ORIGINS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://my-cms.YOUR_SUBDOMAIN.workers.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;BUCKET_NAME&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms-media"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nn"&gt;[[env.production.d1_databases]]&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DB"&lt;/span&gt;
&lt;span class="py"&gt;database_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms-db"&lt;/span&gt;
&lt;span class="py"&gt;database_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_DATABASE_ID"&lt;/span&gt;
&lt;span class="py"&gt;migrations_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"./node_modules/@sonicjs-cms/core/migrations"&lt;/span&gt;
&lt;span class="py"&gt;remote&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[[env.production.r2_buckets]]&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MEDIA_BUCKET"&lt;/span&gt;
&lt;span class="py"&gt;bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms-media"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;remote = true&lt;/code&gt;&lt;/strong&gt; on the production D1 binding helps local scripts (&lt;code&gt;seed:prod&lt;/code&gt;, password reset) talk to real D1 via &lt;code&gt;getPlatformProxy&lt;/code&gt;. Deploy itself ignores that flag.&lt;/li&gt;
&lt;li&gt;Put &lt;strong&gt;CORS&lt;/strong&gt; origins in vars so your frontend can call the public API.&lt;/li&gt;
&lt;li&gt;Never put secrets in &lt;code&gt;[vars]&lt;/code&gt; — those end up in the Worker bundle metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Secrets (auth won’t work without them)
&lt;/h2&gt;

&lt;p&gt;SonicJS / Better Auth need secrets at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32 | npx wrangler secret put BETTER_AUTH_SECRET &lt;span class="nt"&gt;--env&lt;/span&gt; production
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32 | npx wrangler secret put JWT_SECRET &lt;span class="nt"&gt;--env&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rotate the same way later. Treat these like production passwords.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual deploy: migrate, then ship
&lt;/h2&gt;

&lt;p&gt;npm scripts that match the mental model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wrangler dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wrangler deploy --env production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:migrate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wrangler d1 migrations apply DB --remote --env production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:migrate:local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wrangler d1 migrations apply DB --local"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always migrate &lt;strong&gt;before&lt;/strong&gt; deploying code that expects new tables/columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run db:migrate
npm run deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Order matters. Ship Worker code that queries a column D1 doesn’t have yet, and you’ll debug “works locally, 500 in prod” for an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootstrap the first admin (once)
&lt;/h2&gt;

&lt;p&gt;Seeding should never store a password in git. Pass credentials via env:&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="nv"&gt;ADMIN_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@example.com &lt;span class="nv"&gt;ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'your-secure-password'&lt;/span&gt; npm run seed:prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Implementation detail in Part 3 — the script uses Wrangler’s platform proxy against production D1.)&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions: migrate + deploy + smoke
&lt;/h2&gt;

&lt;p&gt;Goal on every push to &lt;code&gt;main&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install + typecheck&lt;/li&gt;
&lt;li&gt;Apply remote D1 migrations&lt;/li&gt;
&lt;li&gt;Deploy Worker (&lt;code&gt;--env production&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt; &lt;code&gt;/health&lt;/code&gt; until it succeeds&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. Cloudflare API token
&lt;/h3&gt;

&lt;p&gt;Dashboard → &lt;strong&gt;My Profile&lt;/strong&gt; → &lt;strong&gt;API Tokens&lt;/strong&gt; → customize &lt;strong&gt;Edit Cloudflare Workers&lt;/strong&gt; with at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Account → Workers Scripts → &lt;strong&gt;Edit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Account → Workers R2 Storage → &lt;strong&gt;Edit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Account → D1 → &lt;strong&gt;Edit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Account → Account Settings → &lt;strong&gt;Read&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. GitHub Environment &lt;code&gt;production&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Repo → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Environments&lt;/strong&gt; → create &lt;code&gt;production&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secret&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;token from step 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLOUDFLARE_ACCOUNT_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your account id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WORKER_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://my-cms.YOUR_SUBDOMAIN.workers.dev&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using a GitHub &lt;strong&gt;Environment&lt;/strong&gt; keeps prod credentials scoped and lets you add required reviewers later.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Worker&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-production&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Migrate &amp;amp; deploy&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;actions/checkout@v4&lt;/span&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;actions/setup-node@v4&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;

      &lt;span class="c1"&gt;# Prefer npm install if npm ci fails on optional platform binaries&lt;/span&gt;
      &lt;span class="c1"&gt;# (some lockfiles list every @esbuild/* optional package).&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install --no-audit --no-fund&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Typecheck&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run type-check&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apply D1 migrations&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;cloudflare/wrangler-action@v3&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;apiToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_ACCOUNT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;wranglerVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4.107.1"&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;d1 migrations apply DB --remote --env production&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Worker&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;cloudflare/wrangler-action@v3&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;apiToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_ACCOUNT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;wranglerVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4.107.1"&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy --env production&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Smoke check&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;WORKER_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.WORKER_URL }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;URL="${WORKER_URL:?Set WORKER_URL in the production environment}"&lt;/span&gt;
          &lt;span class="s"&gt;curl -fsS --retry 5 --retry-delay 3 "$URL/health"&lt;/span&gt;
          &lt;span class="s"&gt;echo&lt;/span&gt;
          &lt;span class="s"&gt;echo "Health check OK ($URL)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin &lt;code&gt;wranglerVersion&lt;/code&gt; to what you use locally so CI and laptops don’t diverge.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;concurrency.cancel-in-progress: false&lt;/code&gt; matters for deploys: you don’t want a second push to cancel a migration mid-flight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate CI for PRs
&lt;/h2&gt;

&lt;p&gt;Keep PRs cheap — typecheck only, no production migrate/deploy:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;typecheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;actions/checkout@v4&lt;/span&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;actions/setup-node@v4&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install --no-audit --no-fund&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run type-check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Checklist after first green deploy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;GET $WORKER_URL/health&lt;/code&gt; returns OK&lt;/li&gt;
&lt;li&gt;[ ] Secrets set (&lt;code&gt;BETTER_AUTH_SECRET&lt;/code&gt;, &lt;code&gt;JWT_SECRET&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Admin seeded&lt;/li&gt;
&lt;li&gt;[ ] Can open &lt;code&gt;/auth/login&lt;/code&gt; and &lt;code&gt;/admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Frontend CORS origin is in production &lt;code&gt;CORS_ORIGINS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Media upload hits R2 (create a post with a featured image)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;Deploy is the easy win. The painful bugs show up in &lt;strong&gt;auth&lt;/strong&gt;: disabled signup, Better Auth credential rows vs profile password hashes, and RBAC so editors can actually open the portal.&lt;/p&gt;




</description>
      <category>cloudflare</category>
      <category>devops</category>
      <category>github</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Build an Edge-Native Headless CMS on Cloudflare Workers (SonicJS + D1 + R2) - Part 1</title>
      <dc:creator>Luqman Hakim</dc:creator>
      <pubDate>Sun, 26 Jul 2026 11:28:21 +0000</pubDate>
      <link>https://dev.to/infinitezone/build-an-edge-native-headless-cms-on-cloudflare-workers-sonicjs-d1-r2-1n5j</link>
      <guid>https://dev.to/infinitezone/build-an-edge-native-headless-cms-on-cloudflare-workers-sonicjs-d1-r2-1n5j</guid>
      <description>&lt;p&gt;Most headless CMS setups still look like 2018: a Node server, a Postgres box in one region, and S3 for uploads. That works — until you want global latency, zero cold starts, and an ops surface you can explain in one diagram.&lt;/p&gt;

&lt;p&gt;This series is about running a real headless CMS on &lt;strong&gt;Cloudflare Workers&lt;/strong&gt;, with &lt;strong&gt;D1&lt;/strong&gt; for data and &lt;strong&gt;R2&lt;/strong&gt; for media, using &lt;a href="https://sonicjs.com" rel="noopener noreferrer"&gt;SonicJS&lt;/a&gt; — an edge-first CMS built for that stack. Part 1 covers the architecture and content model. Part 2 covers deploy + CI/CD. Part 3 covers the auth and admin gotchas you’ll hit in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why put a CMS on the edge?
&lt;/h2&gt;

&lt;p&gt;A traditional CMS request path often looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User hits your API in &lt;code&gt;us-east-1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;App talks to Postgres in the same region&lt;/li&gt;
&lt;li&gt;Media comes from S3 (egress fees optional, but common)&lt;/li&gt;
&lt;li&gt;Every region outside that AZ pays the latency tax&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An edge-native CMS flips that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request hits a Worker near the user&lt;/li&gt;
&lt;li&gt;D1 (SQLite at the edge) serves content/auth data&lt;/li&gt;
&lt;li&gt;R2 stores media with no egress fees&lt;/li&gt;
&lt;li&gt;The same Worker serves admin UI + REST API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SonicJS is designed around that model: TypeScript collections define your schema; the framework generates the admin UI and API; Wrangler binds D1 and R2 so you never invent connection strings for a CMS database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the system
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│         Cloudflare Worker           │
│     (SonicJS + Hono + admin UI)     │
└──────────────┬──────────────────────┘
               │
       ┌───────┴────────┐
       ▼                ▼
┌─────────────┐   ┌─────────────┐
│  D1 (SQLite)│   │ R2 (media)  │
│ content/auth│   │ images/files│
└─────────────┘   └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bindings live in &lt;code&gt;wrangler.toml&lt;/code&gt; — the Worker receives &lt;code&gt;env.DB&lt;/code&gt; and &lt;code&gt;env.MEDIA_BUCKET&lt;/code&gt; at runtime. No ORM connection URL. No S3 access keys in &lt;code&gt;.env&lt;/code&gt; for the happy path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms"&lt;/span&gt;
&lt;span class="py"&gt;main&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src/index.ts"&lt;/span&gt;
&lt;span class="py"&gt;compatibility_date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2024-09-23"&lt;/span&gt;
&lt;span class="py"&gt;compatibility_flags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"nodejs_compat"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[[d1_databases]]&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DB"&lt;/span&gt;
&lt;span class="py"&gt;database_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms-db"&lt;/span&gt;
&lt;span class="py"&gt;database_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_DATABASE_ID"&lt;/span&gt;
&lt;span class="py"&gt;migrations_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"./node_modules/@sonicjs-cms/core/migrations"&lt;/span&gt;

&lt;span class="nn"&gt;[[r2_buckets]]&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MEDIA_BUCKET"&lt;/span&gt;
&lt;span class="py"&gt;bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms-media"&lt;/span&gt;

&lt;span class="nn"&gt;[vars]&lt;/span&gt;
&lt;span class="py"&gt;ENVIRONMENT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"development"&lt;/span&gt;
&lt;span class="py"&gt;BUCKET_NAME&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-cms-media"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bootstrap: one file to start the app
&lt;/h2&gt;

&lt;p&gt;The app entrypoint is small. You register collections, optionally tweak auth/plugins, and export the SonicJS app:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSonicJSApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;registerCollections&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sonicjs-cms/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SonicJSConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sonicjs-cms/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;blogPostsCollection&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./collections/blog-posts.collection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;registerCollections&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;blogPostsCollection&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;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SonicJSConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;register&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="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We'll dig into this in Part 3&lt;/span&gt;
    &lt;span class="na"&gt;extendBetterAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&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;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;emailAndPassword&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="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailAndPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;disableSignUp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;createSonicJSApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the whole Worker surface: collections + config → admin at &lt;code&gt;/admin&lt;/code&gt;, auth at &lt;code&gt;/auth/login&lt;/code&gt;, health at &lt;code&gt;/health&lt;/code&gt;, and a generated content API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema-as-code: define a blog collection
&lt;/h2&gt;

&lt;p&gt;Instead of clicking schema fields in a UI and hoping they sync to prod, you define collections in TypeScript. SonicJS treats them as &lt;strong&gt;managed&lt;/strong&gt; (config-driven) collections.&lt;/p&gt;

&lt;p&gt;Here’s a practical blog post model: title, slug, Lexical rich text, featured image + gallery on R2, author, publish date — with &lt;strong&gt;public read&lt;/strong&gt; for your frontend and a short cache TTL:&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;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CollectionConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sonicjs-cms/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog_post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Blog Post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog-posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage your blog posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxLength&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="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;URL Slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxLength&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="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Excerpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;helpText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Short summary used in listings and SEO meta tags&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lexical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;featuredImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;media&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Featured Image&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;gallery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gallery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;media&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;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Author&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;publishedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;datetime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Published Date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;author&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;listFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;publishedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;searchFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;excerpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;defaultSort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;defaultSortOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="na"&gt;managed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;// Without this, only authenticated admins/editors can read via the API&lt;/span&gt;
  &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&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;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// seconds — tune for your freshness vs speed tradeoff&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;CollectionConfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details worth copying into your own project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;type: 'media'&lt;/code&gt;&lt;/strong&gt; maps uploads to the R2 &lt;code&gt;MEDIA_BUCKET&lt;/code&gt; binding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;access.public: ['read']&lt;/code&gt;&lt;/strong&gt; is opt-in. Private-by-default is the right CMS default; public APIs should be explicit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cache.ttl&lt;/code&gt;&lt;/strong&gt; lets you override per collection when some content can be hotter than others.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Local development loop
&lt;/h2&gt;

&lt;p&gt;With Wrangler, local feels like production: same bindings, Miniflare-backed D1/R2 under &lt;code&gt;.wrangler/&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run db:migrate:local   &lt;span class="c"&gt;# apply SonicJS migrations to local D1&lt;/span&gt;
&lt;span class="c"&gt;# seed an admin (credentials via env — never commit them)&lt;/span&gt;
&lt;span class="nv"&gt;ADMIN_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@example.com &lt;span class="nv"&gt;ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'your-secure-password'&lt;/span&gt; npm run seed
npm run dev                &lt;span class="c"&gt;# wrangler dev — usually http://localhost:8787&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login: &lt;code&gt;http://localhost:8787/auth/login&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Admin: &lt;code&gt;http://localhost:8787/admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Health: &lt;code&gt;http://localhost:8787/health&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’re not “running a CMS in Docker and pretending it’s Cloudflare.” You’re running the Worker runtime locally with the same APIs you’ll use in prod.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this stack shines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Marketing / content sites that need a real admin, not a markdown-only repo&lt;/li&gt;
&lt;li&gt;Frontends (Astro, Next, Remix, plain fetch) that want a fast public JSON API&lt;/li&gt;
&lt;li&gt;Teams that prefer &lt;strong&gt;schema in git&lt;/strong&gt; over schema-only-in-UI&lt;/li&gt;
&lt;li&gt;Projects that want Cloudflare’s free/paid tiers instead of a always-on Node + Postgres bill&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, we’ll ship this to production: production &lt;code&gt;wrangler.toml&lt;/code&gt; envs, secrets, D1 migrations, and a GitHub Actions pipeline that migrates, deploys, and smoke-checks &lt;code&gt;/health&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Docs:&lt;/em&gt; &lt;a href="https://sonicjs.com" rel="noopener noreferrer"&gt;SonicJS&lt;/a&gt; · &lt;a href="https://developers.cloudflare.com/workers/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt; · &lt;a href="https://developers.cloudflare.com/d1/" rel="noopener noreferrer"&gt;D1&lt;/a&gt; · &lt;a href="https://developers.cloudflare.com/r2/" rel="noopener noreferrer"&gt;R2&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>workers</category>
      <category>cms</category>
      <category>typescript</category>
    </item>
    <item>
      <title>CloudFront support mTLS</title>
      <dc:creator>Luqman Hakim</dc:creator>
      <pubDate>Fri, 24 Jul 2026 04:47:49 +0000</pubDate>
      <link>https://dev.to/infinitezone/cloudfront-support-mtls-4p8d</link>
      <guid>https://dev.to/infinitezone/cloudfront-support-mtls-4p8d</guid>
      <description>&lt;p&gt;Here’s the same article with an HA angle woven in — especially contrasting ALB/VPCE toil with CloudFront as a global managed edge.&lt;/p&gt;




&lt;h1&gt;
  
  
  From Private VPC Endpoints to the Edge: mTLS for Mobile APIs on AWS
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — You used to terminate mobile mTLS at API Gateway or ALB, and the ALB path forced Private API Gateway + VPC Endpoints + IP target groups. CloudFront now speaks mTLS at the edge — so you can keep API Gateway &lt;strong&gt;Regional&lt;/strong&gt;, add WAF/Shield/custom headers, and stop plumbing private IPs into load balancers. Bonus: you also stop designing HA for the front door — CloudFront is already a global service.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem nobody enjoys explaining in a design review
&lt;/h2&gt;

&lt;p&gt;Mobile apps talking to backends often need more than “Bearer token in a header.”&lt;/p&gt;

&lt;p&gt;You want &lt;strong&gt;cryptographic client identity&lt;/strong&gt; before the request even looks like an HTTP call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only devices with a valid client certificate connect&lt;/li&gt;
&lt;li&gt;Stolen tokens alone are not enough&lt;/li&gt;
&lt;li&gt;Certificate lifecycle becomes part of your trust model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s &lt;strong&gt;mutual TLS (mTLS)&lt;/strong&gt;: the server proves who it is &lt;em&gt;and&lt;/em&gt; the client proves who it is.&lt;/p&gt;

&lt;p&gt;On AWS, the question has always been: &lt;strong&gt;where do you terminate that handshake?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 1 — mTLS at API Gateway (simple, but limited)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mobile ──mTLS──▶ API Gateway ──▶ Backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What works well&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct path&lt;/li&gt;
&lt;li&gt;Fewer moving parts&lt;/li&gt;
&lt;li&gt;Certificate validation close to your API surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What starts to hurt&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Less natural fit for edge controls (global PoPs, Shield Advanced patterns, rich WAF at the CDN layer)&lt;/li&gt;
&lt;li&gt;Harder to centralize “edge policy” (headers, geo, bot signals, caching rules) in one place&lt;/li&gt;
&lt;li&gt;You still need a clean story for public exposure, rate limits, and DDoS&lt;/li&gt;
&lt;li&gt;Availability is mostly “API Gateway is managed,” but you still think regionally about the entry point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a solid baseline. Many teams start here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 2 — ALB in front (secure… and operationally spicy)
&lt;/h2&gt;

&lt;p&gt;Some teams put an Application Load Balancer in front so mTLS (or TLS policy) lives on the ALB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mobile ──mTLS──▶ ALB ──▶ API Gateway ──▶ Backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On paper: great. In practice: the network plumbing gets loud.&lt;/p&gt;

&lt;p&gt;Because ALB needs a private path into API Gateway, you often end up here:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Why it exists&lt;/th&gt;
&lt;th&gt;Pain point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private API Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not publicly reachable&lt;/td&gt;
&lt;td&gt;Can’t use a simple public Regional endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VPC Endpoint (VPCE)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ALB reaches private API&lt;/td&gt;
&lt;td&gt;Extra networking + IAM/interface endpoint design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP target group&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ALB targets VPCE ENI IPs&lt;/td&gt;
&lt;td&gt;IPs can change; registration/health checks get awkward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private DNS / routing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Glue it together&lt;/td&gt;
&lt;td&gt;More failure modes in prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HA design&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ALB is regional&lt;/td&gt;
&lt;td&gt;Multi-AZ is table stakes; multi-region means &lt;em&gt;another&lt;/em&gt; design&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;h3&gt;
  
  
  Why this feels like a tax
&lt;/h3&gt;

&lt;p&gt;You’re not just “adding a load balancer.” You’re adopting a &lt;strong&gt;private connectivity mini-platform&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make API Gateway private
&lt;/li&gt;
&lt;li&gt;Stand up Interface VPC Endpoints
&lt;/li&gt;
&lt;li&gt;Discover / track VPCE private IPs
&lt;/li&gt;
&lt;li&gt;Register those IPs as ALB targets
&lt;/li&gt;
&lt;li&gt;Keep health checks and IP churn under control
&lt;/li&gt;
&lt;li&gt;Decide AZ placement, failover, and (if you’re serious) multi-region entry
&lt;/li&gt;
&lt;li&gt;Debug “is it certs, ALB, VPCE, or API GW?” at 2 a.m.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works. It’s also a lot of undifferentiated heavy lifting for what you really wanted: &lt;strong&gt;authenticate the device, then call the API&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HA tax:&lt;/strong&gt; With ALB you’re still thinking in &lt;em&gt;regions and AZs&lt;/em&gt;. Multi-AZ is expected. Cross-region active/active or failover is a project. Your mobile clients need a resilient DNS/routing story on top of mTLS.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Option 3 — CloudFront viewer mTLS (the plot twist)
&lt;/h2&gt;

&lt;p&gt;CloudFront can now perform the &lt;strong&gt;viewer mTLS handshake at the edge&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That flips the architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mobile ──mTLS──▶ CloudFront ──▶ Regional API Gateway ──▶ Backend
                      │
                      ├── AWS WAF
                      ├── AWS Shield
                      └── Custom headers / edge policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Why this is a big deal
&lt;/h3&gt;

&lt;p&gt;Because CloudFront is &lt;strong&gt;public by design&lt;/strong&gt;, you no longer need the Private API Gateway + VPCE + IP target group dance just to put a smart front door in front of your API.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expose &lt;strong&gt;Regional API Gateway&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Point CloudFront origin at the API Gateway domain name&lt;/li&gt;
&lt;li&gt;Terminate / validate client certificates &lt;strong&gt;at CloudFront&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Layer &lt;strong&gt;WAF&lt;/strong&gt; and &lt;strong&gt;Shield&lt;/strong&gt; on the same distribution&lt;/li&gt;
&lt;li&gt;Inject &lt;strong&gt;custom headers&lt;/strong&gt; from CloudFront → origin&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Mental model:&lt;/strong&gt; CloudFront becomes your global mTLS gate. API Gateway goes back to being an API — not a networking science project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The HA win you stop overthinking
&lt;/h3&gt;

&lt;p&gt;Here’s the underrated part of the CloudFront path:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don’t design HA for the front door the way you do for ALB.&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;Concern&lt;/th&gt;
&lt;th&gt;ALB + Private API&lt;/th&gt;
&lt;th&gt;CloudFront viewer mTLS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Regional construct&lt;/td&gt;
&lt;td&gt;Global edge network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-AZ&lt;/td&gt;
&lt;td&gt;You plan it&lt;/td&gt;
&lt;td&gt;Inherited from the service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-region entry&lt;/td&gt;
&lt;td&gt;Extra architecture&lt;/td&gt;
&lt;td&gt;Clients hit nearest PoP by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failover story&lt;/td&gt;
&lt;td&gt;DNS, dual stacks, runbooks&lt;/td&gt;
&lt;td&gt;Largely AWS-operated edge capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What you still own&lt;/td&gt;
&lt;td&gt;Everything behind the ALB&lt;/td&gt;
&lt;td&gt;Origin health + Regional API / backend HA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CloudFront is a &lt;strong&gt;global managed service&lt;/strong&gt;. Mobile clients resolve to the edge; certificate handshake and request acceptance happen at PoPs worldwide. You’re not standing up “an HA pair of mTLS terminators” in each region.&lt;/p&gt;

&lt;p&gt;That doesn’t mean your &lt;strong&gt;origin&lt;/strong&gt; is magically multi-region — Regional API Gateway and backends still need their own resilience story. But the &lt;em&gt;client-facing mTLS plane&lt;/em&gt; stops being an HA design exercise.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Punchline:&lt;/strong&gt; With ALB you buy availability with architecture diagrams. With CloudFront you inherit a global front door — and spend your HA budget where it still matters: the origin and the data plane.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Side-by-side: pick your pain
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;API Gateway only&lt;/th&gt;
&lt;th&gt;ALB → Private API GW&lt;/th&gt;
&lt;th&gt;CloudFront viewer mTLS → Regional API GW&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;mTLS termination&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API Gateway&lt;/td&gt;
&lt;td&gt;ALB&lt;/td&gt;
&lt;td&gt;CloudFront edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Gateway type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Regional / as designed&lt;/td&gt;
&lt;td&gt;Often &lt;strong&gt;Private&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Regional&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extra networking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;VPCE + IP targets&lt;/td&gt;
&lt;td&gt;Origin config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Front-door HA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed, regional entry&lt;/td&gt;
&lt;td&gt;You design AZ / region strategy&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Global service — largely out of scope&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Edge WAF / Shield&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate story&lt;/td&gt;
&lt;td&gt;Possible but awkward&lt;/td&gt;
&lt;td&gt;Native with distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom edge headers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;App/ALB rules&lt;/td&gt;
&lt;td&gt;CloudFront native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ops complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low–medium&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium (edge-focused)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple APIs&lt;/td&gt;
&lt;td&gt;Existing ALB-centric estates&lt;/td&gt;
&lt;td&gt;Mobile fleets + edge security&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What “good” looks like with CloudFront
&lt;/h2&gt;

&lt;p&gt;A practical pattern for mobile backends:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Provision device certificates&lt;/strong&gt; from your private CA (or managed PKI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a CloudFront trust store&lt;/strong&gt; with the CA(s) you trust&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable viewer mTLS&lt;/strong&gt; on the distribution

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required&lt;/strong&gt; — every client must present a valid cert (typical for private mobile APIs)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional&lt;/strong&gt; — mixed traffic during migration
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passthrough&lt;/strong&gt; — edge doesn’t fully validate; origin does (migration/legacy)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin = Regional API Gateway&lt;/strong&gt; custom domain / execute-api hostname&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock down the origin&lt;/strong&gt; so it only accepts traffic from CloudFront (custom secret header, Resource Policy, or both)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attach WAF&lt;/strong&gt; for L7 rules; lean on &lt;strong&gt;Shield&lt;/strong&gt; for DDoS posture at the edge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invest HA behind the origin&lt;/strong&gt; (API / compute / data) — not in reinventing a global TLS edge&lt;/li&gt;
&lt;/ol&gt;

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




&lt;h2&gt;
  
  
  The hidden win: security &lt;em&gt;and&lt;/em&gt; availability move to the edge
&lt;/h2&gt;

&lt;p&gt;With ALB + Private API Gateway, a lot of your energy goes into &lt;strong&gt;reachability&lt;/strong&gt; and &lt;strong&gt;keeping the front door up&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With CloudFront mTLS, more of that energy goes into &lt;strong&gt;policy&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who is allowed to connect? → certificates / trust store
&lt;/li&gt;
&lt;li&gt;What traffic shapes are abusive? → WAF
&lt;/li&gt;
&lt;li&gt;Who absorbs volumetric attacks? → Shield + edge capacity
&lt;/li&gt;
&lt;li&gt;How does the origin trust CloudFront? → origin access controls + headers
&lt;/li&gt;
&lt;li&gt;Do we need multi-AZ mTLS terminators? → &lt;strong&gt;No — CloudFront is already global&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a healthier split for mobile APIs: &lt;strong&gt;authenticate early, authorize narrowly, keep the origin boring, and don’t DIY global HA for TLS.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  When you might still choose ALB
&lt;/h2&gt;

&lt;p&gt;CloudFront isn’t magic for every estate.&lt;/p&gt;

&lt;p&gt;Keep ALB (or ALB + Private API) if you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sticky ALB-centric features as the primary entry&lt;/li&gt;
&lt;li&gt;Patterns that already assume VPC-native targets end-to-end&lt;/li&gt;
&lt;li&gt;Strict internal-only topologies where a CDN edge is the wrong trust boundary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;strong&gt;public mobile clients + API Gateway&lt;/strong&gt;, CloudFront viewer mTLS is usually the cleaner story — on security &lt;em&gt;and&lt;/em&gt; on how little HA you have to invent at the perimeter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;mTLS for mobile isn’t hard because TLS is hard.&lt;/p&gt;

&lt;p&gt;It’s hard because teams accidentally buy a &lt;strong&gt;networking + HA architecture&lt;/strong&gt; when they only needed a &lt;strong&gt;client authentication control point&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway alone: simple
&lt;/li&gt;
&lt;li&gt;ALB + Private API + VPCE IPs: powerful, expensive in toil (and you still own regional HA)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront viewer mTLS + Regional API Gateway&lt;/strong&gt;: authenticate at the edge, keep the API public-but-protected, bring WAF/Shield/custom headers along — and &lt;strong&gt;skip front-door HA design because CloudFront is already a global service&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re writing the next version of your mobile API edge: start with certificates and trust stores — not private ENI IP target groups or multi-region ALB runbooks.&lt;/p&gt;




&lt;h3&gt;
  
  
  Suggested subtitle / social blurb
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Stop wiring ALB target groups to VPC Endpoint IPs just to do mTLS. Terminate client certificates at CloudFront, keep API Gateway Regional, get WAF + Shield for free — and stop designing HA for the front door. CloudFront is already global.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;One nuance worth keeping in the article (so reviewers don’t nitpick): CloudFront removes most &lt;strong&gt;front-door&lt;/strong&gt; HA work; you still design HA for &lt;strong&gt;Regional API Gateway + backends&lt;/strong&gt;. The win is that mTLS entry is no longer part of that problem.&lt;/p&gt;

</description>
      <category>cloudfront</category>
      <category>mtls</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Why we decided to go with Kubernetes</title>
      <dc:creator>Luqman Hakim</dc:creator>
      <pubDate>Sun, 01 Sep 2024 10:55:52 +0000</pubDate>
      <link>https://dev.to/infinitezone/why-we-decided-to-go-with-kubernetes-512b</link>
      <guid>https://dev.to/infinitezone/why-we-decided-to-go-with-kubernetes-512b</guid>
      <description>&lt;p&gt;Hi folks, today I'm gonna share why I am so invested in learning Kubernetes and applying it in my production environment that I'm working for right now.&lt;/p&gt;

&lt;p&gt;We have a lot of problems as our application running in a monolith structure rather than microservices (literally only one server). As time goes by, our application increase in usage and functionalities thus making this is a backstab to our current server specs. RAM usage going high all the time. Application crashes quite frequent because the server specs cannot handle the current workloads.&lt;/p&gt;

&lt;p&gt;I suggest to the team to make our application high available, meaning we can share the workload across multiple servers. If one of the applications crashes, we have time to recover it while maintaining the availability of the application.&lt;/p&gt;

&lt;p&gt;Our application mainly running in three separate services.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Main service - handling transactions, calculation etc.&lt;/li&gt;
&lt;li&gt;Batch service - sending emails, reminder, generate incremental numbers etc.&lt;/li&gt;
&lt;li&gt;Report service - generate reports for our client for their usage, audit etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6tfslf781aze532zey4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6tfslf781aze532zey4.png" alt="Our initial application service" width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Initially, all these process running in one application. We know to make our application run in multiple servers, we need to separate out the batch service so that we will not sent our emails on notifications multiple times. So we separate out batch service so it will be running only on one server.&lt;/p&gt;

&lt;p&gt;Now our application can run in high availability mode. Multiple main services will handle the transaction and incoming request from users but our report service still embedded in the main service, that is okay but as the time goes by, many users generating the report thus sometimes our main service crashes.&lt;/p&gt;

&lt;p&gt;The next thing is we separate out our report service so that if a lot of users generating the report, the main service that handle transactions can still work as usual and maintaining our uptime. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdnvrf9g5nn5jvtnmsdzr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdnvrf9g5nn5jvtnmsdzr.png" alt="Our application after we do a little overhaul" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, all of our service running independently. Great! What if the servers crashes? Who's gonna recover it if the team is on holiday or still sleeping? (obviously every human being need to rest right?)&lt;/p&gt;

&lt;p&gt;This when the Kubernetes infrastructure comes in. Autoscale, autorecover, all in one place. Like a one-stop-center for a production grade application right? To be honest, learning all these Kubernetes things are time consuming, pain, sleepless night but who cares? It will benefit us along the time. Cost? Err we might need to discuss it in a separate discussion. Cheers!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>highavailability</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Kubernetes, when to use ?</title>
      <dc:creator>Luqman Hakim</dc:creator>
      <pubDate>Sun, 01 Sep 2024 10:55:16 +0000</pubDate>
      <link>https://dev.to/infinitezone/kubernetes-when-to-use-2614</link>
      <guid>https://dev.to/infinitezone/kubernetes-when-to-use-2614</guid>
      <description>&lt;p&gt;We have a lot of problems as our application running in a monolith structure rather than microservices (literally only one server). As time goes by, our application increase in usage and functionalities thus making this is a backstab to our current server specs. RAM usage going high all the time. Application crashes quite frequent because the server specs cannot handle the current workloads.&lt;/p&gt;

&lt;p&gt;I suggest to the team to make our application high available, meaning we can share the workload across multiple servers. If one of the applications crashes, we have time to recover it while maintaining the availability of the application.&lt;/p&gt;

&lt;p&gt;Our application mainly running in three separate services.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Main service - handling transactions, calculation etc.&lt;/li&gt;
&lt;li&gt;Batch service - sending emails, reminder, generate incremental numbers etc.&lt;/li&gt;
&lt;li&gt;Report service - generate reports for our client for their usage, audit etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Initially, all these process running in one application. We know to make our application run in multiple servers, we need to separate out the batch service so that we will not sent our emails on notifications multiple times. So we separate out batch service so it will be running only on one server.&lt;/p&gt;

&lt;p&gt;Now our application can run in high availability mode. Multiple main services will handle the transaction and incoming request from users but our report service still embedded in the main service, that is okay but as the time goes by, many users generating the report thus sometimes our main service crashes.&lt;/p&gt;

&lt;p&gt;The next thing is we separate out our report service so that if a lot of users generating the report, the main service that handle transactions can still work as usual and maintaining our uptime. &lt;/p&gt;

&lt;p&gt;Now, all of our service running independently. Great! What if the servers crashes? Who's gonna recover it if the team is on holiday on still sleeping? (obviously every human being need to rest right?)&lt;/p&gt;

&lt;p&gt;So we decided to move to Kubernetes. Autoscale, autorecover, all in one place. Like a one-stop-center for a production grade application right?&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
