<?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: Muhammad Moiz</title>
    <description>The latest articles on DEV Community by Muhammad Moiz (@muhammadmoiz269).</description>
    <link>https://dev.to/muhammadmoiz269</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%2F721685%2F71340318-ba53-49c8-82de-bd13bef0eb67.jpg</url>
      <title>DEV Community: Muhammad Moiz</title>
      <link>https://dev.to/muhammadmoiz269</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammadmoiz269"/>
    <language>en</language>
    <item>
      <title>Supabase vs Firebase for Startups: An Honest Comparison</title>
      <dc:creator>Muhammad Moiz</dc:creator>
      <pubDate>Fri, 30 Oct 2026 06:23:45 +0000</pubDate>
      <link>https://dev.to/muhammadmoiz269/supabase-vs-firebase-for-startups-an-honest-comparison-93h</link>
      <guid>https://dev.to/muhammadmoiz269/supabase-vs-firebase-for-startups-an-honest-comparison-93h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapa4hbeovgs1x30b0vo0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapa4hbeovgs1x30b0vo0.jpg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Supabase vs Firebase comes down to one question: do you want a relational Postgres database with SQL, or a document database that syncs to clients with almost no setup? For most SaaS products with users, teams, billing and reports, Supabase is the safer long-term default. Firebase still wins for real-time, mobile-first apps where you want Google to run everything and your data is shaped like documents.&lt;/p&gt;

&lt;p&gt;I've shipped production work on Firebase (&lt;a href="https://muhammadmoiz.dev/work/timegram" rel="noopener noreferrer"&gt;Timegram&lt;/a&gt;, a time-tracking SaaS I built, runs on it) and I regularly work on Supabase apps, many of them generated by AI builders like Lovable. Below is how the two compare on the things that actually hurt later: data model, security, pricing, lock-in and hiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the difference between Supabase and Firebase?
&lt;/h2&gt;

&lt;p&gt;Both are "backend as a service" platforms. You get a database, authentication, file storage, server functions and real-time updates without running your own servers. The difference is what sits in the middle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; is built on Postgres. Its docs describe each project as a full Postgres database rather than an abstraction over one, and Auth, Storage, Realtime and Edge Functions all hang off that database. You write SQL, use foreign keys and joins, and can add Postgres extensions such as pgvector for AI embeddings, PostGIS for maps and pg_cron for scheduled jobs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase&lt;/strong&gt; is built around Cloud Firestore, a NoSQL document database from Google. Data lives in collections of documents, clients read and write directly through the SDK, and access is controlled by Firebase Security Rules. It plugs into the rest of Google Cloud (Cloud Functions, Cloud Storage, Analytics, Crashlytics, Cloud Messaging).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither is "better" in general. They suit different shapes of data and different teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supabase vs Firebase at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Supabase&lt;/th&gt;
&lt;th&gt;Firebase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Postgres (relational, SQL)&lt;/td&gt;
&lt;td&gt;Firestore (NoSQL documents)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;Joins, aggregates, views, full SQL&lt;/td&gt;
&lt;td&gt;Simple queries per collection; joins done in app code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access control&lt;/td&gt;
&lt;td&gt;Postgres Row Level Security policies&lt;/td&gt;
&lt;td&gt;Firebase Security Rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;Plan fee plus usage (storage, egress, MAU)&lt;/td&gt;
&lt;td&gt;Pay per document read, write and delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;2 active projects, paused after a week of inactivity&lt;/td&gt;
&lt;td&gt;Daily free quota on reads, writes and storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosting&lt;/td&gt;
&lt;td&gt;Possible (open source)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;SaaS, dashboards, B2B, reporting, AI features&lt;/td&gt;
&lt;td&gt;Mobile apps, chat, live collaboration, quick prototypes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Supabase vs Firebase pricing: what will you actually pay?
&lt;/h2&gt;

&lt;p&gt;I checked both pricing pages in October 2026. Prices change, so confirm before you budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supabase pricing
&lt;/h3&gt;

&lt;p&gt;Supabase uses plans with included usage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free:&lt;/strong&gt; $0. 500 MB database, 50,000 monthly active users, 5 GB egress, 1 GB file storage, 500,000 Edge Function invocations. You get two active projects, and free projects are paused after one week of inactivity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro:&lt;/strong&gt; from $25 a month. 8 GB database per project, 100,000 MAU, 250 GB egress, 100 GB file storage, 2 million Edge Function invocations, plus $10 of compute credits. Projects are never paused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overages on Pro:&lt;/strong&gt; $0.125 per GB of database, $0.00325 per extra MAU, $0.09 per GB of egress, $0.0213 per GB of file storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team:&lt;/strong&gt; from $599 a month. &lt;strong&gt;Enterprise:&lt;/strong&gt; custom.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful detail: the Pro plan has a spend cap switched on by default, so you won't get a surprise bill unless you turn the cap off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firebase (Firestore) pricing
&lt;/h3&gt;

&lt;p&gt;Firestore charges per operation, not per plan. Google's Firestore pricing page lists a daily free quota of 1 GiB of stored data, 50,000 document reads, 20,000 writes and 20,000 deletes, plus 10 GiB of outbound transfer a month. Above that, in the Iowa (us-central1) location, you pay $0.03 per 100,000 reads, $0.09 per 100,000 writes and $0.01 per 100,000 deletes. Other regions are priced differently.&lt;/p&gt;

&lt;p&gt;For sign-in, Google Cloud's Identity Platform pricing page lists email, phone, anonymous and social sign-ins as free up to 50,000 MAU, then $0.0055 per MAU up to 100,000. SAML and OpenID Connect (enterprise single sign-on) are free for only 50 MAU, then $0.015 per MAU. SMS for phone login is billed separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which one is more affordable?
&lt;/h3&gt;

&lt;p&gt;It depends on how your app reads data, not just how many users you have.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firestore bills every document a query returns. A dashboard that loads 200 documents for every page view costs 200 reads each time. Listeners that re-run on every change add up fast. Badly structured Firestore apps are where surprise bills come from.&lt;/li&gt;
&lt;li&gt;Supabase bills mostly for storage, egress and compute. A heavy SQL query costs CPU time, not per row returned, so read-heavy dashboards and reports are usually more predictable on a fixed plan.&lt;/li&gt;
&lt;li&gt;For a small app with light traffic, both can stay free or close to it for a long time. Just remember the Supabase free plan pauses idle projects, which is fine for prototypes but not for a live product.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is Supabase or Firebase better for a SaaS startup?
&lt;/h2&gt;

&lt;p&gt;Ask what your data looks like six months from now.&lt;/p&gt;

&lt;p&gt;A typical SaaS has organisations, users, roles, projects, invoices, subscriptions and audit logs, all linked to each other. That is relational data. In Postgres you model it with tables and foreign keys, and a report like "hours logged per client per month, grouped by team" is one SQL query. In Firestore you either duplicate data across documents so each screen can read it in one go, or you fetch several collections and join them in your code. Both work, but duplication means keeping copies in sync, and that is where bugs creep in.&lt;/p&gt;

&lt;p&gt;Here is the same question asked both ways. In Supabase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;time_entries&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'org_123'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Firestore you would usually keep a running total on each client document and update it every time an entry is written, often from a Cloud Function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Illustrative: keep a per-client monthly total in sync on every new entry&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;onEntryCreated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onDocumentCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orgs/{orgId}/entries/{entryId}&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;event&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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&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="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`orgs/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/clientTotals/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&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="nx"&gt;month&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FieldValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Firestore version is fast and cheap to read, but you now have to handle edits, deletes and back-filling old data yourself. Every new report means another pre-computed total.&lt;/p&gt;

&lt;p&gt;My rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick Supabase&lt;/strong&gt; if your product is a B2B SaaS, has reporting or admin screens, needs complex permissions, or will add AI search with embeddings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick Firebase&lt;/strong&gt; if you're building a mobile-first or chat-style app, need offline sync on phones out of the box, already use Google Cloud, or your data really is a set of independent documents (user profiles, messages, feeds).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're still scoping your first version, this decision belongs in the planning stage of an &lt;a href="https://muhammadmoiz.dev/services/mvp-development" rel="noopener noreferrer"&gt;MVP build&lt;/a&gt;, not halfway through development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supabase vs Firebase security: where do apps go wrong?
&lt;/h2&gt;

&lt;p&gt;Both platforms let the browser or mobile app talk to the database directly. That's what makes them fast to build with, and it's also why security has to be done right on day one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; relies on Postgres Row Level Security (RLS). If a table is exposed and RLS is off, or the policies are too loose, anyone with your public anon key can read the data. This is the most common problem I find in apps built with Lovable and other AI tools. I wrote a full guide on it: &lt;a href="https://muhammadmoiz.dev/blog/supabase-rls" rel="noopener noreferrer"&gt;Supabase RLS explained&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase&lt;/strong&gt; relies on Security Rules. The classic mistake is shipping with test-mode rules that allow all reads and writes, or rules that check a user is signed in but not that they own the document.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Check this before launch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Whichever you pick, test your access rules with a second, non-admin account. Try to read and change another user's data. If you can, so can anyone else.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Lock-in and switching later
&lt;/h2&gt;

&lt;p&gt;Supabase is open source and the data sits in standard Postgres, so you can export it with normal Postgres tools or move to any Postgres host. That's a real advantage if investors or enterprise clients ask about vendor risk.&lt;/p&gt;

&lt;p&gt;Firebase is a Google product with no self-hosted version. Moving away means rewriting data access code and reshaping documents into tables. Supabase publishes a migration path for this: community scripts that export each Firestore collection to JSON and import it as a Postgres table, flattening fields into text, numeric, boolean or jsonb columns. Nested arrays and objects need custom hooks or your own script to split into related tables, and each collection is migrated separately. It works, but plan time for the data modelling, not just the copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick decision checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write down your five main entities (users, orders, projects and so on) and how they link. Lots of links points to Supabase.&lt;/li&gt;
&lt;li&gt;List the reports and admin screens you'll need in year one. Cross-collection reports point to Supabase.&lt;/li&gt;
&lt;li&gt;Is it mostly a phone app that must work offline? That points to Firebase.&lt;/li&gt;
&lt;li&gt;Estimate reads per user per day. If a single screen loads hundreds of documents, model the Firestore cost before committing.&lt;/li&gt;
&lt;li&gt;Check if your team or future hires know SQL. Postgres skills are easy to find.&lt;/li&gt;
&lt;li&gt;Decide how much vendor lock-in you can accept.&lt;/li&gt;
&lt;li&gt;Whatever you choose, budget time to write and test RLS policies or Security Rules.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this means for founders and app owners
&lt;/h2&gt;

