Skip to content

Run Postgres, Valkey, and Qdrant in one Lovable app

7 min readLovablePostgresValkeyQdrantDatabases

Most Lovable apps start with one database and end up wanting three. The transactional data lives in Postgres. The sessions, rate limits, and hot caches live in Redis or Valkey. The embeddings behind any AI feature live in a vector store like Qdrant. The default Lovable stack, which is Supabase, covers the first one well and leaves you reaching for other vendors for the rest. This post sets up all three on one account with one flat bill, and wires them into a Lovable app the way that actually works.

Contents

How a Lovable app is shaped

Before any of this, one fact decides the whole approach. Lovable generates a Vite React single-page app. It ships to the browser and nothing in it runs on a server. That means you cannot open a raw Postgres connection, a Redis socket, or any other TCP database client from inside the app itself. Those drivers need a server process, and the Lovable-hosted app does not have one. It also means a database password dropped into a Lovable env var is not a secret. Browser env vars (import.meta.env.VITE_*) are baked into the bundle at build time and shipped to every visitor.

So the rule for the rest of this post is simple: every database client below runs server-side, and every credential stays server-side. There are two places to put that server. You can use a Supabase Edge Function, which is the server compute a Lovable-hosted app already has, or you can sync the project to GitHub and self-host with your own API layer. Connect a Postgres database to a Lovable app walks through both paths in full for the single-database case. This post takes the Edge Function path and extends it to all three engines, then points at the self-host path as the alternative.

Why three databases

Each one solves a problem the others handle badly, and keeping them separate is the point.

Postgres is the source of truth. User accounts, orders, content, anything you would write to a normal database and expect to still be there next year. Relational data, JSONB where you want occasional schema flex, constraints for integrity. It is the wrong tool for hot session lookups and for vector search.

Valkey (a drop-in Redis fork) is for things that need to be fast and can be lost. Sessions, rate-limit counters, ephemeral caches, queues, ranked leaderboards. Anything you can rebuild from Postgres if it vanishes. The reason to keep this off Postgres is volume: session and rate-limit reads happen on every request, and running them against your transactional database beats up the side of the system that can least afford it.

Qdrant is for vectors, which are high-dimensional float arrays with similarity search on top. Embeddings from an AI model, image features for visual search, anything where "find the most similar item" is the query. pgvector inside Postgres is fine at small scale. Qdrant scales further and does filtered similarity search better, so it earns its place once semantic search becomes a real feature rather than a demo.

If your app does AI features (retrieval, semantic search, recommendations) you will likely want all three before long. Standing them up together costs less than retrofitting a session store or a vector index into a live app later.

Provision the three databases

Create the databases from the Layerbase Cloud dashboard. Each takes about 30 seconds and they all sit under one account.

  1. Create a Postgres, name it lovable-app-db.
  2. Create a Valkey, name it lovable-app-cache.
  3. Create a Qdrant, name it lovable-app-rag.

Each one gives you a connection string in the dashboard. You will hand these to the server, not to the browser, so store them as function secrets rather than Lovable env vars. Using the Supabase CLI against the project behind your Lovable app:

bash
supabase secrets set LAYERBASE_DATABASE_URL="postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=require"
supabase secrets set LAYERBASE_REDIS_URL="rediss://default:<password>@your-host.cloud.layerbase.dev:6379"
supabase secrets set LAYERBASE_QDRANT_URL="https://your-host.cloud.layerbase.dev"
supabase secrets set LAYERBASE_QDRANT_API_KEY="<long random string>"

None of these values ever reach the browser. That is the whole reason they go here and not into a VITE_ variable.

Wire them in through 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 TCP connections and can pull npm packages with npm: import specifiers, so the same clients you would use on a Node server work here. One function can hold all three clients and hand back exactly the data the frontend asks for.

Here is a function that uses every engine for the job it is good at: Valkey for the rate limit, Qdrant for the search, Postgres for the durable audit trail.

