Skip to content

Connect a Postgres database to a Lovable app

8 min readLovablePostgresSupabaseDatabases

Lovable's backend is Supabase. Not "Supabase by default," Supabase by design. When you use Lovable Cloud, the database, auth, and storage behind your app are a Supabase instance that Lovable manages for you. Supabase confirmed this themselves when Lovable Cloud launched: every Lovable Cloud project is a Supabase project underneath. You can also point Lovable at your own Supabase project, but that is still Supabase. Lovable does not document or support connecting any other database.

Here is the part that trips people up, and that an earlier version of this post got wrong. Lovable generates a Vite React single-page app: TypeScript, Tailwind, shadcn/ui, and nothing that runs on a server. It is a bundle that ships to the browser. That means you cannot open a raw Postgres connection from inside it. A TCP database driver needs a server process, and the Lovable-hosted app does not have one. So "just replace the Supabase client with the postgres package and point it at a new URL" is not a real move. There is no server in that app to run the driver, and you never want a database password sitting in a browser bundle anyway.

If someone tells you swapping the database is a one-env-var change, they have not shipped a Lovable app. The change is real, it is just not that shape. Below are the two paths that actually work.

Contents

Why leave the default Supabase

The default is fine for a first prototype. The reasons people move off it later are usually these:

  • One vendor owns your database, your auth, and your storage. That coupling is convenient until you want to change any single piece, and auth is the part that is hardest to unwind.
  • Supabase Pro is $25 a month plus metered usage. The meters are things like monthly active users (100k included), egress (250 GB), disk, and compute. It does not bill per row read, but it is a usage model, and usage models mean the bill moves. If you would rather know the number in advance, flat pricing is the draw.
  • You want a second engine on the same account and the same bill. Redis for sessions or rate limiting, a vector store for search, that kind of thing.
  • You want a database that outlives the app builder. If you ever leave Lovable, a plain managed Postgres with a standard connection string goes with you.

Layerbase Cloud gives you plain managed Postgres, a normal connection string, flat pricing with no usage meters, and room to add other engines on one account. What it does not do is magically bypass the fact that a browser SPA cannot talk to a database directly. That is a real architecture problem, and both paths below solve it the correct way, by putting a server between the browser and the database.

Path 1: stay on Lovable, move data into an Edge Function

The one place a Lovable app has server-side compute is a Supabase Edge Function, which runs on Deno. Deno can open a TCP connection, so an Edge Function can reach an external Postgres even though your frontend cannot. Your React code calls the function, the function talks to Layerbase, and the connection string lives as a function secret that never reaches the browser.

First create the database. Go to layerbase.com/create/postgresql, name it something like lovable-app-prod, and provision it. You get a connection string in the standard shape:

text
postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=require

Store it as an Edge Function secret rather than a Lovable env var, because Lovable env vars land in the browser bundle:

bash
supabase secrets set LAYERBASE_DATABASE_URL="postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=require"

Then the function itself:

ts
// supabase/functions/list-todos/index.ts
import postgres from 'npm:postgres'

const sql = postgres(Deno.env.get('LAYERBASE_DATABASE_URL')!, {
  ssl: 'require',
})

Deno.serve(async (req) => {
  const { userId } = await req.json()

  const todos = await sql`
    select id, title, done
    from todos
    where user_id = ${userId}
  `

  return new Response(JSON.stringify(todos), {
    headers: { 'Content-Type': 'application/json' },
  })
})

The frontend calls it the way it already calls Supabase functions:

ts
const { data } = await supabase.functions.invoke('list-todos', {
  body: { userId },
})

Be clear about what this buys you. Your data now lives on your own Postgres with flat pricing, and the credentials stay server-side where they belong. You are still on Supabase for the function runtime and for auth, so this is a partial move, not a clean break. It is a good fit when you like Lovable's hosting and only want the database itself to be yours.

Path 2: sync to GitHub and self-host

Lovable has two-way GitHub sync. Connect it and your project code lands in a private repo you own. Changes in Lovable push to the repo, and pushes to the synced branch flow back. Once the code is in your repo, you can clone it and run it in any editor, and Lovable's terms let you deploy it anywhere. At that point it is an ordinary Vite React app that you fully control.