&lt;p&gt;If you're starting fresh, you can't really go wrong with either, as long as the data model fits the tool. The expensive mistakes come from forcing relational data into Firestore, or from launching a Supabase app with RLS switched off.&lt;/p&gt;

&lt;p&gt;If you already have an app built with an AI tool and you're not sure whether the backend is set up safely or will scale, a &lt;a href="https://muhammadmoiz.dev/services/production-audit" rel="noopener noreferrer"&gt;production audit&lt;/a&gt; is the quickest way to find out what needs fixing before real customers arrive. And if you're planning a new product and want help choosing the stack, see how I approach &lt;a href="https://muhammadmoiz.dev/services/mvp-development" rel="noopener noreferrer"&gt;MVP development&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://supabase.com/pricing" rel="noopener noreferrer"&gt;Supabase pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/database/overview" rel="noopener noreferrer"&gt;Supabase docs: Database overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/platform/migrating-to-supabase/firestore-data" rel="noopener noreferrer"&gt;Supabase docs: Migrate from Firestore to Supabase&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/firestore/pricing" rel="noopener noreferrer"&gt;Google Cloud: Firestore pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/identity-platform/pricing" rel="noopener noreferrer"&gt;Google Cloud: Identity Platform pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cover photo: Christopher Gower on Unsplash&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://muhammadmoiz.dev/blog/supabase-vs-firebase" rel="noopener noreferrer"&gt;muhammadmoiz.dev&lt;/a&gt;. I'm Muhammad Moiz, a senior full-stack engineer building production apps with React, Next.js and Node.js. If you need a hand with your product, &lt;a href="https://muhammadmoiz.dev/contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>saas</category>
      <category>database</category>
      <category>startup</category>
    </item>
    <item>
      <title>Vibe Coding Security: 8 Holes in Almost Every AI-Built App</title>
      <dc:creator>Muhammad Moiz</dc:creator>
      <pubDate>Fri, 30 Oct 2026 05:10:06 +0000</pubDate>
      <link>https://dev.to/muhammadmoiz269/vibe-coding-security-8-holes-in-almost-every-ai-built-app-2ffj</link>
      <guid>https://dev.to/muhammadmoiz269/vibe-coding-security-8-holes-in-almost-every-ai-built-app-2ffj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhammadmoiz.dev/blog/vibe-coding-security" rel="noopener noreferrer"&gt;muhammadmoiz.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Vibe coding security comes down to one problem: AI app builders are very good at making features work and much less careful about who is allowed to use them. The same handful of holes turn up again and again in AI-built apps, and most of them are about access control, leaked keys and trusting the browser. Below are the 8 that come up most often, how to check for each one in an afternoon, and what to do when you find them.&lt;/p&gt;

&lt;p&gt;This is written for founders who built with Lovable, Bolt, Replit, Cursor or v0, and for developers who just inherited one of those projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Lovable safe? Why AI-built apps share the same weaknesses
&lt;/h2&gt;

&lt;p&gt;The tools themselves are not the problem. The problem is the default architecture they produce. Most AI builders generate a React frontend that talks directly to a backend-as-a-service such as Supabase or Firebase. There is often no server of your own in between. That means the browser is the only place where "rules" live, unless someone deliberately writes them into the database.&lt;/p&gt;

&lt;p&gt;This is not a theory. CVE-2025-48757, published on 30 May 2025, describes insufficient Row Level Security in sites generated by Lovable up to 15 April 2025, which let unauthenticated attackers read or write arbitrary tables. The CVE record gives it a CVSS 3.1 base score of 9.3 (critical). Lovable disputes the entry, and the record notes their position that each customer is responsible for protecting their own app's data.&lt;/p&gt;

&lt;p&gt;That dispute is the key lesson for any founder: whoever's fault it is, your users' data is your responsibility. The AI tool will not be the one emailing your customers about a breach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8 vibe coding security holes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Row Level Security is missing or does nothing
&lt;/h3&gt;

&lt;p&gt;With Supabase, your publishable (anon) key ships inside the frontend. That is by design. Supabase's own docs say requests made with it are limited to whatever Row Level Security allows. So RLS is the actual lock on your data.&lt;/p&gt;

&lt;p&gt;The common failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RLS is turned off on one or more tables in the &lt;code&gt;public&lt;/code&gt; schema.&lt;/li&gt;
&lt;li&gt;RLS is on, but a policy like &lt;code&gt;using (true)&lt;/code&gt; lets everyone read everything.&lt;/li&gt;
&lt;li&gt;Policies check that a user is logged in, but not that the row belongs to them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A safer pattern for a table where each user owns their rows looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"owners read their invoices"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;owner_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supabase recommends the explicit &lt;code&gt;is not null&lt;/code&gt; check because &lt;code&gt;auth.uid()&lt;/code&gt; returns null for anonymous requests, and wrapping it in &lt;code&gt;select&lt;/code&gt; lets Postgres evaluate it once per query instead of once per row. I covered this hole in depth in &lt;a href="https://muhammadmoiz.dev/blog/supabase-rls" rel="noopener noreferrer"&gt;Supabase RLS explained&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Secret keys shipped to the browser
&lt;/h3&gt;

&lt;p&gt;The Supabase secret key (formerly the &lt;code&gt;service_role&lt;/code&gt; key) bypasses every RLS policy. It belongs on a server, in an Edge Function or in a scheduled job, never in frontend code, never in Git. A common way it ends up in client code: an AI assistant "fixes" a permissions error by swapping the publishable key for the secret one.&lt;/p&gt;

&lt;p&gt;The same mistake happens with environment variables. In Next.js, any variable prefixed with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; is inlined into the JavaScript bundle at build time, so anyone can read it in their browser's dev tools. Other build tools have similar public prefixes. If a payment provider secret, OpenAI key or database password has a public prefix, treat it as leaked: rotate it, then move the call that needs it to the server.&lt;/p&gt;

&lt;p&gt;Quick check: open your live site, open dev tools, search the loaded JavaScript for &lt;code&gt;sb_secret_&lt;/code&gt;, &lt;code&gt;service_role&lt;/code&gt; and the start of any secret key from your payment or AI provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Security rules that only exist in the UI
&lt;/h3&gt;

&lt;p&gt;Hiding the "Delete" button for non-admins is not security. Neither is a redirect on the admin page. Anyone can call your API, your Supabase tables or your server functions directly with a tool like curl.&lt;/p&gt;

&lt;p&gt;The Next.js docs make this point clearly for Server Actions: an exported action can be called with a direct POST request, so the page-level login check does not protect it. Each action needs its own check.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&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;getSession&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;@/lib/session&lt;/span&gt;&lt;span class="dl"&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;db&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;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;archiveProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectId&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;getSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&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="s1"&gt;Not signed in&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;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findFirst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&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="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;project&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="s1"&gt;Not found&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&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="nx"&gt;project&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;archived&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Trusting IDs sent from the client (IDOR)
&lt;/h3&gt;

&lt;p&gt;This is the cousin of hole 3. The user is logged in, but the app takes an ID from the URL or request body and loads that record without asking whether it belongs to them. Change &lt;code&gt;/orders/1041&lt;/code&gt; to &lt;code&gt;/orders/1042&lt;/code&gt; and you see a stranger's order. The OWASP name for this is Insecure Direct Object Reference.&lt;/p&gt;

&lt;p&gt;The fix is always the same: look up the record using both the ID and the current user (as in the example above), or let RLS enforce ownership in the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Admin flags and roles the user can edit
&lt;/h3&gt;

&lt;p&gt;AI-generated schemas often put &lt;code&gt;role&lt;/code&gt; or &lt;code&gt;is_admin&lt;/code&gt; on the same &lt;code&gt;profiles&lt;/code&gt; table that users are allowed to update. If the update policy covers the whole row, any user can promote themselves to admin with one request. Another variant reads a role from local storage or a query string, which the Next.js docs specifically call out as untrusted input.&lt;/p&gt;

&lt;p&gt;Keep roles in a table users cannot write to, or restrict updates to specific columns, and verify the role on the server every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. File storage that is more public than you think
&lt;/h3&gt;

&lt;p&gt;Uploaded invoices, ID photos and contracts often end up in a public bucket because that made the images load during development. In Supabase, objects in a public bucket can be read by anyone who has the URL, with no policy needed. Supabase does block uploads until you add policies on &lt;code&gt;storage.objects&lt;/code&gt;, so AI tools sometimes add a very broad policy to make uploads work.&lt;/p&gt;

&lt;p&gt;Use private buckets for anything personal, generate signed URLs when a user needs to view a file, and scope storage policies to a folder per user.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. No rate limits on expensive actions
&lt;/h3&gt;

&lt;p&gt;Supabase Auth applies rate limits to its own sign-in, sign-up and email endpoints. Your custom functions are a different story. A contact form that sends email, an endpoint that calls an AI model, or a function that writes to the database can be hit thousands of times by a script. The result is a surprise bill, a blacklisted email domain or a flooded database. The Next.js data security guide recommends rate limiting exactly these kinds of operations.&lt;/p&gt;

&lt;p&gt;Add per-IP and per-user limits to anything that costs money or sends messages, and set spending caps on your AI provider account.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Returning far more data than the screen needs
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;select *&lt;/code&gt; that sends the full user record to the browser, including email, phone, internal notes or a password reset token, is a leak even if the UI only shows the name. Anything in a network response is visible to the user who received it.&lt;/p&gt;

&lt;p&gt;Select only the columns the page needs. In Next.js, the docs recommend a server-only data layer that returns small, safe objects rather than raw database rows.&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%2Fimages.unsplash.com%2Fphoto-1555066931-4365d14bab8c%3Fw%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1555066931-4365d14bab8c%3Fw%3D1200%26q%3D80" alt="Laptop screen showing application source code"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo: Arnold Francisca on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Vibe coding best practices: a one-afternoon security checklist
&lt;/h2&gt;

