<?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: Khue Pham</title>
    <description>The latest articles on DEV Community by Khue Pham (@khuepm).</description>
    <link>https://dev.to/khuepm</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%2F1078012%2Fab02a1a2-8dd0-4df8-851f-2e23df1c8c38.jpeg</url>
      <title>DEV Community: Khue Pham</title>
      <link>https://dev.to/khuepm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/khuepm"/>
    <language>en</language>
    <item>
      <title>The Hotel Upgrade Hack: Mass Assignment Vulnerabilities in APIs</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Sun, 12 Jul 2026 07:16:24 +0000</pubDate>
      <link>https://dev.to/khuepm/the-hotel-upgrade-hack-mass-assignment-vulnerabilities-in-apis-3h62</link>
      <guid>https://dev.to/khuepm/the-hotel-upgrade-hack-mass-assignment-vulnerabilities-in-apis-3h62</guid>
      <description>&lt;h2&gt;
  
  
  The hotel check-in trick
&lt;/h2&gt;

&lt;p&gt;Imagine you walk up to a hotel front desk, pick up the paper check-in form, and fill it in normally: your name, check-in date, number of guests, room type: Standard. But before you slide it back across the counter, you quietly pencil in one more line — &lt;em&gt;room type: Presidential Suite&lt;/em&gt;. The receptionist types every field into the system without a second glance. You just upgraded yourself for free.&lt;/p&gt;

&lt;p&gt;That is Mass Assignment. And developers reproduce this exact vulnerability in API code every single day, usually without realising it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Mass Assignment?
&lt;/h2&gt;

&lt;p&gt;Mass Assignment happens when a server takes user-supplied data and binds it directly to an internal model, without filtering which fields the user is actually allowed to set.&lt;/p&gt;

&lt;p&gt;In a Rails-flavoured example it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Dangerous — creates a user from whatever the client sends&lt;/span&gt;
&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Node.js / TypeScript land it is just as easy to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Also dangerous&lt;/span&gt;
&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now suppose your &lt;code&gt;users&lt;/code&gt; table has columns like &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;passwordHash&lt;/code&gt;, &lt;code&gt;isAdmin&lt;/code&gt;, and &lt;code&gt;role&lt;/code&gt;. A normal user submitting a sign-up form sends:&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;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"attacker@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Attacker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hunter2"&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;An attacker submitting the same endpoint sends:&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;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"attacker@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Attacker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hunter2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"isAdmin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"superuser"&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;If the server binds &lt;code&gt;req.body&lt;/code&gt; directly, both requests succeed. The attacker now has an admin account. No password cracking, no token theft, no exploiting a cryptographic weakness — just one extra JSON key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitHub 2012 incident
&lt;/h2&gt;

&lt;p&gt;In March 2012, security researcher Egor Homakov exploited a Mass Assignment vulnerability in GitHub and added himself as a member of the &lt;code&gt;rails/rails&lt;/code&gt; organisation — the official GitHub repository for Ruby on Rails — without any invitation or admin approval. He did this by injecting &lt;code&gt;organization_id&lt;/code&gt; (and related fields) into a form POST that was meant only for updating his own profile.&lt;/p&gt;

&lt;p&gt;GitHub suspended his account initially, then reinstated it and acknowledged the bug. The vulnerability was OWASP-known at the time; it simply hadn't been plugged.&lt;/p&gt;

&lt;p&gt;The incident was a watershed moment for the Rails community. Mass Assignment had been called out before, but this was the first time it was demonstrated publicly against a flagship open-source project with millions of followers. Rails subsequently introduced &lt;code&gt;strong_parameters&lt;/code&gt; to force developers to whitelist fields explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Mass Assignment is particularly dangerous
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Privilege escalation.&lt;/strong&gt; The most direct impact: an attacker sets &lt;code&gt;isAdmin: true&lt;/code&gt;, &lt;code&gt;role: "admin"&lt;/code&gt;, &lt;code&gt;subscriptionTier: "enterprise"&lt;/code&gt;. No brute force needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data corruption.&lt;/strong&gt; If &lt;code&gt;createdAt&lt;/code&gt;, &lt;code&gt;updatedAt&lt;/code&gt;, or audit fields can be overwritten, you lose tamper-evident logging. Compliance audits break down. Forensics become unreliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenancy bypass.&lt;/strong&gt; This is the one that keeps me up at night. In a multi-tenant system every domain record carries a &lt;code&gt;siteId&lt;/code&gt;. If an attacker can inject &lt;code&gt;siteId&lt;/code&gt; into a create or update payload, they can write data into another tenant's namespace, or re-parent their own records to escape rate limits and quotas scoped to their tenant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subtle escalation paths.&lt;/strong&gt; Sometimes the dangerous field isn't &lt;code&gt;isAdmin&lt;/code&gt;. It's &lt;code&gt;planId&lt;/code&gt;, &lt;code&gt;ownerId&lt;/code&gt;, &lt;code&gt;verifiedAt&lt;/code&gt;, &lt;code&gt;emailVerified&lt;/code&gt;, or &lt;code&gt;stripeCustomerId&lt;/code&gt;. Attackers probe every field in a response body and try to send each one back in the next write request. Any field that sticks is a potential attack surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Lumibase is exposed
&lt;/h2&gt;

&lt;p&gt;Lumibase is a Content OS — a headless CMS that accepts writes from multiple surfaces simultaneously: the Studio editor UI, AI skill output piped through the agent harness, incoming webhooks from external platforms, and third-party extension callbacks. Every one of those surfaces eventually calls the same Hono route handlers and lands in the same service layer.&lt;/p&gt;