Now the driver-swap advice becomes true, because you can add a server. Deploy the frontend to Vercel, Netlify, or wherever you like, and stand up a small API layer next to it: Express, Hono, or a Next.js route handler, your choice. That server holds the database URL and runs the query. Here is a Hono route as an example:

ts
// server/index.ts
import { Hono } from 'hono'
import postgres from 'postgres'

const sql = postgres(process.env.DATABASE_URL!, { ssl: 'require' })
const app = new Hono()

app.get('/api/todos/:userId', async (c) => {
  const userId = c.req.param('userId')

  const todos = await sql`
    select id, title, done
    from todos
    where user_id = ${userId}
  `

  return c.json(todos)
})

export default app

postgres (the Porsager client) uses tagged template literals, so ${userId} is bound as a parameter, not string-interpolated. That is safe against SQL injection out of the box. pg works the same way if you prefer it, and a query builder like Drizzle or Kysely sits on top of either.

For schema changes, Drizzle keeps the definition in TypeScript, which fits an app whose codebase is already TypeScript:

ts
// server/schema.ts
import { pgTable, uuid, text, boolean } from 'drizzle-orm/pg-core'

export const todos = pgTable('todos', {
  id: uuid('id').defaultRandom().primaryKey(),
  userId: uuid('user_id').notNull(),
  title: text('title').notNull(),
  done: boolean('done').default(false).notNull(),
})

drizzle-kit push applies it to the Layerbase database. Prefer raw SQL? Keep .sql files in a migrations/ folder and run psql "$DATABASE_URL" -f migrations/001_init.sql. Both live entirely on the server. Neither ever runs in the browser.

This is the full move. Your frontend, your API, and your database are all yours, and Lovable was the tool that scaffolded the UI.

Auth is the real question

The database is the easy part. Auth is where "just move it" gets people into trouble, because your users live in Supabase's auth schema, and that schema only exists inside a Supabase Postgres project. Row-level security guards tables in that project, so the moment your data lives somewhere else, RLS is no longer protecting it.

You can keep Supabase auth as a bridge while the data moves, and that works. It is not a resting place, though, for two reasons:

  • A free Supabase project pauses after about seven days with no activity. A paused project takes its Auth API down with it, so every login fails until you manually restore the project. On a low-traffic app, "keep Supabase just for auth" is exactly the setup that goes dark on a quiet week.
  • Once the data leaves Supabase's Postgres, authorization is your app's job. RLS did that for you before. Now your API layer has to check that the requesting user is allowed to see the row.

For an end state, pick one:

  • Bring your existing users. The Layerbase migration wizard copies auth.users across with the bcrypt hashes byte for byte and the UUIDs preserved, so no one has to reset a password and every user_id foreign key still lines up. You then wire a small login route that looks up the user by email and runs bcrypt.compare. The full walkthrough, including the parts the wizard does not do, is in Migrating from Supabase to Layerbase.
  • Scaffold fresh auth. Layerbase's Better Auth add-on generates an email-and-password server on Postgres, libSQL, MySQL, MariaDB, or SQLite. This creates its own tables with its own hash, so it does not recognize migrated Supabase users. It is the right call for a new app or one you are happy to re-onboard. See Add email and password auth on libSQL.
  • Use a dedicated provider. Clerk, NextAuth, or another library, none of which care which database you use.

The path I would recommend for an app with real users: migrate auth.users with the wizard so the credentials survive, wire login against the migrated table, and treat Supabase auth as the transition, not the destination.

Local development with the Layerbase CLI

You do not want to develop against your production database. The Layerbase CLI, powered by SpinDB under the hood, runs Postgres locally with no Docker:

bash
npm i -g layerbase
lbase create lovable-dev --engine postgresql --start
lbase url lovable-dev

lbase url prints a local connection string. Point your API layer at it, run your migrations, and develop against real Postgres. lbase stop lovable-dev shuts it down without dropping the data. When a feature is ready, deploy against the cloud connection string.

Where to start

If you want your own Postgres for a Lovable app, the honest short version is: a browser SPA cannot connect to a database, so you add a server, either a Supabase Edge Function (partial move) or your own API after a GitHub sync (full move). Then you decide what to do about auth.

Managed Postgres on Layerbase Cloud is plain Postgres with a standard connection string, flat pricing, and room to add Redis or a vector store on the same account when the app needs it.