<?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: Russel Dsouza</title>
    <description>The latest articles on DEV Community by Russel Dsouza (@russel_dsouza_bd584a3cb2a).</description>
    <link>https://dev.to/russel_dsouza_bd584a3cb2a</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%2F3939420%2F1d36f555-5b2b-48e8-a97c-2acbb7603dbd.png</url>
      <title>DEV Community: Russel Dsouza</title>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/russel_dsouza_bd584a3cb2a"/>
    <language>en</language>
    <item>
      <title>Every Way to Run Postgres for Local Dev in 2026, Ranked by Weight</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 23 Sep 2026 05:54:51 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/every-way-to-run-postgres-for-local-dev-in-2026-ranked-by-weight-1ol0</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/every-way-to-run-postgres-for-local-dev-in-2026-ranked-by-weight-1ol0</guid>
      <description>&lt;p&gt;"Just run Postgres locally" hides a real decision. In 2026 there are four genuinely different ways to do it, and they differ by two orders of magnitude in weight. Picking wrong doesn't break anything; it just quietly taxes every &lt;code&gt;git clone&lt;/code&gt;, every CI run, and every laptop on the team for the life of the project.&lt;/p&gt;

&lt;p&gt;Here they are, lightest to heaviest, with the honest trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Embedded: PGlite (~3 MB)
&lt;/h2&gt;

&lt;p&gt;The newest option and the lightest by far. PGlite is Postgres compiled to WebAssembly, packaged as a library. You don't start a database, you import one:&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;PGlite&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;@electric-sql/pglite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PGlite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;            &lt;span class="c1"&gt;// in-memory&lt;/span&gt;
&lt;span class="c1"&gt;// const db = new PGlite('./data') // or persisted to disk&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select 'hello' as message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under 3 MB gzipped, starts in milliseconds, persists to the filesystem or IndexedDB, and supports real extensions including pgvector. This stopped being a curiosity a while ago: it crossed 10 million weekly npm downloads this year, and Prisma, Firebase, and Netlify all ship it as the local engine behind their database products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins:&lt;/strong&gt; test suites (a fresh, real Postgres per test, no containers in CI), instant onboarding, browser playgrounds, coding-agent sandboxes, any environment where Docker isn't available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it doesn't:&lt;/strong&gt; it's single-connection and single-user. No replication, not a production database, and tools that expect a TCP connection string need an adapter. It's a library, and everything that implies cuts both ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Native install (~50 to 300 MB)
&lt;/h2&gt;

&lt;p&gt;Homebrew, apt, or Postgres.app. The old way, and still the fastest raw Postgres you can run, since there's no VM and no container layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins:&lt;/strong&gt; one long-lived database you use daily, maximum performance, lowest conceptual overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it doesn't:&lt;/strong&gt; one global version for the whole machine. Two projects on different Postgres majors means version-manager gymnastics. It pollutes the host, upgrades are a small ritual, and "install Postgres 16 locally" is the onboarding step that fails differently on every teammate's laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Docker (~400 MB image, plus the runtime)
&lt;/h2&gt;

&lt;p&gt;The default answer of the last decade. &lt;code&gt;docker compose up&lt;/code&gt;, one isolated Postgres per project, any version you like, throw it away when you're done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins:&lt;/strong&gt; version isolation per project, parity with a containerized production setup, and the whole team runs the identical environment from one committed file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it doesn't:&lt;/strong&gt; the real cost isn't the image, it's the runtime. Docker Desktop idles at gigabytes of RAM on Mac and Windows, licensing applies at larger companies, CI needs service containers with health checks and port juggling, and there are environments (locked-down laptops, cloud IDEs, agent sandboxes) where it simply can't run. You're running a VM to get a process.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cloud dev branches (0 MB local, network required)
&lt;/h2&gt;

&lt;p&gt;Neon-style branching, or your platform's preview databases: every PR gets its own copy-on-write database in the cloud, nothing on your machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins:&lt;/strong&gt; production-identical infrastructure, instant branches with real data shapes, zero local footprint, great for review apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it doesn't:&lt;/strong&gt; no network, no database. Every query pays a round trip, tests share remote state unless you're disciplined about branching, and it's the only option on this list with a monthly bill. It complements a local option more than it replaces one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit and integration tests&lt;/td&gt;
&lt;td&gt;PGlite (fresh DB per test)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI pipelines&lt;/td&gt;
&lt;td&gt;PGlite, or Docker service containers if you need TCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One daily-driver database&lt;/td&gt;
&lt;td&gt;Native install&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple projects, multiple versions&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matching containerized prod&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR preview environments&lt;/td&gt;
&lt;td&gt;Cloud branches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Demos, workshops, browser sandboxes&lt;/td&gt;
&lt;td&gt;PGlite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most teams end up with two: something heavy for prod parity, something feather-weight for the inner loop. That split is the point. The 200 iterations a day don't need the faithful production copy; the last check before deploy does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Supabase-shaped footnote
&lt;/h2&gt;

&lt;p&gt;One case the table above doesn't cover: your app doesn't talk to bare Postgres. If you build on Supabase, your client calls the platform's REST, Auth, Storage, and Realtime APIs, so a lightweight Postgres alone doesn't give you a lightweight backend. The official local stack solves it with Docker, about a dozen containers of it.&lt;/p&gt;

&lt;p&gt;That's the gap &lt;a href="https://tinbase.dev/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=postgres-local-dev" rel="noopener noreferrer"&gt;tinbase&lt;/a&gt; fills: an open-source (MIT) Supabase-compatible backend that runs the whole API surface on PGlite, in one process, &lt;code&gt;npx tinbase start&lt;/code&gt;, up in about two seconds, with the official supabase-js SDK working unchanged. Same idea as row 1 of the table, extended to the entire platform. It's alpha and aimed at local dev and tests, not production, which is exactly the inner-loop slot in the two-tier split above.&lt;/p&gt;

&lt;p&gt;Whatever your stack, weigh the setup against how you actually spend your day. The database you iterate against should cost you nothing to start, nothing to reset, and nothing to explain in the onboarding doc.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI App Builders in 2026: RapidNative vs Bolt vs v0 vs Lovable, an Honest Comparison</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Fri, 18 Sep 2026 10:05:38 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/ai-app-builders-in-2026-rapidnative-vs-bolt-vs-v0-vs-lovable-an-honest-comparison-lei</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/ai-app-builders-in-2026-rapidnative-vs-bolt-vs-v0-vs-lovable-an-honest-comparison-lei</guid>
      <description>&lt;ul&gt;
&lt;li&gt;These four tools are not four answers to one question; they're answers to different questions&lt;/li&gt;
&lt;li&gt;v0: best-in-class React UI, no backend story&lt;/li&gt;
&lt;li&gt;Lovable: fastest full-stack web MVP, deep Supabase wiring, web only&lt;/li&gt;
&lt;li&gt;Bolt: browser IDE, most framework-flexible (Expo included), deployment is on you&lt;/li&gt;
&lt;li&gt;RapidNative: mobile-first; full React Native + Expo apps with the backend wired&lt;/li&gt;
&lt;li&gt;Disclosure up front: I work on RapidNative. The competitors' wins below are real wins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "AI app builder" category has exploded, and most comparisons read like feature-matrix theater. Having used all four of these for real projects (and shipping one of them), here's the honest version: each tool optimizes for a different job, and picking by hype instead of by job is how you lose a weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  v0 (Vercel)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://v0.dev" rel="noopener noreferrer"&gt;v0&lt;/a&gt; turns prompts into React components with shadcn/ui and Tailwind, and it produces the cleanest, most conventional UI code in the category. If you live in the Next.js/Vercel world, the design fidelity and deployment integration are genuinely excellent.&lt;/p&gt;

&lt;p&gt;The boundary is just as clean: it's a frontend tool. No backend generation; your API routes, database, and auth are your problem. That's not a flaw, it's the product's shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick v0 when:&lt;/strong&gt; the job is beautiful React UI inside a stack you already own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lovable
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://lovable.dev" rel="noopener noreferrer"&gt;Lovable&lt;/a&gt; is the most complete prompt-to-web-app tool: frontend, Supabase database, auth, even RLS policies, wired together from a description. It's also the most accessible of the four for people who don't write code, which explains its trajectory.&lt;/p&gt;

&lt;p&gt;The boundary: it generates web code. React for browsers, not React Native for phones. A Lovable app in a mobile webview is still a website.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Lovable when:&lt;/strong&gt; the job is a full-stack web MVP, fastest, especially if you're non-technical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bolt (StackBlitz)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bolt.new" rel="noopener noreferrer"&gt;Bolt&lt;/a&gt; gives you an AI pair inside a browser IDE, and it's the most framework-flexible of the group: React, Next.js, Svelte, Astro, and yes, Expo. For a technical user who wants direct code control and iteration speed in a tab, it's the strongest fit.&lt;/p&gt;

&lt;p&gt;The mobile boundary is subtler here, and it's where I should be precise since this is my lane: Bolt can generate Expo projects, but mobile is one framework among many rather than the product's center of gravity, and getting from generated code to a store build (EAS setup, certificates, submission) is manual work it doesn't do for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Bolt when:&lt;/strong&gt; you're technical, want an in-browser IDE, and value flexibility across stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  RapidNative
&lt;/h2&gt;

&lt;p&gt;Full disclosure again: this is the one I work on, so weigh accordingly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rapidnative.com/ai-app-builder?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ai-app-builders-comparison" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; exists for one job: native mobile apps. Prompts, screenshots, PRDs, or sketches in; complete React Native + Expo + TypeScript projects out, with navigation, theming, and the Supabase backend wired, built as apps headed for the App Store and Play Store rather than as websites that might one day get wrapped. You export and own the code.&lt;/p&gt;

