The founder messaged us on a Tuesday. Their Lovable app had real users, around 1,400, with two paying customers, one homepage logo, and an investor demo scheduled for the following Thursday. The app worked. It also could not survive the next feature on the roadmap, the Supabase Row-Level Security policies were a maze, and every change broke something three routes away.
The brief was simple to say and hard to do: rewrite this in Next.js without a maintenance window, without logging anyone out, and without missing the Thursday demo. Not next Thursday. Every Thursday, until we were done.
This is the playbook we use for that. It is not new, Stripe, Notion, Shopify, and Figma have all written about pieces of it, but it is rarely applied to the case where the legacy stack is six weeks old and was generated by an AI. The Big Bang Rewrite trap is exactly as expensive there as it is at petabyte scale.
Why the Big Bang Rewrite breaks live products
The reflex on a Lovable app that has hit its ceiling is to stop the world. Branch, rewrite for three weeks, schedule a Saturday-night cutover, switch DNS, drink coffee, hope. We have watched that plan fail enough times to refuse it on principle.
It fails for three reasons that compound. The rewrite is never feature-complete on the date you set: a half-finished cron job, a forgotten OAuth path, a Stripe webhook that only fires for one customer. The cutover can invalidate every session if auth is not bridged, and a meaningful fraction of users never log back in. And the rollback plan is "redeploy the old thing and pray", going back loses any writes the new app accepted in the meantime.
Pre-seed and seed companies live on weekly demos. A botched cutover is the difference between closing a round and not.
The strangler fig: a pattern, not a tool
Martin Fowler named this in 2004 after a vine he saw in Queensland. The fig germinates in the nook of a host tree, draws nutrients up the trunk, eventually replaces the host entirely. The metaphor lands because it is gradual and continuous, there is no day the fig "becomes" the tree.
Applied here: the Lovable app is the host. The new Next.js app germinates in a nook (one route, one form, one job), starts taking traffic, eventually owns everything. The founder never knows when "the rewrite" happened, because there is no day where it happened. There is only the gradual shift.
Five things have to be in place for this to work, in this order:
- A reverse proxy in front of the live app.
- A live database migration that runs in the background, expand, migrate, contract.
- An auth bridge that keeps existing sessions valid.
- Feature flags at the proxy and inside the new app.
- A defined rollback for every step that takes under five minutes.
Take any one out and the whole thing collapses into a Big Bang Rewrite with extra steps.
Reverse proxy: traffic control without downtime
The reverse proxy is the trunk of the fig. It is the only piece of new infrastructure that has to exist before you write a line of replacement code, and it is where every routing decision lives from then on.
The mechanic is boring on purpose. Point the founder's custom domain: app.thefounder.com, at a proxy you control. Vercel rewrites, Cloudflare Workers, and an Nginx VPS are all valid; we default to Cloudflare Workers when we want route changes controlled by code, not DNS. Forward 100% of requests to the existing Lovable origin. Verify in production that cookies pass through, p95 latency is unchanged, and websockets still connect. Nothing has changed for users, and that is the point.
Then you build the first new route in Next.js, deploy it, and add one rewrite: /billing/* goes to the new app, everything else still goes to Lovable. Repeat for /dashboard, then /settings, then /api/*, until the proxy never forwards to Lovable. Decommission. Vercel's incremental migration docs describe this same shape, for four reasons: smaller scope per step, easier rollback, earlier validation of the new stack, and no maintenance windows.
Lovable-specific gotchas worth naming
Lovable's current custom-domain setup has two paths. Normal manual setup uses the A and TXT records Lovable shows in the dashboard; Cloudflare proxying should stay off there, and AAAA records should be removed because they can route traffic around the expected destination. If you want Cloudflare, CloudFront, Fastly, or a custom reverse proxy in front, use Lovable's advanced "Domain uses Cloudflare or a similar proxy" option and point the provided CNAME through your proxy.
Do not treat Lovable's domain status as your source of truth after you move traffic control to the proxy; your proxy health checks are the source of truth. Cloudflare Error 1000 can mean a prohibited DNS target, a proxy loop, or custom-hostname/SNI trouble, so debug the routing path instead of assuming one TLS cause. Lovable apps are React + Vite SPAs, so the proxy is where you can start serving SSR and route-level metadata for migrated pages before the rewrite is finished.
Database live-cutover: expand, migrate, contract
This is the part where most rewrites quietly die. The Lovable app is talking to a Supabase project. The new Next.js app needs to talk to the same data, eventually owned by a self-hosted or paid-tier Supabase you control, without dropping writes, without forcing a maintenance window, and without the founder noticing.
First check who owns the backend. Lovable Cloud is managed Supabase, but the project may not appear in the founder's Supabase dashboard or accept their Supabase credentials. If Lovable Cloud holds the only copy, step one is exporting the data and moving the live app onto a founder-owned Supabase project before any live rewrite plan is credible.
The canonical sequence is expand → migrate → contract, articulated by Pete Hodgson. Stripe's online migration pattern follows the same operational spine: dual-write, backfill, switch reads, then remove the old path. Each phase is deliberately small.
Expand. Add the new structures, columns, tables, indexes, the new database itself, without removing anything. The old app keeps running and is unaware the new shape exists.
Migrate. Three sub-steps, in order:
- Backfill historical rows in batches of 500-5,000, committing between batches and sleeping briefly to keep p95 latency stable on the source.
- Dual-write every new mutation to both the old and the new representation. Stripe's phrasing for this step is "dual writing to the existing and new tables to keep them in sync."
- Read-shadow: read from old, also read from new, compare. GitHub's Scientist library is the canonical tool. Stripe's engineers ran exactly this pattern, and only switched read paths once results matched perfectly.
Contract. Stop writing to the old representation. Drop it.
For Supabase-to-Supabase or Supabase-to-Postgres moves, logical replication is usually the right data-streaming primitive once you control the source and target. CREATE PUBLICATION on the source, CREATE SUBSCRIPTION on the target, wait until every table reports srsubstate = 'r' in pg_subscription_rel, then cut over. Logical replication streams row changes; it does not migrate the whole Supabase project.
The real-world numbers
Three case studies are worth holding in your head as you do this, because they reset what "live cutover" actually means at scale.
1. Notion's Great Re-shard. In 2024 Notion expanded its Postgres cluster from 32 to 96 instances. Some shards had been running at 90%+ CPU at peak; after the migration, around 20%. The lever was PgBouncer, they sharded PgBouncer itself into 4 clusters of 24 shards each, paused at the proxy, swapped backends, resumed. Worst-case user-visible impact: about one second of a "saving" spinner while in-flight queries finished.
2. Stripe's Zero-Downtime Data Movement Platform. Stripe's document database migration writeup describes petabyte-scale shard moves with 99.999% uptime and traffic switches under two seconds. InfoQ's QCon coverage adds the sharper number: traffic cutovers that "typically complete in milliseconds." Six steps: chunk migration registration, bulk import, async replication, correctness checks, traffic switching, deregistration.
**3. Shopify's ghostferry.** Shopify Engineering's Ghostferry writeup supports the mechanism: batch copy, binlog tailing, then an atomic cutover with a routing-table flip and virtually zero consumer-facing downtime. The oft-cited 1.1 TB shop and roughly 13 seconds of read-only time come from a Datadog-hosted Shopify talk transcript, not the engineering article.
Imagine you're the founder of a 1,400-user Lovable app watching those numbers. Notion swapped 96 shards behind a live multiplayer editor with one second of saving spinner. Shopify's public material describes terabyte-scale shop moves with a seconds-level read-only cutover. The point is not that your database is tiny; it is that the cutover discipline scales down.
Dual-write foot-guns to plan for
The pattern is not "easy." It is known, which is different.
- Order matters. If you write to the old DB first, then the new, and the new fails, you are inconsistent. If both writes can live inside one database transaction, make them atomic. If they cross databases or services, use the transactional outbox pattern: write to the primary DB and an
outboxrow in the same transaction; a worker reads the outbox and replays to the new DB. - The outbox grows unbounded. Use partitioning and
TRUNCATE/DROP PARTITION, notDELETE, deletes create heavy locking and transaction-log churn at scale. - Multiple dispatchers fight for the table. Run one dispatcher with horizontal partitioning, not many.
- Sequences. Logical replication does not synchronize the
nextvalvalues for sequences in the source database. Bump them on the target before traffic flips, or the new DB will start handing out IDs that already exist. The number of post-mortems whose root cause is "the new DB started reusing IDs" is embarrassing. - RLS policies. RLS policies, roles, grants, functions, auth config, storage buckets, Edge Functions, and secrets are not carried by logical replication. Move the schema and policies deliberately, migrate roles and grants with the right dump or SQL, then run policy tests on the target before any read switch. This single step has saved more migrations than any other on this list.
Auth stays live, or users get logged out
Auth is the silent killer. Get the database migration right and miss the auth migration, and you can log out every user the day you flip the proxy. Founders notice that one within minutes.
Three plays, in our order of preference:
1. Stay on the same Supabase project and preserve the session model. First identify how the old app stores sessions: Supabase JS local storage in a Vite SPA, @supabase/ssr cookies, or a custom helper. If you keep the same project URL, anon key, and storage model, users are least likely to notice. If the new Next.js route needs SSR auth, make that handoff explicit and test refresh-token rotation on the custom domain before routing real users there.
2. Migrate auth deliberately when you migrate Supabase projects. Move auth.users, identities, sessions, and hashed passwords where supported, and preserve compatible JWT signing material where Supabase allows it. Existing access tokens only continue if the new verifier accepts the same signing material. If the project has moved to asymmetric signing keys, this is not a casual export-import task.
3. Put an identity service in front of both apps. When the founder's auth is genuinely messy (Lovable's email/password plus a half-finished Google OAuth plus a magic-link side door), put Auth0, Clerk, Supabase Auth, or Better Auth in front. Both apps trust the bridge. Existing Supabase users can often migrate with password hashes intact, but you still test the actual provider mix before promising no reset.
The boring detail that decides whether any of this works is storage shape. If the old route uses cookies, match Domain, Path, SameSite, Secure, HttpOnly, and the cookie name exactly. If the old route uses browser local storage, the risk is different: the new SSR route cannot see that session until you bridge it. Inspect the actual browser storage, refresh flow, and cookie jar before designing the bridge. Do not design it from memory.
Feature flags: ship behind the flag, not the deploy
Feature flags are how the rewrite ships without the founder noticing it shipped. The new code path is deployed to production, behind a flag, off for everyone. Then 1% of users. Then 10%. Then 50%. Then everyone. If a metric goes bad at any point, you flip the flag, not the deploy.
Tooling is a budget question. LaunchDarkly is the incumbent and supports guarded rollouts that auto-revert on metric regression. GrowthBook self-hosts on a seed-stage budget. PostHog's flags are a low-friction option if you already use PostHog. For a single Lovable → Next.js rewrite, a feature_flags Postgres table plus a five-line evaluator is enough, what matters is that the flip is seconds, not deploys.
Two rules we hold firm. Percentage rollouts must be sticky per user, not per request, otherwise the user lands on the new dashboard, refreshes, lands on the old one, and the founder gets a Slack message before lunch. LaunchDarkly's hash uses the context key for exactly this reason; replicate the behavior in any homegrown evaluator.
And data-layer flags are never percentage flags. "Dual-write enabled," "read from new DB," "drop old DB", these are global on/off. They should still be flags, because rollback during cutover happens in seconds, not deploys.
Where flags live, in our builds:
- At the proxy for "send 5% of
/dashboardtraffic to the new app." Edge-evaluated. Fastest blast radius control. - In the new app, per-feature, for "show new billing UI to 10% of users." Migrate route-by-route, then feature-by-feature inside a route.
- At the data layer for the global on/off described above.
For anything that touches billing, we default to a guarded rollout that monitors error rate and p95 latency, and pauses or reverts itself if the metric regresses. That is the production-grade default. The founder is not going to find the regression first.
Three layers of rollback, all under five minutes
Operating principle: every migration step has a defined rollback that takes under five minutes. If a step's rollback is "redeploy and pray," that step does not ship.
In practice, we use three layers, cheapest first.
1. Flip the feature flag. Default rollback. Seconds. No deploy, no DNS, no DB. Most incidents end here.
2. Flip the proxy. Change the rewrite rule from /billing/* → new back to /billing/* → old. If the route lives in Worker code, Vercel Edge Config, or a proxy route table, this is a seconds-level rollback. If you are changing DNS records, it is not; TTL and resolver caches decide the timeline.
3. Restore from backup. Last resort. Only relevant for data-shape changes. Daily PITR plus the dual-write window means at worst you replay yesterday's writes from the outbox.
If you cannot articulate which of those three layers gets pulled when a step goes wrong, the step is not ready to ship.
Keep the founder demoable every single day
The hidden requirement of this whole pattern is that the founder stays demoable. They are pitching investors, onboarding customers, doing webinars. If "the rewrite" means "the app is broken until we are done," the rewrite gets killed before it finishes.
Four cheap things make it work. A daily two-paragraph update covering what route shipped behind the proxy, what percentage of traffic is on the new code, and any incidents. One demoable URL the founder can share at any moment: the custom domain, always working, guaranteed by the proxy. A single-page status board: a Notion page is fine, listing every route, its migration state (legacy / dual-write / shadow / migrated), and the percentage of traffic on the new code. And a bi-weekly "what changed for the user" note, usually "nothing visible, here is what got faster."
The rewrite is most successful when the founder cannot point to a single day it happened.
When you should not strangle
There is a case where the strangler fig is the wrong pattern. When the Lovable app has fewer than ~50 users, no paying customers, and no demo schedule, the cheapest rewrite is a weekend with a maintenance-window banner. The fig has overhead, proxy, outbox, dual-write, flags. That overhead is worth it once any one of three things is true: there are paying customers, there is a calendar of investor demos, or there is data that cannot be replayed.
The trap is assuming you are below that line longer than you are.
The thesis, in one paragraph
A Lovable app in production cannot be replaced on a Saturday night. It can be replaced gradually, route by route, while users keep using it and the founder keeps demoing it, reverse proxy in front, database expanded and contracted in the background, auth bridged so existing sessions stay valid, every change hidden behind a flag that flips in seconds, every step shipped with a rollback that takes under five minutes. The rewrite that nobody noticed is the rewrite that made it to production.
If you are sitting on a Lovable app with paying users and a roadmap the current codebase will not survive, book a call and we will walk you through the plan. We do this work as part of our engineering practice, small senior teams, week one to a live migration plan and first controlled route cutover, no maintenance windows.