Articles

What it actually takes to move a Lovable app to AWS

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 build produces static files in dist/, and those can be served straight from S3 and CloudFront. Most of this article is about this kind of app.
  • @tanstack/react-start is 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, including verify_jwt.
  • .env: the Supabase URL, publishable key and project ID, all prefixed VITE_.

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 main as the branch Lovable syncs to and deploy from a separate production branch. Every Lovable prompt becomes a commit, so deploying main directly ships half-finished work. Protect production in GitHub. Lovable never writes to it, so the protection doesn't get in its way.
  • Don't trust the committed .env for production. Lovable rewrites it, sometimes back to its own backend. Production values should come from your deploy pipeline. Also check that src/integrations/supabase/client.ts reads the URL and key from import.meta.env rather 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.lock and your pipeline uses npm, run npm install and commit package-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 /settings or /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.html and 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.html keeps its name. Either give it a short cache lifetime or invalidate it on every deploy. Otherwise users get yesterday's index.html pointing at files that no longer exist. The first 1,000 invalidation paths each month are free, and /* counts as one (AWS docs).
  • 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-1 and creates the DNS records in Route 53, or gives you a CNAME to add at your DNS provider. Custom domains must be subdomains, like app.example.com. ScaleBop doesn't add AWS WAF.

Step 3: The pipeline

A GitHub Actions workflow that runs on every push to production:

  1. Install dependencies and build, with the production values injected: VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY for your production Supabase project.
  2. Upload dist/ to S3, deleting files that are no longer in the build.
  3. Invalidate the CloudFront cache.
  4. 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 production and does steps 1 to 4. It injects your VITE_ 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 moves production up to your latest commit on main, 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 run supabase db push before 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.

  1. 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).
  2. 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.
  3. Know what's in it. It includes the schema, all rows, RLS policies, functions and triggers, and the auth schema with password hashes, so users keep their passwords. It doesn't include storage files, Edge Function code, secret values or sign-in provider settings.
  4. Create a new Supabase project and restore into it with pg_restore. The export is a compressed custom-format archive, so you need pg_restore from Postgres 16 or later, built with zstd support. Homebrew's libpq doesn't have it, and postgresql@18 does. 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).
  5. Expect errors. Your new project already has its own auth and storage schemas, 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.
  6. Check users twice. Compare auth.users with auth.identities. If identities are missing, people who signed in with Google or GitHub can't sign in. Restore them on their own with pg_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.objects rows 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/functions with supabase functions deploy. Keep the verify_jwt settings from config.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 copy SUPABASE_URL or 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 with cron.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_KEY that 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 anon and service_role keys 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 nitro package 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 preset in vite.config.ts beats NITRO_PRESET. If someone pinned nitro: { preset: "cloudflare-module" }, the build still targets Cloudflare. Remove it, along with nitro: false.
  • Version 1 of Lovable's wrapper only builds for Cloudflare. Projects from May 2026 may have it. Upgrade @lovable.dev/vite-tanstack-config to version 2 or later.
  • Install with bun. Lovable uses bun, and its package.json often has an overrides field. On an open-source Lovable app we tested, npm install with npm 10 (the version in the node:22 image) crashed on it with Cannot read properties of null (reading 'edgesOut'). bun install worked. If the repository has bun.lock or bunfig.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 with process.env in 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-side SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY as well as the VITE_ 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 behind VITE_.
  • The home page may redirect. If / redirects (ours went to /feed with 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 your VITE_ values in as build args and runs .output/server/index.mjs. Your vite.config.ts isn'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

  1. Check everything against your inventory: row counts, users and identities, file counts, functions responding, jobs scheduled, secrets present.
  2. 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.
  3. Lower your DNS TTL a day ahead, so the switch spreads quickly.
  4. Pause writes and take the final export if you're coming from Lovable Cloud, and restore it.
  5. Switch the domain and watch your logs, CloudWatch and Supabase, for the first few hours.
  6. 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.
  7. Unpublish the lovable.app site. Lovable's Publish button still deploys there, and you don't want two live copies.
  8. 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.json for @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
  • production branch created and protected, main left for Lovable
  • src/integrations/supabase/client.ts reads from import.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.html with status 200
  • index.html invalidated 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 production only
  • Migrations and Edge Functions deployed by their own steps, migrations reviewed first

TanStack Start apps only

  • nitro installed, @lovable.dev/vite-tanstack-config at version 2 or later, no preset or nitro: false in vite.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 as SUPABASE_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.users and auth.identities counts match
  • Storage files uploaded, signed URLs regenerated
  • Edge Functions deployed with the right verify_jwt settings
  • Secrets re-entered with exact names
  • pg_cron enabled, 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.app site 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.

See if your app is ready

Paste your public GitHub repo for a free check: whether ScaleBop can deploy it, what it would set up, and how many common security problems it found. No sign-up.