&lt;p&gt;The schema for a content item looks roughly like this (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/database/src/schema/items.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;site_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;           &lt;span class="c1"&gt;// tenant boundary&lt;/span&gt;
  &lt;span class="na"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;collection_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&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="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;createdBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_by&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;defaultNow&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updated_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;defaultNow&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="nf"&gt;timestamp&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_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a route handler did something naive like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/cms/src/routes/items.ts — BAD example, do not do this&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;itemService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;siteId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker could include &lt;code&gt;createdBy&lt;/code&gt;, &lt;code&gt;publishedAt&lt;/code&gt;, or &lt;code&gt;status: "published"&lt;/code&gt; in the body, skipping the review workflow entirely. Worse, if the spread order were reversed, they could overwrite &lt;code&gt;siteId&lt;/code&gt; itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix layer 1: Zod strips unknown keys at the boundary
&lt;/h2&gt;

&lt;p&gt;The first and most important line of defence is to never let raw &lt;code&gt;req.body&lt;/code&gt; touch business logic. Parse every incoming request through a Zod schema, and let Zod strip fields the schema doesn't declare.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/shared/src/schemas/items.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&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;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createItemSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&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;review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// Notice: no siteId, no createdBy, no publishedAt, no id&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CreateItemInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;createItemSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, &lt;code&gt;z.object()&lt;/code&gt; strips unknown keys when it parses (&lt;code&gt;.parse()&lt;/code&gt; drops them; &lt;code&gt;.strict()&lt;/code&gt; would throw instead). Either way the poisoned fields never make it through.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/cms/src/routes/items.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createItemSchema&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;@lumibase/shared/schemas/items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// parse() drops any key not declared in createItemSchema&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createItemSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// input is now typed as CreateItemInput — no isAdmin, no siteId, no createdBy&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;itemService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;siteId&lt;/code&gt; and &lt;code&gt;createdBy&lt;/code&gt; are injected from the verified middleware context — not from the request body. The client never gets to supply them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix layer 2: Typed DTOs in the service layer
&lt;/h2&gt;

&lt;p&gt;Zod at the boundary is excellent, but defence in depth means the service layer should also be structurally incapable of accepting unsafe inputs. The pattern is to define a strict DTO type and have every service function accept only that type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/cms/src/services/item-service.ts&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;CreateItemInput&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;@lumibase/shared/schemas/items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nanoid&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;nanoid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&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;@lumibase/database/schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;createdBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateItemInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// only the fields Zod already validated&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;nanoid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// from verified tenant context, not dto&lt;/span&gt;
      &lt;span class="nx"&gt;createdBy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;// from verified auth context, not dto&lt;/span&gt;
      &lt;span class="na"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// only set by publish workflow, never by create&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;inserted&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;returning&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;inserted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function signature makes the contract machine-checkable: TypeScript will refuse to compile if you accidentally pass something that has &lt;code&gt;isAdmin&lt;/code&gt; or &lt;code&gt;siteId&lt;/code&gt; on it and try to spread it into &lt;code&gt;record&lt;/code&gt;. The type system becomes an automatic audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix layer 3: Readonly fields — never let user-supplied data touch system fields
&lt;/h2&gt;

&lt;p&gt;Some fields should be immutable after creation, or only writable by internal system processes. The cleanest way to express this in a Drizzle + TypeScript stack is to never include those columns in any user-facing DTO and to always set them programmatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Readonly fields checklist for every entity:&lt;/span&gt;
&lt;span class="c1"&gt;// id           → nanoid() at creation, never updatable&lt;/span&gt;
&lt;span class="c1"&gt;// siteId       → from tenant middleware, never from body&lt;/span&gt;
&lt;span class="c1"&gt;// createdBy    → from auth middleware, never from body&lt;/span&gt;
&lt;span class="c1"&gt;// createdAt    → DB default, never from body&lt;/span&gt;
&lt;span class="c1"&gt;// updatedAt    → set by service on every write, never from body&lt;/span&gt;
&lt;span class="c1"&gt;// publishedAt  → set only by the publish workflow, never by generic update&lt;/span&gt;
&lt;span class="c1"&gt;// auditFields  → written only by the audit module&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For updates, define a separate schema that only allows the fields that genuinely should change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateItemSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&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;review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="c1"&gt;// collectionId is also omitted — you can't move items between collections via a plain update&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UpdateItemInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;updateItemSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attempting to pass &lt;code&gt;{ siteId: "other-tenant", publishedAt: "2020-01-01" }&lt;/code&gt; to an endpoint that parses through &lt;code&gt;updateItemSchema&lt;/code&gt; will silently drop both fields before the service ever sees them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whitelist, never blacklist
&lt;/h2&gt;

&lt;p&gt;The instinct when you first learn about Mass Assignment is to write a blacklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Tempting but wrong&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;isAdmin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&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;createdBy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;safeBody&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;safeBody&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This breaks the moment you add a new sensitive column and forget to add it to the destructuring. You are playing catch-up with your own schema.&lt;/p&gt;

&lt;p&gt;The whitelist approach flips the default: only explicitly declared fields pass through. Adding a new sensitive column to your database has zero impact on the attack surface because it was never in the allowlist to begin with.&lt;/p&gt;

&lt;p&gt;One extra field in a JSON body should not hand someone a Presidential Suite. Or admin access. Or a ticket into another tenant's data. The framework's convenience — &lt;code&gt;req.body&lt;/code&gt;, &lt;code&gt;Object.assign&lt;/code&gt;, spread operators — is exactly what makes this vulnerability so pervasive. Comfortable patterns are the ones that hide the worst assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Three layers working together: Zod parses and strips at the HTTP boundary, typed DTOs constrain what the service can receive, and system fields are never in the DTO at all. Each layer catches what the previous one might miss. Any one of them alone is useful; all three together make Mass Assignment effectively impossible to sneak through.&lt;/p&gt;

&lt;p&gt;I'm building &lt;a href="https://lumibase.dev" rel="noopener noreferrer"&gt;Lumibase&lt;/a&gt; — a Content OS designed for AI agents operating at the edge. Security is structural from day one: multi-tenant isolation, typed service contracts, and strict schema validation at every entry point — whether the write comes from a human editor, an AI skill, a webhook, or an extension. If that sounds interesting, come take a look at &lt;a href="https://lumibase.dev" rel="noopener noreferrer"&gt;lumibase.dev&lt;/a&gt; 🌱&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The Key That Unlocks Everything: Prototype Pollution in JavaScript</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Sat, 11 Jul 2026 18:26:13 +0000</pubDate>
      <link>https://dev.to/khuepm/the-key-that-unlocks-everything-prototype-pollution-in-javascript-307o</link>
      <guid>https://dev.to/khuepm/the-key-that-unlocks-everything-prototype-pollution-in-javascript-307o</guid>
      <description>&lt;p&gt;Imagine a hotel where every room key is cut from a master template. When a guest checks in, the front desk hands them a key that opens only their room. Simple enough. Now imagine a guest who, during check-in, sneaks a tiny modification into the key-cutting machine itself — changing the template so that &lt;em&gt;every new key cut from that moment on&lt;/em&gt; also opens the manager's office, the safe, and the server room.&lt;/p&gt;

&lt;p&gt;The guest didn't break a lock. They didn't clone anyone's key. They changed the &lt;em&gt;factory&lt;/em&gt; that makes all keys.&lt;/p&gt;

&lt;p&gt;That factory is JavaScript's &lt;code&gt;Object.prototype&lt;/code&gt;. And the attack is called &lt;strong&gt;Prototype Pollution&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The Labor Illusion: Why Making Users Wait Can Build More Trust</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:09:31 +0000</pubDate>
      <link>https://dev.to/khuepm/the-labor-illusion-why-making-users-wait-can-build-more-trust-1i5m</link>
      <guid>https://dev.to/khuepm/the-labor-illusion-why-making-users-wait-can-build-more-trust-1i5m</guid>
      <description>&lt;p&gt;Here is a counterintuitive idea from behavioral psychology that quietly runs a lot of the software you use every day: &lt;strong&gt;the labor illusion&lt;/strong&gt;. The core finding is that people tend to trust and value a product more when they can &lt;em&gt;see the effort&lt;/em&gt; that went into producing it — even when the final result is exactly the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flight-search story
&lt;/h2&gt;

&lt;p&gt;The classic example is travel search sites like Kayak or Skyscanner. Early on, they could return flight results almost instantly. You'd expect users to love that. Instead, many of them were suspicious: &lt;em&gt;if it's that fast, how could it possibly have found me the cheapest fare?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So the teams did something that sounds absurd on paper — they &lt;strong&gt;deliberately slowed the system down&lt;/strong&gt;. They added a progress bar and a stream of status lines: "searching Delta…", "comparing prices across 200 airlines…", "checking baggage fees…". The actual work hadn't changed, but now users could watch it happen.&lt;/p&gt;

&lt;p&gt;The result? People waited &lt;em&gt;longer&lt;/em&gt; and yet &lt;strong&gt;booked more&lt;/strong&gt;. Seeing the system visibly grind away on their behalf made them trust the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works
&lt;/h2&gt;

&lt;p&gt;Waiting with no feedback feels like being ignored. Waiting while watching concrete, legible work feels like being served. The perceived value of an outcome isn't just the outcome — it's the outcome plus the visible evidence of the effort behind it. Strip away that evidence and, paradoxically, the same result feels cheaper and less trustworthy.&lt;/p&gt;

&lt;p&gt;There's a flip side worth naming: this is a tool, not a trick to abuse. Fake delays that don't map to real work erode trust the moment users catch on. The honest version of the labor illusion is &lt;strong&gt;exposing work that is genuinely happening&lt;/strong&gt; — not inventing work that isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I'm applying it to Lumibase
&lt;/h2&gt;

&lt;p&gt;Building &lt;a href="https://lumibase.dev" rel="noopener noreferrer"&gt;lumibase.dev&lt;/a&gt;, I kept running into moments where the honest version of this principle fits perfectly. When you hit &lt;strong&gt;deploy&lt;/strong&gt; or &lt;strong&gt;sync&lt;/strong&gt;, plenty is actually happening under the hood — validating config, running migrations, warming caches, spinning up the database. The old default was to hide all of that behind a spinner: a bored little circle going round and round while the user wonders whether anything is working at all.&lt;/p&gt;

&lt;p&gt;So instead of the spinner, Lumibase surfaces a live &lt;strong&gt;terminal-style log&lt;/strong&gt; that lists each real task as it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;▸ validating config…            ✓
▸ initializing database…        ✓
▸ applying migrations (3)…      ✓
▸ warming edge cache…           ⋯
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The operation takes the same amount of time it always did. Nothing is faked — every line maps to a step that's really executing. But by &lt;strong&gt;exposing the process instead of hiding it&lt;/strong&gt;, the wait stops feeling like dead air and starts feeling like the system working hard for you. Users come away calmer and more confident, because they can feel the value behind every click.&lt;/p&gt;

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

&lt;p&gt;Progress is not just a technical state; it's a story you tell the user about their time. If real work is happening, &lt;strong&gt;show it&lt;/strong&gt;. A legible process turns an anxious wait into evidence of effort — and evidence of effort is one of the cheapest, most honest ways to earn trust.&lt;/p&gt;

&lt;p&gt;I'm building all of this in public. Lumibase is a Content OS — a headless, AI-native CMS operated by governed agents. If you like this kind of UX-meets-engineering thinking, come hang out at &lt;a href="https://lumibase.dev" rel="noopener noreferrer"&gt;lumibase.dev&lt;/a&gt;. 🌱&lt;/p&gt;

</description>
      <category>ux</category>
      <category>psychology</category>
      <category>webdev</category>
      <category>design</category>
    </item>
    <item>
      <title>The Server That Robs Itself: How We Shut Down SSRF in LumiBase</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Mon, 06 Jul 2026 08:45:00 +0000</pubDate>
      <link>https://dev.to/khuepm/the-server-that-robs-itself-how-we-shut-down-ssrf-in-lumibase-2kla</link>
      <guid>https://dev.to/khuepm/the-server-that-robs-itself-how-we-shut-down-ssrf-in-lumibase-2kla</guid>
      <description>&lt;p&gt;Imagine your company has a courier everyone trusts completely. You hand him an address, he drives there, picks up the package, and brings it back — no questions asked. Most of the time customers give him a normal street address, so everything is fine. Then one day a con artist hands him an address that points &lt;em&gt;inside your own building&lt;/em&gt; — straight to the vault where every key and password is kept. The courier, being helpful, walks in, grabs the contents, and cheerfully hands them to the con artist, never realizing he just helped rob the place.&lt;/p&gt;

&lt;p&gt;That courier is your server. That con artist is an attacker. And that little heist has a name: &lt;strong&gt;Server-Side Request Forgery (SSRF)&lt;/strong&gt; — CWE-918.&lt;/p&gt;

&lt;p&gt;This is the story of how Lumibase closed that door.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SSRF actually is
&lt;/h2&gt;

&lt;p&gt;Lots of applications need to fetch a URL on the user's behalf: importing an image from a link, verifying a webhook endpoint, rendering a link preview, pulling a remote config. The user provides a URL, and the server makes the request.&lt;/p&gt;

&lt;p&gt;SSRF happens when an attacker supplies a URL that points somewhere they shouldn't be able to reach — and the server, which usually lives &lt;em&gt;inside&lt;/em&gt; the trusted network, dutifully makes the request for them. The attacker never touches the internal network directly; they borrow the server's privileges, and the firewall waves the request through because it came from a machine it trusts.&lt;/p&gt;

&lt;p&gt;The classic targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal services that are only reachable from inside the network (admin panels, databases, dashboards).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;localhost&lt;/code&gt; / loopback services bound to &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Link-local addresses.&lt;/li&gt;
&lt;li&gt;And the crown jewel in the cloud: the &lt;strong&gt;instance metadata endpoint&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the metadata endpoint makes SSRF terrifying
&lt;/h2&gt;

&lt;p&gt;On most cloud platforms there is a special internal address — &lt;code&gt;169.254.169.254&lt;/code&gt; — that any instance can call to retrieve information about itself, including &lt;strong&gt;temporary credentials&lt;/strong&gt;. It's convenient for the platform and lethal in the wrong hands.&lt;/p&gt;

&lt;p&gt;If an attacker can trick your server into fetching &lt;code&gt;http://169.254.169.254/…&lt;/code&gt;, the response can hand them the keys to the kingdom: cloud credentials scoped to whatever your instance can do. That is essentially the shape of the breach that leaked personal data of over a hundred million customers at a major US bank a few years ago. One trusted server, one carefully chosen URL, one very bad day.&lt;/p&gt;

&lt;p&gt;The scary part of SSRF is not brute force. It's misplaced trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  How LumiBase fixed it
&lt;/h2&gt;

&lt;p&gt;LumiBase is an edge-native, AI-native headless CMS being reshaped into a Content OS, so it does plenty of legitimate server-side fetching — extension bundles, remote resources, and outbound calls made by governed agents. Every one of those is a potential SSRF sink. Rather than sprinkle ad-hoc checks around, we put a single guard in front of &lt;em&gt;every&lt;/em&gt; request the server makes on a user's behalf.&lt;/p&gt;

&lt;p&gt;The guard runs before the fetch and enforces a strict deny list of destinations. Conceptually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse and resolve the target.&lt;/strong&gt; Never trust the raw string. Resolve the hostname to its actual IP(s) so a friendly-looking domain can't smuggle you to an internal address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject anything that isn't a real, public destination&lt;/strong&gt;, specifically:

&lt;ul&gt;
&lt;li&gt;Private ranges (RFC 1918): &lt;code&gt;10.0.0.0/8&lt;/code&gt;, &lt;code&gt;172.16.0.0/12&lt;/code&gt;, &lt;code&gt;192.168.0.0/16&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Loopback: &lt;code&gt;127.0.0.0/8&lt;/code&gt; and &lt;code&gt;::1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Link-local: &lt;code&gt;169.254.0.0/16&lt;/code&gt; and &lt;code&gt;fe80::/10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The cloud metadata endpoint (&lt;code&gt;169.254.169.254&lt;/code&gt;) explicitly&lt;/li&gt;
&lt;li&gt;Unspecified / reserved / unique-local ranges&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cover IPv6 too.&lt;/strong&gt; Attackers love format tricks — IPv6, IPv4-mapped IPv6 (&lt;code&gt;::ffff:169.254.169.254&lt;/code&gt;), decimal or hex-encoded IPs. The guard normalizes and checks both families so nobody sneaks in by changing notation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail closed.&lt;/strong&gt; If the address can't be confidently classified as safe and public, the request is blocked, not allowed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a &lt;em&gt;representative&lt;/em&gt; (simplified, illustrative) version of the approach — not a verbatim copy of the production file, but it captures what the guard does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ssrf-guard.ts — illustrative version of the approach&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isIP&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="s2"&gt;node:net&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ipaddr&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ipaddr.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Ranges we never allow a server-side fetch to reach.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BLOCKED_RANGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10.0.0.0/8&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="s2"&gt;172.16.0.0/12&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="s2"&gt;192.168.0.0/16&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="s2"&gt;127.0.0.0/8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// loopback&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;169.254.0.0/16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// link-local, incl. 169.254.169.254 metadata&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;::1/128&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// IPv6 loopback&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fe80::/10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// IPv6 link-local&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fc00::/7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// IPv6 unique-local&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isBlockedIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ipaddr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Normalize IPv4-mapped IPv6 (e.g. ::ffff:169.254.169.254) to IPv4.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ipv6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ipaddr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isIPv4MappedAddress&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;addr&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ipaddr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toIPv4Address&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;BLOCKED_RANGES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ipaddr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseCIDR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bits&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertSafeUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https:&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SSRF: only http(s) is allowed&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="c1"&gt;// If the host is a literal IP, check it directly.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isBlockedIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SSRF: blocked address &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Otherwise resolve the hostname and check every answer.&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;lookup&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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:dns/promises&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="nx"&gt;records&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;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;all&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="k"&gt;for &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;address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isBlockedIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SSRF: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; resolves to blocked &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then anywhere the server fetches on a user's behalf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;safeUrl&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;assertSafeUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userProvidedUrl&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;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;safeUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that matter as much as the list itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's centralized.&lt;/strong&gt; One guard, applied to every outbound-on-behalf-of-user request, so there's no forgotten code path. In Lumibase this sits alongside the extension sandbox that only loads bundles from an allowlisted origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's covered by tests.&lt;/strong&gt; The blocked ranges, the metadata endpoint, and the IPv6/encoding tricks all have test cases. So if a future refactor accidentally weakens the guard, the suite screams before it ships. A silent regression here is exactly the kind of thing that becomes a breach.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The most dangerous vulnerabilities usually aren't about a clever exploit chain. They're about trust that was never questioned. A helpful, obedient server that fetches whatever it's told is, in the wrong moment, more dangerous than the attacker — because it has credentials and network access the attacker doesn't.&lt;/p&gt;

&lt;p&gt;So the rule we keep coming back to: &lt;strong&gt;anything that goes out to the internet on a user's behalf must have its destination inspected before it's allowed to leave.&lt;/strong&gt; Validate, resolve, and fail closed.&lt;/p&gt;

&lt;p&gt;We're building all of this in public. LumiBase is a Content OS — a headless, AI-native CMS operated by governed agents against declarative intents, with full provenance and earned autonomy. If this kind of security-first, build-in-public engineering is your thing, come hang out at &lt;a href="https://lumibase.dev" rel="noopener noreferrer"&gt;lumibase.dev&lt;/a&gt;. 🌱&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Fail-open vs fail-closed: the security decision you make without realizing it</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Fri, 03 Jul 2026 02:51:12 +0000</pubDate>
      <link>https://dev.to/khuepm/fail-open-vs-fail-closed-the-security-decision-you-make-without-realizing-it-15kl</link>
      <guid>https://dev.to/khuepm/fail-open-vs-fail-closed-the-security-decision-you-make-without-realizing-it-15kl</guid>
      <description>&lt;p&gt;If you build software long enough, you eventually ship a bug that isn't a typo or a logic error — it's a &lt;em&gt;default&lt;/em&gt;. Specifically, it's the answer to a question nobody asked out loud: "What happens when this check fails?"&lt;/p&gt;

&lt;p&gt;That question has a name in security engineering: &lt;strong&gt;fail-open vs fail-closed&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The door analogy
&lt;/h2&gt;

&lt;p&gt;Picture a badge-controlled office door. Normally you need a card to get in. Then the power dies and the lock controller crashes. In that instant the door has to "decide": swing open so people can move freely, or stay locked so no one gets through. Swinging open is &lt;strong&gt;fail-open&lt;/strong&gt;. Staying locked is &lt;strong&gt;fail-closed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Neither is universally correct. A fire exit is designed to fail-open — in an emergency, human life beats access control. A bank vault is the opposite: it fails closed, because a vault that pops open when the system glitches defeats its entire purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same choice in code
&lt;/h2&gt;

&lt;p&gt;Say your auth service goes down mid-request. What does your app do?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fail-open: keep serving, but the door is wide open&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth check failed, allowing request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// an attacker's dream&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fail-closed: deny by default when anything is uncertain&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth check failed, denying request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// safe, even if annoying&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fail-open keeps the site alive at the cost of letting everyone in. Fail-closed keeps attackers out at the cost of locking real users out too. This is the classic trade-off between &lt;strong&gt;availability&lt;/strong&gt; and &lt;strong&gt;security&lt;/strong&gt;, and the whole point is that you have to pick a side &lt;em&gt;on purpose&lt;/em&gt; — not leave it to a stray &lt;code&gt;catch&lt;/code&gt; block.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rule of thumb
&lt;/h2&gt;

&lt;p&gt;The more sensitive the thing, the more it should fail-closed. Payment gateways, authentication layers, data-access checks — when these break, the default must be "deny", because a single fail-open here can cost you the whole system.&lt;/p&gt;

&lt;p&gt;The inverse is also true. Auxiliary concerns — logging, notifications, analytics — should usually fail-open. If your metrics pipeline goes down, blocking the entire app over it is the actual bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for an AI-native CMS
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://lumibase.dev" rel="noopener noreferrer"&gt;LumiBase&lt;/a&gt;, the Content OS I'm building in public, this decision shows up everywhere — and it gets sharper once &lt;strong&gt;AI agents&lt;/strong&gt; operate content on your behalf.&lt;/p&gt;

&lt;p&gt;An agent that wants to publish or delete content touches a &lt;code&gt;schema:write&lt;/code&gt;-class capability. If the approval service or policy check is unavailable, the agent must fail-closed: no write happens, the action lands in an approval queue, and a human keeps the veto. Failing open here would mean an autonomous agent shipping changes to production content while your guardrails are offline — exactly the nightmare scenario a headless CMS should never allow.&lt;/p&gt;

&lt;p&gt;Meanwhile, the parts that only observe — provenance logging, reconciliation telemetry, usage stats — fail-open. A hiccup in the audit stream should never stop a legitimate content operation.&lt;/p&gt;

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

&lt;p&gt;Fail-open vs fail-closed isn't about which one is "better". It's about whether you chose at all. The scariest vulnerabilities are rarely dramatic — they're the quiet spots where a developer never asked "what happens when this breaks?", and the system silently failed open while everyone assumed it was safe.&lt;/p&gt;

&lt;p&gt;Ask the question early. Make the default explicit. Your future incident report will thank you.&lt;/p&gt;

</description>
      <category>lumibase</category>
      <category>headlesscms</category>
      <category>ai</category>
    </item>
    <item>
      <title>Day 5/100: Integrating Payments for App and Web 💳</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Thu, 17 Jul 2025 15:37:54 +0000</pubDate>
      <link>https://dev.to/khuepm/day-5100-integrating-payments-for-app-and-web-582h</link>
      <guid>https://dev.to/khuepm/day-5100-integrating-payments-for-app-and-web-582h</guid>
      <description>&lt;h3&gt;
  
  
  On Today's Agenda
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Hitting the Limits of AI Tooling
&lt;/li&gt;
&lt;li&gt; The Core Question: Stripe vs. RevenueCat
&lt;/li&gt;
&lt;li&gt; The Verdict &amp;amp; A Local Twist
&lt;/li&gt;
&lt;li&gt; An Indie Developer's Philosophy in the AI Era
&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Hitting the Limits of AI Tooling
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wow! A guide to making money with AI in 2025? 🤩&lt;/p&gt;

&lt;p&gt;Not quite. This is a deep dive into payment gateways and how to integrate them into the website I've built with AI so I can actually start selling.&lt;/p&gt;

&lt;p&gt;After getting a relatively complete interface, coded 100% by AI with a ton of prompts, I went through the loop of testing, committing, deploying... over and over. I started to find Bolt AI becoming extremely sluggish, with bugs that I just couldn't fix. It seems it only works well for simple landing pages. The moment I got to the Dashboard screens, Bolt ran into a lot of issues: fixing one thing would break another, fixing that would cause a new page error, fixing the new error would bring back the original bug... So, it's best to just use it to "farm" UIs for quantity during its one-month trial (from the hackathon). The internal logic will probably require Windsurf and Cursor.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F5tuvy2k6fiw0n0owsosq.gif" 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.amazonaws.com%2Fuploads%2Farticles%2F5tuvy2k6fiw0n0owsosq.gif" alt="Frustrated Bug Fixing GIF" width="500" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the bright side, reviving old projects with a new interface is something it does in a snap. I had saved some old web designs that I loved but whose sites are now defunct. I found them on the Internet Archive, took screenshots, and had Bolt bring them back to life. Just like that, I had a one-of-a-kind website.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Core Question: Stripe vs. RevenueCat
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back to payment gateways. RevenueCat requires using Stripe or a Merchant of Record (MoR) with integrated billing, like Paddle. Paddle is great because it handles VAT and is a solution when you can't register for Stripe with a Vietnamese legal entity. In terms of fees, Stripe is cheaper, but the key requirement is having a legal entity in a supported country like the US or UK.&lt;/p&gt;

&lt;p&gt;This raises the question: if I can get Stripe, why do I even need RevenueCat?&lt;/p&gt;

&lt;p&gt;This is the crucial distinction. I had AI create a comparison table to break it down, but here's the summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; solves the problem of &lt;strong&gt;"getting paid"&lt;/strong&gt; on the web. It's the payment processor that handles the transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RevenueCat (RC)&lt;/strong&gt; solves the problem of &lt;strong&gt;"what has the customer bought, and what should the app unlock?"&lt;/strong&gt; It manages the subscription lifecycle, provides analytics, and syncs purchases across multiple platforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, Stripe handles the cash; RevenueCat handles the customer's subscription status and entitlements. It's the management and growth tool that sits on top of your payment processor.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fmq5w0uzp5xgl0pse9pom.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fmq5w0uzp5xgl0pse9pom.gif" alt="Money Transaction GIF" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Verdict &amp;amp; A Local Twist
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your product has (or will have) a mobile app, if you want to A/B test paywalls, or if you simply want to save on maintenance time, RC is definitely worth keeping alongside Stripe.&lt;/p&gt;

&lt;p&gt;The main reason for me is integrating payments on the App itself. Customers will buy through Apple/Google, and RC helps sync that user data, allowing them to &lt;strong&gt;buy once and use anywhere&lt;/strong&gt;. For this reason, I'm willing to pay the extra 1% fee to RC for now and consider cost-saving measures later.&lt;/p&gt;

&lt;p&gt;One more note: if my customers are primarily from Vietnam (&amp;gt;80%), I will redirect them to &lt;strong&gt;9Pay&lt;/strong&gt;. Using a local Vietnamese payment gateway will save quite a bit on fees.&lt;/p&gt;




&lt;h3&gt;
  
  
  An Indie Developer's Philosophy in the AI Era
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since the app is in its early development stage, I desperately need users to use it, give feedback, and request new features.&lt;/p&gt;

&lt;p&gt;For indie developers like me, the greatest joy is creating products that have &lt;strong&gt;real users&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So many products get thrown away. The code can be beautiful, the technology can be fancy, you can even register a company in some far-off land to dodge taxes... but the product has... no users. :D&lt;/p&gt;

&lt;p&gt;With the current wave of AI, creating new products has never been so fast—it's like making instant noodles.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fyhjbc01d3kndr3k0yprh.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fyhjbc01d3kndr3k0yprh.gif" alt="Indie Developer Building GIF" width="370" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Day 3/100: Assembling the Toolkit on a Shoestring Budget 🛠️💰</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Tue, 08 Jul 2025 11:01:52 +0000</pubDate>
      <link>https://dev.to/khuepm/day-3100-assembling-the-toolkit-on-a-shoestring-budget-3a45</link>
      <guid>https://dev.to/khuepm/day-3100-assembling-the-toolkit-on-a-shoestring-budget-3a45</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/wlh"&gt;World's Largest Hackathon Writing Challenge&lt;/a&gt;: Building with Bolt.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Table of Contents: 100-Day Journey
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1/100&lt;/td&gt;
&lt;td&gt;Starting the 100-Day Journey &amp;amp; Introduction&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/khuepm/a-multi-part-series-architecting-a-giant-17-products-enhanced-by-ai-2cgm"&gt;View Post&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2/100&lt;/td&gt;
&lt;td&gt;AI Showdown: Which AI to choose for project analysis?&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/khuepm/day-2100-which-ai-to-choose-for-analyzing-investment-projects-35e7"&gt;View Post&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3/100&lt;/td&gt;
&lt;td&gt;Assembling the Toolkit on a Shoestring Budget&lt;/td&gt;
&lt;td&gt;This post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99/100&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100/100&lt;/td&gt;
&lt;td&gt;Reflections on the 100-Day Journey&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  On Today's Agenda
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; The Hackathon Opportunity: Why I Jumped In
&lt;/li&gt;
&lt;li&gt; The Ultimate Low-Cost Tech Stack
&lt;/li&gt;
&lt;li&gt; Let the Building Begin! From Image to UI
&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  The Hackathon Opportunity
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bolt announced its Hackathon two months ago, but I only decided to join near the end. Partly because I used to think AI tools were like v0—not great for building UIs. Plus, I was already happy with my setup of Cursor and Windsurf. A year or two ago, AI code assistants were riddled with bugs, but they've gradually evolved to the point where they can now code the entire frontend.&lt;/p&gt;

&lt;p&gt;However, after trying out Loveable and Bolt, I feel like the options for UI generation have multiplied. The GitHub integration is incredibly fast for pushing code. Just by registering for the Bolt hackathon, I was given &lt;strong&gt;1 million free tokens&lt;/strong&gt; to build with, along with tons of coupons for other tools. Most importantly, it gave me free access to the &lt;strong&gt;RevenueCat&lt;/strong&gt; payment gateway.&lt;/p&gt;

&lt;p&gt;I had also signed up for another gateway called Ruul, which has the advantage of accepting crypto (USDT). However, RevenueCat waives its fees for revenue under $2.5k/month. I decided to use RevenueCat for web payments, Bolt to build the new app, ChatGPT to help shape the idea, and Gemini to choose the app's name and write the Terms &amp;amp; Conditions.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F8lys0v11evp4q37zu6vd.gif" 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.amazonaws.com%2Fuploads%2Farticles%2F8lys0v11evp4q37zu6vd.gif" alt="Let's do this GIF" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ultimate Low-Cost Tech Stack
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the complete stack I've assembled for this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logo &amp;amp; Banners:&lt;/strong&gt; Canva&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual UI Design:&lt;/strong&gt; Figma&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Frontend Generation:&lt;/strong&gt; Bolt AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain:&lt;/strong&gt; Namecheap (already purchased, so I didn't use the free one from the Hackathon).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; Supabase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Server &amp;amp; Deployment:&lt;/strong&gt; Netlify&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN &amp;amp; DNS:&lt;/strong&gt; Cloudflare&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; Google Workspace (I'm lucky to have a grandfathered free Basic plan from 2011, as they no longer offer it. A good alternative is Lark by Bytedance—I've already registered a few accounts just in case they follow Google and drop their free tier ^^!).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Store Account:&lt;/strong&gt; Apple Developer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'll deal with other security considerations later on during the project. Today's task is to register for the Apple Developer account to push the app to the Apple Store. The cost is &lt;strong&gt;$99&lt;/strong&gt;, which came out to about 1.2 million VNĐ according to the developer site notification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total Projected Cost:&lt;/strong&gt; &lt;strong&gt;$7 for the domain + $99/year for the Apple account.&lt;/strong&gt; Everything else is free.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fddpk6q23jos17yabvgqm.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fddpk6q23jos17yabvgqm.gif" alt="Toolbox GIF" width="480" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Let the Building Begin!
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alright then, time to build the interface! Bolt's ability to convert an image into a web interface is excellent. You can find a design you like on a site like UI8, take a screenshot, and feed it to Bolt. Then you just ask it to replicate the image for your idea, swapping out this or that section.&lt;/p&gt;