&lt;p&gt;You can work through this list yourself before paying anyone. Each line is a yes/no question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;How to test it&lt;/th&gt;
&lt;th&gt;Red flag&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RLS on every table&lt;/td&gt;
&lt;td&gt;Check each table's RLS status in the Supabase dashboard&lt;/td&gt;
&lt;td&gt;Any exposed table with RLS off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policies scoped to owner&lt;/td&gt;
&lt;td&gt;Read each policy's &lt;code&gt;using&lt;/code&gt; clause&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt;, or no &lt;code&gt;auth.uid()&lt;/code&gt; reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No secrets in the bundle&lt;/td&gt;
&lt;td&gt;Search the live site's JS in dev tools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sb_secret_&lt;/code&gt;, &lt;code&gt;service_role&lt;/code&gt;, any secret key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server checks on every action&lt;/td&gt;
&lt;td&gt;Call an endpoint with curl while logged out&lt;/td&gt;
&lt;td&gt;It succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership checks&lt;/td&gt;
&lt;td&gt;Change an ID in the URL while logged in&lt;/td&gt;
&lt;td&gt;You see another user's data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roles protected&lt;/td&gt;
&lt;td&gt;Try updating your own &lt;code&gt;role&lt;/code&gt; column via the API&lt;/td&gt;
&lt;td&gt;It works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private storage&lt;/td&gt;
&lt;td&gt;Open a document URL in a private window&lt;/td&gt;
&lt;td&gt;It loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limits&lt;/td&gt;
&lt;td&gt;Submit a form 50 times quickly&lt;/td&gt;
&lt;td&gt;Every request succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimal responses&lt;/td&gt;
&lt;td&gt;Inspect network responses on profile pages&lt;/td&gt;
&lt;td&gt;Fields the UI never shows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.env files out of Git&lt;/td&gt;
&lt;td&gt;Check your repo history&lt;/td&gt;
&lt;td&gt;A committed &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Test with a second account:&lt;/strong&gt; Most of these holes only show up when you are logged in as one user and try to reach another user's data. Create two test accounts and try to read, edit and delete each other's records. If you can, so can anyone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to do when you find a hole
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Rotate anything leaked first.&lt;/strong&gt; A key that was ever in a public bundle or repository is compromised, even if you remove it now.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Lock down the database.&lt;/strong&gt; Enable RLS everywhere, then add the narrowest policies that keep the app working. Supabase's docs suggest testing allowed and denied operations for both the anon and authenticated roles.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Move privileged calls to the server.&lt;/strong&gt; Payments, emails, AI calls and admin operations belong in server routes or Edge Functions.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Check your logs.&lt;/strong&gt; Look for unusual reads or writes before the fix, in case you need to notify users.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Stop the AI from undoing it.&lt;/strong&gt; When you prompt for new features, tell the tool explicitly not to change policies or swap keys, and review every diff that touches auth or the database.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this means for founders and app owners
&lt;/h2&gt;

&lt;p&gt;An AI builder can get you to a working demo in days, and that is genuinely valuable. But "it works" and "it is safe to put real customer data in" are separate milestones. If you are about to take payments, store personal data, onboard a business client or raise money, a security review is part of shipping, not an extra.&lt;/p&gt;

&lt;p&gt;If the checklist turned up red flags, or you are not sure how to read the results, a &lt;a href="https://muhammadmoiz.dev/services/production-audit" rel="noopener noreferrer"&gt;production audit&lt;/a&gt; gives you a prioritised list of what is exposed and how to fix it. If you would rather hand the whole thing over, I also &lt;a href="https://muhammadmoiz.dev/services/ai-app-production-ready" rel="noopener noreferrer"&gt;take AI-built apps to production&lt;/a&gt;, keeping what works and rebuilding what does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/CVEProject/cvelistV5/blob/main/cves/2025/48xxx/CVE-2025-48757.json" rel="noopener noreferrer"&gt;CVE-2025-48757 record (CVE Program, cvelistV5 on GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/api/api-keys" rel="noopener noreferrer"&gt;Supabase docs: Understanding API keys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase docs: Row Level Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/storage/security/access-control" rel="noopener noreferrer"&gt;Supabase docs: Storage access control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/auth/rate-limits" rel="noopener noreferrer"&gt;Supabase docs: Auth rate limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/guides/environment-variables" rel="noopener noreferrer"&gt;Next.js docs: Environment variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/guides/data-security" rel="noopener noreferrer"&gt;Next.js docs: Data security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-project-top-ten/" rel="noopener noreferrer"&gt;OWASP Top 10&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cover photo: FlyD on Unsplash&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I'm Muhammad Moiz, a full-stack developer who builds web apps, SaaS and POS systems, and takes AI-built apps (Lovable, Bolt, Replit) to production. &lt;a href="https://muhammadmoiz.dev/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>webdev</category>
      <category>supabase</category>
    </item>
    <item>
      <title>PHP Critical RCE Vulnerabilities CVE-2026-109618 and CVE-2026-109619: How to Patch</title>
      <dc:creator>Muhammad Moiz</dc:creator>
      <pubDate>Fri, 30 Oct 2026 05:10:00 +0000</pubDate>
      <link>https://dev.to/muhammadmoiz269/php-critical-rce-vulnerabilities-cve-2026-109618-and-cve-2026-109619-how-to-patch-i1i</link>
      <guid>https://dev.to/muhammadmoiz269/php-critical-rce-vulnerabilities-cve-2026-109618-and-cve-2026-109619-how-to-patch-i1i</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhammadmoiz.dev/blog/php-rce-cve-2026-109618-cve-2026-109619-patch-guide" rel="noopener noreferrer"&gt;muhammadmoiz.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two critical bugs in PHP itself, CVE-2026-109618 and CVE-2026-109619, can let an attacker run code on a web server without logging in. They were fixed on 21 October 2026 in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, and every earlier release on those branches is affected. If your website, WordPress install or API runs on PHP, the fix is to upgrade the PHP runtime, not your plugins or framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;The PHP project published two GitHub security advisories on 21 October 2026, both rated Critical with a CVSS v4 score of 9.2. On the same day it shipped patch releases on all four supported branches. The release notes put it bluntly: "There are no complete mitigations, updating is the only safe choice."&lt;/p&gt;

&lt;p&gt;The score reflects a network attack that needs no account and no user interaction, but does need certain conditions to line up (that is why attack complexity is marked high). In plain terms: it is not trivial to pull off, but an outsider can attempt it against a public server, and the payoff is full control of the PHP worker process.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Vulnerable&lt;/th&gt;
&lt;th&gt;Fixed in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PHP 8.5&lt;/td&gt;
&lt;td&gt;below 8.5.12&lt;/td&gt;
&lt;td&gt;8.5.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP 8.4&lt;/td&gt;
&lt;td&gt;below 8.4.27&lt;/td&gt;
&lt;td&gt;8.4.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP 8.3&lt;/td&gt;
&lt;td&gt;below 8.3.36&lt;/td&gt;
&lt;td&gt;8.3.36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP 8.2&lt;/td&gt;
&lt;td&gt;below 8.2.35&lt;/td&gt;
&lt;td&gt;8.2.35&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note that the 8.2.35 release notes also remind users that PHP 8.2 reaches end of life on 31 December 2026. Branches older than 8.2 are not listed in the advisories, so treat them as unpatched and unsupported.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two bugs, explained without the jargon
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CVE-2026-109618: memory exhaustion leaves PHP in a broken state
&lt;/h3&gt;

&lt;p&gt;When a PHP request runs out of memory, the engine historically bailed out of whatever it was doing with an abrupt jump. The advisory explains that some code paths make freshly allocated request memory visible before it is fully set up. If any allocation fails at that exact moment, even an unrelated one, the request is left holding half-initialised data, and the shutdown routine later trusts it.&lt;/p&gt;

&lt;p&gt;The scary part is how an attacker forces the out-of-memory condition. The advisory lists several ways, including multipart (file upload style) requests with a huge number of headers per part, many empty file fields that still create metadata, and deeply nested field names. Under Apache with mod_php, the researchers confirmed full remote code execution over HTTP with default memory and input limits, before any of your application code even runs. PHP-FPM is believed to be susceptible based on source analysis, but no complete FPM exploit was demonstrated. FrankenPHP and LSAPI are flagged as likely susceptible but not validated.&lt;/p&gt;

&lt;p&gt;The fix replaces the abrupt jump with a graceful "soft" out-of-memory stop at a safe point, plus a hard process exit if memory keeps climbing past a second threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-109619: leftover state from a previous request
&lt;/h3&gt;

&lt;p&gt;Long-running PHP workers handle many requests in a row. This advisory describes global state (error, session, object and extension data) that can survive after the memory behind it is freed. A later request that touches that state before normal start-up reads freed memory, which is a classic use-after-free.&lt;/p&gt;

&lt;p&gt;Two confirmed paths stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHP 8.5 with &lt;code&gt;fatal_error_backtraces&lt;/code&gt; enabled.&lt;/strong&gt; The backtrace data is not cleared after a fatal error, and a later diagnostic reads it. This is the basis of the confirmed mod_php exploit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session upload progress.&lt;/strong&gt; This feature is on by default and does not need your code to call &lt;code&gt;session_start()&lt;/code&gt;. A fatal error or memory exhaustion during input processing can leave it pointing at freed memory. It was validated against both mod_php and PHP-FPM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An attacker needs two requests to land on the same worker: one to plant the stale state and one to trigger it. More workers make that harder, but the advisory is clear that it does not remove the risk.&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%2Fimages.unsplash.com%2Fphoto-1564457461758-8ff96e439e83%3Fw%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1564457461758-8ff96e439e83%3Fw%3D1200%26q%3D80" alt="Rows of server cabinets in a data center connected by blue network cables"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo: Taylor Vick on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is affected
&lt;/h2&gt;

&lt;p&gt;If any of these describe your setup, you should act this week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A WordPress, WooCommerce, Laravel, Symfony, Drupal or custom PHP site on a PHP version below the fixed releases above.&lt;/li&gt;
&lt;li&gt;Apache with mod_php. This is the configuration with confirmed, application-independent exploitation.&lt;/li&gt;
&lt;li&gt;PHP-FPM behind nginx or Apache. Exposure is conditional, but session upload progress was validated on FPM.&lt;/li&gt;
&lt;li&gt;Docker images that pin an older &lt;code&gt;php:8.x&lt;/code&gt; tag and have not been rebuilt since 21 October.&lt;/li&gt;
&lt;li&gt;Shared or managed hosting. Your host controls the PHP binary, so you need to confirm they have rolled out the patched build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Updating WordPress core, a theme or a plugin does nothing here. The bug lives in the language runtime underneath them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to patch: a practical checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Find your real PHP version.&lt;/strong&gt; The command-line PHP and the web-server PHP are sometimes different builds, so check both.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# CLI version&lt;/span&gt;
php &lt;span class="nt"&gt;-v&lt;/span&gt;