&lt;p&gt;The honest boundaries: it's not the tool for a web SaaS (use Lovable), not a general-purpose IDE (use Bolt), and if your job is pixel-perfect web UI, v0 will out-design it. Mobile-first cuts both ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick RapidNative when:&lt;/strong&gt; the deliverable is an iOS/Android app, not a website.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual decision
&lt;/h2&gt;

&lt;p&gt;The category's dirty secret is that the web/mobile split matters more than any feature comparison. Almost every tool in this space builds web apps; if you're shipping to a browser, you have great options and this article barely matters. If you're shipping to the app stores, the field narrows sharply, and "supports Expo" and "built for Expo" are different products.&lt;/p&gt;

&lt;p&gt;These tools also compose. Plenty of teams run Lovable or Bolt for their web app and RapidNative for the mobile app, same Supabase underneath. Treating this as a winner-take-all bracket is the feature-matrix brain talking.&lt;/p&gt;

&lt;p&gt;Pricing, for completeness: all four land in the same rough $20-25/month ballpark for pro tiers as of writing, with credit systems that vary. Don't pick on price; at these numbers, one saved afternoon pays the annual difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Web UI: v0. Full-stack web, fastest: Lovable. Browser IDE with maximum flexibility: Bolt. Native mobile apps, end to end: RapidNative.&lt;/p&gt;

&lt;p&gt;What are you building, and which of these did you end up on? Genuinely curious how the split looks in the comments, especially from anyone who's shipped with two of them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>reactnative</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Postgres RLS in Local Dev: The Three Silent-Fail Modes That Ship to Prod</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 16 Sep 2026 05:28:31 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/postgres-rls-in-local-dev-the-three-silent-fail-modes-that-ship-to-prod-46el</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/postgres-rls-in-local-dev-the-three-silent-fail-modes-that-ship-to-prod-46el</guid>
      <description>&lt;ul&gt;
&lt;li&gt;RLS doesn't fail loudly. A table with no policies and RLS off returns every row, to everyone, and looks like a working app&lt;/li&gt;
&lt;li&gt;Three ways local dev hides that: migrations that never run &lt;code&gt;ENABLE ROW LEVEL SECURITY&lt;/code&gt;, a dev connection that bypasses RLS, and views that evaluate as their owner&lt;/li&gt;
&lt;li&gt;All three pass every test you'd write in the happy path, because the happy path is "the data shows up"&lt;/li&gt;
&lt;li&gt;A 30-line SQL check at boot catches every one of them, in about 50ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Row Level Security has one property that makes it different from almost every other security control: when it's not working, the app works better. More rows come back. Nothing errors. Your local dev environment looks great right up until a customer in production sees another customer's invoices.&lt;/p&gt;

&lt;p&gt;I've now seen this ship three different ways, and the fix for all three is the same boring boot-time check. Here's each failure and then the check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silent-fail 1: your migrations never enabled it
&lt;/h2&gt;

&lt;p&gt;RLS is off by default on every table in Postgres. &lt;code&gt;CREATE TABLE&lt;/code&gt; gives you a table with no row security, and it stays that way until something explicitly runs:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Supabase dashboard's table editor does this for you. Nothing else does. If you write migrations in raw SQL, dbmate, Prisma, Drizzle, Knex, or anything that turns a schema file into DDL, you get a table with RLS off, no warning, and no policies. And a table with RLS off and no policies is a table with no access control at all.&lt;/p&gt;

&lt;p&gt;The Prisma case is the sneakiest, because Prisma's schema language has no concept of RLS. You write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Invoice {
  id      String @id @default(uuid())
  userId  String
  amount  Int
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;code&gt;prisma migrate dev&lt;/code&gt; generates a perfectly correct &lt;code&gt;CREATE TABLE&lt;/code&gt; with no &lt;code&gt;ENABLE ROW LEVEL SECURITY&lt;/code&gt;. You have to add it in a hand-written migration, and remember to do it for every table, forever.&lt;/p&gt;

&lt;p&gt;There's a second half most people miss even after they've enabled it. The table &lt;em&gt;owner&lt;/em&gt; bypasses RLS unless you also run:&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="k"&gt;force&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;force&lt;/code&gt;, the role that created the table (often the same role your migrations run as, often the same role your app connects as in dev) walks straight through every policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silent-fail 2: your dev connection can't see RLS at all
&lt;/h2&gt;

&lt;p&gt;Superusers bypass RLS. Roles with the &lt;code&gt;BYPASSRLS&lt;/code&gt; attribute bypass RLS. Table owners bypass RLS unless &lt;code&gt;force&lt;/code&gt; is set. In a lot of local setups, the app connects as one of those.&lt;/p&gt;

&lt;p&gt;The common version: local Postgres, connection string is &lt;code&gt;postgres://postgres:postgres@localhost&lt;/code&gt;, the &lt;code&gt;postgres&lt;/code&gt; role is a superuser. Every query the app makes runs with RLS silently disabled. Your seed script inserted rows for five fake users, your UI shows all five users' data when logged in as one of them, and it looks like a demo, not a leak.&lt;/p&gt;

&lt;p&gt;In production the app doesn't connect as a superuser. It connects as &lt;code&gt;authenticated&lt;/code&gt; through PostgREST, or as a scoped application role, and now the policies apply. If they're wrong, or missing, that's the first time anyone finds out.&lt;/p&gt;

&lt;p&gt;You can check what you're running as right now:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rolsuper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rolbypassrls&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_roles&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;rolname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If either boolean is &lt;code&gt;true&lt;/code&gt;, RLS is decorative in this session.&lt;/p&gt;

&lt;p&gt;The fix isn't "don't use the postgres role locally," because you need it for migrations. The fix is to run the &lt;em&gt;app&lt;/em&gt; as the same role it runs as in prod, and only the migration step as the owner. Supabase-style stacks do this for you because the API layer always connects as &lt;code&gt;anon&lt;/code&gt; or &lt;code&gt;authenticated&lt;/code&gt;. Anything that hands your app a raw connection string doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silent-fail 3: views evaluate as their owner
&lt;/h2&gt;

&lt;p&gt;This one bit me after I'd fixed the first two.&lt;/p&gt;

&lt;p&gt;Before Postgres 15, a view runs its underlying queries with the &lt;em&gt;view owner's&lt;/em&gt; privileges, not the caller's. That's security-definer semantics, and it means RLS on the base tables is evaluated as the owner. If the owner is &lt;code&gt;postgres&lt;/code&gt;, or the table owner, or anything with &lt;code&gt;BYPASSRLS&lt;/code&gt;, the view returns every row regardless of who's querying it.&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;-- invoices has RLS enabled, forced, with a correct policy.&lt;/span&gt;
&lt;span class="c1"&gt;-- this view still leaks every row, because it runs as its owner.&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;view&lt;/span&gt; &lt;span class="n"&gt;invoice_summary&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;
  &lt;span class="k"&gt;select&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;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;
  &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&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;Postgres 15 added a way out:&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="k"&gt;view&lt;/span&gt; &lt;span class="n"&gt;invoice_summary&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;security_invoker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;
  &lt;span class="k"&gt;select&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;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;
  &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&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;With &lt;code&gt;security_invoker&lt;/code&gt;, the view runs as whoever called it and the base-table policies apply normally. On Postgres 14 or earlier there is no equivalent; you have to either not expose the view through your API layer, or replicate the policy logic inside the view's &lt;code&gt;WHERE&lt;/code&gt; clause and hope nobody edits one without the other.&lt;/p&gt;

&lt;p&gt;The reason this ships to prod is that the view works identically in dev and prod, and in both places it returns all the rows. The difference is only that in dev, all the rows is what you expected to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-line boot check
&lt;/h2&gt;

