Moving a Lovable app to AWS isn't one job. It's three: the code, the infrastructure and the data.
The code is the easy part, and most guides stop there: connect GitHub, run a build, upload the files. The infrastructure takes longer than people expect, and the data is where people get stuck. When we moved a Vite app off Lovable Cloud, nearly all the lost time went on the database restore and on two frontend problems: deep links returning 404, and a blank page because environment variables weren't there at build time. This article covers all three parts in the order you'll hit them.
One decision comes first: do you keep building in Lovable after the move, or leave it behind? Either works with everything below. If you keep it, Lovable becomes your editor and staging environment, and production runs from your own repository and AWS account. Keep Building in Lovable, Ship to Your Own Cloud makes the case for keeping it.
First, which Lovable app do you have?
Lovable changed its stack in 2026. Open package.json in your repository:
- No
@tanstack/react-start: you have a React + Vite single-page app.npm run buildproduces static files indist/, and those can be served straight from S3 and CloudFront. Most of this article is about this kind of app. @tanstack/react-startis there: you have a TanStack Start app. Lovable creates new projects this way from 13 May 2026, and older projects can be upgraded in place (Lovable docs). TanStack Start renders pages on a server, so it needs somewhere to run Node, not just a bucket. Steps 1, 4 and 5 are the same for you. For the infrastructure and the pipeline, read "If you have a TanStack Start app" below instead of Steps 2 and 3.
The database steps are the same for both.
Before you start: take an inventory
Write these down before you touch anything. After the move you'll check every number against this list, and it's the only reliable way to know nothing was left behind. This list comes from Carol Monroe's export guide, which is the most thorough walkthrough of the Lovable Cloud side.
- Tables, and the row count of each
- Auth users, and which social sign-in providers you use (Google, GitHub and so on)
- Storage buckets, and the number of files in each
- Edge Function names
- Secret names (you can't export the values, only the names)
- Scheduled jobs (
pg_cron) - Custom domain and DNS records
- Any AI features. Lovable's built-in AI runs through its own gateway and bills your Lovable credits.
Step 1: Get the code into a repository you own
In your Lovable project, connect GitHub. Lovable installs its GitHub App on your account or organisation and creates a new repository. You can't point it at an existing one. From then on the sync runs both ways: edits in Lovable are pushed as commits, and commits pushed to the synced branch show up in Lovable (Lovable docs).
What you'll find in the repository:
- The app source: React and TypeScript, built with Vite (or TanStack Start).
supabase/migrations/: the SQL for every schema change Lovable has made.supabase/functions/: your Edge Functions.supabase/config.toml: function settings, includingverify_jwt..env: the Supabase URL, publishable key and project ID, all prefixedVITE_.
What you won't find: any database rows, storage files or secret values.
A few decisions to make now:
- Put the repository in an organisation, not a personal account, if real people use the app. You can transfer it later, and the sync survives as long as your Lovable workspace is also connected to the new owner. If it isn't, the project disconnects, and reconnecting creates another new repository.
- Leave
mainas the branch Lovable syncs to and deploy from a separateproductionbranch. Every Lovable prompt becomes a commit, so deployingmaindirectly ships half-finished work. Protectproductionin GitHub. Lovable never writes to it, so the protection doesn't get in its way. - Don't trust the committed
.envfor production. Lovable rewrites it, sometimes back to its own backend. Production values should come from your deploy pipeline. Also check thatsrc/integrations/supabase/client.tsreads the URL and key fromimport.meta.envrather than containing them as literal strings. If they're literals, the new values never take effect. - Commit a lockfile CI can use. If the repository only has
bun.lockand your pipeline uses npm, runnpm installand commitpackage-lock.json. Or install with bun in CI, which is what Lovable uses. For TanStack Start apps that's the safer choice (see below).
What ScaleBop does here: imports the repository from GitHub, recognises it as a Lovable app and works out whether it's a static site or a TanStack Start app that needs a server. It then runs a security scan aimed at AI-built code: Supabase tables without Row Level Security, a service role key in browser code, secrets in
VITE_variables, public storage buckets, unsigned Stripe webhooks and a committed.env. The scan is advisory. It tells you what it found and leaves the fixes to you.
Step 2: The AWS infrastructure
A Lovable Vite app is a set of static files, so the hosting is smaller than people expect. (A TanStack Start app needs more; see "If you have a TanStack Start app".) Each piece, and what it's for:
- S3 bucket. Holds the built files. Keep it private, with all public access blocked, and don't turn on S3's "static website hosting". CloudFront reads from the bucket directly.
- CloudFront distribution. Serves the files over HTTPS from locations near your users, and caches them. It reads the private bucket through Origin Access Control, which AWS recommends over the older Origin Access Identity (AWS docs).
- Gotcha: deep links. Your app's routes, like
/settingsor/projects/42, don't exist as files. Load one directly or refresh the page and S3 has nothing to return. Because the bucket is private, it answers 403, not 404. Configure CloudFront to answer both 403 and 404 with/index.htmland status 200, so React Router can take over. This is the one that caught us: the app worked perfectly from the home page, and every shared link was broken. - Gotcha: stale files after a deploy. Vite gives JS and CSS files hashed names, so they can be cached for a long time.
index.htmlkeeps its name. Either give it a short cache lifetime or invalidate it on every deploy. Otherwise users get yesterday'sindex.htmlpointing at files that no longer exist. The first 1,000 invalidation paths each month are free, and/*counts as one (AWS docs).
- Gotcha: deep links. Your app's routes, like
- ACM certificate. The free HTTPS certificate for your domain. For CloudFront it must be in
us-east-1, wherever everything else lives (AWS docs). - Route 53 or your DNS provider. Points your domain at CloudFront. Plan the cutover from the old domain now (Step 5).
- An IAM role for GitHub Actions. GitHub signs in to AWS with OpenID Connect and gets short-lived credentials, so no AWS access keys sit in your GitHub secrets.
- Infrastructure as code. Define all of this with AWS CDK or CloudFormation in your repository, not by clicking through the console. Then you can review changes in a pull request and rebuild it if you need to.
- As the app grows: CloudWatch alarms on errors, an AWS Budgets alert, and AWS WAF in front of CloudFront if you start getting abuse.
Browser ──► Your domain (Route 53) ──► CloudFront ──► S3 (private, OAC)
│
└──────► Supabase (API, auth, storage, Edge Functions)
The browser talks to Supabase directly, exactly as it did on Lovable. Only the frontend moves to AWS.
What ScaleBop does here: writes the AWS CDK and the GitHub Actions workflow into your repository as a pull request, then deploys into your own AWS account through an OIDC role it sets up for you. For a Vite app it creates:
- a private, encrypted S3 bucket behind CloudFront with Origin Access Control
- the 403 and 404 fallback to
index.html- HTTPS-only access with AWS's security headers
- a CloudWatch alarm on CloudFront 5xx errors, with a dashboard
- an AWS Budgets alert on the account
When you add a custom domain, it issues the certificate in
us-east-1and creates the DNS records in Route 53, or gives you a CNAME to add at your DNS provider. Custom domains must be subdomains, likeapp.example.com. ScaleBop doesn't add AWS WAF.
Step 3: The pipeline
A GitHub Actions workflow that runs on every push to production:
- Install dependencies and build, with the production values injected:
VITE_SUPABASE_URLandVITE_SUPABASE_PUBLISHABLE_KEYfor your production Supabase project. - Upload
dist/to S3, deleting files that are no longer in the build. - Invalidate the CloudFront cache.
- Load the live URL and check it serves the real app.
Vite writes VITE_ values into the JavaScript at build time. There's no server to read them at runtime. If a value is missing when the build runs, the app ships with undefined in its place, and you usually get a blank page and a console error about the Supabase URL. That was our other early failure. To change a value, change it in the pipeline and rebuild.
Everything with a VITE_ prefix ends up in files anyone can download. The Supabase URL and publishable (anon) key are meant to be public, because Row Level Security protects the data. A service role or secret key is not, and must never have a VITE_ prefix.
Database migrations and Edge Functions need their own steps. To keep them out of the deploy workflow, a separate workflow on the same branch can push them with the Supabase CLI:
# .github/workflows/supabase.yml
name: Supabase
on:
push:
branches: [production]
paths: ["supabase/**"]
jobs:
deploy:
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
steps:
- uses: actions/checkout@v4
- uses: supabase/setup-cli@v1
- run: supabase link --project-ref ${{ vars.SUPABASE_PROJECT_REF }}
- run: supabase db push
- run: supabase functions deploy
Read every migration before it reaches production. Lovable writes them from prompts, and a destructive one can't be undone by redeploying.
What ScaleBop does here: the generated workflow runs on pushes to
productionand does steps 1 to 4. It injects yourVITE_values from ScaleBop's configuration, and refuses to build a static site if a secret is about to be put into the browser. Ship in ScaleBop movesproductionup to your latest commit onmain, so you decide when Lovable's work goes live. It also checks newly added code for leaked keys before shipping, if you're on a paid plan. It can also runsupabase db pushbefore each deploy: turn on Run migrations on deploy on the Supabase card, and a failed push stops the deploy. It doesn't deploy Edge Functions, so use a workflow like the one above for those.
Step 4: The database (the hard part)
Keep Supabase, or move to AWS-native?
Keep Supabase, but in your own project. This is right for most apps. A Lovable backend isn't just Postgres. It's Supabase Auth, Storage, Row Level Security and Edge Functions, and your frontend calls all of them through the Supabase client. Moving to your own Supabase project changes a URL and a key. Pick a Supabase region close to your AWS region and your users.
Move to RDS or Aurora, Cognito, S3 and Lambda only for a concrete reason: a data residency requirement, a rule that everything lives in one AWS account, or scale Supabase can't meet. Be honest with yourself that this is a rewrite of the backend, not a migration. The rest of this section assumes your own Supabase project.
If you're on Lovable Cloud
Lovable Cloud runs on Supabase, but you can't connect to its database or use the Supabase CLI against it. The way out is Lovable's export.
- Export. In Lovable, open More → Database → Manage → Settings and choose Export project data. The export works for databases up to 15 GB, produces a file of at most 5 GB, and can run once every 24 hours. The database has to be running, not paused (Lovable docs).
- Download it straight away. The export is saved into your project's own storage. If you remove Lovable Cloud first, the export goes with it.
- Know what's in it. It includes the schema, all rows, RLS policies, functions and triggers, and the
authschema with password hashes, so users keep their passwords. It doesn't include storage files, Edge Function code, secret values or sign-in provider settings. - Create a new Supabase project and restore into it with
pg_restore. The export is a compressed custom-format archive, so you needpg_restorefrom Postgres 16 or later, built with zstd support. Homebrew'slibpqdoesn't have it, andpostgresql@18does. Use--no-owner --no-privileges. Connect through the Session pooler if the direct connection fails (it's IPv6 only), and percent-encode special characters in the password (!becomes%21). - Expect errors. Your new project already has its own
authandstorageschemas, so the restore reports hundreds of "already exists" errors. Most are harmless. This is where we lost the most time: separating the harmless ones from the real ones. Don't scroll past them. Check the counts against your inventory instead. - Check users twice. Compare
auth.userswithauth.identities. If identities are missing, people who signed in with Google or GitHub can't sign in. Restore them on their own withpg_restore --data-only -n auth -t identities.
Treat the export file like a password. It holds password hashes and every user's personal data. Delete it when you're finished.
The export is a snapshot. Anything written after it is missing from the new project. For the final cutover, pause writes, take a fresh export and restore again. Time a practice run first, so you know how long the pause will last.
If you're already on your own Supabase
Create a separate production project and keep the existing one as staging, the one Lovable keeps talking to. Apply supabase/migrations to the new project with supabase db push. Copy data across only if production needs the existing data. Supabase's backup and restore guide covers dumping roles, schema and data separately.
Either way, users have to sign in again. The new project has a different JWT secret, so existing sessions stop working (Supabase docs).
The rest of the backend
- Storage files. Download each bucket and upload it to the new project, keeping the folder structure. Watch for
storage.objectsrows from the restore that point at files not uploaded yet. They can block uploads to the same paths. Signed URLs belong to the old project, so generate new ones rather than search-and-replacing them. - Edge Functions. Deploy from
supabase/functionswithsupabase functions deploy. Keep theverify_jwtsettings fromconfig.toml. Webhook functions usually need it off. - Secrets. Re-enter each one with
supabase secrets set, with exactly the same name and case. Generate fresh keys at each provider rather than hunting for the old values. Don't copySUPABASE_URLor the Supabase keys: the new project provides its own. - Scheduled jobs. Enable
pg_cron, then update any job that calls a URL on the old project withcron.alter_job(). Otherwise jobs keep calling the old backend. - Auth providers. Set up Google and other sign-in providers again, and add your new domain to the allowed redirect URLs, both in Supabase and in each provider's console.
- Lovable AI. If your app uses Lovable's built-in AI, its calls go through Lovable's gateway using a
LOVABLE_API_KEYthat Lovable manages and you can't take with you (Lovable docs). Switch those calls to your own OpenAI, Anthropic or Google key, in an Edge Function so the key stays server-side. - API keys. Supabase is replacing the
anonandservice_rolekeys with new publishable and secret keys before the end of 2026 (Supabase docs). A new project is a good moment to switch.
What ScaleBop does here: finds the Supabase project your code points at and sorts your Supabase variables into the public ones the browser needs and the server keys that must stay secret. It flags a service role key with a
VITE_prefix. Connect your Supabase account and it can create a new production project or use an existing one. It then fetches the keys, stores server keys and the database connection string in AWS Secrets Manager in your account, and wires the public values into the build. The data move itself is still manual: the export, restore, storage, Edge Functions and secrets above. Importing a Lovable Cloud export and running migrations from your deploy workflow are both planned.
If you have a TanStack Start app
A TanStack Start app renders pages on a server, and its server functions (createServerFn) run your backend code there. Lovable publishes it to Cloudflare Workers. On AWS it runs as a Node.js server in a container, usually ECS on Fargate behind a load balancer, with CloudFront in front for HTTPS and caching. Lovable's external hosting guide covers the basics. The database steps above don't change.
Build it for Node, not Cloudflare
Lovable's vite.config.ts uses its own wrapper, @lovable.dev/vite-tanstack-config, which builds through Nitro and targets Cloudflare by default. Outside Lovable, the NITRO_PRESET environment variable overrides that default:
NITRO_PRESET=node-server npm run build
node .output/server/index.mjs # listens on PORT, default 3000
.output/ holds everything the server needs, so the image that runs it doesn't need node_modules. Set HOST=0.0.0.0 so the load balancer can reach the server.
The gotchas we hit, roughly in the order you'll meet them:
- The
nitropackage has to be installed. Without it, Lovable's wrapper skips Nitro without saying so, and the build has no server to run. Lovable's current template includes it. - A
presetinvite.config.tsbeatsNITRO_PRESET. If someone pinnednitro: { preset: "cloudflare-module" }, the build still targets Cloudflare. Remove it, along withnitro: false. - Version 1 of Lovable's wrapper only builds for Cloudflare. Projects from May 2026 may have it. Upgrade
@lovable.dev/vite-tanstack-configto version 2 or later. - Install with bun. Lovable uses bun, and its
package.jsonoften has anoverridesfield. On an open-source Lovable app we tested,npm installwith npm 10 (the version in thenode:22image) crashed on it withCannot read properties of null (reading 'edgesOut').bun installworked. If the repository hasbun.lockorbunfig.toml, install with bun. - Cloudflare-only code won't run.
cloudflare:*imports and Wrangler bindings (KV, D1, R2, Durable Objects) don't exist on AWS. Read server secrets withprocess.envin server functions instead. - Two kinds of environment variables.
VITE_values are still written into the browser JavaScript at build time, so pass them as Docker build args. Variables without the prefix are server-only, read at runtime from the container's environment. Lovable's TanStack apps often read server-sideSUPABASE_URLandSUPABASE_PUBLISHABLE_KEYas well as theVITE_ones. Without them, the app we tested answered every request with a 500 (supabaseUrl is required). Server secrets, such as the Supabase service role key, belong in AWS Secrets Manager, never behindVITE_. - The home page may redirect. If
/redirects (ours went to/feedwith a 307), make sure the load balancer health check accepts 3xx responses, or point it at a page that returns 200.
It costs more than a static site
A server runs all the time, and the load balancer has an hourly fee, so a TanStack Start app costs more each month than the same app as a static site. ScaleBop's rough estimate for a small Lovable app on Supabase is about $2.50–30 a month as a static site and about $27.50–90 a month on Fargate. Data transfer and the free tier move both numbers.
If your app doesn't need a server, there's a cheaper option: turn on SPA mode (tanstackStart: { spa: { enabled: true } }) and avoid server functions and server routes. The build is then static files again, served from S3 and CloudFront as in Step 2. One difference: SPA mode writes _shell.html instead of index.html, so CloudFront's root object and its 403 and 404 fallback have to point at /_shell.html.
What ScaleBop does here: recognises TanStack Start and deploys it as a container on ECS Fargate behind a load balancer and CloudFront. If your repository has no Dockerfile, it adds one in its setup pull request. That Dockerfile installs with bun when your repository uses it, builds with
NITRO_PRESET=node-server, passes yourVITE_values in as build args and runs.output/server/index.mjs. Yourvite.config.tsisn't changed, so Lovable keeps working on the same files. Server-only values come from ScaleBop's configuration, with secrets stored in Secrets Manager in your account.Before anything deploys, the readiness check stops if the build can't produce a Node server (no
nitro, a pinned preset, the version 1 wrapper) or if the app uses Cloudflare-only code. It gives you a prompt to paste into Lovable. In SPA mode with no server code, ScaleBop deploys a static site and serves_shell.html. If you upgrade a static Lovable app to TanStack Start, ScaleBop pauses Ship and shows the monthly cost before and after the move. More in Lovable apps on TanStack Start.
Step 5: Cutover
- Check everything against your inventory: row counts, users and identities, file counts, functions responding, jobs scheduled, secrets present.
- Smoke-test the production URL before touching DNS. Use the CloudFront address or a temporary subdomain. Sign up, sign in with each provider, upload a file, and open a deep link directly.
- Lower your DNS TTL a day ahead, so the switch spreads quickly.
- Pause writes and take the final export if you're coming from Lovable Cloud, and restore it.
- Switch the domain and watch your logs, CloudWatch and Supabase, for the first few hours.
- Keep the old environment until production is proven. Don't remove Lovable Cloud on day one. When you do, it's permanent: the database, backups, users and files all go.
- Unpublish the
lovable.appsite. Lovable's Publish button still deploys there, and you don't want two live copies. - Tidy up: delete temporary helper functions from the repository, not only from Supabase, or the sync brings them back. Rotate the database password if you shared it, and delete the export file.
Keeping Lovable after the move
You can keep building in Lovable. It stays connected to the repository and to a Supabase project you treat as staging, and production deploys from the production branch. A Lovable project connects to one backend, so it never points at production.
Three things to watch:
- Lovable can rewrite
.env. That's fine as long as production values come from the pipeline, not the file. - Lovable writes migrations from prompts. Review them before they reach
production. - Changes reach users when you decide. Lovable commits on every prompt, so release on purpose.
Working with your AI tool after launch covers this loop in more detail.
Checklist
Copy this into an issue or a doc and work down it.
Before you start
- Checked
package.jsonfor@tanstack/react-start(Vite or TanStack Start?) - Inventory written down: tables and row counts, users and providers, buckets and file counts, function names, secret names, cron jobs, domain and DNS, AI features
Code
- Lovable connected to GitHub, repository in an organisation
-
productionbranch created and protected,mainleft for Lovable -
src/integrations/supabase/client.tsreads fromimport.meta.env - Lockfile committed for the package manager CI uses
- No secrets in
VITE_variables or in the repository history
Infrastructure
- Private S3 bucket behind CloudFront with Origin Access Control
- 403 and 404 fall back to
/index.htmlwith status 200 -
index.htmlinvalidated or short-cached on each deploy - ACM certificate in
us-east-1 - GitHub Actions deploys through OIDC, no stored AWS keys
- Infrastructure defined in code, in the repository
- Budget alert and error alarm set up
Pipeline
- Production
VITE_values injected at build time - Deploys on push to
productiononly - Migrations and Edge Functions deployed by their own steps, migrations reviewed first
TanStack Start apps only
-
nitroinstalled,@lovable.dev/vite-tanstack-configat version 2 or later, nopresetornitro: falseinvite.config.ts - Installed with bun, built with
NITRO_PRESET=node-server - No
cloudflare:*imports or Wrangler bindings -
VITE_values passed at build time, server-only values (such asSUPABASE_URL) set at runtime - Load balancer health check accepts the home page's response, redirects included
- Or, if nothing needs a server: SPA mode on, CloudFront pointed at
/_shell.html
Database
- Production Supabase project created, region close to your AWS region
- Lovable Cloud export downloaded before anything was removed
- Restored with
pg_restore(Postgres 16+, zstd), errors reviewed -
auth.usersandauth.identitiescounts match - Storage files uploaded, signed URLs regenerated
- Edge Functions deployed with the right
verify_jwtsettings - Secrets re-entered with exact names
-
pg_cronenabled, job URLs updated - Sign-in providers and redirect URLs set for the new domain
- Lovable AI calls moved to your own provider key
Cutover
- Every number checked against the inventory
- Production smoke-tested before DNS: sign-up, each sign-in provider, upload, deep link
- DNS TTL lowered a day ahead
- Final export taken and restored with writes paused
- Domain switched, logs watched
-
lovable.appsite unpublished - Old backend kept until production is proven, then removed
- Export file deleted, shared passwords rotated
What ScaleBop handles, and what's still on you
| ScaleBop handles | Still on you |
|---|---|
| Detecting a Lovable app, Vite or TanStack Start, and its Supabase project | Choosing between your own Supabase and AWS-native |
| Security scan for common AI-builder mistakes | Fixing what the scan finds, and Row Level Security on every table |
| CDK and GitHub Actions workflow, committed as a pull request | Reviewing that pull request before it runs |
| OIDC role, so no AWS keys in GitHub | Tightening the role's permissions if your account needs it (it has administrator access, so CDK can create any resource) |
| S3, CloudFront, deep-link fallback, HTTPS, alarm, budget | AWS WAF, if you need it |
| For TanStack Start, a Fargate container built for Node, and a check for Cloudflare-only code | Replacing Cloudflare bindings and cloudflare:* imports, if your app has them |
| Custom subdomain with certificate and DNS records | Apex domains, and the DNS cutover timing |
| Production Supabase project, keys in Secrets Manager, values wired into the build | Exporting, restoring and checking your data |
| Releasing when you press Ship, with rollback to an earlier version | Migrations, Edge Functions, secrets, cron jobs and auth redirect URLs in Supabase |
| Replacing Lovable's AI gateway |
To see what ScaleBop makes of your app before you commit to anything, run the free readiness check on your repository, or import it on the free starter tier.