&lt;span class="c"&gt;# FPM version (binary name varies by distro, e.g. php-fpm8.3)&lt;/span&gt;
php-fpm &lt;span class="nt"&gt;-v&lt;/span&gt;

&lt;span class="c"&gt;# Settings that matter for the mitigations below&lt;/span&gt;
php &lt;span class="nt"&gt;-i&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'memory_limit|session.upload_progress.enabled|fatal_error_backtraces'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the web server, a temporary &lt;code&gt;phpinfo()&lt;/code&gt; page works, but delete it straight after.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Upgrade to the fixed release for your branch.&lt;/strong&gt; Use your distribution's security repository, your host's control panel, or a rebuilt container image. For Docker, bump the base image tag and rebuild rather than relying on a cached layer.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: confirm the image you ship actually contains the fix&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; your-app-image php &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Restart every worker.&lt;/strong&gt; A new binary on disk does not help while old FPM children or Apache processes stay in memory. Restart the service, not just reload configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify in production.&lt;/strong&gt; Re-run the version check against the live server and record the result.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan your branch.&lt;/strong&gt; If you are on 8.2, schedule a move to 8.3 or newer before the end of December. If you are on anything older than 8.2, upgrading is now urgent rather than optional.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Settings tweaks are a stopgap, not a fix:&lt;/strong&gt; The advisories say updating is the only complete mitigation. Use the settings below only to reduce risk for the few days it takes to get a patched build deployed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  If you cannot upgrade today
&lt;/h2&gt;

&lt;p&gt;Both advisories list temporary measures. In summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turn off session upload progress&lt;/strong&gt; (&lt;code&gt;session.upload_progress.enabled=0&lt;/code&gt;) and restart workers. The advisory suggests leaving it off even after patching if you don't use it, and most sites don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On PHP 8.5, set &lt;code&gt;fatal_error_backtraces=0&lt;/code&gt;&lt;/strong&gt; at the administrative level and restart. This removes the confirmed exploit path for the second bug, but not every possible one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tighten request limits at the edge.&lt;/strong&gt; Keep &lt;code&gt;post_max_size&lt;/code&gt; conservative, cap decoded request size at your reverse proxy or WAF, and block multipart requests entirely on endpoints that never accept uploads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower input limits&lt;/strong&gt; such as &lt;code&gt;max_input_nesting_level&lt;/code&gt; and the number of input variables, parts and uploads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid &lt;code&gt;RLIMIT_AS&lt;/code&gt; and &lt;code&gt;RLIMIT_DATA&lt;/code&gt;&lt;/strong&gt; as a memory cap. The advisory warns these make allocations fail in exactly the way that triggers the first bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first advisory also mentions &lt;code&gt;memory_limit=-1&lt;/code&gt; as a partial configuration mitigation, with the caveat that it is incomplete and that an unbounded memory limit brings its own denial-of-service risk. Treat it with care.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it means for business and app owners
&lt;/h2&gt;

&lt;p&gt;For a non-technical founder, the short version is this: your website's engine had a serious flaw, and the engine has to be replaced, not the bodywork. Ask whoever runs your hosting or code three direct questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which PHP version is serving our live site right now?&lt;/li&gt;
&lt;li&gt;When was it updated, and were all workers restarted?&lt;/li&gt;
&lt;li&gt;Are we on a branch that still gets security fixes after this year?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If nobody can answer quickly, that is a sign your stack lacks basic visibility, and a one-off &lt;a href="https://muhammadmoiz.dev/services/production-audit" rel="noopener noreferrer"&gt;production audit&lt;/a&gt; of versions, hosting and patching is usually great value compared with cleaning up after a breach. If you run WordPress and want someone to handle the PHP upgrade and test your plugins against the new version, &lt;a href="https://muhammadmoiz.dev/services/wordpress-development" rel="noopener noreferrer"&gt;WordPress development support&lt;/a&gt; covers exactly that kind of job.&lt;/p&gt;

&lt;p&gt;The broader lesson is that "keep plugins updated" is only half of PHP security. The runtime, the web server and the container image all need an owner and a patch routine, and this month's release is a good prompt to set one up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;PHP security advisory GHSA-w8hw-w9fx-j79w: RCE via OOM corruption (CVE-2026-109618)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;PHP security advisory GHSA-6wmf-4rv5-p3rp: RCE via stale global state (CVE-2026-109619)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/php/php-src/releases" rel="noopener noreferrer"&gt;php-src releases on GitHub (8.5.12, 8.4.27, 8.3.36, 8.2.35)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/php/php-src/security" rel="noopener noreferrer"&gt;php-src security advisories overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cover photo: FlyD on Unsplash&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I'm Muhammad Moiz, a full-stack developer who builds web apps, SaaS and POS systems, and takes AI-built apps (Lovable, Bolt, Replit) to production. &lt;a href="https://muhammadmoiz.dev/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Lovable vs Replit vs Bolt vs Cursor: Which to Use, and When You Need a Developer</title>
      <dc:creator>Muhammad Moiz</dc:creator>
      <pubDate>Thu, 29 Oct 2026 05:10:58 +0000</pubDate>
      <link>https://dev.to/muhammadmoiz269/lovable-vs-replit-vs-bolt-vs-cursor-which-to-use-and-when-you-need-a-developer-32j7</link>
      <guid>https://dev.to/muhammadmoiz269/lovable-vs-replit-vs-bolt-vs-cursor-which-to-use-and-when-you-need-a-developer-32j7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhammadmoiz.dev/blog/lovable-vs-replit" rel="noopener noreferrer"&gt;muhammadmoiz.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lovable vs Replit&lt;/strong&gt; comes down to what you want to own at the end. Lovable is the faster path to a polished web app when you mostly describe screens in chat, Replit suits founders who want an AI agent plus a full coding workspace and hosting in one place, Bolt sits between them, and Cursor is a code editor for people (or developers) who already work in a codebase. All four can get you to a working prototype; none of them replaces the engineering work a real product needs once paying users, private data and growth arrive.&lt;/p&gt;

&lt;p&gt;Below is a practical comparison written for founders, with prices taken from each vendor's pricing page as of October 2026, plus the signals that tell you it's time to bring in a developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lovable vs Replit vs Bolt vs Cursor at a glance
&lt;/h2&gt;

&lt;p&gt;These tools are often lumped together, but they solve different problems. Three are "describe it and the AI builds it" app builders. Cursor is a different category: an AI-first editor and agent for working inside real code.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Lovable&lt;/th&gt;
&lt;th&gt;Replit&lt;/th&gt;
&lt;th&gt;Bolt&lt;/th&gt;
&lt;th&gt;Cursor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Non-technical founders building web apps by chat&lt;/td&gt;
&lt;td&gt;Founders who want an agent plus a full dev environment&lt;/td&gt;
&lt;td&gt;Quick web apps, prototypes and sites&lt;/td&gt;
&lt;td&gt;Developers and technical founders editing real code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How you work&lt;/td&gt;
&lt;td&gt;Chat, visual preview&lt;/td&gt;
&lt;td&gt;Chat with Agent, plus code editor&lt;/td&gt;
&lt;td&gt;Chat, preview, start from Figma or GitHub&lt;/td&gt;
&lt;td&gt;IDE, CLI and cloud agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Lovable Cloud, billed from your credits&lt;/td&gt;
&lt;td&gt;Built-in database and user auth&lt;/td&gt;
&lt;td&gt;Built-in databases on every plan&lt;/td&gt;
&lt;td&gt;Whatever your project uses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code ownership&lt;/td&gt;
&lt;td&gt;You own your code; GitHub sync&lt;/td&gt;
&lt;td&gt;Code lives in your Replit workspace&lt;/td&gt;
&lt;td&gt;Can start from your GitHub repo&lt;/td&gt;
&lt;td&gt;Your repo from day one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starting paid price (Oct 2026)&lt;/td&gt;
&lt;td&gt;Credit-based Pro and Business plans&lt;/td&gt;
&lt;td&gt;Core: $20/month ($18 billed yearly)&lt;/td&gt;
&lt;td&gt;Pro: $25/month&lt;/td&gt;
&lt;td&gt;Plus: $8/month (Grok models only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on Lovable pricing: its pricing page sells Pro and Business on credits rather than seats, and the plan table didn't load when I checked, so I'm not quoting a number. What the page does confirm is that the free plan gives 5 build credits a day (capped at 30 a month) plus 20 Cloud credits a month, and paid plans add a monthly credit balance on top of those daily grants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lovable vs Bolt: which is better for a first version?
&lt;/h2&gt;

&lt;p&gt;Both are aimed at people who want a working web app without writing code, so this is the comparison most founders actually face.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lovable&lt;/strong&gt; describes itself as a full-stack engineer you chat with, and it pushes your project to GitHub and deploys in one click. Its newer model puts building, hosting and AI features in your app on one credit balance. Hosting a small app is usually covered by the included grant, but the pricing FAQ is clear that apps with serious traffic or size start drawing on your credits. That's fair, but it means your hosting bill and your "AI typing" bill now share one pot, so watch it as you grow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bolt&lt;/strong&gt; is token-based. The free plan includes 1M tokens a month (with a 300K daily limit), and Pro at $25/month includes 10M tokens with rollover, a custom domain and no Bolt branding. Teams is $30 per member per month. Every plan, including free, lists unlimited databases and hosting, and you can start from a blank prompt, a Figma file or a GitHub repo.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick &lt;strong&gt;Lovable&lt;/strong&gt; if you want the most guided experience and a clean GitHub handoff later.&lt;/li&gt;
&lt;li&gt;Pick &lt;strong&gt;Bolt&lt;/strong&gt; if you want predictable token budgets, want to start from an existing design or repo, or need a custom domain on the entry paid plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lovable vs Replit: what's the real difference?
&lt;/h2&gt;

&lt;p&gt;Replit is a full cloud development environment that grew an AI agent, rather than an AI builder that later exposed the code. That changes how it feels.&lt;/p&gt;

&lt;p&gt;According to Replit's product page, its Agent builds from a chat description, uses Replit's own database and user authentication, can connect services like Stripe through stored keys, and periodically tests the app in a browser and fixes what it finds. Core costs $20/month (or $18/month billed yearly) and includes $20 of usage toward the more powerful models. Pro is $100/month ($90 billed yearly) with $100 of credits, up to 10 parallel agents and database rollback up to 28 days.&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%2Fimages.unsplash.com%2Fphoto-1504707748692-419802cf939d%3Fw%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1504707748692-419802cf939d%3Fw%3D1200%26q%3D80" alt="Open MacBook Pro on a grey desk, ready for building an app" width="1200" height="822"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo: Ben Kolde on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So the honest answer to "Lovable vs Replit" is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lovable&lt;/strong&gt; is better when the person driving is non-technical and the product is a web app with a typical dashboard, forms and auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replit&lt;/strong&gt; is better when you want to see and touch everything (files, terminal, database) in the same place, or when the app includes background jobs, bots or scripts that don't fit neatly into a web-app template.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lovable vs Cursor: different tools for different stages
&lt;/h2&gt;

&lt;p&gt;People compare these because both "write code with AI", but they sit at opposite ends of a project.&lt;/p&gt;

&lt;p&gt;Cursor is an editor and agent that works on a real codebase: a desktop IDE, a terminal CLI, cloud agents that work in parallel and review pull requests in GitHub. You choose between models from several labs, including Cursor's own Composer. It also changed hands this year: Cursor's site says it is now part of SpaceX, in a post dated August 14, 2026, and its pricing now bundles Grok models. Plus is $8/month for Grok models only, Premium is $30/month with cloud agents and code reviews, and third-party models on the paid tiers are billed separately by usage.&lt;/p&gt;

&lt;p&gt;That makes the decision simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you can't read code, Cursor will feel like being handed the keys to a workshop. Start with an app builder.&lt;/li&gt;
&lt;li&gt;If your Lovable or Bolt project is already on GitHub and a developer is now maintaining it, Cursor (or a similar editor) is where that work tends to move.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What about Lovable vs v0?
&lt;/h2&gt;

&lt;p&gt;v0 is Vercel's AI app builder (Vercel's own v0 docs now point to v0.app). I couldn't verify its current plans or features this month, so check its site directly before deciding. The general rule still holds: if your app's success depends on SEO and fast page loads, a framework with server rendering (like Next.js) gives you a better base than a purely client-rendered single-page app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you use? A quick decision guide
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You're non-technical and need a clickable product to show investors or early users:&lt;/strong&gt; Lovable or Bolt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want an agent, but also a real environment with a database, terminal and hosting:&lt;/strong&gt; Replit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You (or your developer) already have a codebase and want AI to speed up the work:&lt;/strong&gt; Cursor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You care about search traffic from day one:&lt;/strong&gt; favour a server-rendered setup such as Next.js, whichever tool generates it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're handling payments, health or personal data:&lt;/strong&gt; use any tool to prototype, but plan a security review before launch.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When the AI tool stops being enough
&lt;/h2&gt;