&lt;p&gt;I'm determined to build an MVP with core functions, a smooth end-to-end flow, minimal bugs, and basic security. Therefore, I'm not stressing over the UI being perfect; as long as I think it looks good and isn't buggy, it's a win.&lt;/p&gt;

&lt;p&gt;I repeated these steps for the Login, Signup, Pricing, and Dashboard screens... and in a flash, I had dozens of commits. It's been a while since I've felt this productive.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F5iwwvkntritoxd0u6ejx.gif" 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.amazonaws.com%2Fuploads%2Farticles%2F5iwwvkntritoxd0u6ejx.gif" alt="Productivity GIF" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>build24dottech</category>
      <category>wlhchallenge</category>
      <category>bolt</category>
    </item>
    <item>
      <title>Day 2/100 - Which AI to choose for analyzing investment projects?</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Sat, 05 Jul 2025 13:39:10 +0000</pubDate>
      <link>https://dev.to/khuepm/day-2100-which-ai-to-choose-for-analyzing-investment-projects-35e7</link>
      <guid>https://dev.to/khuepm/day-2100-which-ai-to-choose-for-analyzing-investment-projects-35e7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/wlh"&gt;World's Largest Hackathon Writing Challenge&lt;/a&gt;: Building with Bolt.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents: Architecting a Giant - 17 Products Enhanced by AI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;(100-Day Journey)&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;Day&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Notes / Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1/100&lt;/td&gt;