&lt;p&gt;None of the three failures produce an error. So produce one yourself. This runs at app boot in development and in CI, against whatever Postgres you're using locally, and refuses to start if any of the three conditions are true:&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;do&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;declare&lt;/span&gt;
  &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="c1"&gt;-- 1. every table in public must have RLS enabled AND forced&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;string_agg&lt;/span&gt;&lt;span class="p"&gt;(&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;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&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;relnamespace&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&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;and&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;relkind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'r'&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;not&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;relrowsecurity&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;not&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;relforcerowsecurity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bad&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;then&lt;/span&gt;
    &lt;span class="n"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="s1"&gt;'RLS not enabled+forced on: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- 2. the current role must not bypass RLS&lt;/span&gt;
  &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_roles&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;rolname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;current_user&lt;/span&gt;
      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rolsuper&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;rolbypassrls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="s1"&gt;'connected as % which bypasses RLS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- 3. every view in public must be security_invoker (PG15+)&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;string_agg&lt;/span&gt;&lt;span class="p"&gt;(&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;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&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;relnamespace&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&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;and&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;relkind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'v'&lt;/span&gt;
    &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;coalesce&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;option_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;
       &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_options_to_table&lt;/span&gt;&lt;span class="p"&gt;(&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;reloptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;option_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'security_invoker'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bad&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;then&lt;/span&gt;
    &lt;span class="n"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="s1"&gt;'views without security_invoker: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three catalog queries, roughly 50ms on a schema with a few hundred objects. Run it right after migrations apply and before the app accepts a request. If you have tables that legitimately shouldn't have RLS (lookup tables, public config), exclude them by name in check 1; the point is that the exclusion is explicit and reviewable rather than the default.&lt;/p&gt;

&lt;p&gt;Check 2 is the one people push back on, because it means the app can't connect as &lt;code&gt;postgres&lt;/code&gt; locally. That's the point. If your local app connects as a role that ignores RLS, you have never once tested your policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually runs
&lt;/h2&gt;

&lt;p&gt;The check only means something if local dev is real Postgres with RLS semantics. Mocks, SQLite adapters, and in-memory Postgres emulators either don't implement RLS or implement it loosely enough that the check passes on schemas that would fail in prod. I ended up running it against &lt;a href="https://tinbase.dev/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=postgres-rls-local-dev-silent-fail-modes" rel="noopener noreferrer"&gt;tinbase&lt;/a&gt;, which is a single-process Supabase-compatible backend on real Postgres 17: the API layer connects as &lt;code&gt;anon&lt;/code&gt;/&lt;code&gt;authenticated&lt;/code&gt; the way hosted Supabase does, so check 2 passes naturally, and the catalog queries behave exactly as they will in prod. Full Docker Supabase works too; it's just heavier for a check that takes 50ms.&lt;/p&gt;

&lt;p&gt;Whichever you use, the requirement is the same: the Postgres your app boots against locally has to be the Postgres that enforces RLS, or the boot check is testing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Three silent failures, one shape: RLS not applying looks like RLS working, only with more data. Nothing in the normal test loop distinguishes "policy correctly returned my rows" from "no policy, returned all rows, and mine were among them."&lt;/p&gt;

&lt;p&gt;The boot check turns the silent failure into a loud one at the earliest possible moment, which is the only place it's cheap. If you've got a fourth mode, I'd like to hear it. My current suspicion is &lt;code&gt;SECURITY DEFINER&lt;/code&gt; functions called from policies, but I haven't seen that one ship yet.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>supabase</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your App Store reviewer is a user. Your listing is a product page.</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:06:01 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/your-app-store-reviewer-is-a-user-your-listing-is-a-product-page-39mk</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/your-app-store-reviewer-is-a-user-your-listing-is-a-product-page-39mk</guid>
      <description>&lt;p&gt;Most submission guides are engineering documents. They explain how to make the technical parts work.&lt;/p&gt;

&lt;p&gt;Almost none treat submission as a UX problem, which it is. The reviewer is a user with a time budget. Your listing is a product page competing in a grid. The same first-impression rules that apply to onboarding apply here, and almost nobody applies them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reviewer is a user
&lt;/h2&gt;

&lt;p&gt;A first-pass reviewer spends a short, bounded time in your app. Roughly what they encounter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The launch screen. Is it branded, or is it a white rectangle?&lt;/li&gt;
&lt;li&gt;A sign-in screen, if you have one. Do they even need to sign in?&lt;/li&gt;
&lt;li&gt;The first functional screen. Does it work with the account you provided?&lt;/li&gt;
&lt;li&gt;One or two feature interactions. Do these match what your description claims?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of that breaks, you're rejected. If it merely takes too long to work out — a demo account that doesn't support the sign-in method they tried first, for instance — you can also be rejected for missing information, which is the same outcome with more steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dry run:&lt;/strong&gt; hand your app and your demo account to someone who has never seen it, give them a minute, and watch. Don't help. Wherever they hesitate, the reviewer will hesitate too, except the reviewer has other apps to get to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review notes: write a route, not credentials
&lt;/h2&gt;

&lt;p&gt;This is the highest-value item here and it costs ten minutes.&lt;/p&gt;

&lt;p&gt;Most teams fill the App Review Notes field with the minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; reviewer@yourapp.com&lt;/span&gt;
&lt;span class="nt"&gt;Password&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; TestPass123!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gets the reviewer into the app and no further. They then explore on their own, in whatever order occurs to them, and whatever they don't find, they conclude isn't there.&lt;/p&gt;

&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email: reviewer@yourapp.com
Password: TestPass123!

Once signed in:
1. Tap + on the home screen to create a Budget.
2. Add two or three expenses.
3. Tap Reports in the tab bar for the visualisation.
4. Tap Share, top right, for the export flow.

All features are unlocked on this account. No paywall.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same amount of reviewer time, far more of your app actually seen. It also pre-empts the most common completeness rejection, which is a reviewer concluding a feature doesn't exist because they didn't reach it.&lt;/p&gt;

&lt;p&gt;The last line matters more than it looks. A reviewer hitting an unexpected paywall has to decide whether that's the intended experience, and that decision doesn't go your way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots that survive being small
&lt;/h2&gt;

&lt;p&gt;Search results render your screenshots at thumbnail size. That's where the install decision gets made, and text sized for a full-screen preview is unreadable there.&lt;/p&gt;

&lt;p&gt;A hierarchy that works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot 1.&lt;/strong&gt; One enormous headline stating the single most valuable outcome. Nothing else. It must be readable at thumbnail size, which means far larger type than feels reasonable in the design file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshots 2 and 3.&lt;/strong&gt; One feature each, device UI, short caption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshots 4 onward.&lt;/strong&gt; Supporting features and social proof, for the minority who swipe.&lt;/p&gt;

&lt;p&gt;The test costs nothing: scale your first screenshot down to roughly the width of a thumbnail and look at it. If you can't read the headline, neither can anyone else, and that's the only one most people ever see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changelogs people actually read
&lt;/h2&gt;

&lt;p&gt;"Bug fixes and improvements" tells a user nothing, which means it gives them no reason to update, which means they stay on an older build and eventually conclude the app is abandoned.&lt;/p&gt;

&lt;p&gt;One line per user-visible change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;• Faster launch on iOS 18
• Fixed: widget showing stale data after a sync
• Fixed: dark mode contrast on Settings
• Added: CSV export for Reports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what that does beyond informing. "Fixed: widget showing stale data" tells someone who hit that bug that you saw it. That's worth more than the fix, because a user who reported something and never heard back assumes nobody is home.&lt;/p&gt;

&lt;h2&gt;
  
  
  The connecting idea
&lt;/h2&gt;

&lt;p&gt;Every item here is the same move: treating someone you can't talk to as a user with limited time and no context.&lt;/p&gt;

&lt;p&gt;The reviewer can't ask you where the feature is. The person scrolling search results can't zoom in. The user on an old build can't tell whether the update is worth the download. In each case they make a decision with whatever you gave them, and in each case most teams gave them very little.&lt;/p&gt;

&lt;p&gt;Automating the mechanical parts — generating changelog stubs from the release diff, checking listing assets before submission, keeping the workflow in one place — is what &lt;a href="https://www.letsdeploy.it/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=app-store-submission-ux" rel="noopener noreferrer"&gt;letsdeploy.it&lt;/a&gt; does. The judgement parts stay yours, and the review notes especially are worth writing by hand.&lt;/p&gt;




&lt;p&gt;What's in your App Review Notes field right now? My guess is an email and a password, and that ten minutes spent there has a better return than anything else on this list.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>appstore</category>
      <category>ux</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Self-Hosted Supabase Doesn't Break Because Postgres Is Hard</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Fri, 28 Aug 2026 12:21:49 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/self-hosted-supabase-doesnt-break-because-postgres-is-hard-cf0</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/self-hosted-supabase-doesnt-break-because-postgres-is-hard-cf0</guid>
      <description>&lt;p&gt;I spoke to teams who had run self-hosted Supabase in production. Some still do. Some moved back to hosted. A couple ended up assembling their own stack from primitives.&lt;/p&gt;

&lt;p&gt;The question I wanted answered: is the pain "Postgres is hard", or something more specific?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure before anything else:&lt;/strong&gt; I work on a Supabase-compatible runtime, so I had a stake in one of the possible answers. The findings below happen to point at a product shape close to what my team builds, and you should weigh them accordingly. I've tried to report what people said rather than what would be convenient.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nobody complained about Postgres. Every team described it as boring and working.&lt;/li&gt;
&lt;li&gt;The pain is the surrounding services: auth, realtime, storage, proxy, meta, and the rest&lt;/li&gt;
&lt;li&gt;Version drift between local and production was named unprompted by every team&lt;/li&gt;
&lt;li&gt;Realtime dropping events during deploy restarts was the second most common&lt;/li&gt;
&lt;li&gt;Both are distribution problems, not database problems&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The three failure modes
&lt;/h2&gt;

&lt;p&gt;Ranked by how many teams raised each without prompting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Auth service version drift between local and production.&lt;/strong&gt; Every team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime falling over during deploy churn.&lt;/strong&gt; Most of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage service behaving differently from S3 behind a reverse proxy.&lt;/strong&gt; Several.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Postgres was described as boring and working, by everyone. Not one complaint.&lt;/p&gt;

&lt;p&gt;The pain lives in the distribution: the wrapping services that turn a database into a platform. Auth, realtime, storage, functions, image proxy, gateway, metadata. Roughly ten moving parts, each with its own version, its own restart behaviour, and its own proxy quirks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version drift, in detail
&lt;/h2&gt;

&lt;p&gt;This came up in every conversation.&lt;/p&gt;

&lt;p&gt;The shape: you develop locally against current service versions. Production runs older ones, because upgrading is a maintenance window and there is always a reason to defer it. Then code that works locally fails in production, because something changed between those versions.&lt;/p&gt;

&lt;p&gt;Error taxonomies are the usual culprit. An auth error name that exists in the version on your laptop does not exist in the version on your server, so the client falls through to a generic unknown-error path and tells you nothing useful.&lt;/p&gt;

&lt;p&gt;One team described losing a full day to exactly this. The conclusion they reached, which several others reached independently: nothing about that day was Postgres.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workaround everyone lands on&lt;/strong&gt; is pinning local development to the same service versions as production. Which means every developer runs deliberately outdated services, nobody benefits from upstream fixes, and onboarding a new engineer means reproducing a bespoke pinned setup that exists in one person's head.&lt;/p&gt;

&lt;p&gt;That is a distribution problem. Not a code problem, and not a database one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realtime during deploys
&lt;/h2&gt;

&lt;p&gt;The second most common, and the more insidious of the two, because it produces no errors.&lt;/p&gt;

&lt;p&gt;Rolling deploy. The realtime service restarts. WebSocket clients disconnect, then reconnect, which works correctly. But during that reconnect window, subscriptions can miss changes.&lt;/p&gt;

&lt;p&gt;Nobody sees an error. Users just don't get updates. The team finds out days later, when a customer mentions that a change on one device never showed up on another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workaround:&lt;/strong&gt; deploy realtime only in low-traffic windows and manually verify subscription counts either side. That is toil, and it scales with team size in the wrong direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage behind a proxy
&lt;/h2&gt;

&lt;p&gt;Less universal, sharper when it lands. Several teams lost an afternoon to 403s on presigned uploads that worked locally and failed behind nginx or HAProxy.&lt;/p&gt;

&lt;p&gt;The cause was some combination of header case sensitivity and a Host header mismatch. Straightforward once you know where to look, and genuinely baffling if you assumed "S3-compatible" means "behaves identically to S3 in every deployment topology."&lt;/p&gt;

&lt;p&gt;It doesn't. Compatibility claims describe an API surface, not a network path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually implies
&lt;/h2&gt;

&lt;p&gt;The useful finding isn't that self-hosting is hard. It's that the difficulty is misattributed.&lt;/p&gt;

&lt;p&gt;Self-hosted Supabase doesn't fail because a database is hard to run. It fails because a platform assembled from around ten independently versioned services has a coordination problem, and coordination problems don't announce themselves. They present as a weird error in production, an event that didn't arrive, a 403 that only happens behind the proxy.&lt;/p&gt;

&lt;p&gt;Two things would remove most of what these teams described:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship every service at one version, by construction.&lt;/strong&gt; Not tooling that makes alignment easier. A shape where misalignment isn't expressible, because the whole platform is one artifact with one version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make realtime restarts transparent.&lt;/strong&gt; Resumption plus event replay across the disconnect window, so a deploy stops being a silent data-loss event.&lt;/p&gt;

&lt;p&gt;Proxy compatibility is a third, and it becomes essential rather than nice-to-have the moment anyone puts this behind enterprise networking.&lt;/p&gt;

&lt;p&gt;I'll be straightforward about the conflict here: the first of those describes &lt;a href="https://tinbase.dev/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=self-hosted-supabase-version-drift" rel="noopener noreferrer"&gt;tinbase&lt;/a&gt;, which is what I work on. It is a single binary, so services can't drift apart. It does not solve the second, and I'm not aware of anything that does yet.&lt;/p&gt;

&lt;p&gt;It's also a local development runtime rather than a production self-hosting replacement, which is worth stating plainly given that everything above is about production.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're running it now
&lt;/h2&gt;

&lt;p&gt;If you're hitting these, you aren't doing anything wrong. The shape of the distribution is doing it, and every workaround people described was a reasonable response to an unreasonable coordination problem.&lt;/p&gt;

&lt;p&gt;Pin your versions deliberately rather than accidentally. Write down which ones, somewhere a new engineer will find. Deploy realtime when it costs least. And treat "S3-compatible" as a claim about an API, not a promise about your network.&lt;/p&gt;




&lt;p&gt;If you run self-hosted Supabase in production, what broke that wasn't the database? I'm collecting these, and my expectation is that almost none of the answers are Postgres.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Should You Self-Host Supabase? It's Three Decisions, Not One</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Thu, 27 Aug 2026 10:53:45 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/should-you-self-host-supabase-its-three-decisions-not-one-517b</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/should-you-self-host-supabase-its-three-decisions-not-one-517b</guid>
      <description>&lt;p&gt;The self-hosting argument never resolves because both sides are right about different things.&lt;/p&gt;

&lt;p&gt;Own your data, no lock-in, cheaper at scale. Also true: it's an operational tax, upgrades cascade, and local drifts from prod.&lt;/p&gt;

&lt;p&gt;Both hold. "Should I self-host Supabase" isn't one question, it's three, and most people answer all three with a single decision.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local dev, staging, and production are separate decisions with separate constraints&lt;/li&gt;
&lt;li&gt;Picking one answer for all three is where the pain comes from&lt;/li&gt;
&lt;li&gt;Local: worst default, cheapest to change&lt;/li&gt;
&lt;li&gt;Production: usually-correct default, most expensive to change&lt;/li&gt;
&lt;li&gt;Most people optimise the expensive one and inherit the cheap one&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The three decisions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Optimises for&lt;/th&gt;
&lt;th&gt;Cost of getting it wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local dev&lt;/td&gt;
&lt;td&gt;Iteration speed, low friction&lt;/td&gt;
&lt;td&gt;Paid daily, compounds quietly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging&lt;/td&gt;
&lt;td&gt;Fidelity to production&lt;/td&gt;
&lt;td&gt;Bugs reach users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;Reliability, operational burden&lt;/td&gt;
&lt;td&gt;Paid at the worst possible moment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They pull in opposite directions. Local wants to be fast and disposable. Staging wants to be identical to prod. Production wants to be reliable and ideally someone else's problem.&lt;/p&gt;

&lt;p&gt;No single answer satisfies all three, which is why whichever one you pick feels wrong somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local: the one worth changing
&lt;/h2&gt;

&lt;p&gt;Worst default, and almost nobody revisits it.&lt;/p&gt;

&lt;p&gt;The standard setup runs the full container stack. Gigabytes of RAM, tens of seconds to boot, a laptop that gets loud. The consequence isn't complaints, it's that people stop running it.&lt;/p&gt;

&lt;p&gt;That's the actual cost and it appears in no comparison table. A local environment nobody runs is one that quietly stops working, and you find out the day prod breaks and three people spend an afternoon building a repro from scratch.&lt;/p&gt;

&lt;p&gt;Switching cost here is also the lowest of the three. Local is disposable. If a lighter setup doesn't work, you lost an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to tell:&lt;/strong&gt; is anyone on your team pointing at staging instead of running locally? Then you already have your answer.&lt;/p&gt;

&lt;p&gt;Single-binary runtimes exist for this now. &lt;a href="https://tinbase.dev/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=where-should-supabase-run" rel="noopener noreferrer"&gt;tinbase&lt;/a&gt; is the one my team builds, so discount accordingly. It serves the Supabase-compatible surface from one process. It's a local dev runtime, not a production self-hosting replacement, which is exactly why these decisions need separating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staging: the one people skip
&lt;/h2&gt;

&lt;p&gt;Staging is where the parity argument belongs, and it's usually where nobody's looking.&lt;/p&gt;

&lt;p&gt;If local runs a different implementation from prod, local passing stops guaranteeing prod passing. Real cost. But the fix isn't making local heavier, it's having something between local and prod that's genuinely faithful.&lt;/p&gt;

&lt;p&gt;Run the full stack in staging. Once before a release, not every morning. One boot a week instead of one a day, and fidelity lands where fidelity matters.&lt;/p&gt;

&lt;p&gt;Skip staging and try to get parity out of local, and you end up with the worst combination available: a heavy local setup that still isn't identical to prod.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production: highest stakes, usually-correct default
&lt;/h2&gt;

&lt;p&gt;Self-hosting prod means owning image updates, CVE patching, cert renewal, backup verification, and restore drills.&lt;/p&gt;

&lt;p&gt;With a platform team, that amortises across roles that already exist. Without one, it's evenings and weekends, and it has no deadline pressure right up until it has all of it.&lt;/p&gt;

&lt;p&gt;The honest test isn't cost. It's: &lt;strong&gt;when did you last restore from a backup?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If never, you're not self-hosting. You're holding an unverified assumption about data you can't afford to lose. A backup nobody has restored is a hypothesis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hosting prod is reasonable when:&lt;/strong&gt; someone owns operations, upgrades have a runbook, restore drills are scheduled, and the hosted bill justifies the work. All four. Not one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake
&lt;/h2&gt;

&lt;p&gt;People spend their thinking on the production decision, where the default is usually right and switching is expensive. Then they inherit the local decision, where the default is usually wrong and switching costs an afternoon.&lt;/p&gt;

&lt;p&gt;Flip the allocation. Change local this week. Leave prod alone until all four conditions above are true.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Optimise the decision you pay for daily, not the one you argue about online.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Worked examples
&lt;/h2&gt;

&lt;p&gt;Solo dev with a live product:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Lightweight, no containers&lt;/td&gt;
&lt;td&gt;Runs constantly, friction compounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging&lt;/td&gt;
&lt;td&gt;Full stack, before releases&lt;/td&gt;
&lt;td&gt;Parity where it counts, weekly not daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;Hosted&lt;/td&gt;
&lt;td&gt;No ops capacity, no restore drills happening&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Team of fifteen with a platform engineer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Developer preference&lt;/td&gt;
&lt;td&gt;Individual cost, individual call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging&lt;/td&gt;
&lt;td&gt;Full stack, always running&lt;/td&gt;
&lt;td&gt;Fidelity is cheap when someone owns it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;Either, run the numbers&lt;/td&gt;
&lt;td&gt;Ops capacity makes it a real option&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice what moves. Production changes with team structure. Staging barely moves. Local is personal and should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;If you have no staging environment, local is doing two jobs and doing at least one badly. Fix that first.&lt;/p&gt;

&lt;p&gt;Then: light local, faithful staging, boring production. Revisit production only when all four conditions hold.&lt;/p&gt;




&lt;p&gt;Which of the three did you actually decide on purpose? My bet is production, because it feels consequential, and that local came from a quickstart guide three years ago and nobody has touched it since.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Migrating a Supabase Local Dev Project to Tinbase in 10 Minutes</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Mon, 24 Aug 2026 12:26:47 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/migrating-a-supabase-local-dev-project-to-tinbase-in-10-minutes-189g</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/migrating-a-supabase-local-dev-project-to-tinbase-in-10-minutes-189g</guid>
      <description>&lt;p&gt;If you run Docker-based Supabase for local development and want to try the same stack on a single binary, this is the walkthrough. It takes about ten minutes for a typical project and nothing about it is destructive.&lt;/p&gt;

&lt;p&gt;Tinbase is a Supabase-compatible runtime that ships as one executable. Your app code doesn't change, the SDK is the same, only the connection string and the way you boot the backend differ.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install the binary, dump your schema, import it, swap the connection string&lt;/li&gt;
&lt;li&gt;Your Docker Supabase state is never touched, so rollback is a &lt;code&gt;git checkout&lt;/code&gt; away&lt;/li&gt;
&lt;li&gt;Auth and realtime work through the same &lt;code&gt;@supabase/supabase-js&lt;/code&gt; client&lt;/li&gt;
&lt;li&gt;Edge functions and non-standard extensions are where the gaps are&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;macOS, Linux, or Windows&lt;/li&gt;
&lt;li&gt;A working Docker-based Supabase local environment you can currently run&lt;/li&gt;
&lt;li&gt;A Node project using &lt;code&gt;@supabase/supabase-js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This assumes your app connects through the standard SDK. Direct low-level Postgres connections work too, but aren't covered here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: install the binary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://tinbase.dev/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather not pipe a script into a shell, download and read it first. Same URL, no pipe.&lt;/p&gt;

&lt;p&gt;Verify the install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tinbase &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: dump your Supabase schema
&lt;/h2&gt;

&lt;p&gt;With Docker Supabase running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase db dump &lt;span class="nt"&gt;--file&lt;/span&gt; schema.sql &lt;span class="nt"&gt;--schema&lt;/span&gt; public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces a &lt;code&gt;schema.sql&lt;/code&gt; containing your tables, functions, RLS policies, and triggers.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;auth&lt;/code&gt; schema doesn't need exporting. Both platforms manage it themselves, and importing one over the other creates conflicts rather than continuity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: import into Tinbase
&lt;/h2&gt;

&lt;p&gt;Start in a fresh directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;tinbase-dev &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;tinbase-dev
tinbase up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a second terminal, import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tinbase psql &lt;span class="nt"&gt;-f&lt;/span&gt; ../schema.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the tables landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tinbase psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'\dt public.*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this errors, read the message rather than re-running. Import failures are almost always a missing extension, and the error names it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: point your app at it
&lt;/h2&gt;

&lt;p&gt;Docker Supabase serves on &lt;code&gt;54321&lt;/code&gt;. Tinbase uses a different default port, configurable through an environment variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- SUPABASE_URL=http://localhost:54321
- SUPABASE_ANON_KEY=eyJhbGciOiJI...
&lt;/span&gt;&lt;span class="gi"&gt;+ SUPABASE_URL=http://localhost:8080
+ SUPABASE_ANON_KEY=&amp;lt;from `tinbase config show`&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;tinbase config show&lt;/code&gt; to get the anon key for your local instance. It's different from your Docker Supabase key, which is the step people miss and then spend twenty minutes debugging a 401.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: verify
&lt;/h2&gt;

&lt;p&gt;Three checks, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign up a test user&lt;/strong&gt; through your app's normal signup flow, then confirm the row exists:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tinbase psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'SELECT id, email FROM auth.users'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sign in as that user&lt;/strong&gt; and confirm a protected route resolves. This exercises JWT issuing and verification, which is the part most likely to differ.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trigger a realtime change&lt;/strong&gt; if your app subscribes to anything, and confirm the subscription fires.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all three pass, you're done. If step two fails, it's the anon key from step four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling back
&lt;/h2&gt;

&lt;p&gt;Your Docker Supabase state was never modified.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout .env
supabase start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire rollback. Worth knowing before you start, because it changes how much you need to think about this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't migrate cleanly
&lt;/h2&gt;

&lt;p&gt;Known gaps, worth checking against your project before you begin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge functions written for Deno.&lt;/strong&gt; Tinbase runs a v8 isolate rather than Deno, so Deno-specific APIs need adjusting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncommon Postgres extensions.&lt;/strong&gt; The widely used ones are preloaded; anything niche may not be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; Tinbase uses the local filesystem rather than S3-compatible object storage, which is usually what you want locally but differs from production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a standard project using Postgres, auth, realtime, and the JS client, none of these apply.&lt;/p&gt;

&lt;p&gt;That third one is worth a thought regardless of which tool you use. If your local storage layer behaves differently from production storage, you have a category of bug that only appears after deploy, and no local setup will surface it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;The honest version: if Docker Supabase boots fast enough on your machine and you never avoid running it, there's no problem to solve here.&lt;/p&gt;

&lt;p&gt;The case for a single binary is about the days you'd otherwise skip local entirely. Boot time and memory footprint determine whether a local environment gets used, and an environment nobody runs is one that quietly stops working.&lt;/p&gt;

&lt;p&gt;Full docs and source are at &lt;a href="https://tinbase.dev/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=supabase-to-tinbase-migration" rel="noopener noreferrer"&gt;tinbase.dev&lt;/a&gt;. MIT licensed.&lt;/p&gt;




&lt;p&gt;If you try this, I'm interested in what broke. Particularly edge functions, since that's the gap where "small syntax adjustments" covers a wide range of actual experiences.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your App Store Submission Should Be a Git Tag, Not a Ritual</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Mon, 17 Aug 2026 10:37:00 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/your-app-store-submission-should-be-a-git-tag-not-a-ritual-36b3</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/your-app-store-submission-should-be-a-git-tag-not-a-ritual-36b3</guid>
      <description>&lt;p&gt;Every team I've seen ship a React Native app has the same undocumented ceremony. Someone opens Xcode. Someone bumps a version number by hand. Someone drags screenshots into App Store Connect. Someone remembers, halfway through, that the Android build needs a different keystore.&lt;/p&gt;

&lt;p&gt;It takes a day, it happens every release, and roughly none of it is a decision. It's clicking.&lt;/p&gt;

&lt;p&gt;Here's the version where you push a tag and go do something else.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;eas build&lt;/code&gt; and &lt;code&gt;eas submit&lt;/code&gt; cover the binary path end to end, no local Xcode&lt;/li&gt;
&lt;li&gt;Store metadata belongs in a JSON file in your repo, not in a web form&lt;/li&gt;
&lt;li&gt;Build numbers should auto-increment in CI, never by hand&lt;/li&gt;
&lt;li&gt;Google Play's track model is the part that changes your &lt;em&gt;schedule&lt;/em&gt;, not just your tooling&lt;/li&gt;
&lt;li&gt;Screenshots and review replies stay manual, and should&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The parts that are actually mechanical
&lt;/h2&gt;

&lt;p&gt;Sort the release into two piles: things requiring judgement, and things requiring clicking.&lt;/p&gt;

&lt;p&gt;Judgement: what the screenshots say, what the release notes claim, whether the feature is ready, how to respond to a rejection.&lt;/p&gt;

&lt;p&gt;Clicking: compiling, signing, uploading, incrementing versions, pushing descriptions and keywords, promoting between tracks.&lt;/p&gt;

&lt;p&gt;The second pile is most of the day and none of the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and submit without touching Xcode
&lt;/h2&gt;

&lt;p&gt;Two commands, once &lt;code&gt;eas.json&lt;/code&gt; is configured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eas build &lt;span class="nt"&gt;--platform&lt;/span&gt; all &lt;span class="nt"&gt;--profile&lt;/span&gt; production
eas submit &lt;span class="nt"&gt;--platform&lt;/span&gt; all &lt;span class="nt"&gt;--latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The config that makes &lt;code&gt;submit&lt;/code&gt; work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cli"&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;"version"&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;= 5.0.0"&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;"build"&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;"production"&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;"autoIncrement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"APP_ENV"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"submit"&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;"production"&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;"ios"&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;"appleId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ascAppId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1234567890"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"appleTeamId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ABCD123456"&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;"android"&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;"serviceAccountKeyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./secrets/play-service-account.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"track"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal"&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;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in there matter more than they look.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;autoIncrement&lt;/code&gt; kills an entire category of failed uploads. Manually managed build numbers get duplicated roughly every time two people cut a release in the same week, and the error you get back from App Store Connect is not helpful about why.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;track&lt;/code&gt; is the Google Play release channel, and it's the thing worth understanding properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Google Play track model
&lt;/h2&gt;

&lt;p&gt;Play has four tracks: &lt;code&gt;internal&lt;/code&gt;, &lt;code&gt;alpha&lt;/code&gt; (closed testing), &lt;code&gt;beta&lt;/code&gt; (open testing), and &lt;code&gt;production&lt;/code&gt;. Builds get promoted between them.&lt;/p&gt;

&lt;p&gt;This is normally described as a testing convenience. For a new developer account it's a scheduling constraint, because personal accounts created after November 2023 have to run closed testing with at least 12 testers opted in for 14 continuous days before production access is even available.&lt;/p&gt;

&lt;p&gt;Which means your CI config has a scheduling consequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"android"&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;"serviceAccountKeyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./secrets/play-service-account.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"track"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alpha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"releaseStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the pipeline at &lt;code&gt;alpha&lt;/code&gt; on day one, with whatever build you have. The fourteen days run while you keep working. If you wait until the app is finished to think about tracks, you've added two weeks to the end of your timeline instead of overlapping them with the middle.&lt;/p&gt;

&lt;p&gt;Automating the upload is the easy part. Automating it &lt;em&gt;early&lt;/em&gt; is the part that actually saves time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata as a file, not a form
&lt;/h2&gt;

&lt;p&gt;App descriptions, keywords, categories, and release notes are content. Content belongs in version control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"configVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apple"&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;"info"&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;"en-US"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your App"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"subtitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Short value proposition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Full description text."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&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="s2"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"another"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"releaseNotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What changed in this build."&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;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 shell"&gt;&lt;code&gt;eas metadata:push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefit isn't saved keystrokes. It's that your store listing goes through code review like everything else, and you can see in &lt;code&gt;git log&lt;/code&gt; when the description changed and who changed it. Ask anyone who has tried to work out why their keywords are different from last month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it to a tag
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ship&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;expo/expo-github-action@v8&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;eas-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EXPO_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eas build --platform all --profile production --non-interactive&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eas submit --platform all --latest --non-interactive&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eas metadata:push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--non-interactive&lt;/code&gt; matters. Without it the CLI will sit waiting for a prompt nobody is there to answer, and your job times out twenty minutes later with no useful output.&lt;/p&gt;

&lt;p&gt;Credentials go in CI secrets. The Play service account JSON should be a base64-encoded secret written to disk at runtime, never a file in the repo. Same for the Apple API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should stay manual
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Screenshots.&lt;/strong&gt; Generating them is automatable and mostly a bad idea. Store screenshots are a message hierarchy, not a screen dump, and the automated ones look exactly like what they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release notes.&lt;/strong&gt; "Bug fixes and performance improvements" is what happens when you automate this. Write them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rejection responses.&lt;/strong&gt; A rejection is a conversation. Attaching a demo video and explaining your native functionality resolves a surprising share of them, and none of that is scriptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decision to ship.&lt;/strong&gt; Obvious, but worth stating, because a pipeline this smooth makes it easy to release without anyone actually deciding to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that compounds
&lt;/h2&gt;

&lt;p&gt;The first release you automate takes longer than doing it by hand. The fifth one takes four minutes.&lt;/p&gt;

&lt;p&gt;More usefully, it changes what a release costs psychologically. When shipping is a day of clicking, you batch changes, which makes each release bigger, which makes each release riskier. When it's a tag, you ship the one-line fix on Tuesday instead of holding it for the next big push.&lt;/p&gt;

&lt;p&gt;Teams doing this across several apps eventually want the pipeline itself to be a product rather than a &lt;code&gt;.github&lt;/code&gt; folder copied between repos. &lt;a href="https://www.letsdeploy.it/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=app-store-submission-pipeline" rel="noopener noreferrer"&gt;letsdeploy.it&lt;/a&gt; does that part, carrying builds, metadata, and store assets through submission so each release stops consuming a day.&lt;/p&gt;

&lt;p&gt;But the tooling matters less than the reclassification. Look at your last release and mark each step as judgement or clicking. The clicking pile is bigger than you think, and all of it is someone's afternoon.&lt;/p&gt;




&lt;p&gt;What's still manual in your release that shouldn't be? I'll bet at least one person reading this is still incrementing build numbers by hand.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>devops</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Mobile App Analytics in React Native: What to Track (and What to Skip)</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Thu, 13 Aug 2026 05:01:19 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/mobile-app-analytics-in-react-native-what-to-track-and-what-to-skip-25jp</link>
      <guid>https://dev.to/rapidnative-ai/mobile-app-analytics-in-react-native-what-to-track-and-what-to-skip-25jp</guid>
      <description>&lt;p&gt;I have shipped enough React Native apps to see the same pattern every time: on launch day someone wires up fifty analytics events and six months later nobody on the team can explain what any of them are for. This is a working developer's guide to instrumenting a React Native or Expo app so the numbers actually mean something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Buckets
&lt;/h2&gt;

&lt;p&gt;Every useful mobile app metric fits into one of four categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Acquisition&lt;/strong&gt;: installs, CPI, CAC, install source, store conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activation&lt;/strong&gt;: activation rate, time to value, onboarding funnel completion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement and Retention&lt;/strong&gt;: DAU, MAU, stickiness (DAU/MAU), session length, D1/D7/D30 retention cohorts, churn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monetization&lt;/strong&gt;: ARPU, conversion rate, LTV, trial-to-paid, renewal rate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Track a couple from each bucket. Skip the rest until you outgrow them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Metrics That Are Actually Product Metrics
&lt;/h2&gt;

&lt;p&gt;Performance is not a "nice to have." A slow, crashy app poisons every other metric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Crash-Free Session Rate&lt;/strong&gt;: target 99.5%+ for consumer, 99.9%+ if you take payment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANR Rate&lt;/strong&gt; (Android only): Play Store visibility suffers when this crosses Google's bad-behavior threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold Start Time&lt;/strong&gt;: under 500ms good, over 1500ms bad. Hermes bytecode substantially cuts JS parse time vs JSC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API p95 Latency&lt;/strong&gt;: measured from the client, not the server. The tail is where users feel pain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Stack I Actually Use in 2026
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product analytics&lt;/td&gt;
&lt;td&gt;PostHog (self-hosted) or Amplitude (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors / performance&lt;/td&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscriptions&lt;/td&gt;
&lt;td&gt;RevenueCat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attribution&lt;/td&gt;
&lt;td&gt;SKAdNetwork + Play Install Referrer (free, built-in)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of these have first-class Expo config plugins in 2026. &lt;code&gt;npx expo install @sentry/react-native posthog-react-native react-native-purchases&lt;/code&gt; gets you 80% of the way there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One Instrumentation Pattern That Scales
&lt;/h2&gt;

&lt;p&gt;Wrap your SDK. Always. Every screen and component should import &lt;code&gt;track()&lt;/code&gt; from a single module, never the SDK directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/analytics/track.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PostHog&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;posthog-react-native&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="nx"&gt;EventName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_completed&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="s1"&gt;paywall_viewed&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="s1"&gt;subscription_started&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="s1"&gt;workout_logged&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="nx"&gt;EventProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;track&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;EventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;PostHog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture&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;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;identify&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;PostHog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identify&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="nx"&gt;props&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;Rules I have learned the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event names are verbs, past tense.&lt;/strong&gt; &lt;code&gt;paywall_viewed&lt;/code&gt;, not &lt;code&gt;paywall&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User properties are stable, few, semantic.&lt;/strong&gt; &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;cohort_week&lt;/code&gt;, &lt;code&gt;platform_version&lt;/code&gt;. That is enough for 90% of segmentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No PII in event names or properties.&lt;/strong&gt; Not even hashed. Just don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type the event names.&lt;/strong&gt; If your event schema is a &lt;code&gt;type&lt;/code&gt;, refactoring becomes safe. Untyped strings are how event soup starts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Metrics I Have Deleted From Every Dashboard
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Total installs, unless you are showing it next to activated installs.&lt;/li&gt;
&lt;li&gt;Screen views as a KPI. Useful for debugging nav, useless as a headline.&lt;/li&gt;
&lt;li&gt;"Engagement" as a single number. It's a portfolio, not a scalar.&lt;/li&gt;
&lt;li&gt;App Store rating without volume and recency alongside it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule: for every metric, name the decision it drives, the owner of that decision, and the cadence they act on it. Can't do all three? Delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy: The 2026 Baseline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt;&lt;/strong&gt; is mandatory if any of your SDKs touch required-reason APIs. Most analytics SDKs do. Skip this and your App Store submission gets rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consent for EU&lt;/strong&gt;: non-negotiable for behavioral events. Use Klaro if you want an OSS option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-party &amp;gt; third-party&lt;/strong&gt;: server-side event streams from your app to your own backend are increasingly the safe default. PostHog and Amplitude both support this.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to Go From Here
&lt;/h2&gt;

&lt;p&gt;If you're starting from scratch, spin up an app with an AI React Native builder like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-analytics-what-to-track" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;. It scaffolds Expo apps with the wrapper pattern above, and event schemas become just another thing you can prompt for. Faster than doing the boilerplate by hand, and consistent across every screen.&lt;/p&gt;

&lt;p&gt;If you already have an app, delete half your events tomorrow. You'll be fine.&lt;/p&gt;

&lt;p&gt;Which metric have you deleted from a dashboard and never missed? That list is more useful than any tracking plan; drop yours in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>analytics</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Best React Native Starter Templates in 2026 (Compared)</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:55:59 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/best-react-native-starter-templates-in-2026-compared-9he</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/best-react-native-starter-templates-in-2026-compared-9he</guid>
      <description>&lt;h1&gt;
  
  
  Best React Native Starter Templates in 2026 (Compared)
&lt;/h1&gt;

&lt;p&gt;One of the first questions every React Native developer asks is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"What's the best React Native starter template?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's a fair question.&lt;/p&gt;

&lt;p&gt;Nobody wants to spend the first week of a project configuring navigation, authentication, theming, folder structure, and dozens of reusable screens before building their actual product.&lt;/p&gt;

&lt;p&gt;A good starter template should help you ship faster—not introduce more decisions.&lt;/p&gt;

&lt;p&gt;After trying several popular React Native starter kits and boilerplates, here are the things that actually matter.&lt;/p&gt;




&lt;h1&gt;
  
  
  What makes a good React Native starter template?
&lt;/h1&gt;

&lt;p&gt;Most comparison articles focus on the tech stack.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo vs React Native CLI&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;State management&lt;/li&gt;
&lt;li&gt;Navigation&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are important, but they're only part of the picture.&lt;/p&gt;

&lt;p&gt;A great starter template should also include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production-ready screen layouts&lt;/li&gt;
&lt;li&gt;Dark mode support&lt;/li&gt;
&lt;li&gt;Consistent design system&lt;/li&gt;
&lt;li&gt;Reusable components&lt;/li&gt;
&lt;li&gt;Clean project structure&lt;/li&gt;
&lt;li&gt;Easy customization&lt;/li&gt;
&lt;li&gt;Good documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't just to start a project.&lt;/p&gt;

&lt;p&gt;It's to reduce the amount of UI you have to rebuild.&lt;/p&gt;




&lt;h1&gt;
  
  
  What developers usually search for
&lt;/h1&gt;

&lt;p&gt;Most developers aren't actually searching for a "starter template."&lt;/p&gt;

&lt;p&gt;They're trying to solve a specific problem.&lt;/p&gt;

&lt;p&gt;Common searches include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React Native login screen&lt;/li&gt;
&lt;li&gt;React Native onboarding template&lt;/li&gt;
&lt;li&gt;React Native dashboard UI&lt;/li&gt;
&lt;li&gt;React Native settings screen&lt;/li&gt;
&lt;li&gt;React Native profile screen&lt;/li&gt;
&lt;li&gt;React Native e-commerce UI&lt;/li&gt;
&lt;li&gt;React Native chat UI&lt;/li&gt;
&lt;li&gt;React Native admin dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your starter template doesn't include these, you'll probably end up building them yourself.&lt;/p&gt;




&lt;h1&gt;
  
  
  Popular React Native starter templates
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Expo Router Starter
&lt;/h2&gt;

&lt;p&gt;Perfect if you want the official Expo experience.&lt;/p&gt;

&lt;p&gt;Pros&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Officially maintained&lt;/li&gt;
&lt;li&gt;Great developer experience&lt;/li&gt;
&lt;li&gt;Excellent documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimal UI&lt;/li&gt;
&lt;li&gt;You'll build most application screens yourself&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Ignite
&lt;/h2&gt;

&lt;p&gt;Ignite has been around for years and provides a solid architecture.&lt;/p&gt;

&lt;p&gt;Pros&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opinionated structure&lt;/li&gt;
&lt;li&gt;State management included&lt;/li&gt;
&lt;li&gt;Great documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focuses more on architecture than reusable UI.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  React Native Boilerplates
&lt;/h2&gt;

&lt;p&gt;There are dozens of community boilerplates.&lt;/p&gt;

&lt;p&gt;Some are excellent.&lt;/p&gt;

&lt;p&gt;Some haven't been updated in years.&lt;/p&gt;

&lt;p&gt;Before choosing one, always check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Last commit&lt;/li&gt;
&lt;li&gt;Issue activity&lt;/li&gt;
&lt;li&gt;Documentation quality&lt;/li&gt;
&lt;li&gt;React Native version&lt;/li&gt;
&lt;li&gt;Expo compatibility&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  gluestack-ui Pro
&lt;/h2&gt;

&lt;p&gt;Unlike traditional starter templates, &lt;strong&gt;gluestack-ui Pro&lt;/strong&gt; focuses on the part most templates leave out—production-ready app screens.&lt;/p&gt;

&lt;p&gt;Instead of only giving you components, it includes reusable screens for common app flows such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Onboarding&lt;/li&gt;
&lt;li&gt;Dashboard&lt;/li&gt;
&lt;li&gt;Settings&lt;/li&gt;
&lt;li&gt;Profile&lt;/li&gt;
&lt;li&gt;Commerce&lt;/li&gt;
&lt;li&gt;Chat&lt;/li&gt;
&lt;li&gt;Wallet&lt;/li&gt;
&lt;li&gt;Media&lt;/li&gt;
&lt;li&gt;Social&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo 54&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;NativeWind v5&lt;/li&gt;
&lt;li&gt;gluestack-ui v5&lt;/li&gt;
&lt;li&gt;Design tokens&lt;/li&gt;
&lt;li&gt;Light &amp;amp; Dark mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pro.gluestack.io/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=starter_templates_2026" rel="noopener noreferrer"&gt;Learn more&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Questions to ask before choosing a starter template
&lt;/h1&gt;

&lt;p&gt;Before adopting any template, ask yourself:&lt;/p&gt;

&lt;p&gt;✅ Can I customize it easily?&lt;/p&gt;

&lt;p&gt;✅ Does it support theming?&lt;/p&gt;

&lt;p&gt;✅ Is the code easy to understand?&lt;/p&gt;

&lt;p&gt;✅ Will it still work after upgrading React Native?&lt;/p&gt;

&lt;p&gt;✅ Does it include the screens I'll end up building anyway?&lt;/p&gt;

&lt;p&gt;These questions matter more than the number of GitHub stars.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final thoughts
&lt;/h1&gt;

&lt;p&gt;The best React Native starter template isn't necessarily the one with the biggest feature list.&lt;/p&gt;

&lt;p&gt;It's the one that removes the most repetitive work.&lt;/p&gt;

&lt;p&gt;Whether that's a lightweight boilerplate, a full architecture starter, or a reusable screen library depends on your workflow.&lt;/p&gt;

&lt;p&gt;If your team repeatedly builds the same authentication flows, dashboards, settings pages, and onboarding experiences, starting with production-ready screens can save days of development on every project.&lt;/p&gt;

&lt;p&gt;What starter template are you using today, and what's the first thing you always end up changing?&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>mobile</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Gluestack-ui Pro: 55+ Production-Ready React Native Screens</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 05 Aug 2026 07:30:45 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/gluestack-ui-pro-55-production-ready-react-native-screens-2b2m</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/gluestack-ui-pro-55-production-ready-react-native-screens-2b2m</guid>
      <description>&lt;p&gt;Hey everyone!&lt;/p&gt;

&lt;p&gt;If you've ever built a React Native application, you've probably faced this problem. You start a new project, you're excited to build features, solve real user problems, and ship something amazing.&lt;/p&gt;

&lt;p&gt;But before you get there, every application needs the same set of screens.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Profile&lt;/li&gt;
&lt;li&gt;Settings&lt;/li&gt;
&lt;li&gt;Chat&lt;/li&gt;
&lt;li&gt;Dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Almost every project starts with these. The problem isn't that they're difficult to build. The problem is building them well.&lt;/p&gt;

&lt;p&gt;Because this is where users get their first impression of your product. If these screens don't feel polished, the whole application doesn't feel premium. So developers spend hours perfecting layouts, spacing, typography, animations, responsiveness, and design consistency.&lt;/p&gt;

&lt;p&gt;And that's where a lot of development time disappears. Not into building unique features, but into rebuilding the same UI foundation over and over again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing gluestack-ui Pro
&lt;/h2&gt;

&lt;p&gt;That's exactly why we built &lt;a href="https://pro.gluestack.io/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=gluestack-ui-pro-launch" rel="noopener noreferrer"&gt;gluestack-ui Pro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Instead of starting every project from scratch, you get a collection of premium, production-ready React Native screens that are ready to integrate into your application. Simply customize them according to your brand, connect your own APIs and business logic, and focus on building the features that make your product unique.&lt;/p&gt;

&lt;h2&gt;
  
  
  55+ Ready-to-use Screens
&lt;/h2&gt;

&lt;p&gt;Right now, it comes with 55+ of the most commonly used React Native screens.&lt;/p&gt;

&lt;p&gt;Whether you're building a startup, an MVP, or a client project, you'll find most of the common screens already available.&lt;/p&gt;

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

&lt;p&gt;The layouts are clean. The spacing is consistent. The typography feels polished.&lt;/p&gt;

&lt;p&gt;We've also added smooth animations throughout the application, making the entire experience feel modern and premium. And these aren't just UI mockups. These are real React Native screens that you can integrate directly into your application and customize however you like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Codebase &amp;amp; Architecture
&lt;/h2&gt;

&lt;p&gt;The codebase is clean and well-structured. The project is easy to navigate, making customization straightforward even for larger applications.&lt;/p&gt;

&lt;p&gt;Design tokens are a big part of that. Instead of hardcoded values scattered throughout the project, everything is driven by tokens.&lt;/p&gt;

&lt;p&gt;So if your designer decides to change the brand color later, you don't have to hunt through dozens of files updating components one by one. Just update the token once, and the changes are reflected across the entire application. The entire styling system is built using Tailwind, which keeps the code clean, readable, and easy to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modern Tech Stack
&lt;/h2&gt;

&lt;p&gt;gluestack-ui Pro is built with the latest React Native technologies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expo&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NativeWind&lt;/td&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gluestack UI&lt;/td&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Seamless Customization
&lt;/h2&gt;

&lt;p&gt;Changing the primary color updates it across the entire application. Same for the background color. One small change updates every screen that uses those design tokens.&lt;/p&gt;

&lt;p&gt;That's the advantage of a proper tokenized design system. You update your design in one place, and the entire application stays consistent.&lt;/p&gt;

&lt;p&gt;There are also several beautifully designed themes included out of the box, and every theme works seamlessly with both Light Mode and Dark Mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;One important thing before we wrap up. gluestack-ui Pro isn't trying to replace development. You still own your backend. Your APIs. Your authentication. Your business logic.&lt;/p&gt;

&lt;p&gt;gluestack-ui Pro simply removes the repetitive UI work, giving you a strong foundation so you can focus on building your actual product.&lt;/p&gt;

&lt;p&gt;If you're looking to build React Native applications faster, without spending weeks rebuilding the same UI over and over again, you know where to start. gluestack-ui Pro gives you a production-ready foundation that you can integrate, customize, and make your own.&lt;/p&gt;

&lt;p&gt;Spend less time building common screens, and more time building the features that make your product unique.&lt;/p&gt;

&lt;p&gt;We're launching on Product Hunt today. If any of this is useful to you, a look means a lot.&lt;/p&gt;

&lt;p&gt;What's the screen you rebuild most often? Curious whether it matches ours.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>typescript</category>
      <category>ui</category>
    </item>
    <item>
      <title>How tinbase Reimplements Supabase's Wire Protocols in a Single Process (and Runs in a Browser Tab)</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Thu, 30 Jul 2026 07:47:14 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/how-tinbase-reimplements-supabases-wire-protocols-in-a-single-process-and-runs-in-a-browser-tab-3h9l</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/how-tinbase-reimplements-supabases-wire-protocols-in-a-single-process-and-runs-in-a-browser-tab-3h9l</guid>
      <description>&lt;p&gt;&lt;em&gt;tinbase is live on Product Hunt today — an open-source, Supabase-compatible backend that runs without Docker. Support the launch here: [&lt;a href="https://www.producthunt.com/products/tinbase" rel="noopener noreferrer"&gt;https://www.producthunt.com/products/tinbase&lt;/a&gt;]&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Local Supabase development means Docker: a 12-container stack and over 2 GB of images to get Postgres, PostgREST, GoTrue, Storage, and Realtime running on a laptop. The platform itself is excellent; the local loop is heavy. And in some environments it isn't just heavy — it's impossible. tinbase was originally built for &lt;a href="https://lifo.sh" rel="noopener noreferrer"&gt;lifo&lt;/a&gt;, a project that maps Linux APIs into the browser, to let Expo apps run fully in-browser with full-stack capability — database, auth, storage, realtime, no server. It's part of the &lt;a href="https://rapidnative.com" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; ecosystem, and Docker was never an option in that environment.&lt;/p&gt;

&lt;p&gt;That raised the question: what if the entire Supabase API surface were just a process? One process, embeddable anywhere Node — or a browser tab — runs?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Supabase compatibility instead of yet another backend
&lt;/h2&gt;

&lt;p&gt;tinbase deliberately avoids inventing another proprietary BaaS API. Supabase's real strength is that it functions as an open standard: PostgREST's REST conventions, GoTrue's auth flows, and a documented realtime protocol, all served by the most widely adopted SDK in the space. Implement those protocols faithfully, and every existing supabase-js app, tutorial, and starter template works with zero changes — and every project stays portable back to hosted Supabase.&lt;/p&gt;

&lt;p&gt;That portability is the design constraint everything else follows from. tinbase reads the same &lt;code&gt;supabase/migrations/*.sql&lt;/code&gt; files and follows the Supabase CLI's migration conventions, tracking them in &lt;code&gt;supabase_migrations.schema_migrations&lt;/code&gt;. It's a different runtime, not a different platform.&lt;/p&gt;

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

&lt;p&gt;tinbase is a single TypeScript process with three layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Protocol layer.&lt;/strong&gt; Every service is a pure &lt;code&gt;(Request) =&amp;gt; Response&lt;/code&gt; fetch handler: the PostgREST query grammar (filters, embeds — to-one, to-many, many-to-many via junction, &lt;code&gt;!inner&lt;/code&gt; — JSON paths, upsert, RPC), GoTrue's auth endpoints (email/password, anonymous sign-in, OAuth with PKCE, session refresh with rotation), storage APIs with signed URLs, and realtime speaking the Phoenix protocol over a hand-rolled ~150-line RFC 6455 WebSocket server. Because these are plain fetch handlers rather than a bound HTTP server, they run behind &lt;code&gt;node:http&lt;/code&gt; (or &lt;code&gt;Bun.serve&lt;/code&gt;) on a machine — or get handed to supabase-js as a custom &lt;code&gt;fetch&lt;/code&gt; and invoked directly in-process, with no server at all. That one decision is what makes browser mode possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Engine adapter layer.&lt;/strong&gt; A thin interface over "a thing that runs Postgres SQL," with three implementations and honest tradeoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedded native Postgres 17&lt;/strong&gt; — the default on macOS/Linux. First run downloads ~12 MB of platform binaries, then &lt;code&gt;initdb&lt;/code&gt; with memory-lean settings: ~59 MB of RAM at boot, listening only on a private unix socket. Real RLS, triggers, foreign keys, jsonb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PGlite (wasm)&lt;/strong&gt; — ElectricSQL's Postgres compiled to WASM. Zero setup, runs anywhere Node runs and in the browser, and it's the default on Windows. The tradeoff is memory: its WASM heap sits around 575–650 MB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pg-mem&lt;/strong&gt; — a pure-JS in-memory subset at a 3.6 MB install, the lightest option for browser previews. No RLS or cron, but the REST CRUD surface, auth, edge functions, and realtime all work, with change events synthesized in JS by the REST layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Studio.&lt;/strong&gt; A Supabase-Studio-style dashboard at &lt;code&gt;/_/&lt;/code&gt; — table editor with row CRUD, SQL editor, user management, storage, RLS policies — compiled to a single self-contained HTML file so it works even inside the single binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RLS across engines.&lt;/strong&gt; Every REST and storage request runs inside a transaction with &lt;code&gt;SET LOCAL role&lt;/code&gt; and &lt;code&gt;request.jwt.claims&lt;/code&gt;, so a policy like &lt;code&gt;using (user_id = auth.uid())&lt;/code&gt; behaves identically to hosted Supabase — whether the SQL executes in native Postgres 17 or in WASM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP without HTTP.&lt;/strong&gt; In a browser tab there is no listener socket. supabase-js expects a URL, so tinbase hands it a custom &lt;code&gt;fetch&lt;/code&gt; that routes requests straight into the protocol handlers in-process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anonKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&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;&lt;strong&gt;Realtime without WAL.&lt;/strong&gt; Hosted Supabase reads the write-ahead log; tinbase feeds &lt;code&gt;postgres_changes&lt;/code&gt; from triggers plus &lt;code&gt;pg_notify&lt;/code&gt;, and even applies per-subscriber RLS filtering on INSERT/UPDATE events by re-checking the row as that user. (DELETE events can't be re-queried — the row is gone — which is documented as a known gap.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proving compatibility.&lt;/strong&gt; Claiming "supabase-js works unchanged" is easy; keeping it true is a test-suite problem. 120 tests run the real &lt;code&gt;@supabase/supabase-js&lt;/code&gt; against the backend — REST via in-process fetch, realtime over actual WebSockets, zero mocks — and pass on both the wasm and native engines. Overall coverage lands around 80% of the supabase-js SDK surface, and roughly 90% of what a typical CRUD + auth + storage + realtime app actually calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;The footprint benchmark (reproducible via &lt;code&gt;bench/footprint.ts&lt;/code&gt; in the repo) against the same workload — boot, 1,000 inserts, 1,000 filtered reads:&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;tinbase (single binary)&lt;/th&gt;
&lt;th&gt;Supabase local&lt;/th&gt;
&lt;th&gt;PocketBase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory at boot&lt;/td&gt;
&lt;td&gt;49 MB&lt;/td&gt;
&lt;td&gt;1,441 MB&lt;/td&gt;
&lt;td&gt;15 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory after workload&lt;/td&gt;
&lt;td&gt;66 MB&lt;/td&gt;
&lt;td&gt;1,626 MB&lt;/td&gt;
&lt;td&gt;24 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install size&lt;/td&gt;
&lt;td&gt;92 MB&lt;/td&gt;
&lt;td&gt;2,291 MB&lt;/td&gt;
&lt;td&gt;30 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processes&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;12 containers + Docker&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;real Postgres 17 + RLS&lt;/td&gt;
&lt;td&gt;Postgres 17&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest read: versus Supabase local, ~16–24x less memory and a ~2s boot instead of a minute, with the same SDK and APIs. Versus PocketBase, roughly 2.7x the RAM — but running real Postgres with RLS behind Supabase's exact wire APIs, so code and migrations move to hosted Supabase unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tinbase start

&lt;span class="c"&gt;#   API URL: http://127.0.0.1:54321&lt;/span&gt;
&lt;span class="c"&gt;#   anon key: eyJ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&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;@supabase/supabase-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://127.0.0.1:54321&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ANON_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// everything below this line is unchanged app code&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signUp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;me@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*, author:users(name)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or skip the terminal and run the whole backend in a tab: [browser demo link]&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;tinbase is alpha (currently v0.6.x). It's built for local development, prototyping, and embedded/browser use — for production, hosted Supabase remains the recommendation, and a tinbase project migrates there unchanged, which is the point of protocol compatibility. Known gaps are documented in the README: no MFA/SSO/phone auth yet, no resumable (TUS) uploads or image transformations, no pgvector, and the engines currently serialize writes over a single connection — fine for dev tools and small apps, not high-concurrency production. The issue tracker is open and good first issues are labeled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub (MIT): &lt;a href="https://github.com/tinbase/tinbase" rel="noopener noreferrer"&gt;https://github.com/tinbase/tinbase&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live on Product Hunt today: &lt;a href="https://www.producthunt.com/products/tinbase" rel="noopener noreferrer"&gt;https://www.producthunt.com/products/tinbase&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://tinbase.dev" rel="noopener noreferrer"&gt;https://tinbase.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Built by the team behind &lt;a href="https://rapidnative.com" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions about the protocol reimplementation, the engine adapters, or the fetch-layer trick are welcome in the comments — the team is answering everything today.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
