<?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>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>
    <item>
      <title>React Native Performance Monitoring: Tools and Techniques for 2026</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Tue, 28 Jul 2026 13:42:21 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/react-native-performance-monitoring-tools-and-techniques-for-2026-dh4</link>
      <guid>https://dev.to/rapidnative-ai/react-native-performance-monitoring-tools-and-techniques-for-2026-dh4</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flipper is gone&lt;/strong&gt; (removed as default in RN 0.76). React Native DevTools is the sanctioned replacement.&lt;/li&gt;
&lt;li&gt;Track &lt;strong&gt;eight metrics&lt;/strong&gt; — the two most under-monitored are &lt;strong&gt;slow frames %&lt;/strong&gt; and &lt;strong&gt;frozen frames %&lt;/strong&gt;, the closest proxy for "feels laggy."&lt;/li&gt;
&lt;li&gt;The pragmatic 2026 production default: &lt;strong&gt;Sentry&lt;/strong&gt; for crashes + performance, optionally layered with Firebase Crashlytics (free redundancy) or Datadog RUM (if you're already on Datadog).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;New Architecture&lt;/strong&gt; shifts the culprits: TurboModule calls blocking the JS thread, and Fabric commit contention on the UI thread.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The React Native performance monitoring landscape looks nothing like it did two years ago. Flipper is gone as the default debugger. The New Architecture — Fabric, TurboModules, and the bridgeless runtime — is on by default in every fresh app. Hermes is the assumed engine.&lt;/p&gt;

&lt;p&gt;If you're shipping in 2026, you need a stack that reflects those changes: dev-time profilers that speak Perfetto and Hermes, production observability that captures slow frames per screen, and a workflow for turning that data into fixes before your App Store rating quietly drops from 4.6 to 4.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "performance" actually means in React Native
&lt;/h2&gt;

&lt;p&gt;React Native has a two-thread execution model, and every performance conversation lives inside it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The JS thread&lt;/strong&gt; — business logic, Redux reducers, &lt;code&gt;Animated&lt;/code&gt; orchestration, most third-party code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The UI (main) thread&lt;/strong&gt; — layout, drawing, gesture recognition, native module execution, &lt;code&gt;useNativeDriver&lt;/code&gt; animations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A janky animation is almost always a UI thread problem. A slow list scroll under load is usually a JS thread problem. A screen that takes three seconds to appear after a tap is likely JS thread — &lt;em&gt;unless&lt;/em&gt; it's blocking on a network call, in which case it's a waterfall issue. A frozen screen after login is often a JSI serialization stall.&lt;/p&gt;

&lt;p&gt;Monitoring means tracking both threads, tying the data to specific user journeys, and being able to answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is it slow?&lt;/strong&gt; — objective metrics on frame rate, TTI, startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where is it slow?&lt;/strong&gt; — which screen, component, native module.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why is it slow?&lt;/strong&gt; — flame graphs, network waterfalls, memory pressure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your setup only answers question one, you don't have monitoring — you have a smoke alarm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight metrics that matter in 2026
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;Healthy target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JS FPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frame rate on the JS thread&lt;/td&gt;
&lt;td&gt;60 fps sustained (120 on ProMotion)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI FPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frame rate on the native main thread&lt;/td&gt;
&lt;td&gt;60 fps sustained (120 on ProMotion)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cold start time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Icon tap to first interactive frame&lt;/td&gt;
&lt;td&gt;&amp;lt; 2.0s on mid-tier Android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Warm start time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Time to interactive when backgrounded&lt;/td&gt;
&lt;td&gt;&amp;lt; 400ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TTI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Screen mount → first tap-responsive frame&lt;/td&gt;
&lt;td&gt;&amp;lt; 1s per screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Slow frames %&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frames rendered in &amp;gt; 16.67ms&lt;/td&gt;
&lt;td&gt;&amp;lt; 5% per session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frozen frames %&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frames rendered in &amp;gt; 700ms&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.1% per session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JS bundle size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Compressed bundle on first launch&lt;/td&gt;
&lt;td&gt;&amp;lt; 2MB gzip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two most under-monitored metrics are &lt;strong&gt;slow frames %&lt;/strong&gt; and &lt;strong&gt;frozen frames %&lt;/strong&gt; — the closest proxy for the qualitative "laggy." Sentry, Firebase, and Embrace expose them natively; if your stack doesn't, that's the first upgrade to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 tooling stack
&lt;/h2&gt;

&lt;p&gt;Monitoring splits into two phases with different tools: &lt;strong&gt;dev-time profiling&lt;/strong&gt; (finding the problem locally) and &lt;strong&gt;production observability&lt;/strong&gt; (knowing it exists at all).&lt;/p&gt;

&lt;h3&gt;
  
  
  Development-time tools
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React Native DevTools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JS profiling, network, component inspector&lt;/td&gt;
&lt;td&gt;Default since RN 0.76. Replaces Flipper.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hermes Sampling Profiler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deep JS CPU flame graphs&lt;/td&gt;
&lt;td&gt;Export as &lt;code&gt;.cpuprofile&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Perfetto / Systrace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;System-wide traces across all threads&lt;/td&gt;
&lt;td&gt;Android. Best for bridge/native contention.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Xcode Instruments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;iOS CPU, memory, energy, Core Animation&lt;/td&gt;
&lt;td&gt;Time Profiler + Animation Hitches.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Android Studio Profiler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Java/Kotlin allocations, native memory&lt;/td&gt;
&lt;td&gt;Pair with Perfetto.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React DevTools Profiler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Render counts, wasted renders&lt;/td&gt;
&lt;td&gt;Ships with RN DevTools.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a tutorial tells you to install Flipper, it's out of date. Flipper was removed as the default debugger in RN 0.76 and the community plugins are largely unmaintained. React Native DevTools is the replacement, and it's better for JS profiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production observability
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Strengths&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sentry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Crash reporting, integrated perf traces, native + JS stack merging&lt;/td&gt;
&lt;td&gt;Sampling costs add up at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Firebase Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free, Crashlytics integration, custom traces&lt;/td&gt;
&lt;td&gt;Sparse UI, no session replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Datadog RUM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Backend APM correlation, cohort analysis&lt;/td&gt;
&lt;td&gt;Expensive at high MAU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Embrace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Session-first, full journey replay&lt;/td&gt;
&lt;td&gt;Smaller ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instabug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bug reporting + perf in one SDK&lt;/td&gt;
&lt;td&gt;Duplicates Sentry if you have it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New Relic Mobile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enterprise-grade infra correlation&lt;/td&gt;
&lt;td&gt;Heavier SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pragmatic default for most teams: &lt;strong&gt;Sentry for crashes + performance + source-mapped traces&lt;/strong&gt;, optionally with &lt;strong&gt;Firebase Crashlytics&lt;/strong&gt; as a free redundant crash pipeline, or &lt;strong&gt;Datadog RUM&lt;/strong&gt; if you're already on Datadog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum-viable production setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Install Sentry with performance monitoring.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @sentry/wizard@latest &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; reactNative
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;tracesSampleRate: 0.2&lt;/code&gt; and enable &lt;code&gt;enableNativeFramesTracking: true&lt;/code&gt; and &lt;code&gt;enableAutoPerformanceTracing: true&lt;/code&gt;. Native frames tracking is what gives you slow/frozen frame percentages per screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Instrument navigation.&lt;/strong&gt; Wrap your app with Sentry's navigation integration so every screen transition becomes a labelled transaction. Without this, traces are anonymous and useless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Upload source maps on every release.&lt;/strong&gt; Untranslated stack traces are worthless. Bake source map upload into CI so every TestFlight or Play build has readable traces waiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Add custom spans around slow operations.&lt;/strong&gt; Any function over 100ms — cache hydration, a DB query, a large image decode — gets a manual span. This turns "screen X is slow sometimes" into "screen X is slow because recipe hydration takes 800ms on cold launch."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Set alerts for regressions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crash-free session rate below 99.5%&lt;/li&gt;
&lt;li&gt;P75 cold start rising &amp;gt; 20% between releases&lt;/li&gt;
&lt;li&gt;Slow frame % above 8% on any top-10 screen&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A real diagnostic walkthrough: "the feed screen is laggy"
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Confirm in production data
&lt;/h3&gt;

&lt;p&gt;Filter to the screen, look at P75 and P95 of slow frames per session. If P75 is fine and P95 is bad, it's a device-tier issue — likely mid-tier Android under a specific data condition. Note the device and OS distribution of the worst sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Reproduce on the right hardware
&lt;/h3&gt;

&lt;p&gt;Never diagnose Android performance on an iPhone 15 Pro. Grab a Pixel 6a or a mid-tier Samsung and reproduce there. Enable the perf overlay for live JS FPS and UI FPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Capture a trace
&lt;/h3&gt;

&lt;p&gt;For JS-thread issues: React Native DevTools → Performance tab → record the interaction. Look for long JS tasks (&amp;gt; 50ms bars), high re-render counts, serialization work.&lt;/p&gt;

&lt;p&gt;For UI-thread issues on Android: capture a Perfetto trace. Look for main-thread frames overflowing 16.67ms, and whether the overflow is in &lt;code&gt;Choreographer#doFrame&lt;/code&gt; or in native module calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Isolate the offender
&lt;/h3&gt;

&lt;p&gt;Common culprits, ranked by frequency in 2026 codebases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;FlatList&lt;/code&gt; with heavy &lt;code&gt;renderItem&lt;/code&gt; work&lt;/strong&gt; — no memoization, inline arrows. Fix: memoize the row, stabilize props, add &lt;code&gt;getItemLayout&lt;/code&gt; if heights are known.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A large state slice re-rendering the whole tree&lt;/strong&gt; — a selector returning new object identity every time. Fix: narrow the selector, add shallow comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An image decode blocking the UI thread&lt;/strong&gt; — full-res images in small views. Fix: server-side resize, &lt;code&gt;expo-image&lt;/code&gt; with &lt;code&gt;contentFit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A synchronous &lt;code&gt;AsyncStorage&lt;/code&gt; migration on cold start&lt;/strong&gt; — reading 5MB before render. Fix: defer via &lt;code&gt;InteractionManager.runAfterInteractions&lt;/code&gt;, or MMKV with lazy hydration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A third-party analytics SDK spawning threads on startup&lt;/strong&gt; — 200–400ms blocks. Fix: initialize lazily after first frame.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 5: Verify in a canary
&lt;/h3&gt;

&lt;p&gt;Ship behind a flag or to a beta cohort. Compare P75 slow-frame % on the screen before and after. If the number doesn't move, the fix isn't the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Architecture changes what you monitor
&lt;/h2&gt;

&lt;p&gt;Under the old bridge, the biggest sinkholes were JSON serialization between JS and native, visible in traces as huge queues. The New Architecture's synchronous, typed JSI calls mostly remove that class of problem — but introduce two new ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous TurboModule calls that block the JS thread.&lt;/strong&gt; A native module doing 50ms of I/O now blocks JS directly, where it used to buffer through the async bridge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fabric commit contention on the UI thread.&lt;/strong&gt; Shadow-tree commits are more efficient on average but spike sharply when a large tree diff lands — watch navigation transitions where the whole screen tree re-mounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern Sentry SDKs instrument both Fabric commits and TurboModule calls when auto-performance tracing is on. On an older SDK, upgrade — you'll see spans you didn't have before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundle size and startup — the fixes that compound
&lt;/h2&gt;

&lt;p&gt;Startup time is the metric most correlated with day-1 retention, and the one most engineers stop optimizing after the first release.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enable Hermes.&lt;/strong&gt; If you're not on it, you're leaving 30–50% of cold-start performance on the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precompile Hermes bytecode.&lt;/strong&gt; Ship &lt;code&gt;.hbc&lt;/code&gt;, not &lt;code&gt;.js&lt;/code&gt;. RN does this for release builds — verify it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship inline requires on Android.&lt;/strong&gt; &lt;code&gt;inlineRequires: true&lt;/code&gt; in Metro defers module evaluation until first use, cutting 200–500ms on large apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split third-party SDKs.&lt;/strong&gt; Anything not needed in the first second — analytics, remote config, feature flags — initializes after &lt;code&gt;InteractionManager.runAfterInteractions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure with production RUM, not local.&lt;/strong&gt; Cold start on your dev machine is meaningless. Trust P75 from real devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Track JS bundle size release-over-release. A 100KB regression per release compounds fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best free monitoring tool in 2026?&lt;/strong&gt; Firebase Performance + Crashlytics. Automatic startup and HTTP traces, custom traces, native crash reporting, all free up to a generous tier. Limitations: sparse UI, no session replay — most teams add Sentry for deeper diagnostics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Flipper still usable?&lt;/strong&gt; No — removed as default in RN 0.76, plugins unmaintained. Use React Native DevTools, plus Perfetto (Android) and Xcode Instruments (iOS) for native profiling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I measure startup time in production?&lt;/strong&gt; Sentry's &lt;code&gt;enableAppStartTracking: true&lt;/code&gt;, or Firebase's automatic &lt;code&gt;_app_start&lt;/code&gt; trace. Track &lt;strong&gt;P75 cold start&lt;/strong&gt; — the average hides regressions behind a few fast flagship sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What FPS should an app maintain?&lt;/strong&gt; 60 fps sustained on both threads (120 on ProMotion). Track slow frames (&amp;gt; 16.67ms) as a percentage and aim under 5% per session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need separate monitoring for the New Architecture?&lt;/strong&gt; Same metrics, different culprits — TurboModule calls and Fabric commit contention. Any modern SDK instruments both automatically; just don't run a pre-GA-era SDK.&lt;/p&gt;

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

&lt;p&gt;The dev-time story is React Native DevTools plus platform-native profilers (Perfetto, Instruments). The production story is Sentry as the default, with Firebase Crashlytics as free redundancy or Datadog RUM for existing Datadog teams.&lt;/p&gt;

&lt;p&gt;The bigger shift is philosophical: performance is a metric you monitor continuously, not a project you do once. The teams shipping the smoothest apps in 2026 have slow-frame alerts wired to Slack, source maps on every release, and a P75 startup dashboard someone actually looks at each Monday.&lt;/p&gt;

&lt;p&gt;I write more about how we handle this in &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=rn-performance-monitoring" rel="noopener noreferrer"&gt;AI-generated React Native apps at RapidNative&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's your current monitoring stack — and what's the worst "laggy screen" bug you've tracked down? Mine was a 5MB AsyncStorage migration running on every cold start.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>performance</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Supabase local dev needs 2.3 GB of Docker — and how we got it to 58 MB</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Fri, 24 Jul 2026 10:38:36 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/why-supabase-local-dev-needs-23-gb-of-docker-and-how-we-got-it-to-58-mb-5303</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/why-supabase-local-dev-needs-23-gb-of-docker-and-how-we-got-it-to-58-mb-5303</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;supabase start&lt;/code&gt; runs the &lt;em&gt;production&lt;/em&gt; architecture on your laptop: 12 containers, &lt;strong&gt;2,291 MB&lt;/strong&gt; on disk, &lt;strong&gt;1,626 MB&lt;/strong&gt; RAM under load&lt;/li&gt;
&lt;li&gt;Those services are mostly protocol translators — and protocols can be reimplemented&lt;/li&gt;
&lt;li&gt;We rebuilt the surface as &lt;strong&gt;one process&lt;/strong&gt;: pure &lt;code&gt;(Request) ⇒ Response&lt;/code&gt; handlers on a swappable Postgres engine&lt;/li&gt;
&lt;li&gt;Result: a &lt;strong&gt;58 MB&lt;/strong&gt; executable, ~2 s cold start, and the official supabase-js SDK passing &lt;strong&gt;168/168&lt;/strong&gt; integration tests unchanged&lt;/li&gt;
&lt;li&gt;Side effect: the same backend runs inside a &lt;strong&gt;browser tab&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1: Where the 2.3 GB goes
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;supabase start&lt;/code&gt; on a fresh machine and watch &lt;code&gt;docker ps&lt;/code&gt; fill up. What you're looking at is Supabase's cloud architecture, faithfully reproduced locally — which is precisely the problem. Each box in their production diagram becomes a container on your laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────┐
│  supabase start                                │
│                                                │
│  kong          → API gateway / routing         │
│  postgrest     → REST API over Postgres        │
│  gotrue (auth) → JWTs, OAuth, magic links      │
│  realtime      → WebSockets (Elixir/Phoenix)   │
│  storage-api   → file storage over Postgres    │
│  imgproxy      → image transforms              │
│  edge-runtime  → Deno functions                │
│  studio        → dashboard (Next.js)           │
│  postgres-meta → introspection API for Studio  │
│  logflare      → log aggregation               │
│  vector        → log shipping                  │
│  db (postgres) → the actual database           │
└────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service ships as its own image with its own runtime — an Elixir VM here, a Deno runtime there, Node for Studio, Go for auth. None of it is waste &lt;em&gt;in production&lt;/em&gt;: these are independently scalable services doing real jobs across a fleet.&lt;/p&gt;

&lt;p&gt;But locally, every one of those runtimes is overhead for a single developer making requests from &lt;code&gt;localhost&lt;/code&gt;. The measured cost:&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;Install footprint&lt;/th&gt;
&lt;th&gt;Memory under load&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supabase local (12 containers)&lt;/td&gt;
&lt;td&gt;2,291 MB&lt;/td&gt;
&lt;td&gt;1,626 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tinbase (single binary)&lt;/td&gt;
&lt;td&gt;92 MB&lt;/td&gt;
&lt;td&gt;66 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tinbase (native, via npx)&lt;/td&gt;
&lt;td&gt;36 MB&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And that's before the Docker Desktop tax on macOS and Windows, or environments where Docker isn't available at all: CI sandboxes, cloud IDEs, school machines, a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: The key observation — it's protocols all the way down
&lt;/h2&gt;

&lt;p&gt;Here's what makes the whole thing tractable. Your app never talks to those containers directly. It talks to &lt;strong&gt;documented HTTP and WebSocket protocols&lt;/strong&gt; through supabase-js:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/rest/v1&lt;/code&gt; speaks the &lt;strong&gt;PostgREST grammar&lt;/strong&gt; — &lt;code&gt;?select=*,author:users(name)&amp;amp;done=eq.false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/auth/v1&lt;/code&gt; speaks &lt;strong&gt;GoTrue's API&lt;/strong&gt; — signup, OTP, PKCE, refresh tokens&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/storage/v1&lt;/code&gt; speaks the &lt;strong&gt;Storage API&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Realtime speaks the &lt;strong&gt;Phoenix channel protocol&lt;/strong&gt; over WebSockets&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/functions/v1&lt;/code&gt; invokes edge functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A protocol doesn't care what implements it. PostgREST is ~"take this URL grammar, compile it to SQL, run it with the caller's JWT claims applied." GoTrue is "manage users in &lt;code&gt;auth.users&lt;/code&gt;, mint JWTs." These are translation layers over Postgres — and translation layers can be reimplemented in-process.&lt;/p&gt;

&lt;p&gt;What you &lt;em&gt;cannot&lt;/em&gt; fake is Postgres itself. RLS evaluation, &lt;code&gt;auth.uid()&lt;/code&gt; in policies, jsonb operators, triggers, &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; — behavior differences there produce the worst kind of bug: works locally, breaks in production. So that became the design rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Reimplement the translation layers. Never reimplement Postgres.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 3: The architecture — one fetch handler
&lt;/h2&gt;

&lt;p&gt;Every tinbase service is a pure function: &lt;code&gt;(Request) ⇒ Response&lt;/code&gt;. No sockets owned, no ports assumed, no filesystem requirements baked in. The whole backend composes into a single handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;supabase-js (unmodified)
        │
        ▼
one (Request) ⇒ Response handler
  ├─ /rest/v1      → PostgREST grammar → SQL
  ├─ /auth/v1      → GoTrue flows → auth schema
  ├─ /storage/v1   → Storage API
  ├─ Realtime      → Phoenix protocol
  ├─ /functions/v1 → in-process handlers
  └─ /_/           → Studio dashboard
        │
        ▼
DbEngine adapter
  ├─ native → embedded Postgres 17
  ├─ wasm   → PGlite (Postgres compiled to WASM)
  └─ pg-mem → pure JS, in-memory (subset)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The REST layer parses the PostgREST grammar and compiles it to parameterized SQL — embedded resources become joins, filters become WHERE clauses — then executes with the request's JWT claims set, so your RLS policies run exactly as Postgres intends:&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;-- every request runs with claims applied, e.g.:&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{"sub":"&amp;lt;user-uuid&amp;gt;","role":"authenticated"}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- so this policy Just Works, locally and in prod:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"own todos"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Realtime does the same per-subscriber: change events are filtered through RLS before delivery, so a user only receives events for rows they're allowed to see.&lt;/p&gt;

&lt;p&gt;Things production Supabase does with Postgres &lt;em&gt;extensions&lt;/em&gt; get reimplemented natively instead, because a single process can just... do them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database webhooks — no &lt;code&gt;pg_net&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cron.schedule()&lt;/code&gt; — no &lt;code&gt;pg_cron&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a pgmq subset — no &lt;code&gt;pgmq&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part 4: The consequences of "pure fetch handler"
&lt;/h2&gt;

&lt;p&gt;This constraint looked academic. It turned out to be the most productive decision in the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Node&lt;/strong&gt;, the handler binds to a port and becomes an ordinary HTTP + WebSocket server:&lt;br&gt;
&lt;/p&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;# ~2 seconds → http://127.0.0.1:54321, Studio at /_/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;As a binary&lt;/strong&gt;, it compiles to a single 58 MB executable — no Node, npm, or Docker on the target machine. That's the CI story: download one file, run it, test against real Postgres.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In a browser&lt;/strong&gt;, you hand the same handler to supabase-js as a custom &lt;code&gt;fetch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createTinbase&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;tinbase&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;tin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createTinbase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wasm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// PGlite under the hood&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://tinbase.local&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="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="nx"&gt;tin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// ← the entire backend is this function&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// no server anywhere; Postgres is running inside the tab&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;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello from a browser tab&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;Database included, running in-process. No server, no cloud. (Live demo: &lt;a href="https://www.tinbase.dev/browser?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=supabase-local-without-docker" rel="noopener noreferrer"&gt;tinbase.dev/browser&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;We didn't set out to build a curiosity — this was the original requirement. tinbase came out of building &lt;a href="https://rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=supabase-local-without-docker" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, where we needed a full dev stack — database, auth, storage, realtime — running in browsers and on phones with no VMs behind it. The single-process backend fell out of that constraint; replacing local Docker was the happy accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 5: How we know it's compatible
&lt;/h2&gt;

&lt;p&gt;Claiming "Supabase-compatible" is easy. Our test is blunt: run the &lt;strong&gt;official supabase-js SDK's integration suite&lt;/strong&gt; against tinbase, unmodified.&lt;/p&gt;

&lt;p&gt;Current score: &lt;strong&gt;168/168 passing&lt;/strong&gt;, across both the native Postgres engine and the WASM engine. That covers the REST grammar (filters, embedded resources, RPC), the auth flows (email/password, anonymous, OTP, magic links, recovery, OAuth with PKCE), storage, and realtime's &lt;code&gt;postgres_changes&lt;/code&gt; / broadcast / presence.&lt;/p&gt;

&lt;p&gt;Migration compatibility is the other half. tinbase reads &lt;code&gt;supabase/migrations/*.sql&lt;/code&gt; and &lt;code&gt;seed.sql&lt;/code&gt; exactly like the Supabase CLI, tracked in the same table — so the exit path is: push the same files to hosted Supabase and keep going. Compatibility that doesn't include leaving isn't compatibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't claim
&lt;/h2&gt;

&lt;p&gt;tinbase is alpha (v0.10.0), and honesty is cheaper than a disappointed issue tracker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not production-ready.&lt;/strong&gt; It's for local dev, prototypes, and embedded/browser use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not 100% of the surface.&lt;/strong&gt; It covers the common paths (the test suite defines "common"); edges remain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a Supabase replacement.&lt;/strong&gt; It's the local half of a workflow that ends at hosted Supabase. Real Postgres in production is still the right call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pg-mem is a subset.&lt;/strong&gt; The pure-JS engine trades fidelity for zero-WASM environments; use native or PGlite when you can.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try the numbers yourself
&lt;/h2&gt;



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

&lt;/div&gt;



&lt;p&gt;Repo (MIT): &lt;a href="https://github.com/tinbase/tinbase" rel="noopener noreferrer"&gt;github.com/tinbase/tinbase&lt;/a&gt; — the benchmark methodology is on the site if you want to reproduce the table above.&lt;/p&gt;

&lt;p&gt;If you've solved local-Supabase pain a different way — Nix, compose tricks, &lt;code&gt;pg_regress&lt;/code&gt; heroics — I genuinely want to hear it in the comments. And if you try the browser engine, tell me what you build with it.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>docker</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Introducing tinbase — local Supabase without Docker (one process, real Postgres)</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Thu, 23 Jul 2026 10:04:50 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/introducing-tinbase-local-supabase-without-docker-one-process-real-postgres-o0b</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/introducing-tinbase-local-supabase-without-docker-one-process-real-postgres-o0b</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Supabase local dev = a 12-container, &lt;strong&gt;2.3 GB&lt;/strong&gt; Docker stack. tinbase serves the same APIs from &lt;strong&gt;one 58 MB process&lt;/strong&gt;. No Docker.&lt;/li&gt;
&lt;li&gt;It's &lt;strong&gt;real Postgres 17&lt;/strong&gt; — RLS, &lt;code&gt;auth.uid()&lt;/code&gt;, jsonb, triggers, foreign keys all behave like hosted Supabase.&lt;/li&gt;
&lt;li&gt;The official &lt;strong&gt;supabase-js SDK works unchanged&lt;/strong&gt; (168/168 integration tests pass).&lt;/li&gt;
&lt;li&gt;The whole backend — database included — can run &lt;strong&gt;inside a browser tab&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Open source, MIT. Try it: &lt;code&gt;npx tinbase start&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;You know the drill. &lt;code&gt;supabase start&lt;/code&gt;, then Docker pulls Postgres, PostgREST, GoTrue, Storage, Realtime, Studio... twelve containers, 2.3 GB on disk, ~1.6 GB of RAM under load. All you wanted was to run your app locally.&lt;/p&gt;

&lt;p&gt;I love Supabase. I did not love paying a 2 GB tax to write a todo app on the train.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tinbase does instead
&lt;/h2&gt;

&lt;p&gt;One process. Same wire protocols:&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;Install footprint&lt;/th&gt;
&lt;th&gt;Memory under load&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supabase local (12 containers)&lt;/td&gt;
&lt;td&gt;2,291 MB&lt;/td&gt;
&lt;td&gt;1,626 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tinbase (single binary)&lt;/td&gt;
&lt;td&gt;92 MB&lt;/td&gt;
&lt;td&gt;66 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tinbase (native)&lt;/td&gt;
&lt;td&gt;36 MB&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



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

&lt;/div&gt;



&lt;p&gt;~2 seconds later you're serving requests. There's also a single 58 MB executable that needs no Node, npm, or Docker on the target machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your code doesn't change
&lt;/h2&gt;

&lt;p&gt;This is the whole point. tinbase implements the PostgREST query grammar, GoTrue auth flows, the Storage API, and the Realtime Phoenix protocol — verified by running the &lt;strong&gt;official SDK&lt;/strong&gt; against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="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="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="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;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;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&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="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="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;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;feed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres_changes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;table&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="nx"&gt;handleNewTodo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No fork of the SDK. No wrapper. No different query language. RLS runs with your JWT claims applied, so &lt;code&gt;auth.uid()&lt;/code&gt; policies work as-is — and Realtime does per-subscriber RLS filtering, so users only get change events for rows they can see.&lt;/p&gt;

&lt;p&gt;Auth covers email/password, anonymous, OTP, magic links, password recovery, and OAuth (Google/GitHub + generic) with PKCE. Edge Functions run in-process via &lt;code&gt;supabase.functions.invoke()&lt;/code&gt;. Webhooks, &lt;code&gt;cron.schedule()&lt;/code&gt;, and a pgmq subset are in there natively — no &lt;code&gt;pg_net&lt;/code&gt;/&lt;code&gt;pg_cron&lt;/code&gt;/&lt;code&gt;pgmq&lt;/code&gt; extensions to install. And a Supabase-Studio-style dashboard ships at &lt;code&gt;/_/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weird part: it runs in a browser tab
&lt;/h2&gt;

&lt;p&gt;Every service in tinbase is a pure &lt;code&gt;(Request) ⇒ Response&lt;/code&gt; fetch handler on top of a swappable DB engine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;supabase-js (unmodified)
        │
        ▼
one (Request) ⇒ Response handler
  ├─ /rest/v1      (PostgREST)
  ├─ /auth/v1      (GoTrue)
  ├─ /storage/v1   (Storage)
  ├─ Realtime      (WebSocket)
  ├─ /functions/v1 (Edge Fns)
  └─ /_/           (Studio)
        │
        ▼
DbEngine adapter
  ├─ native  → embedded Postgres 17
  ├─ wasm    → PGlite (Postgres in WASM)
  └─ pg-mem  → pure JS, in-memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Node, that handler is an HTTP + WebSocket server. In the browser, you hand it to supabase-js as a custom &lt;code&gt;fetch&lt;/code&gt; — and the entire backend, Postgres included, runs in-process in the tab. No server. No cloud. You can play with it at &lt;a href="https://www.tinbase.dev/browser?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=introducing-tinbase" rel="noopener noreferrer"&gt;tinbase.dev/browser&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not a dead end
&lt;/h2&gt;

&lt;p&gt;tinbase reads &lt;code&gt;supabase/migrations/*.sql&lt;/code&gt; and &lt;code&gt;seed.sql&lt;/code&gt; exactly like the Supabase CLI, tracked in the same table. Outgrow it? Push the same files to hosted Supabase and keep moving. You can also point it at a Postgres you already run with &lt;code&gt;--database-url&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we built it
&lt;/h2&gt;

&lt;p&gt;tinbase came out of building &lt;a href="https://rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=introducing-tinbase" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; and lifo, with a stubborn goal: run an entire dev stack — database, auth, storage, realtime — in the browser and on phones, with no VMs and no cloud behind it. Cutting the backend down to a single process was step one; making that same process run inside a tab was step two. Somewhere along the way it turned into a genuinely useful Docker-free replacement for local Supabase dev, so we open-sourced the whole thing (MIT).&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest caveats
&lt;/h2&gt;

&lt;p&gt;It's &lt;strong&gt;alpha (v0.10.0)&lt;/strong&gt;. Built for local dev, prototypes, and embedded/browser use — not production. 168/168 integration tests pass across both engines, but if you hit an API edge we haven't covered, open an issue.&lt;/p&gt;

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



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

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/tinbase/tinbase" rel="noopener noreferrer"&gt;github.com/tinbase/tinbase&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What would you build if your whole backend fit in a browser tab? Drop a comment — especially if you try the browser engine, I want to see what you make with it. 🚀&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Publish Your Lovable App to the App Store (Without Rebuilding It Yourself)</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:43:09 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/how-to-publish-your-lovable-app-to-the-app-store-without-rebuilding-it-yourself-2jia</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/how-to-publish-your-lovable-app-to-the-app-store-without-rebuilding-it-yourself-2jia</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Lovable builds a &lt;strong&gt;web app&lt;/strong&gt;. The App Store distributes &lt;strong&gt;native&lt;/strong&gt; apps. That's a category gap, not a skill gap.&lt;/li&gt;
&lt;li&gt;WebView wrappers feel wrong (scroll momentum, transitions, haptics, back gesture) and risk rejection under &lt;strong&gt;App Store Guideline 4.2&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The real fix is a React Native + Expo rebuild — plus certificates, provisioning profiles, signing keys, screenshots, and metadata.&lt;/li&gt;
&lt;li&gt;If your Lovable app uses Supabase, a native rebuild can point at the same project. No migration needed.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;You built something in Lovable over a weekend. It works. You sent the link to friends and they said "wait, you built this?"&lt;/p&gt;

&lt;p&gt;Then someone asked: &lt;em&gt;"Is it on the App Store?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's where most Lovable projects stall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you can't submit a Lovable app to the App Store directly
&lt;/h2&gt;

&lt;p&gt;Building an app and shipping an app are two different projects.&lt;/p&gt;

&lt;p&gt;Lovable is excellent at the first one. But the App Store distributes native apps, and Lovable produces a web app. That's not a skill gap — it's a category gap.&lt;/p&gt;

&lt;p&gt;To publish, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Apple Developer account ($99/year)&lt;/li&gt;
&lt;li&gt;A bundle identifier&lt;/li&gt;
&lt;li&gt;A distribution certificate and provisioning profile&lt;/li&gt;
&lt;li&gt;A signed &lt;code&gt;.ipa&lt;/code&gt; binary&lt;/li&gt;
&lt;li&gt;Screenshots at every required device size&lt;/li&gt;
&lt;li&gt;A privacy nutrition label and data-usage disclosure&lt;/li&gt;
&lt;li&gt;A human reviewer who might say no&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that comes out of a web app builder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why wrapping it in a WebView doesn't work
&lt;/h2&gt;

&lt;p&gt;The obvious shortcut is a wrapper: put the web app in a native shell and ship the shell. Two problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It feels wrong.&lt;/strong&gt; Scroll momentum is off. Transitions don't match the platform. Text inputs behave like a browser instead of a native keyboard. No haptics. The back gesture doesn't do what a thumb expects. Users can't name it, but they register it as "website in a box."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple notices too.&lt;/strong&gt; Guideline 4.2 exists specifically for repackaged websites that don't provide enough native value. Plenty of wrappers slip through, but you're rolling dice on review — and each rejection costs a full cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real conversion actually involves
&lt;/h2&gt;

&lt;p&gt;The honest answer is a native rebuild: React Native + Expo, real navigation, real gestures, real device APIs (camera, push, the rest).&lt;/p&gt;

&lt;p&gt;Technically that's the right call. Practically it's where projects die — you spent two days building the product and you're now looking at learning the entire Apple release toolchain, or paying someone who already has.&lt;/p&gt;

&lt;p&gt;That mismatch is the whole problem. The building was fun and fast. The shipping is neither.&lt;/p&gt;

&lt;p&gt;If you'd rather hand that part off, &lt;a href="https://www.rapidnative.com/lovable-to-app-store?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=lovable-to-app-store" rel="noopener noreferrer"&gt;RapidNative converts Lovable apps to React Native and handles the full submission&lt;/a&gt; — signing, screenshots, metadata, the lot. You get a preview build on your device first, the full Expo source code afterward (no lock-in), and rejections are handled at no extra cost until you're approved. Typical turnaround is 1–2 weeks.&lt;/p&gt;

&lt;p&gt;Worth noting for anyone doing this themselves too: &lt;strong&gt;if your Lovable app already talks to Supabase, you don't need to migrate anything.&lt;/strong&gt; Point the React Native app at the same project and your data layer is done.&lt;/p&gt;

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

&lt;p&gt;Building and shipping are separate problems. Being good at one tells you nothing about how hard the other will be.&lt;/p&gt;

&lt;p&gt;Lovable made building dramatically easier. Apple's requirements haven't moved — and Google added a 12-tester closed-testing gate on top.&lt;/p&gt;

&lt;p&gt;If your app is stuck one wall away from the store, that's not a verdict on your product. It's just a different category of work than the one you finished.&lt;/p&gt;




&lt;p&gt;Been through App Store submission yourself? Which screen broke you? Mine was the privacy nutrition label.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>nocode</category>
      <category>ios</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