&lt;td&gt;Starting the 100-Day Journey &amp;amp; Introduction&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/khuepm/a-multi-part-series-architecting-a-giant-17-products-enhanced-by-ai-2cgm"&gt;View&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2/100&lt;/td&gt;
&lt;td&gt;Which AI to choose for analyzing investment projects?&lt;/td&gt;
&lt;td&gt;This page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3/100&lt;/td&gt;
&lt;td&gt;Assembling the Toolkit on a Shoestring Budget&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/khuepm/day-3100-assembling-the-toolkit-on-a-shoestring-budget-3a45"&gt;View&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100/100&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  🤖 AI Showdown: Gemini vs. ChatGPT vs. Grok for Project Analysis 🚀
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Part 1: The Initial Test - Methodology 🧠
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;(Continued from Day 1...)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first question to the AIs was simple: &lt;strong&gt;"What is your methodology?"&lt;/strong&gt; 🤔&lt;/p&gt;

&lt;p&gt;Every AI provides a pretty slick answer, but in my research, Gemini was the one that presented a clear, structured methodology right from the start. Before even touching the project details, Gemini laid out a strategic analysis framework based on &lt;strong&gt;FAST&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;F&lt;/strong&gt;easibility ✅&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A&lt;/strong&gt;ccelerated cash flow generation 💸&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;S&lt;/strong&gt;ustainability &amp;amp; &lt;strong&gt;T&lt;/strong&gt;hreat (Risk) level 🛡️&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This wasn't something I prompted! It showed me that each AI has its own way of thinking. ChatGPT and Grok were similar, giving a general overview and objectives. ChatGPT did a bit better by listing evaluation criteria, but it wasn't nearly as detailed or systematic as Gemini.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scores for this round:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini:&lt;/strong&gt; +1 🥇&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT:&lt;/strong&gt; +0.5 👍&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fgdvoovussjewdz1i5ib3.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fgdvoovussjewdz1i5ib3.gif" alt="Strategy GIF" width="480" height="264"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: The Data Wars - Who Has the Best Fuel? ⛽
&lt;/h2&gt;

