Connect a Postgres database to a Lovable app

LovablePostgresSupabaseDatabases

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

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:

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

Copy 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:

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

If 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:

ts
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:

bash
pnpm add postgres

postgres (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:

ts
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:

bash
pnpm add drizzle-orm drizzle-kit
ts
import { 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.

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

spindb url prints a local connection string. Put it in .env.local:

text
DATABASE_URL=postgresql://localhost:5433/lovable_dev

Run 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 bcrypt and 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:

  1. Create a Postgres at layerbase.com/create/postgresql
  2. Set DATABASE_URL in Lovable's env settings
  3. Replace createClient(SUPABASE_URL, ...) with postgres(DATABASE_URL)
  4. 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.

Something not working?