Connect a Postgres database to a Lovable app
Lovable ships with Supabase as the default backend. That is fine for the first prototype, but if you want to own the database (predictable pricing, room to add Redis or vector storage later, no auth lock-in) you can swap it. The Postgres part is a one-env-var change. The Supabase client is the only thing that needs replacing in the generated code.
Here is the working version.
Contents
- Why swap the database
- Create a Layerbase Postgres database
- Put the connection string in Lovable
- Replace the Supabase client
- Local development with SpinDB
- What you give up, what you keep
Why swap the database
The Supabase free tier is generous. The reasons to leave it are usually not the free tier itself, they are:
- You want a database vendor that is not also your auth vendor and your storage vendor. Auth lock-in is the part that costs you later.
- You want predictable monthly pricing instead of per-row-read billing once you cross the free tier.
- You want to add a second database (Redis for caching, Qdrant for vector search) and have it on the same dashboard and same bill.
- You want to be able to point a local copy of the app at a real database, fast, without spinning up Docker.
Layerbase Cloud handles all four. The Postgres is plain managed Postgres with a normal connection string. No proprietary client, no proprietary auth layer attached to it.
Create a Layerbase Postgres database
Visit layerbase.com/create/postgresql, name it (something like lovable-app-prod), and click Sign in and create. Provisioning is about ten seconds.
The dashboard gives you a connection string in the standard shape:
postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=requireCopy it. That is everything Postgres-side.
Put the connection string in Lovable
In your Lovable project, set an environment variable. Lovable exposes env vars through their settings panel and they are injected into the generated app at build time. Name it DATABASE_URL:
DATABASE_URL=postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=requireIf you also kept Supabase around for auth (a reasonable middle ground while you migrate), leave NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in place. They will not conflict.
Replace the Supabase client
Lovable generates calls that look like this:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
const { data, error } = await supabase
.from('todos')
.select('id, title, done')
.eq('user_id', userId)You want plain Postgres. Install a driver:
pnpm add postgrespostgres (the package, by Porsager) is a tiny modern client with no native dependencies, which means it builds cleanly on Vercel and Cloudflare Workers if you ever move there. pg works too if you prefer it.
Then the same query becomes:
import postgres from 'postgres'
const sql = postgres(process.env.DATABASE_URL!, {
ssl: 'require',
})
const todos = await sql`
select id, title, done
from todos
where user_id = ${userId}
`A few things to notice:
- Tagged template literals do parameterized queries automatically. The
${userId}is bound as a parameter, not string-interpolated, so this is safe against SQL injection. - There is no
.from()or.select()builder. You write SQL. For complex apps this is shorter and easier to debug. - The result is a plain array. No
{ data, error }envelope.
If you prefer a query builder, drizzle and kysely both work the same way on top of postgres or pg. Both are popular in the Lovable code patterns and neither locks you into a vendor.
Schema and migrations
Supabase makes you run schema changes through their dashboard or a CLI. With your own Postgres you have all the usual options. The two I recommend for Lovable-shaped apps:
Drizzle migrations if you want the schema in TypeScript:
pnpm add drizzle-orm drizzle-kitimport { pgTable, serial, text, boolean, uuid } from 'drizzle-orm/pg-core'
export const todos = pgTable('todos', {
id: serial('id').primaryKey(),
user_id: uuid('user_id').notNull(),
title: text('title').notNull(),
done: boolean('done').default(false).notNull(),
})drizzle-kit generate writes the SQL. drizzle-kit push applies it.
Raw SQL files if you want the schema as plain SQL. Drop them in migrations/ and apply them with psql $DATABASE_URL -f migrations/001_init.sql. This is the simplest possible setup and is what I run on most small projects.
Local development with SpinDB
The Postgres in your Lovable project is the production database. You do not want to run app development against it. SpinDB is the open-source CLI for running databases locally without Docker.
npm i -g spindb
spindb create lovable-dev --engine postgresql --start
spindb url lovable-devspindb url prints a local connection string. Put it in .env.local:
DATABASE_URL=postgresql://localhost:5433/lovable_devRun your migrations against it, develop locally, push to production when you are happy. spindb stop lovable-dev shuts it down without removing the data.
What is SpinDB? covers the rest, including how to point Layerbase Desktop at the same local database for a GUI.
What you give up, what you keep
You give up Supabase auth, Supabase row-level security, and the Supabase storage bucket if you were using them. The auth piece is the largest. Options:
- Keep Supabase auth. It works fine on its own and you can call it from a Lovable app without the database part. The cost stays low because auth alone has a generous free tier.
- Swap to Clerk, NextAuth, Better-Auth, or Lucia. Any of them work, and none of them care which database you use.
- Build it yourself with
bcryptand JWTs in your own Postgres. Fine for a small app, but the failure modes around password reset and email verification are annoying enough that I would not start from scratch.
You keep the Lovable developer experience, the AI-driven UI generation, and the deploy story. The database is just a connection string, and the connection string can point anywhere.
Wrapping up
The short version:
- Create a Postgres at layerbase.com/create/postgresql
- Set
DATABASE_URLin Lovable's env settings - Replace
createClient(SUPABASE_URL, ...)withpostgres(DATABASE_URL) - Develop locally with SpinDB
If your app eventually needs Redis (sessions, rate limiting) or vector search (RAG, semantic search), you can add either of those on the same Layerbase account without changing hosts. That portability is the reason most people make this swap in the first place.