&lt;p&gt;An AI is only as good as its data, right?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Google (Gemini):&lt;/strong&gt; Has a colossal data advantage. We're talking Google Search, Google Scholar 📚, Google Translate, YouTube, Google Docs... plus the entire internet that's constantly feeding its bots for SEO purposes 😅. This massive, diverse dataset is an advantage rivals can only dream of. 👑&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenAI (ChatGPT):&lt;/strong&gt; Has the first-mover advantage. It has a gigantic user base feeding it endless questions, feedback, and prompts. Plus, it's backed by the financial muscle of Microsoft. 💪&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;xAI (Grok):&lt;/strong&gt; Has access to the real-time, chaotic data firehose of X (formerly Twitter) with its ~650 million users. However, for deep research, this isn't ideal. X is mostly short-form news, memes, and entertainment. The in-depth data just can't compete with the other two giants.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scores for this round:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini:&lt;/strong&gt; +1.5 🥇&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT:&lt;/strong&gt; +1.0 👍&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grok:&lt;/strong&gt; +0.5 🤷&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fybwsj1cwyxmess67yhnv.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fybwsj1cwyxmess67yhnv.gif" alt="Data GIF" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The Powerhouse - Context &amp;amp; Ecosystem 🔌
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How much information can they handle at once?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini:&lt;/strong&gt; Supports up to a &lt;strong&gt;1 million token&lt;/strong&gt; context window. 🤯 This is massive, allowing it to analyze thousands of pages or an entire complex codebase in one go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT &amp;amp; Grok:&lt;/strong&gt; Limited to 128k tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What about professional integration?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini:&lt;/strong&gt; Deeply integrated into the Google Workspace ecosystem (Docs, Sheets, etc.), making it highly synchronized. 🔗&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT:&lt;/strong&gt; Integrated into the Microsoft ecosystem as Copilot. While the MS ecosystem (Windows, Office) is huge, the integration feels less seamless since OpenAI and Microsoft are separate companies. 🤝&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grok:&lt;/strong&gt; Just has the Tesla/xAI ecosystem. At this point, I'm taking Grok out of the race. 👋&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scores for this round:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini:&lt;/strong&gt; +1 🥇&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT &amp;amp; Grok:&lt;/strong&gt; +0.5 👍&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2F1o0cq2xvzevw61t07ku3.gif" 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.amazonaws.com%2Fuploads%2Farticles%2F1o0cq2xvzevw61t07ku3.gif" alt="Power GIF" width="480" height="208"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: The Real-World Challenge - Project Analysis 📊
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting.&lt;/p&gt;

&lt;p&gt;ChatGPT was very smart and immediately identified the project I was most passionate about. ❤️&lt;/p&gt;

&lt;p&gt;But Gemini did something incredible that ChatGPT completely missed (unless I specifically asked for it): &lt;strong&gt;it analyzed the legal risks.&lt;/strong&gt; ⚖️ Gemini independently found the project's GitHub repo, analyzed its open-source license, and gave me clear, serious warnings ⚠️ about potential policy violations and abuses. It even gave my projects cool names like "Project Phoenix" (for a project I wanted to revive) and "Navigating the Walled Garden" (for projects with hidden mandatory costs like a $99 Apple Developer fee). I absolutely loved this. ChatGPT just ignored this critical aspect.&lt;/p&gt;

&lt;p&gt;However, when it came to the &lt;em&gt;detailed project analysis&lt;/em&gt; (ignoring legalities), ChatGPT shined. ✨ It did the most thorough research on the technical feasibility. As a solo dev, this is the most crucial factor! I don't know why Gemini overlooked it. If you can't overcome the technical hurdles, the project is dead on arrival. 💀&lt;/p&gt;

&lt;p&gt;Furthermore, on my question about the &lt;strong&gt;fastest path to cash flow&lt;/strong&gt; 💰, ChatGPT provided a much more realistic timeline, considering development, app store reviews, payment gateway holding periods, etc. Gemini's analysis felt a bit superficial here; it would quickly dismiss a project if it hit any obstacle, without providing a deep, comparative breakdown.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fnfylg2nr2bczk9q0xi5a.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fnfylg2nr2bczk9q0xi5a.gif" alt="Analysis GIF" width="480" height="320"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: The Final Verdict 🏆
&lt;/h2&gt;

&lt;p&gt;So, what's the conclusion?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gemini is probably the best AI tool for initial, high-level strategic research.&lt;/strong&gt; It sets up a fantastic framework and catches critical things like legal risks that others miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT seems to be better for deep, granular analysis, especially on the technical side.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All AIs need good prompts to deliver great results, but Gemini seems to make the initial process a bit easier and safer.&lt;/p&gt;

&lt;p&gt;But in the end, why choose? 🤔 I'm a grown-up, I don't choose A or B... I choose them all! 😁 The best approach is to use both, compare their outputs, and synthesize the results yourself.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fliunt62ovf5ki1mmr4ci.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fliunt62ovf5ki1mmr4ci.gif" alt="Why Not Both GIF" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>build24dottech</category>
      <category>devchallenge</category>
      <category>wlhchallenge</category>
      <category>bolt</category>
    </item>
    <item>
      <title>A Multi-Part Series: Architecting a Giant - 17 Products Enhanced by AI - Day 1</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Fri, 04 Jul 2025 13:46:00 +0000</pubDate>
      <link>https://dev.to/khuepm/a-multi-part-series-architecting-a-giant-17-products-enhanced-by-ai-2cgm</link>
      <guid>https://dev.to/khuepm/a-multi-part-series-architecting-a-giant-17-products-enhanced-by-ai-2cgm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/wlh"&gt;World's Largest Hackathon Writing Challenge&lt;/a&gt;: Building with Bolt.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here you go, my friend. Let me retell you the story of my "AI bake-off," just like we're chilling and chatting over a beer.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Day 1 - First Contact: The Initial AI Test / Picking AI (ChatGPT, Gemini, Grok) for a Deep Dive?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;So, here's the deal. My brain's been firing off ideas like a broken firework launcher lately. We're talking everything from Open-source projects, Chrome Extensions, Games, Web3, E-commerce, to Health apps, apps for kids... I've even thought about resurrecting old dead projects, making social media videos, or just straight-up cloning extensions and data. I did a quick count, and we're talking about 17 projects with a clear concept. For most of them, I've just dipped my toes in, you know, a little Proof of Concept (PoC) to check the technical feasibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;So, What's the Mission?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Since my brain is basically a messy browser with too many tabs open, I asked the AIs to pick a starter for me. The main rule? It had to be something that could make money ASAP. Cold, hard cash. The more the merrier (though let's be real, any amount is a good start). Of course, "generating cash flow" is a vague beast and depends on a million things, but I just threw that general request out there to see how the AIs would handle it.&lt;/p&gt;

&lt;p&gt;My AI squad is all top-tier stuff: Gemini 2.5 Pro, Grok 3 Pro, and ChatGPT o3-pro. To analyze the feasibility, I made them all use their strongest "Deep Research" features: For Gemini, it was "Deep research" on the Gemini 2.5 Pro model; for ChatGPT, it was o3-pro (the "best at reasoning" one) combined with its Research feature; and for Grok, it was "Deeper Research" with Grok3. To keep things fair, I fed them the exact same prompts in two different ways: first, a series of smaller prompts adding more details each time, and second, a version where I crammed all the questions into a single, massive prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Round 1: The Face-Off&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Kicking things off with Gemini, the Google guy, it shot back with a whole research strategy for me to confirm. Meanwhile, ChatGPT wanted more deets, asking a bunch of follow-up questions. And Grok from xAI? Just jumped straight into the analysis. Ballsy.&lt;/p&gt;

&lt;p&gt;I gotta say, I liked ChatGPT's approach. It asked some really legit questions, hitting angles I hadn't even thought of. Honestly, this guy made me create new prompts just to feed the other two :)) By the end, even when I combined everything into one monster prompt, it &lt;em&gt;still&lt;/em&gt; managed to find more questions to ask @.@ Anyway, I'll admit, this thorough approach is solid. &lt;strong&gt;Score: ChatGPT (C): 1, Gemini (Ge): 0, Grok (Gr): 0.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Fgdvoovussjewdz1i5ib3.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fgdvoovussjewdz1i5ib3.gif" alt="Thinking GIF" width="480" height="264"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;p&gt;But... and it's a big but... OpenAI's golden boy has this super annoying bug where its 'thinking' process just... hangs. Forever. I'd heard a buddy complain about this before, so I wasn't shocked, but man, is it irritating. I had to F5 the page to see the results, which made me wonder: if I hadn't refreshed, would it still be 'thinking' right now? The UI not updating when the job is done is just awful! But hey, all I care about is the result, so I'll give the other two a little bonus point for not giving me a headache. &lt;strong&gt;Score: ChatGPT (C): 1, Gemini (Ge): 0.5, Grok (Gr): 0.5.&lt;/strong&gt;&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F1k06uq46xg560xeybo5r.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.amazonaws.com%2Fuploads%2Farticles%2F1k06uq46xg560xeybo5r.png" alt=" " width="800" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While the other two dudes were cruising along, showing their progress as they thought, ChatGPT was noticeably slower. &lt;em&gt;What gives?&lt;/em&gt; Out of curiosity, I peeked into its thinking process and saw it was totally different. To analyze a project's real-world potential, ChatGPT spent ages searching and analyzing YouTube videos for a project I'd already shelved, then compared it to other similar projects to scrape for info.&lt;/p&gt;