&lt;p&gt;Every one of these tools is excellent at the first 70% of an app. The trouble shows up in the last 30%, and it usually arrives as one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The AI starts breaking things it fixed yesterday.&lt;/strong&gt; Each prompt patches one screen and quietly damages another, and credits or tokens burn on going in circles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security is "probably fine".&lt;/strong&gt; Database rules are open, API keys sit in the frontend, or every logged-in user can read every other user's rows. On Supabase-backed apps this is usually a Row Level Security problem; I wrote about it in &lt;a href="https://muhammadmoiz.dev/blog/supabase-rls" rel="noopener noreferrer"&gt;Supabase RLS explained&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments, roles and edge cases get complicated.&lt;/strong&gt; Subscriptions with upgrades, refunds and team accounts are where generated code gets fragile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google can't see your site.&lt;/strong&gt; A client-rendered app often shows search engines an almost empty page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't hire anyone to maintain it.&lt;/strong&gt; Nobody can explain how the code is structured, so every change is a gamble.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep the AI tool, fix the foundation:&lt;/strong&gt; Hitting these limits doesn't mean starting over. Export the code to GitHub, get it reviewed, fix the risky parts, and keep building on it with a developer (and AI) in the loop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What this means for a founder
&lt;/h2&gt;

&lt;p&gt;The tool you start with matters less than the moment you notice it's no longer enough. Use Lovable, Replit or Bolt to learn what users want quickly and spend little doing it. Once the app holds real data or real revenue, treat the generated code like code from any new contractor: review it, secure it and give it a structure someone can maintain.&lt;/p&gt;

&lt;p&gt;If you're at that point, I take AI-built apps from Lovable, Bolt and Replit and &lt;a href="https://muhammadmoiz.dev/services/ai-app-production-ready" rel="noopener noreferrer"&gt;make them production-ready&lt;/a&gt;, or start with a &lt;a href="https://muhammadmoiz.dev/services/production-audit" rel="noopener noreferrer"&gt;production audit&lt;/a&gt; if you just want to know where you stand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://lovable.dev/pricing" rel="noopener noreferrer"&gt;Lovable pricing and FAQ&lt;/a&gt; (checked October 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://replit.com/pricing" rel="noopener noreferrer"&gt;Replit pricing&lt;/a&gt; (checked October 2026)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://replit.com/products/agent" rel="noopener noreferrer"&gt;Replit Agent product page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bolt.new/pricing" rel="noopener noreferrer"&gt;Bolt pricing&lt;/a&gt; (checked October 2026)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bolt.new" rel="noopener noreferrer"&gt;Bolt homepage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cursor.com/pricing" rel="noopener noreferrer"&gt;Cursor pricing&lt;/a&gt; (checked October 2026)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cursor.com" rel="noopener noreferrer"&gt;Cursor homepage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/docs/v0" rel="noopener noreferrer"&gt;Vercel v0 documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cover photo: Mohammad Rahmani on Unsplash&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I'm Muhammad Moiz, a full-stack developer who builds web apps, SaaS and POS systems, and takes AI-built apps (Lovable, Bolt, Replit) to production. &lt;a href="https://muhammadmoiz.dev/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>startup</category>
      <category>nocode</category>
    </item>
    <item>
      <title>PostgreSQL 19 Release Candidate: What Shipped, What Was Cut, How to Upgrade</title>
      <dc:creator>Muhammad Moiz</dc:creator>
      <pubDate>Thu, 29 Oct 2026 05:10:55 +0000</pubDate>
      <link>https://dev.to/muhammadmoiz269/postgresql-19-release-candidate-what-shipped-what-was-cut-how-to-upgrade-5foi</link>
      <guid>https://dev.to/muhammadmoiz269/postgresql-19-release-candidate-what-shipped-what-was-cut-how-to-upgrade-5foi</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhammadmoiz.dev/blog/postgresql-19-release-candidate-upgrade-guide" rel="noopener noreferrer"&gt;muhammadmoiz.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL 19 is not out yet. The first release candidate shipped on October 15, 2026, and the project now plans general availability for November 19, 2026, later than many teams expected. Several headline features announced in the June beta were pulled before the RC, so if you planned an upgrade around them, this guide covers what actually made the cut, what changes under your feet, and how to get ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the release stands right now
&lt;/h2&gt;