ts
// supabase/functions/search-and-record/index.ts
import postgres from 'npm:postgres'
import Redis from 'npm:ioredis'
import { QdrantClient } from 'npm:@qdrant/js-client-rest'

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

const redis = new Redis(Deno.env.get('LAYERBASE_REDIS_URL')!, {
  maxRetriesPerRequest: 3,
})

const qdrant = new QdrantClient({
  url: Deno.env.get('LAYERBASE_QDRANT_URL')!,
  apiKey: Deno.env.get('LAYERBASE_QDRANT_API_KEY')!,
})

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

  // Rate limit with Valkey.
  const count = await redis.incr(`search:${userId}`)
  if (count === 1) await redis.expire(`search:${userId}`, 60)
  if (count > 30) {
    return new Response('Rate limited', { status: 429 })
  }

  // Semantic search with Qdrant.
  const results = await qdrant.search('documents', {
    vector: embedding,
    limit: 10,
    with_payload: true,
  })

  // Durable audit trail in Postgres.
  await sql`
    insert into search_log (user_id, query, result_count, created_at)
    values (${userId}, ${query}, ${results.length}, now())
  `

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

The postgres client uses tagged template literals, so ${userId} is bound as a parameter rather than interpolated into the string. That is safe against SQL injection out of the box.

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

ts
const { data } = await supabase.functions.invoke('search-and-record', {
  body: { userId, query, embedding },
})

Do the embedding step wherever you already do it. If you call a model provider from the frontend you can pass the vector in as above, or move that call into the function too so the model key stays server-side as well. Either way, three databases are now doing three jobs behind one function, and the credentials for all of them live as secrets the browser never sees.

The full-ownership alternative

The Edge Function keeps you on Lovable's hosting and Supabase's auth, which is the right trade when you like that setup and only want the data layer to be yours. If you want the whole thing under your control, Lovable has two-way GitHub sync: connect it, and the generated Vite app lands in a repo you own. From there you self-host the frontend and stand up your own small API layer (Express, Hono, or a Next.js route handler) that holds the same three clients. Same architecture, same division of labor, just your server instead of an Edge Function. The self-host walkthrough in Connect a Postgres database to a Lovable app covers that path in detail.

What this costs

Postgres and Valkey both run on the Layerbase free tier ($0, two databases). If your app is state plus cache and no vector search, that stack costs nothing.

Qdrant is a Pro-tier engine. Adding it means the Pro plan at $15 a month flat, which covers up to 10 databases across the whole engine catalog. So the full Postgres + Valkey + Qdrant stack is $15 a month, all three databases on one bill, no usage meters, and still less than most standalone managed vector stores charge for the vector piece alone. If you want to skip the counting entirely, The multi-database tax makes the case for one flat bill over juggling three metered services.

Local development with the Layerbase CLI

You do not want to develop against production, and you do not want to burn Pro-tier resources while iterating. The Layerbase CLI, powered by SpinDB under the hood, runs all three engines on your machine with no Docker and no account:

bash
npm i -g layerbase
lbase create lovable-db --engine postgresql --start
lbase create lovable-cache --engine valkey --start
lbase create lovable-rag --engine qdrant --start

lbase url <name> prints a local connection string for each. Point your Edge Function or API layer at the local URLs while you build, then swap in the cloud strings when a feature is ready. lbase stop <name> shuts an engine down without dropping its data.

Where to start

The short version: a Lovable app is a browser SPA with no server, so all three database clients live server-side, either in a Supabase Edge Function or in your own API after a GitHub sync. Postgres carries state, Valkey carries the fast and disposable, Qdrant carries search.

  • Create the databases at layerbase.com/cloud. Postgres plus Valkey is free; add Qdrant and the stack is Pro at $15 a month flat.
  • Wire them through one Edge Function, with every connection string set as a secret.
  • Mirror the whole stack locally with lbase while you build.

One account, one dashboard, one bill for the data layer means more time on the app and less on plumbing, which is exactly the trade a fast builder like Lovable is supposed to make.