&lt;p&gt;Obviously, this deep-dive method paid off in terms of application analysis—the results were super detailed and spot-on. I guess there's a tradeoff here: more research time for more detailed results. I really want to give ChatGPT points for this, but if it goes down the wrong rabbit hole, all that detail is useless, right? I'll put a pin in that for now and see how other factors play out.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F87lvqr5lywlvmhqlaxdu.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.amazonaws.com%2Fuploads%2Farticles%2F87lvqr5lywlvmhqlaxdu.png" alt=" " width="800" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>build24dottech</category>
      <category>devchallenge</category>
      <category>wlhchallenge</category>
      <category>bolt</category>
    </item>
    <item>
      <title>React-Native + Web + Firebase (Part 02) — Implement React Native Firebase cross-app</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Fri, 20 Jun 2025 18:38:12 +0000</pubDate>
      <link>https://dev.to/khuepm/react-native-web-firebase-part-02-implement-react-native-firebase-cross-app-2ike</link>
      <guid>https://dev.to/khuepm/react-native-web-firebase-part-02-implement-react-native-firebase-cross-app-2ike</guid>
      <description>&lt;h2&gt;
  
  
  Project Structure Overview and Login Flow
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;In &lt;a href="https://dev.to/khuepm/react-native-web-firebase-part-01-why-we-use-react-native-to-develop-both-native-and-web-fdg"&gt;part one&lt;/a&gt;, we gained an overview of the directory structure and the packages we will install for the project. Now, we’ll discuss the login flow, which will be implemented as follows:&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2Fuq366h1r2o4cwz6ebvqb.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.amazonaws.com%2Fuploads%2Farticles%2Fuq366h1r2o4cwz6ebvqb.png" alt="Image description" width="800" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Login Flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Opening the App:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;If the user is not logged in:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Display the &lt;strong&gt;Login Screen&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user taps “Login with Apple” to initiate the login process on their iPhone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Searching for the User based on the account:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the user does not exist:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create a new user and proceed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the user exists and is valid:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retrieve the valid user information and proceed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the user exists but is invalid:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Display an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Returning to the App:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store the login information in &lt;strong&gt;Context&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the user is valid, navigate to the &lt;strong&gt;Home Screen&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the user is invalid, navigate to the &lt;strong&gt;Error Screen&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. When the User is already logged in:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open the app and check for the latest user data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the user does not exist:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create a new user and proceed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the user exists and is valid:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retrieve the valid user information and proceed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the user exists but is invalid:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Display an error&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;App Structure:&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The app can have multiple &lt;strong&gt;Context Providers&lt;/strong&gt; serving different purposes, such as theme settings or authentication information.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;An index file for the providers needs to be created to manage them.&lt;/p&gt;

&lt;p&gt;export function AppContextProvider({ children }: { children: React.ReactNode }): JSX.Element {&lt;/p&gt;

&lt;p&gt;return (&lt;br&gt;
   &lt;br&gt;
    &lt;br&gt;
     {children}&lt;br&gt;
    &lt;br&gt;
   &lt;br&gt;
  )&lt;br&gt;
}&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AppConfigProvider&lt;/strong&gt; is an example of other contexts, which we won’t discuss here.&lt;/p&gt;

&lt;p&gt;Now, let’s create the &lt;strong&gt;AuthProvider.tsx&lt;/strong&gt; file with the following content:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// import files
...
type LoginAction = {
  type: 'LOGIN',
  user: User
}
type LogoutAction = {
  type: 'LOGOUT'
}
type ChangeAction = {
  type: 'CHANGE',
  user: User
}
type OnUserChangedAction = {
  type: 'OnUserChangedAction',
  user: User
}
type AuthDispatchAction = LoginAction | LogoutAction | ChangeAction | OnUserChangedAction

export type AuthContextState = User | null
export const AuthContext = createContext&amp;lt;AuthContextState&amp;gt;(null);
export const AuthDispatchContext = createContext&amp;lt;Dispatch&amp;lt;AuthDispatchAction&amp;gt; | null&amp;gt;(null)
export function authReducers(preAuthState: AuthContextState, action: AuthDispatchAction): AuthContextState {
  switch (action.type) {
    case 'LOGIN':
    case 'CHANGE':
      return action.user
    case 'OnUserChangedAction':
      return action.user;
    case 'LOGOUT':
      return null
    default:
      throw Error('Unknown action: ' + JSON.stringify(action));
  }
}