&lt;p&gt;The PostgreSQL 19 timeline this year has been longer than usual. Here are the dates that matter, all from the project's own announcements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Milestone&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Beta 1&lt;/td&gt;
&lt;td&gt;June 4, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beta 4 (features removed)&lt;/td&gt;
&lt;td&gt;September 24, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release Candidate 1&lt;/td&gt;
&lt;td&gt;October 15, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RC 2 and minor updates for supported versions&lt;/td&gt;
&lt;td&gt;November 12, 2026 (planned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL 14 end of life&lt;/td&gt;
&lt;td&gt;November 12, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL 19 general availability&lt;/td&gt;
&lt;td&gt;November 19, 2026 (planned)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RC 1 announcement explains that the release team chose to fold a large batch of fixes into version 19 before calling it final, which is why a second release candidate is on the calendar. The GA date can still move if a serious issue turns up.&lt;/p&gt;

&lt;p&gt;Why so many late fixes? According to a DEV Community write-up that collects the public discussion, long-time committer Peter Eisentraut wrote on October 9 that LLM-assisted code review was at least partly responsible for the wave of reverts, because it surfaced defects in already-committed features much faster than before. The same write-up notes that some community members pushed back, saying a large share of AI-reported bugs turned out not to be real. Either way, the practical result for you is the same: a smaller, more conservative release.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was cut from PostgreSQL 19
&lt;/h2&gt;

&lt;p&gt;If you read the Beta 1 announcement and got excited, check this list before you plan anything. The official Beta 4 announcement says these were removed so they can be reconsidered for a later major version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL/PGQ property graph queries&lt;/strong&gt; (graph-style queries in standard SQL)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turning data checksums on or off while the cluster is online&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Temporal &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; using &lt;code&gt;FOR PORTION OF&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ALTER TABLE ... MERGE PARTITIONS&lt;/code&gt; and &lt;code&gt;SPLIT PARTITIONS&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pg_get_role_ddl()&lt;/code&gt;, &lt;code&gt;pg_get_tablespace_ddl()&lt;/code&gt; and &lt;code&gt;pg_get_database_ddl()&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;GROUP BY ALL&lt;/code&gt;, which appeared in the Beta 1 highlights, is also missing from the current release notes. So if a blog post or tutorial shows any of these as "new in Postgres 19", treat it as outdated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did make it in
&lt;/h2&gt;

&lt;p&gt;The good news is that a lot of useful work survived. Based on the RC-era release notes, these are the changes most app developers will notice:&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%2Fimages.unsplash.com%2Fphoto-1762163516269-3c143e04175c%3Fw%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1762163516269-3c143e04175c%3Fw%3D1200%26q%3D80" alt="Server rack with rows of blinking green status lights" width="1200" height="795"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo: Domaintechnik on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  For application code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INSERT ... ON CONFLICT DO SELECT ... RETURNING&lt;/code&gt;.&lt;/strong&gt; An upsert can now hand back the row that already existed instead of silently skipping it. That removes a common "insert, and if nothing came back, run a second SELECT" round trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster foreign key checks.&lt;/strong&gt; The notes list a general performance improvement for foreign key constraint checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;WAIT&lt;/code&gt; command for replicas.&lt;/strong&gt; A session on a standby can wait until a given WAL position has been written, flushed or replayed. That gives you read-your-writes behaviour when you send writes to the primary and reads to a replica.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More &lt;code&gt;jsonpath&lt;/code&gt; string methods&lt;/strong&gt;, including &lt;code&gt;lower()&lt;/code&gt;, &lt;code&gt;upper()&lt;/code&gt;, &lt;code&gt;replace()&lt;/code&gt;, &lt;code&gt;split_part()&lt;/code&gt; and the trim family.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;COPY TO&lt;/code&gt; can output JSON&lt;/strong&gt;, with a &lt;code&gt;FORCE_ARRAY&lt;/code&gt; option to produce a single JSON array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smarter &lt;code&gt;NOTIFY&lt;/code&gt;&lt;/strong&gt;, which now wakes only the backends listening on the relevant channel. Apps that use &lt;code&gt;LISTEN&lt;/code&gt;/&lt;code&gt;NOTIFY&lt;/code&gt; for real-time features should see less overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the kind of upsert that gets simpler. This is an illustration, not code from the docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create a tag if it doesn't exist, and always get its row back&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'billing'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For operations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;REPACK&lt;/code&gt;&lt;/strong&gt;, a built-in way to rebuild bloated tables, with a &lt;code&gt;CONCURRENTLY&lt;/code&gt; option that avoids blocking reads and writes. It shipped with a narrower scope than first planned, and it received many crash and correctness fixes during the betas, so test it on a copy first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel autovacuum&lt;/strong&gt; via &lt;code&gt;autovacuum_max_parallel_workers&lt;/code&gt;, plus a new scoring system for deciding which tables to vacuum first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic I/O worker scaling&lt;/strong&gt; between the new &lt;code&gt;io_min_workers&lt;/code&gt; and &lt;code&gt;io_max_workers&lt;/code&gt; settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pg_plan_advice&lt;/code&gt; and &lt;code&gt;pg_stash_advice&lt;/code&gt;&lt;/strong&gt;, new modules for steering and pinning planner decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logical replication of sequences&lt;/strong&gt;, which makes replication-based online upgrades less fiddly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS SNI support&lt;/strong&gt; through a new &lt;code&gt;pg_hosts.conf&lt;/code&gt; file, so one server can present different certificates per hostname.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Breaking changes to check before upgrading
&lt;/h2&gt;

&lt;p&gt;This is the section to read twice. These items come from the migration section and other behaviour changes in the release notes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;JIT is off by default.&lt;/strong&gt; If you have analytical queries that benefited from JIT compilation, benchmark them and turn it back on explicitly if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RADIUS authentication is gone.&lt;/strong&gt; The notes describe the UDP-only implementation as unfixably insecure. Move to another method before upgrading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MD5 password logins now trigger a warning&lt;/strong&gt; after successful authentication (controlled by &lt;code&gt;md5_password_warnings&lt;/code&gt;). It's a nudge to switch to SCRAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;standard_conforming_strings&lt;/code&gt; is always on.&lt;/strong&gt; Dumps taken from older versions with this setting off will not load correctly into 19.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max_locks_per_transaction&lt;/code&gt; defaults to 128 instead of 64&lt;/strong&gt;, and lock sizing changed. If you tuned this value, the notes say you effectively need to double it to keep the same capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;json_array()&lt;/code&gt; returns an empty array instead of NULL&lt;/strong&gt; when there are no rows. Check any code that tests for NULL here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default TOAST compression is now &lt;code&gt;lz4&lt;/code&gt;&lt;/strong&gt; rather than &lt;code&gt;pglz&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vacuumdb --analyze-only&lt;/code&gt; now includes partitioned tables&lt;/strong&gt; when no targets are given.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A quick way to spot settings that matter on your current server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setting&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_settings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'jit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'standard_conforming_strings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'max_locks_per_transaction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'password_encryption'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'default_toast_compression'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to find roles still using MD5 password hashes (run as a superuser):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rolname&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_authid&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rolpassword&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'md5%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A practical upgrade checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't run an RC in production:&lt;/strong&gt; A release candidate is for testing. Wait for the final 19.0 release, and for most teams, for the first minor release after it, before moving production data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Know your current version.&lt;/strong&gt; Run &lt;code&gt;SELECT version();&lt;/code&gt;. If you're on PostgreSQL 14, that matters more than 19: version 14 stops getting fixes on November 12, 2026. Plan to move to 17 or 18 now, or straight to 19 once it's stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your provider.&lt;/strong&gt; Managed services (RDS, Cloud SQL, Supabase, Neon and others) adopt new majors on their own schedules. Look up when they will offer 19 rather than assuming it will be there on day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spin up the RC in a container&lt;/strong&gt; and restore a recent copy of your database into it. Run your test suite and your slowest real queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work through the breaking-change list above&lt;/strong&gt;, especially MD5, RADIUS and &lt;code&gt;standard_conforming_strings&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick an upgrade method.&lt;/strong&gt; The project recommends a major-version approach such as &lt;code&gt;pg_upgrade&lt;/code&gt; (fast, in place) or &lt;code&gt;pg_dump&lt;/code&gt; / &lt;code&gt;pg_restore&lt;/code&gt; (slower, cleaner). Logical replication is another option if you need minimal downtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;pg_upgrade --check&lt;/code&gt; first&lt;/strong&gt; against a copy. It reports incompatibilities without changing anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update drivers and ORMs&lt;/strong&gt; (node-postgres, Prisma, Drizzle, psycopg and so on) in staging, then confirm migrations still run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have a rollback plan&lt;/strong&gt;: a verified backup and a tested restore, not just a backup.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this means for business and app owners
&lt;/h2&gt;

&lt;p&gt;If you run a SaaS product, an e-commerce store or an internal tool on Postgres, PostgreSQL 19 is not urgent. Nothing in it is a security fix you must rush to apply, and the most eye-catching features were deferred. The more pressing date is November 12: if your database is on version 14, it will stop receiving security patches that day.&lt;/p&gt;

&lt;p&gt;A sensible plan for most teams looks like this: get off 14 this quarter, test 19 in staging once it hits GA, and schedule the production upgrade for early 2027 after the first point release. The features that did ship, like &lt;code&gt;ON CONFLICT DO SELECT&lt;/code&gt;, replica &lt;code&gt;WAIT&lt;/code&gt; and faster foreign key checks, are real quality-of-life wins worth adopting once you're there.&lt;/p&gt;

&lt;p&gt;If your app was built quickly, perhaps with an AI builder, and nobody is sure which Postgres version it runs, which auth method it uses, or whether backups restore, that's worth finding out before any upgrade. A &lt;a href="https://muhammadmoiz.dev/services/production-audit" rel="noopener noreferrer"&gt;production audit&lt;/a&gt; covers exactly those questions, and if you need help planning a database move as part of backend work, &lt;a href="https://muhammadmoiz.dev/contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/about/news/postgresql-19-rc-1-released-3409/" rel="noopener noreferrer"&gt;PostgreSQL 19 RC 1 Released (postgresql.org)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/about/news/postgresql-19-beta-4-released-3386/" rel="noopener noreferrer"&gt;PostgreSQL 19 Beta 4 Released (postgresql.org)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/about/news/postgresql-19-beta-1-released-3313/" rel="noopener noreferrer"&gt;PostgreSQL 19 Beta 1 Released (postgresql.org)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/19/release-19.html" rel="noopener noreferrer"&gt;PostgreSQL 19 release notes (RC documentation)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/support/versioning/" rel="noopener noreferrer"&gt;PostgreSQL versioning policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/alanwest/postgresql-19-release-date-moved-to-nov-19-after-ai-review-found-too-many-bugs-48n"&gt;PostgreSQL 19 Release Date Moved to Nov 19 After AI Review Found Too Many Bugs (DEV Community)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cover photo: Taylor Vick on Unsplash&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I'm Muhammad Moiz, a full-stack developer who builds web apps, SaaS and POS systems, and takes AI-built apps (Lovable, Bolt, Replit) to production. &lt;a href="https://muhammadmoiz.dev/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Offline-First POS: Lessons From Building Oscar in React</title>
      <dc:creator>Muhammad Moiz</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:07:55 +0000</pubDate>
      <link>https://dev.to/muhammadmoiz269/offline-first-pos-lessons-from-building-oscar-in-react-284l</link>
      <guid>https://dev.to/muhammadmoiz269/offline-first-pos-lessons-from-building-oscar-in-react-284l</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhammadmoiz.dev/blog/offline-first-pos-react" rel="noopener noreferrer"&gt;muhammadmoiz.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A point of sale system has one job that matters more than any feature: it must keep selling. On a busy Friday night a restaurant can't tell a queue of customers to wait until the Wi-Fi comes back.&lt;/p&gt;

&lt;p&gt;When I worked on Oscar, a POS for restaurants and retail businesses in Pakistan, "works offline" wasn't a nice-to-have. It shaped the whole architecture. Here's how an offline-first POS works in the browser, and the lessons I'd pass on to any founder planning one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "offline-first" actually means
&lt;/h2&gt;

&lt;p&gt;Many apps say they "work offline" when they really mean they show a friendly error. Offline-first flips the usual design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The app reads and writes a local database first.&lt;/strong&gt; The screen never waits for the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server is updated in the background&lt;/strong&gt; whenever a connection is available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cloud stays the source of truth for reporting,&lt;/strong&gt; so owners still see every branch's sales in one place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a cashier, the result is simple: punching an order, taking payment and printing a receipt behave exactly the same with or without internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture in one picture
&lt;/h2&gt;

&lt;p&gt;The React app reads and writes a local database in the browser, so it works offline. When a connection is available, a sync process sends local changes to the backend, which updates the main database used for reporting.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;React POS app ↔ Local database: Read / write&lt;/li&gt;
&lt;li&gt;Local database ↔ Backend API: Sync when online&lt;/li&gt;
&lt;li&gt;Backend API → Main database&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On Oscar, the local database was &lt;strong&gt;WatermelonDB&lt;/strong&gt; running on browser storage, so there was no app to install. The backend received the synced changes and mapped them to the main database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: design the data before the screens
&lt;/h2&gt;

&lt;p&gt;With a normal web app you can sketch the UI first and add API calls later. Offline-first doesn't forgive that. Every feature started with &lt;strong&gt;data modelling&lt;/strong&gt;: the tables, the relations between them, and the functions that create, update and query records.&lt;/p&gt;

&lt;p&gt;An order, for example, isn't one row. It has items, modifiers, discounts and payments, and all of them must be created locally, in one go, without asking the server for IDs. Here's a simplified model to show the idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Model&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;@nozbe/watermelondb&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;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&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;@nozbe/watermelondb/decorators&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;Order&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;associations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;order_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;has_many&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;foreignKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;has_many&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;foreignKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_id&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="kd"&gt;as const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;type&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;dine_in&lt;/span&gt;&lt;span class="dl"&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;takeaway&lt;/span&gt;&lt;span class="dl"&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;delivery&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="nd"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;!&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;children&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_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;items&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;children&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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 the data model is right, the screens are easy. If it's wrong, every new feature fights the sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: sync is a product feature, not a background detail
&lt;/h2&gt;

&lt;p&gt;The hardest bugs in offline apps aren't on the screen. They're in the sync: an order that arrives twice, a discount that disappears, two devices editing the same table. What helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every record is created with its own unique ID on the device,&lt;/strong&gt; so the server never has to invent one and duplicates are easy to detect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes sync as a batch,&lt;/strong&gt; so a half-sent order never shows up in reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The backend owns the rules for conflicts.&lt;/strong&gt; When two devices change the same thing, one place decides, the same way every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lesson 3: real-time and offline can live together
&lt;/h2&gt;

&lt;p&gt;Kitchens want orders the moment they're punched. On Oscar, orders reached the &lt;strong&gt;Kitchen Display System over WebSockets&lt;/strong&gt;, and prints went to the right printer (receipt, kitchen or bar) through &lt;strong&gt;QZ Tray&lt;/strong&gt;. Each real-time feature needs its own answer to "what happens offline?", decided at design time, not after the first outage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;For founders:&lt;/strong&gt; If your product is used at a counter, in a warehouse or on the road, ask early: "What happens when the internet drops?" Adding offline support later usually means rebuilding the data layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Lesson 4: test the bad days, not the good ones
&lt;/h2&gt;

&lt;p&gt;An offline-first app needs testing in the conditions it was built for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turn the network off in the middle of an order, then back on.&lt;/li&gt;
&lt;li&gt;Punch orders on two devices while one is offline.&lt;/li&gt;
&lt;li&gt;Close the browser before a sync finishes and reopen it.&lt;/li&gt;
&lt;li&gt;Fill the local database with a busy week of orders and check the app is still fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests find the problems that real customers would otherwise find for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is offline-first right for your product?
&lt;/h2&gt;

&lt;p&gt;It adds real work, so it's worth it when people &lt;strong&gt;must keep working without a connection&lt;/strong&gt;: POS systems, field-service apps, inventory and delivery tools. For a typical SaaS dashboard used at a desk, a well-built online app with good error handling is usually enough.&lt;/p&gt;

&lt;p&gt;If you're planning a POS or another app that has to keep working when the internet doesn't, you can read how I built Oscar in the &lt;a href="https://muhammadmoiz.dev/work/oscar-pos" rel="noopener noreferrer"&gt;case study&lt;/a&gt;, or &lt;a href="https://muhammadmoiz.dev/contact" rel="noopener noreferrer"&gt;tell me about your project&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;I'm Muhammad Moiz, a full-stack developer who builds web apps, SaaS and POS systems, and takes AI-built apps (Lovable, Bolt, Replit) to production. &lt;a href="https://muhammadmoiz.dev/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>pwa</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Node.js October 2026 Security Release: Brotli RCE and 9 More CVEs</title>
      <dc:creator>Muhammad Moiz</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:07:51 +0000</pubDate>
      <link>https://dev.to/muhammadmoiz269/nodejs-october-2026-security-release-brotli-rce-and-9-more-cves-4b1i</link>
      <guid>https://dev.to/muhammadmoiz269/nodejs-october-2026-security-release-brotli-rce-and-9-more-cves-4b1i</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhammadmoiz.dev/blog/nodejs-october-2026-security-release-brotli-rce" rel="noopener noreferrer"&gt;muhammadmoiz.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On October 21, 2026, Node.js shipped security releases that fix 10 vulnerabilities across the 22.x, 24.x and 26.x lines. Four are rated High. The most serious, CVE-2026-111497, is an out-of-bounds write in the bundled Brotli decoder, and the project says it permits remote code execution. To fix it, upgrade to &lt;strong&gt;v22.23.4, v24.22.1 or v26.12.1&lt;/strong&gt;. The advisory lists no workarounds.&lt;/p&gt;

&lt;p&gt;This post covers what was fixed, who needs to act, and a step-by-step upgrade checklist for teams and app owners.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the October 2026 Node.js security release fixes
&lt;/h2&gt;

&lt;p&gt;The official advisory on nodejs.org lists ten CVEs. Here they are, grouped by severity:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Affected lines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-111497&lt;/td&gt;
&lt;td&gt;Brotli decoder out-of-bounds write permits remote code execution&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;26.x, 24.x, 22.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-105482&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dns.resolve&lt;/code&gt; truncates names containing a NUL character&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;26.x, 24.x, 22.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-105483&lt;/td&gt;
&lt;td&gt;TLS certificate hostname bypass when the IP is written in fullwidth digits&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;26.x, 24.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-106069&lt;/td&gt;
&lt;td&gt;TLS certificate hostname bypass when the host loosely resolves to an IP address&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;26.x, 24.x, 22.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-105481&lt;/td&gt;
&lt;td&gt;Keep-alive socket processes a response written before the request was sent&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;26.x, 24.x, 22.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-106072&lt;/td&gt;
&lt;td&gt;Non-canonical IPv4 hostnames let DNS SANs authenticate IP endpoints&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;26.x, 24.x, 22.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-106074&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;crypto.randomUUID()&lt;/code&gt; and &lt;code&gt;randomInt()&lt;/code&gt; entropy caches saved into startup snapshots and SEAs&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;26.x, 24.x, 22.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-106070&lt;/td&gt;
&lt;td&gt;ZipFile returns attacker-controlled prefix bytes after failed header validation&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;26.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-106071&lt;/td&gt;
&lt;td&gt;Permission Model filesystem over-grant via wildcard radix lookup&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;26.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-106073&lt;/td&gt;
&lt;td&gt;Permission Model bypass with &lt;code&gt;fs.realpathSync&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;26.x, 24.x, 22.x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The release also updates several bundled dependencies, including c-ares, OpenSSL (to 3.5.9 on the 22.x and 24.x lines), undici, SQLite (on 22.x) and the root certificate store. The advisory says the certificate update relates to an eMudhra intermediate CA compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Brotli RCE: why it is the headline
&lt;/h2&gt;

&lt;p&gt;Brotli is a compression format that web servers and browsers use widely, and Node.js ships its own copy of the Brotli library. According to the release notes, the fix for CVE-2026-111497 cherry-picks an upstream Brotli commit. It also exposes a new decoder error constant, &lt;code&gt;BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH&lt;/code&gt;, so malformed input now produces a clean error.&lt;/p&gt;

&lt;p&gt;The problem is in the upstream library itself, not just in Node.js. Google's Brotli project published patched releases (v1.2.1, v1.1.1 and v1.0.10) on the same day, each labeled "out-of-bounds write permits remote code execution" with the same CVE ID. Other runtimes and tools that bundle Brotli may also need updates, so watch the advisories for your other stacks too.&lt;/p&gt;

&lt;p&gt;Neither the Node.js advisory nor the Brotli release notes publish exploit details. In practice, the code most worth checking is any code that decompresses Brotli data you don't control. Examples include request bodies sent by clients, responses from third-party APIs, and files uploaded by users. If your app does any of that, treat this as a priority patch.&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%2Fimages.unsplash.com%2Fphoto-1695668548342-c0c1ad479aee%3Fw%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1695668548342-c0c1ad479aee%3Fw%3D1200%26q%3D80" alt="A rack of servers in a dimly lit server room" width="1200" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo: Kevin Ache on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The TLS and DNS bugs: quieter but still serious
&lt;/h2&gt;

&lt;p&gt;Three of the four High-severity issues involve how Node.js checks who it is talking to.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two TLS hostname bypasses (CVE-2026-105483, CVE-2026-106069).&lt;/strong&gt; In some edge cases, Node.js could accept a certificate for the wrong host when the address was written in an unusual form, such as fullwidth digits or a loosely parsed IP. The fix commits are titled "tls: fix ip normalization regression" and "tls: reject non-canonical IP addresses".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS NUL-byte truncation (CVE-2026-105482).&lt;/strong&gt; &lt;code&gt;dns.resolve&lt;/code&gt; could truncate a name at a NUL character, so the name that gets resolved is not the one your code passed in. The fix validates inputs for null bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These bugs matter most for apps that build hostnames or URLs from user input. Common examples are webhook senders, link previewers, URL importers and anything that fetches a URL a customer typed in. In those features, a validation gap can turn into server-side request forgery (SSRF) or a man-in-the-middle risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on random values in snapshots
&lt;/h3&gt;

&lt;p&gt;CVE-2026-106074 is easy to miss. If you build user-land startup snapshots or single executable applications (SEAs), the entropy caches behind &lt;code&gt;crypto.randomUUID()&lt;/code&gt; and &lt;code&gt;randomInt()&lt;/code&gt; could be saved into the snapshot. As a result, every process started from it would generate the same values. If you use either feature and rely on those functions for IDs or tokens, upgrade and rebuild your snapshots or executables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Node.js versions are affected (and which are end-of-life)
&lt;/h2&gt;

&lt;p&gt;According to the Node.js release schedule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 26.x&lt;/strong&gt; is the Current line. Fixed in &lt;strong&gt;v26.12.1&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 24.x&lt;/strong&gt; is Active LTS. Fixed in &lt;strong&gt;v24.22.1&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 22.x&lt;/strong&gt; is Maintenance LTS until April 30, 2027. Fixed in &lt;strong&gt;v22.23.4&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 20.x&lt;/strong&gt; reached end-of-life on April 30, 2026. &lt;strong&gt;Node.js 18.x&lt;/strong&gt; ended on April 30, 2025, and &lt;strong&gt;25.x&lt;/strong&gt; ended on June 1, 2026.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;End-of-life versions get no patch:&lt;/strong&gt; The Node.js project states that "End-of-Life versions are always affected when a security release occurs." If you are still on Node.js 20 or older, this release does not fix you. The only real fix is to move to a supported line.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Upgrade checklist for your team
&lt;/h2&gt;

&lt;p&gt;Here is a practical order of operations that works for most teams.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find every place Node.js runs.&lt;/strong&gt; Check your servers, Docker images, CI runners, serverless functions, build machines and Electron apps. Don't forget internal tools and cron workers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the version on each one.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="c"&gt;# Patched: v22.23.4+, v24.22.1+ or v26.12.1+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin the patched version in config.&lt;/strong&gt; Update &lt;code&gt;.nvmrc&lt;/code&gt;, &lt;code&gt;engines&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;, your CI setup, and your Docker base images.
&lt;/li&gt;
&lt;/ol&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;"engines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=22.23.4 &amp;lt;23 || &amp;gt;=24.22.1 &amp;lt;25 || &amp;gt;=26.12.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- FROM node:22.23.3-alpine
&lt;/span&gt;&lt;span class="gi"&gt;+ FROM node:22.23.4-alpine
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use floating tags such as &lt;code&gt;node:22-alpine&lt;/code&gt;, rebuild and redeploy anyway. An image built last week still contains the old binary.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check your hosting platform.&lt;/strong&gt; On managed platforms and serverless runtimes, check the provider's docs or status page to see when the patched runtime is rolled out, and whether you need to redeploy to pick it up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioritize risky code paths.&lt;/strong&gt; Start with services that decompress untrusted Brotli data or make outbound TLS or DNS calls to hosts derived from user input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild snapshots and SEAs&lt;/strong&gt; if you use them, so the CVE-2026-106074 fix takes effect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run your test suite and smoke tests&lt;/strong&gt; before rolling out to production. These are patch releases, but bundled dependencies such as OpenSSL and undici also changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan your exit from Node.js 20 or older&lt;/strong&gt; if any service is still on an end-of-life line.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this means for business and app owners
&lt;/h2&gt;

&lt;p&gt;If you run a SaaS product, an online store, or an app someone built for you, you probably have Node.js running somewhere, even if you never chose it. It might sit behind your API, your Next.js frontend, your background jobs or your build pipeline. Apps built quickly with AI tools such as Lovable, Bolt or Replit often run on Node.js too, and nobody may be tracking which version they use.&lt;/p&gt;

&lt;p&gt;Here are the questions to ask your developer or agency this week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which Node.js version does each part of our app run on?&lt;/li&gt;
&lt;li&gt;Have we upgraded to the October 21 patched releases, and when was the last redeploy?&lt;/li&gt;
&lt;li&gt;Is anything still on Node.js 20 or older?&lt;/li&gt;
&lt;li&gt;Do we have a routine for applying runtime security releases, or do we rely on someone noticing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If nobody can answer those clearly, the gap is in process, not just this one patch. A &lt;a href="https://muhammadmoiz.dev/services/production-audit" rel="noopener noreferrer"&gt;production audit&lt;/a&gt; can map what you run, how current it is, and where the risky code paths are. If your backend needs a longer-term owner, see &lt;a href="https://muhammadmoiz.dev/services/nodejs-backend" rel="noopener noreferrer"&gt;Node.js backend development&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases" rel="noopener noreferrer"&gt;Node.js Security Releases: Wednesday, October 21, 2026 (nodejs.org)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nodejs/node/releases/tag/v24.22.1" rel="noopener noreferrer"&gt;Node.js v24.22.1 release notes (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nodejs/node/releases/tag/v22.23.4" rel="noopener noreferrer"&gt;Node.js v22.23.4 release notes (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/google/brotli/releases" rel="noopener noreferrer"&gt;google/brotli releases (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nodejs/release" rel="noopener noreferrer"&gt;Node.js release schedule (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cover photo: FlyD on Unsplash&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I'm Muhammad Moiz, a full-stack developer who builds web apps, SaaS and POS systems, and takes AI-built apps (Lovable, Bolt, Replit) to production. &lt;a href="https://muhammadmoiz.dev/contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>node</category>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Supabase RLS Explained: The Security Hole in Most AI-Built Apps</title>
      <dc:creator>Muhammad Moiz</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:49:44 +0000</pubDate>
      <link>https://dev.to/muhammadmoiz269/supabase-rls-explained-the-security-hole-in-most-ai-built-apps-3pn0</link>
      <guid>https://dev.to/muhammadmoiz269/supabase-rls-explained-the-security-hole-in-most-ai-built-apps-3pn0</guid>
      <description>&lt;p&gt;If you built your app with Lovable, Bolt or v0, there's a good chance it runs on Supabase. And there's a real chance that anyone on the internet can read your users' data right now.&lt;/p&gt;

&lt;p&gt;Not because Supabase is insecure. Because of one setting that's easy to get wrong: &lt;strong&gt;Row Level Security (RLS)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post explains what RLS is, the mistakes that leave data exposed, and how to check your own app in five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your Supabase key is public (and why that's fine)
&lt;/h2&gt;

&lt;p&gt;Supabase gives every project an API that talks directly to your Postgres database. Your frontend calls it with the &lt;strong&gt;anon key&lt;/strong&gt; (newer projects call it the &lt;strong&gt;publishable key&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;That key is meant to be public. It ships in your JavaScript bundle, and anyone can open DevTools and copy it.&lt;/p&gt;

&lt;p&gt;So what stops someone from using your key to read every row in your &lt;code&gt;profiles&lt;/code&gt; or &lt;code&gt;orders&lt;/code&gt; table?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only RLS.&lt;/strong&gt; RLS policies are rules inside Postgres that decide which rows each request is allowed to see or change. If RLS is off, the public key is effectively a master key to that table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 mistakes I'd look for first
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. RLS is turned off
&lt;/h3&gt;

&lt;p&gt;Tables created in the Supabase dashboard's Table Editor have RLS enabled by default. Tables created with raw SQL or migrations, which is how many AI tools generate them, often don't.&lt;/p&gt;

&lt;p&gt;With RLS off, this request returns every row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: YOUR_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_ANON_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. RLS is on, but the policy allows everything
&lt;/h3&gt;

&lt;p&gt;When you turn RLS on with no policies, Postgres blocks everything and the app "breaks." The quick fix people (and AI tools) reach for is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"allow all"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;profiles&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&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;using (true)&lt;/code&gt; means "every row, for everyone." RLS is technically on, but it protects nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Reading is protected, but writing isn't
&lt;/h3&gt;

&lt;p&gt;Policies have two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;using (...)&lt;/code&gt; decides which existing rows you can &lt;strong&gt;see, update or delete&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;with check (...)&lt;/code&gt; decides what new or changed rows you're &lt;strong&gt;allowed to write&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common gap: a solid &lt;code&gt;select&lt;/code&gt; policy, but an &lt;code&gt;insert&lt;/code&gt; or &lt;code&gt;update&lt;/code&gt; policy without &lt;code&gt;with check&lt;/code&gt;. A user can then write rows with someone else's &lt;code&gt;user_id&lt;/code&gt;, or update a field like &lt;code&gt;role = 'admin'&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The service role key is in the frontend
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;service role key&lt;/strong&gt; (newer projects: &lt;strong&gt;secret key&lt;/strong&gt;) skips RLS completely. It belongs only on a server. If you can find it in your frontend code or &lt;code&gt;.env&lt;/code&gt; files that get bundled, rotate it now.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Policies trust data the user controls
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;user_metadata&lt;/code&gt; (&lt;code&gt;raw_user_meta_data&lt;/code&gt;) can be edited by the user. A policy like "allow if the user's metadata says &lt;code&gt;role = admin&lt;/code&gt;" can be bypassed by the user simply editing their own metadata. Store roles in &lt;code&gt;app_metadata&lt;/code&gt; or a separate table only your server can write.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a correct setup looks like
&lt;/h2&gt;

&lt;p&gt;Here's a typical "users only see their own rows" setup for a &lt;code&gt;notes&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Turn RLS on&lt;/span&gt;
&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Read: only your own notes&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can read their own notes"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Create: only notes that belong to you&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can create their own notes"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Update: only your notes, and you can't hand them to someone else&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can update their own notes"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 5. Delete: only your own notes&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can delete their own notes"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 6. Keep it fast&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&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 worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrapping &lt;code&gt;auth.uid()&lt;/code&gt; in &lt;code&gt;(select ...)&lt;/code&gt; lets Postgres evaluate it once per query instead of once per row, which Supabase recommends for performance.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;to authenticated&lt;/code&gt; means logged-out visitors get nothing, even before the condition runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check your own app in 5 minutes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: find tables with RLS off.&lt;/strong&gt; Run this in the Supabase SQL Editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any table with &lt;code&gt;rowsecurity = false&lt;/code&gt; is readable with your public key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: look for "allow all" policies.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;qual&lt;/code&gt; or &lt;code&gt;with_check&lt;/code&gt; equal to &lt;code&gt;true&lt;/code&gt;, and for &lt;code&gt;insert&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt; policies with an empty &lt;code&gt;with_check&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: open the Security Advisor.&lt;/strong&gt; In the Supabase dashboard, the Security Advisor flags tables without RLS and other common issues. Fix everything marked as an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: test as a stranger.&lt;/strong&gt; Run the &lt;code&gt;curl&lt;/code&gt; request from mistake #1 against your important tables. If you get real data back without logging in, that table is exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;None of this is specific to AI tools. I worked with the same idea for years with Firebase security rules: the browser can never be trusted, so every permission has to be enforced in the database or on the server. AI builders are great at getting the screens working. They're less reliable at this invisible layer, because a missing policy doesn't show up as a bug until someone finds it.&lt;/p&gt;

&lt;p&gt;If your app handles real user data, it's worth an hour to check before launch.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Muhammad Moiz, a full-stack developer who helps founders take AI-built apps to production. If you'd like a second pair of eyes on your Supabase setup, I offer a &lt;a href="https://muhammadmoiz.dev/services/production-audit" rel="noopener noreferrer"&gt;fixed-price production audit&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>database</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