export function AuthProvider({ children }: PropsWithChildren&amp;lt;any&amp;gt;) {
  const [initializing, setInitializing] = useState(true);
  const [authState, authDispatch] = useReducer&amp;lt;Reducer&amp;lt;AuthContextState, AuthDispatchAction&amp;gt;&amp;gt;(authReducer, defaultUserState,)
  const handleAuthStateChanged = useCallback(async (user: FirebaseAuthUser | null) =&amp;gt; {
    if (!user) {
      authDispatch({ type: 'LOGOUT' })
    }
    else {
      const authUser = await getAuthenticatedUser(user.uid);
      if (authUser)
        authDispatch({ type: 'CHANGE', user: authUser, });
    }
    if (initializing) {
      setInitializing(false);
    }
  }, [initializing])

  useEffect(() =&amp;gt; {
    // ... 
  }, [])

  return (
    &amp;lt;AuthContext.Provider value={authState}&amp;gt;
      &amp;lt;AuthDispatchContext.Provider value={authDispatch}&amp;gt;
        {!initializing &amp;amp;&amp;amp; children}
      &amp;lt;/AuthDispatchContext.Provider&amp;gt;
    &amp;lt;/AuthContext.Provider&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It’s important to note that in the useEffect, we will call the onAuthStateChanged service to update the state. When the app receives the user information, it will call the reducers to update the context:&lt;/p&gt;

&lt;p&gt;Add the following to the useEffect:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { onAuthStateChanged } from "../services/Firebase/users/userManagement";

//...

useEffect(() =&amp;gt; {
 return onAuthStateChanged(handleAuthStateChanged);
}, [handleAuthStateChanged])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In the services/Firebase/users/ directory, we will create three files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;userManagement.ts&lt;/strong&gt;: Contains interfaces to call functions from outside, helping to define types so that other hooks/services can understand them. This is where we declare onAuthStateChanged as mentioned earlier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;userManagement.native.ts&lt;/strong&gt; and &lt;strong&gt;userManagement.web.ts&lt;/strong&gt;: These files implement onAuthStateChanged for each platform. Since web and app use different libraries, they cannot share the same source code.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s continue:&lt;/p&gt;

&lt;p&gt;First, declare the interface for userManagement in the userManagement.ts file. When the user information is updated, Firebase will call this function, returning a callback with the user information for further processing:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface UserManagement {
  onAuthStateChanged: &amp;lt;T&amp;gt;(callback: (user: T) =&amp;gt; void) =&amp;gt; () =&amp;gt; void;
  // getUserByDevice: () =&amp;gt; Promise&amp;lt;User | undefined&amp;gt;;
  // getUserById: (userId: string) =&amp;gt; Promise&amp;lt;User | undefined&amp;gt;;
  // updateUser: (userId: string, userData: any) =&amp;gt; Promise&amp;lt;void&amp;gt;;
  // subscribeOnUserChanged: (userId: string, callback: (user: User | undefined) =&amp;gt; void) =&amp;gt; () =&amp;gt; void;
  // signOut: () =&amp;gt; Promise&amp;lt;void&amp;gt;;
  // getCurrentUser: () =&amp;gt; Promise&amp;lt;User | undefined&amp;gt;;
  // signInWithCredential: &amp;lt;T, G&amp;gt;(credential: T) =&amp;gt; Promise&amp;lt;G&amp;gt;;
  // signInAnonymously: &amp;lt;T&amp;gt;() =&amp;gt; Promise&amp;lt;T&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Next, we need to define where we call the function. In the same userManagement.ts file, add the following:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Platform } from "react-native";

const userManagement = Platform.select({
 web: () =&amp;gt; require('./userManagement.web'),
 default: () =&amp;gt; require('./userManagement.native'),
});

export const {
 onAuthStateChanged,

 // getUserByDevice,
 // getUserById,
 // updateUser,
 // subscribeOnUserChanged,
 // signOut,
 // getCurrentUser,
 // signInWithCredential,
 // signInAnonymously
} = userManagement as UserManagement;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;userManagement.native.ts&lt;/strong&gt; version uses the package @react-native-firebase/auth, while the web version calls the firebase/auth library to invoke the onAuthStateChanged function.&lt;/p&gt;

&lt;p&gt;This function is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Listening for changes in the user’s authentication state (logging in and out).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This method returns an unsubscribe function to stop listening to events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Always ensure you unsubscribe from the listener when it is no longer needed to prevent updates to components that are no longer in use.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;userManagement.native.ts&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;import auth, { FirebaseAuthTypes } from "@react-native-firebase/auth";&lt;/p&gt;

&lt;p&gt;export type FirebaseAuthUser = FirebaseAuthTypes.User;&lt;br&gt;
export function onAuthStateChanged(callback: (user: FirebaseAuthUser | null) =&amp;gt; void): () =&amp;gt; void {&lt;br&gt;
 return auth().onAuthStateChanged(callback);&lt;br&gt;
}&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The native version requires the configuration of the GoogleService-Info.plist file for the Apple app and/or the google-services.json file for the Android app to function properly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;userManagement.web.ts&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { auth, db } from '../config';
import { User as FirebaseUser } from "firebase/auth";

export type FirebaseAuthUser = FirebaseUser;
export function onAuthStateChanged(
 callback: (user: FirebaseAuthUser | null) =&amp;gt; void
): () =&amp;gt; void {
 return auth.onAuthStateChanged(callback);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Unlike the native version, the web version requires a configuration to call Firebase through the initializeAuth function. Therefore, we need to add a config file to the Firebase service as follows:&lt;/p&gt;

&lt;p&gt;Folder Structure Updated:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
services/
 Firebase/
    config/
     index.ts
     index.native.ts
     index.web.ts
  users/
   index.ts
   userManagement.ts
   userManagement.native.ts
   userManagement.web.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Similar to the &lt;strong&gt;userManagement.ts&lt;/strong&gt; file, the &lt;strong&gt;index.ts&lt;/strong&gt; file for Firebase config only needs to export the functions from both web and native versions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;config/index.ts&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Platform } from "react-native";

const app = Platform.select({
 web: require('./index.web'),
 default: require('./index.native'),
});
export const { auth, firebaseApp } = app;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The files in the config are primarily intended for Firebase on the web; therefore, the content in &lt;strong&gt;index.native.ts&lt;/strong&gt; does not need to return any data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;config/index.native.ts&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let firebaseApp
let db
let auth
let analytics

export { analytics, auth, db, firebaseApp };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;index.web.ts&lt;/strong&gt; file contains the main logic that allows us to access Firebase services on the web. The content of the file is as follows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;config/index.web.ts&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { initializeApp } from 'firebase/app';
import { initializeAuth } from 'firebase/auth';

// Firebase JS SDK work with expo web
// for more information: https://docs.expo.dev/guides/using-firebase/#using-firebase-js-sdk

// Initialize Firebase
const firebaseConfig = {
 apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
 authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN,
 // databaseURL: process.env.EXPO_PUBLIC_FIREBASE_DATABASE_URL,
 projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
 storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET,
 messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
 appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
 measurementId: process.env.EXPO_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

let firebaseApp: ReturnType&amp;lt;typeof initializeApp&amp;gt;;
let auth: ReturnType&amp;lt;typeof initializeAuth&amp;gt;;

try {
 firebaseApp = initializeApp(firebaseConfig);
 auth = initializeAuth(firebaseApp);
 auth.useDeviceLanguage();
} catch (error) {
 console.error("Error initializing Firestore:", (error as any).message);
 console.error("Error Stack:", (error as any).stack);
 console.error("Error Details:", error);
}

export { auth, firebaseApp };

// To apply the default browser preference instead of explicitly setting it.
// For more information on how to access Firebase in your project,
// see the Firebase documentation: https://firebase.google.com/docs/web/setup#access-firebase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;firebase/app&lt;/strong&gt; and &lt;strong&gt;firebase/auth&lt;/strong&gt; are separate releases of the Firebase JS SDK. TODO: Update from the Firebase blog to provide further explanation for this section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;firebase/app&lt;/strong&gt; and &lt;strong&gt;firebase/auth&lt;/strong&gt; are package dependencies of &lt;strong&gt;@react-native-firebase/app&lt;/strong&gt;. There is no need to install them separately, and they can be called directly.&lt;/p&gt;

&lt;p&gt;If you attempt to manually install these packages, you will encounter the following error:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RNFNFirestoreDocumentModule.getDocument got 4 arguments, expected 2 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This error occurs due to a version conflict between the Firebase JS SDK and the Firebase JS SDK, which is a dependency of &lt;strong&gt;@react-native-firebase&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;About this snippet:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const firebaseConfig = {
 apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
 authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN,
 // databaseURL: process.env.EXPO_PUBLIC_FIREBASE_DATABASE_URL,
 projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
 storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET,
 messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
 appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
 measurementId: process.env.EXPO_PUBLIC_FIREBASE_MEASUREMENT_ID,
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Returning to the Firebase Config in the Google Console, create a new app for the web. Firebase will send you the JS SDK configuration.&lt;/p&gt;

&lt;p&gt;Add the configuration to the .env file of your project. In this example, I am using Expo, so the environment variables have the prefix EXPO_PUBLIC_. You can use a library like react-native-dotenv to manage these environment variables.&lt;/p&gt;

&lt;p&gt;Thank you for joining me on this comprehensive article. If you found this tutorial helpful, don’t forget to give this post *&lt;em&gt;50 claps👏 **and **follow *&lt;/em&gt;🚀 if you enjoyed this post and want to see more. Your enthusiasm and support fuel my passion for sharing knowledge in the tech community.&lt;/p&gt;

&lt;p&gt;You can find more such articles on my profile -&amp;gt; &lt;a href="https://medium.com/@khuepm" rel="noopener noreferrer"&gt;https://medium.com/@khuepm&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/khuepm" rel="noopener noreferrer"&gt;https://github.com/khuepm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stay tuned for more in-depth tutorials and insights on Javascript and React Native development.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;And that’s it! Place a console.log() statement in the callback handleAuthStateChanged within the &lt;strong&gt;AuthProvider.tsx&lt;/strong&gt; file to check the returned data. In the next part (if anyone purpose for this), I will return to the Login section. Happy coding! 🦄&lt;br&gt;
 Here the Part 01 of this story: &lt;a href="https://dev.to/khuepm/react-native-web-firebase-part-01-why-we-use-react-native-to-develop-both-native-and-web-fdg"&gt;https://dev.to/khuepm/react-native-web-firebase-part-01-why-we-use-react-native-to-develop-both-native-and-web-fdg&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>reactnative</category>
      <category>react</category>
      <category>firebase</category>
    </item>
    <item>
      <title>React Native + Web + Firebase (Part 01) — Why we use React Native to develop both Native and Web version</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Fri, 20 Jun 2025 18:36:03 +0000</pubDate>
      <link>https://dev.to/khuepm/react-native-web-firebase-part-01-why-we-use-react-native-to-develop-both-native-and-web-fdg</link>
      <guid>https://dev.to/khuepm/react-native-web-firebase-part-01-why-we-use-react-native-to-develop-both-native-and-web-fdg</guid>
      <description>&lt;h2&gt;
  
  
  Introduction — Part 01
&lt;/h2&gt;

&lt;p&gt;You are building a cross-platform app using &lt;strong&gt;React Native&lt;/strong&gt;, which will target both &lt;strong&gt;mobile&lt;/strong&gt; (iOS and Android) and &lt;strong&gt;web&lt;/strong&gt; platforms. Leveraging &lt;strong&gt;Expo&lt;/strong&gt; provides an easy and efficient way to set up, develop, and deploy your app while React Native Web allows you to render the same React Native components in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Technologies:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expo&lt;/strong&gt;: A framework that helps developers build React Native applications quickly, with many built-in tools for managing the app lifecycle, testing, and deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Benefits&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Managed workflow: Expo handles configurations and simplifies setup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Quick testing: Easy to test and deploy on devices and simulators.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Universal development: Allows a unified codebase for mobile and web.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. React Native&lt;/strong&gt;: The core framework used for building cross-platform mobile apps with a single JavaScript codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Native performance: It provides access to native UI components.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modular: Huge ecosystem with community-driven libraries.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. React Native Web&lt;/strong&gt;: A library that makes React Native components and APIs work on the web. It allows you to reuse your mobile app’s codebase to build a web version with the same UI and functionality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Code reusability: Most of your code can be shared between web and mobile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easier web deployment: Supports popular web technologies like HTML and CSS, while keeping the React Native architecture.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. React Native Fỉrebase&lt;/strong&gt;: Combining &lt;strong&gt;Firebase&lt;/strong&gt; with &lt;strong&gt;React Native&lt;/strong&gt; allows for fast, cross-platform development with a fully managed backend solution. You get real-time data sync, secure authentication, push notifications, analytics, and more, without having to worry about managing your own server infrastructure. This combination results in faster development, easier scaling, and a smoother user experience on both iOS and Android platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Create a Project on Firebase&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Firestore:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firebase’s &lt;strong&gt;Real-Time Database&lt;/strong&gt; and &lt;strong&gt;Firestore&lt;/strong&gt; provide fast, real-time data synchronization between your app and the cloud. For React Native apps that need live updates (such as messaging apps or live feeds), this is a huge benefit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Authentication:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firebase’s &lt;strong&gt;Authentication&lt;/strong&gt; service supports multiple sign-in methods like Google, Facebook, email/password, etc. This makes it easy to implement secure user authentication with minimal effort, which integrates smoothly with a React Native front end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create an app in &lt;strong&gt;Firebase Settings&lt;/strong&gt; and download the following files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;GoogleService-Info.plist for the Apple app version.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;google-services.json for the Android app version.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, we will not dive deep into how to implement React Native Firebase into your app. Please refer to the React Native Firebase documentation for detailed instructions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rnfirebase.io/" rel="noopener noreferrer"&gt;https://rnfirebase.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create files following the directory structure below:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App.tsx&lt;br&gt;
.env&lt;br&gt;
context/&lt;br&gt;
  AuthProvider.tsx&lt;br&gt;
hooks/&lt;br&gt;
  useUserCredential.tsx&lt;br&gt;
screens/&lt;br&gt;
  Login.tsx&lt;br&gt;
  Home.tsx&lt;br&gt;
services/&lt;br&gt;
  Firebase/&lt;br&gt;
    users/&lt;br&gt;
      index.ts&lt;br&gt;
      management.ts&lt;br&gt;
      management.native.ts&lt;br&gt;
      management.web.ts&lt;br&gt;
  utils.ts&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Explanation for each component in the directory tree:&lt;br&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;App.tsx&lt;/strong&gt;: This is the root component of the app. Here, we will wrap the ContextProvider for the routers, which contain screens like Login, Home, and logic to check if user data exists, loading the corresponding screens accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AuthProvider.tsx&lt;/strong&gt;: User information is stored in React Context. This file contains the context and reducers to update user information after receiving data from Firebase.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;hooks&lt;/strong&gt;: For user information that needs to be displayed on the UI or used for logic in components, we have a custom hook useUserCredential.ts, which returns this information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;screens&lt;/strong&gt;: This folder contains the screens for displaying the UI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Login.tsx&lt;/strong&gt;: The login screen with a single button “Login By Apple.” When the user taps this button, the app will initiate login using the Apple account on the iPhone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Home.tsx&lt;/strong&gt;: After successful login, the data from Apple is recognized by Firebase Authentication and returned to the app. We will process this data and then display the Home screen.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;services&lt;/strong&gt;: This folder contains the business logic performed in the app, independent of the UI. In addition to displaying data on the native app, we also handle the display on the web. Therefore, there are separate files for each environment:&lt;/p&gt;

&lt;p&gt;Files with .web.ts are for the web version.&lt;/p&gt;

&lt;p&gt;Files with .native.ts are for both iOS and Android versions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;services/utils.ts&lt;/strong&gt;: A collection of functions for processing data within the services. These are pure functions and are limited to the service layer of the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you for joining me on this comprehensive article. If you found this tutorial helpful, don’t forget to give this post *&lt;em&gt;50 claps👏 **and **follow *&lt;/em&gt;🚀 if you enjoyed this post and want to see more. Your enthusiasm and support fuel my passion for sharing knowledge in the tech community.&lt;/p&gt;

&lt;p&gt;You can find more such articles on my profile -&amp;gt; &lt;a href="https://medium.com/@khuepm" rel="noopener noreferrer"&gt;https://medium.com/@khuepm&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/khuepm" rel="noopener noreferrer"&gt;https://github.com/khuepm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stay tuned for more in-depth tutorials and insights on Javascript and React Native development.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ok, so in the Part Two, we will continue implement the code. Check this out!&lt;br&gt;
 &lt;a href="https://dev.to/khuepm/react-native-web-firebase-part-02-implement-react-native-firebase-cross-app-2ike"&gt;https://dev.to/khuepm/react-native-web-firebase-part-02-implement-react-native-firebase-cross-app-2ike&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>reactnative</category>
      <category>webdev</category>
      <category>firebase</category>
    </item>
    <item>
      <title>Environment File (.env) Checker and Syncer for ReactJS / NodeJS Project</title>
      <dc:creator>Khue Pham</dc:creator>
      <pubDate>Fri, 20 Jun 2025 18:24:20 +0000</pubDate>
      <link>https://dev.to/khuepm/environment-file-env-checker-and-syncer-for-reactjs-nodejs-project-4m5h</link>
      <guid>https://dev.to/khuepm/environment-file-env-checker-and-syncer-for-reactjs-nodejs-project-4m5h</guid>
      <description>&lt;p&gt;This bash script provides functionality to check and sync environment files in your project.&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2F29kr05ha0b1fk1zkna1o.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.amazonaws.com%2Fuploads%2Farticles%2F29kr05ha0b1fk1zkna1o.png" alt="Image description" width="202" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Checks .env files against a template (.env.template)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identifies missing variables in .env files&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Offers to sync .env files with the template&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Skips syncing content for specified files&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Source code in my Github repository here:&lt;br&gt;
&lt;a href="https://github.com/khuepm/misc/blob/main/check_env_files.sh" rel="noopener noreferrer"&gt;&lt;strong&gt;Check file here: misc/check_env_files.sh at main · khuepm/misc&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Place the script (check_env_files.sh) in your project root directory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure you have a .env.template file with all required environment variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Make the script executable:&lt;/p&gt;

&lt;p&gt;chmod +x checkenv.sh&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the script:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;./checkenv.sh&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;(optional) Add to &lt;code&gt;package.json&lt;/code&gt; to check every start yarn&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;{&lt;br&gt;
  …&lt;br&gt;
  scripts: {&lt;br&gt;
    "prestart": "sh checkenv.sh",&lt;br&gt;
   }&lt;br&gt;
}&lt;/p&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checking Environment Files&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The script checks all .env* files in the current directory against .env.template.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It displays missing variables in each file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A summary of errors (if any) is shown for each file.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Syncing Environment Files&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If errors are found, the script offers to sync .env files with .env.template.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you choose to sync (by entering ‘y’), the script will:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Comment out variables not present in the template&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add new variables from the template&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;keep existing variables: preserving the current value and adding a comment with the template value&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The script uses .env.template as the reference. Ensure this file contains all required variables for your project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Note
&lt;/h2&gt;

&lt;p&gt;After syncing, the script cleans the yarn cache. You may want to adjust or remove this step based on your project needs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comment out yarn cache clean&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How do I created it?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Initial Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

template=".env.template"

# Check if the template file exists
if [ ! -f "$template" ]; then
  echo "Error: $template not found"
  return 1
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This section sets up the script and defines the template file. It also checks if the template file exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. check_env_files() function:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Use for later step, a flag to notice when we need to sync the .env
errors_found=false

check_env_files() {
  files_not_to_check=(".env.template")
  # Get all .env files in the current directory
  files_to_check=($(find . -maxdepth 1 -type f -name '.env*'))

  # Remove files that are in files_not_to_check
  for file in "${files_not_to_check[@]}"; do
    files_to_check=(${files_to_check[@]/$file})
  done

  # Remove any empty elements
  files_to_check=(${files_to_check[@]})

  # Get terminal height and width
  terminal_height=$(tput lines)
  terminal_width=$(tput cols)

  # Variables to store output and result
  output=""
  result=""

  # Function to update the screen, keep to notice text at the bottom of screen
  update_screen() {
    # Clear the screen
    tput clear

    # Print the output
    echo "$output"

    # Move cursor to the bottom and print the result
    tput cup $((terminal_height - 3)) 0
    printf '%*s\n' "$terminal_width" '' | tr ' ' '-'
    echo "$result"
    printf '%*s\n' "$terminal_width" '' | tr ' ' '-'

    # Move cursor back to the top
    tput cup 0 0
  }

  for file in "${files_to_check[@]}"; do
    if [ -f "$file" ]; then
      output+="\n"
      output+="Checking $file...\n\n"
      error_count=0
      while IFS= read -r line || [[ -n "$line" ]]; do
        key=$(echo "$line" | cut -d'=' -f1)
        if ! grep -q "^$key=" "$file"; then
          output+="\033[31m  💔 Missing in $file: $key\033[0m\n"
          ((error_count++))
          errors_found=true
        fi
        update_screen
      done &amp;lt; "$template"

      # Update final result
      output+="\n\n==========================================\n"

      if [ $error_count -gt 0 ]; then
        result+="\n\033[31m❌ Done checking $file with $error_count error(s)\033[0m\n"
        output+="$result"
      else
        result+="\n\033[32m✅ Done checking $file with no errors\033[0m\n"
        output+="$result"
      fi

      update_screen
      output+="\n"

      # Add a small delay to make the result visible at first check
      # Remove if you want to see the result immediately
      if [ "$file" = "${files_to_check[0]}" ]; then
        sleep 1
      fi

    else
      #output+="\033[33m🟠 Check $file: $file not found, skipping\033[0m\n"
      #result+="\033[33m⚠️ $file not found, skipping\033[0m\n"
      update_screen
    fi

  done

  # Clear the result at the end
  result=""
  update_screen

  # Reset terminal settings
  tput rmcup  # Exit alternate screen buffer
  tput cnorm  # Show cursor
  tput sgr0   # Reset all attributes

  # Clear the screen and move cursor to top-left
  clear
  tput cup 0 0

  # Display final output
  echo "$output"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This function is responsible for checking .env files against the template. It does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Finds all .env files in the current directory&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Removes files that should not be checked&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sets up a dynamic terminal display&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Checks each .env file against the template, highlighting missing keys&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Updates the screen with progress and results&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem when template has an empty line or a comment line, the checker will not work property. So I need to adjust it a little bit:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      ...
      while IFS= read -r line || [[ -n "$line" ]]; do
        # Skip empty lines and comment lines
        if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then
          continue
        fi
      ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;3. sync_env_files() function:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This function synchronizes .env files with the template. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Finds all .env files to sync&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For each file:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Comments out lines not in the template&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Updates existing keys with new values from the template&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Adds new keys from the template&lt;/p&gt;

&lt;p&gt;sync_env_files() {&lt;br&gt;
  # Get all .env files in the current directory&lt;br&gt;
  files_to_sync=($(find . -maxdepth 1 -type f -name '.env*'))&lt;/p&gt;

&lt;p&gt;# Exclude .env.template from files to sync&lt;br&gt;
  files_to_sync=(${files_to_sync[@]/$template})&lt;/p&gt;

&lt;p&gt;# Remove any empty elements&lt;br&gt;
  files_to_sync=(${files_to_sync[@]})&lt;/p&gt;

&lt;p&gt;# Sync files&lt;br&gt;
  for file in "${files_to_sync[@]}"; do&lt;br&gt;
    if [ -f "$file" ]; then&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  # Check if the file is in the list of files not to sync

  echo "Syncing $file with $template..."

  # Comment lines not in template
  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ $line =~ ^[[:space:]]*# || -z $line ]]; then
      continue  # Skip comments and empty lines
    fi
    key=$(echo "$line" | cut -d'=' -f1)
    if ! grep -q "^$key=" "$template"; then
      sed -i '' "s|^$key=.*|# $line # Not in template|" "$file"
      echo "\033[31m  Commented: $key (not in template)\033[0m"
    fi
  done &amp;lt; "$file"

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ $line =~ ^[[:space:]]*# || -z $line ]]; then
      continue  # Skip comments and empty lines
    fi
    key=$(echo "$line" | cut -d'=' -f1)
    value=$(echo "$line" | cut -d'=' -f2-)

    if grep -q "^$key=" "$file"; then
      # Key exists, check if value is different
      current_value=$(grep "^$key=" "$file" | cut -d'=' -f2-)
      if [ "$current_value" != "$value" ]; then
        # Value is different, update it
        sed -i '' "/# $key=.*# Updated from template/d" "$file"
        sed -i '' "s|^$key=.*|# $key=$value # Updated from template\n$key=$current_value|" "$file"
        echo "\033[33m  Updated: $key\033[0m"
      else
        echo "  Skipped: $key (value unchanged)"
      fi
    else
      # Key doesn't exist, add it
      echo "$key=$value # Added from template" &amp;gt;&amp;gt; "$file"
      echo "\033[32m  Added: $key\033[0m"
    fi

  done &amp;lt; "$template"

  echo "Finished syncing $file"
  echo

fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;done&lt;br&gt;
}&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Main execution flow:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Call the function
check_env_files

if [ "$errors_found" = true ]; then
  # Ask user if they want to sync files
  # If yes, call sync_env_files()
    # After sync successfully, do clean the cache
  # If no, cancel the operation
else
  echo "No errors found in .env files. Sync not needed."
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This section calls the &lt;strong&gt;check_env_files()&lt;/strong&gt; function and then, based on whether errors were found, either prompts the user to sync the files or informs them that no sync is needed.&lt;/p&gt;

&lt;p&gt;Key features of this script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Dynamic terminal display for real-time progress updates&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Color-coded output for easy reading (red for errors, green for success, yellow for warnings)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Careful handling of existing values in .env files during sync&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Option to sync files only if errors are found&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This script is useful for maintaining consistency across multiple .env files in a project, ensuring that all required environment variables are present and up-to-date.&lt;/p&gt;

&lt;h1&gt;
  
  
  ReactJS #ReactNative #.env #.env.template #uptodate-env-file
&lt;/h1&gt;

</description>
      <category>frontend</category>
      <category>webdev</category>
      <category>backend</category>
      <category>fullstack</category>
    </item>
  </channel>
</rss>
