Never connect an external database directly from your Lovable frontend — browser code exposes connection strings to anyone who views the page source. Instead, create a Supabase Edge Function that acts as a secure proxy between your frontend and the external database. Store the database connection string in Cloud tab, then Secrets, and call it from the Edge Function. The frontend only communicates with your Edge Function, keeping credentials completely hidden.
Why connecting to external databases requires a secure proxy in Lovable
Lovable projects run entirely in the browser. The React frontend, Tailwind styles, and all JavaScript code are downloaded to the user's device and executed there. This means anything you put in frontend code — including database connection strings, passwords, and API keys — is visible to anyone who opens the browser developer tools. Connecting directly to an external PostgreSQL, MySQL, or MongoDB database from frontend code is a critical security vulnerability. Anyone could extract the connection string and gain full access to your database, including the ability to read, modify, or delete all data. The safe approach is to use Supabase Edge Functions as a data proxy. Your frontend sends a request to the Edge Function (which runs on the server), the Edge Function connects to the external database using credentials stored in Lovable Secrets, queries the data, and returns only the results to the frontend. The database credentials never leave the server.
- Database connection strings hardcoded in frontend code — visible in browser source to any user
- VITE_ prefixed database credentials — these are embedded at build time and shipped to every browser
- Direct PostgreSQL or MySQL connections from React components — browsers cannot make raw TCP database connections
- Missing Edge Function proxy — no server-side layer to safely hold database credentials
- Row Level Security not configured — even with Supabase, missing RLS policies expose all table data
Error messages you might see
TypeError: pg.Client is not a constructorYou are trying to use a Node.js PostgreSQL library (like 'pg') directly in the browser. Browser JavaScript cannot make raw TCP connections to databases. You need a server-side proxy like a Supabase Edge Function to handle database connections.
net::ERR_CONNECTION_REFUSED connecting to localhost:5432Your frontend code is trying to connect to a PostgreSQL database on localhost, which does not exist in the user's browser. Database connections must be made from server-side code (Edge Functions), not from the frontend.
FATAL: password authentication failed for user 'app_user'The database credentials are wrong. If you see this in Cloud tab Logs, check that your connection string in Secrets has the correct username, password, host, port, and database name.
Before you start
- A Lovable project with Supabase connected (Lovable Cloud or self-hosted)
- The external database connection string (host, port, database name, username, password)
- The external database must allow connections from Supabase's IP range (check your database firewall rules)
- Basic understanding of what a database connection string looks like
How to fix it
Store the database connection string in Lovable Secrets
Secrets are encrypted and only accessible from Edge Functions — never exposed in frontend code
Store the database connection string in Lovable Secrets
Secrets are encrypted and only accessible from Edge Functions — never exposed in frontend code
Open the Cloud tab in your Lovable project (click the + button next to Preview). Navigate to Secrets. Click Add Secret. Name it something descriptive like EXTERNAL_DB_URL. For the value, enter your full connection string in this format: postgresql://username:password@host:port/database_name. Click Save. This secret is now encrypted and automatically available to all your Supabase Edge Functions. Never put this connection string in a VITE_ variable or anywhere in frontend code.
// DANGEROUS: Connection string in frontend code — visible to everyoneconst DB_URL = "postgresql://admin:secretpass@db.example.com:5432/myapp";// Or worse: in a VITE_ env variable (also shipped to browser)const DB_URL = import.meta.env.VITE_DATABASE_URL;// SAFE: Connection string stored in Cloud tab → Secrets// Name: EXTERNAL_DB_URL// Value: postgresql://admin:secretpass@db.example.com:5432/myapp// This secret is only accessible from Edge Functions// It never appears in frontend code or browser sourceExpected result: Your database connection string is stored securely in Lovable Secrets, encrypted and only accessible from server-side Edge Functions.
Create a Supabase Edge Function as a database proxy
The Edge Function runs on the server and can safely connect to external databases without exposing credentials
Create a Supabase Edge Function as a database proxy
The Edge Function runs on the server and can safely connect to external databases without exposing credentials
Prompt Lovable in Agent Mode: 'Create a Supabase Edge Function called external-db-query that connects to an external PostgreSQL database using the EXTERNAL_DB_URL secret. It should accept a POST request with a query parameter, execute the query safely using parameterized statements, and return the results as JSON.' Lovable will create the function in supabase/functions/external-db-query/. The function reads the connection string from Deno.env.get('EXTERNAL_DB_URL'), which Lovable Secrets automatically injects.
// No server-side proxy exists — frontend tries to connect directlyimport { Client } from "pg";async function fetchData() { const client = new Client("postgresql://admin:secretpass@db.example.com:5432/myapp"); await client.connect(); const result = await client.query("SELECT * FROM products"); return result.rows;}// Edge Function: supabase/functions/external-db-query/index.tsimport { serve } from "https://deno.land/std@0.168.0/http/server.ts";import { Client } from "https://deno.land/x/postgres@v0.17.0/mod.ts";serve(async (req) => { try { // Connection string comes from Lovable Secrets — never hardcoded const dbUrl = Deno.env.get("EXTERNAL_DB_URL"); if (!dbUrl) throw new Error("EXTERNAL_DB_URL secret is not set"); const client = new Client(dbUrl); await client.connect(); const { table, limit } = await req.json(); // Use parameterized query to prevent SQL injection const result = await client.queryObject( `SELECT * FROM ${table} LIMIT $1`, [limit || 100] ); await client.end(); return new Response(JSON.stringify({ data: result.rows }), { headers: { "Content-Type": "application/json" }, }); } catch (error) { console.error("Database query error:", error.message); return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { "Content-Type": "application/json" }, }); }});Expected result: A Supabase Edge Function is created that securely connects to your external database. The connection string stays on the server.
Call the Edge Function from your frontend
The frontend communicates only with your Edge Function — no database credentials are exposed
Call the Edge Function from your frontend
The frontend communicates only with your Edge Function — no database credentials are exposed
In your React components, call the Edge Function using supabase.functions.invoke(). This sends the request to the Supabase server, which executes the Edge Function with access to your secrets. The function queries the external database and returns only the results to the frontend. If setting up this proxy pattern across multiple database tables and functions feels complex, RapidDev's engineers have built this exact architecture across hundreds of Lovable projects and can handle it safely.
// Direct database access from frontend — UNSAFEconst fetchProducts = async () => { const client = new Client(DB_URL); await client.connect(); const result = await client.query("SELECT * FROM products"); return result.rows;};import { supabase } from "@/integrations/supabase/client";const fetchProducts = async () => { // Call Edge Function — credentials stay on server const { data, error } = await supabase.functions.invoke( "external-db-query", { body: { table: "products", limit: 50 }, } ); if (error) { console.error("Failed to fetch products:", error.message); return []; } return data.data;};Expected result: Products load from the external database through the Edge Function proxy. No database credentials appear in the browser.
Add input validation and SQL injection protection
Without validation, malicious users could craft requests that run arbitrary SQL on your external database
Add input validation and SQL injection protection
Without validation, malicious users could craft requests that run arbitrary SQL on your external database
Your Edge Function accepts input from the frontend, which means a malicious user could send crafted payloads. Always validate inputs and use parameterized queries. Never interpolate user input directly into SQL strings. Whitelist the allowed table names and column names instead of accepting arbitrary strings.
// DANGEROUS: Table name comes directly from user inputconst { table, limit } = await req.json();const result = await client.queryObject( `SELECT * FROM ${table} LIMIT $1`, [limit]);// SAFE: Whitelist allowed tables and validate inputconst ALLOWED_TABLES = ["products", "categories", "orders"];const { table, limit } = await req.json();if (!ALLOWED_TABLES.includes(table)) { return new Response( JSON.stringify({ error: "Invalid table name" }), { status: 400, headers: { "Content-Type": "application/json" } } );}const safeLimit = Math.min(Math.max(Number(limit) || 100, 1), 1000);const result = await client.queryObject( `SELECT * FROM ${table} LIMIT $1`, [safeLimit]);Expected result: The Edge Function rejects requests for unauthorized tables and limits query size, preventing SQL injection and data exfiltration.
Complete code example
1import { serve } from "https://deno.land/std@0.168.0/http/server.ts";2import { Client } from "https://deno.land/x/postgres@v0.17.0/mod.ts";34const ALLOWED_TABLES = ["products", "categories", "orders"];5const MAX_LIMIT = 1000;67serve(async (req) => {8 // Handle CORS for browser requests9 if (req.method === "OPTIONS") {10 return new Response(null, {11 headers: {12 "Access-Control-Allow-Origin": "*",13 "Access-Control-Allow-Methods": "POST",14 "Access-Control-Allow-Headers": "authorization, content-type",15 },16 });17 }1819 try {20 const dbUrl = Deno.env.get("EXTERNAL_DB_URL");21 if (!dbUrl) throw new Error("EXTERNAL_DB_URL secret is not set");2223 const { table, limit } = await req.json();2425 // Validate table name against whitelist26 if (!table || !ALLOWED_TABLES.includes(table)) {27 return new Response(28 JSON.stringify({ error: "Invalid table name" }),29 { status: 400, headers: { "Content-Type": "application/json" } }30 );31 }3233 const safeLimit = Math.min(Math.max(Number(limit) || 100, 1), MAX_LIMIT);3435 const client = new Client(dbUrl);36 await client.connect();3738 const result = await client.queryObject(39 `SELECT * FROM ${table} LIMIT $1`,40 [safeLimit]41 );4243 await client.end();4445 return new Response(JSON.stringify({ data: result.rows }), {46 headers: {47 "Content-Type": "application/json",48 "Access-Control-Allow-Origin": "*",49 },50 });51 } catch (error) {52 console.error("External DB error:", error.message);53 return new Response(54 JSON.stringify({ error: "Database query failed" }),55 { status: 500, headers: { "Content-Type": "application/json" } }56 );57 }58});Best practices to prevent this
- Never put database connection strings in frontend code or VITE_ environment variables — they are shipped to every browser
- Always use Supabase Edge Functions as a proxy layer between the frontend and any external database
- Store all database credentials in Cloud tab, then Secrets — they are encrypted and only accessible from Edge Functions
- Whitelist allowed table names in the Edge Function — never accept arbitrary table names from frontend requests
- Use parameterized queries to prevent SQL injection — never interpolate user input directly into SQL strings
- Limit query results with a maximum cap (e.g., 1000 rows) to prevent accidental full-table dumps
- Add proper error handling in Edge Functions with console.error logging so failures appear in Cloud tab Logs
- Check that your external database firewall allows connections from Supabase's IP range
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Lovable (lovable.dev) project with Supabase. I need to connect to an external PostgreSQL database securely. My external database details: - Host: [your-db-host] - Port: [port] - Database: [database-name] - Tables I need to access: [list tables] Here is my current code that tries to connect directly from the frontend: [Paste your current code here] Please: 1. Explain why direct frontend database connections are dangerous 2. Create a Supabase Edge Function that proxies database queries 3. Show me how to store the connection string in Lovable Secrets 4. Add input validation and SQL injection protection 5. Show the frontend code that calls the Edge Function
Create a Supabase Edge Function called 'external-db-query' that connects to my external PostgreSQL database. Read the connection string from the EXTERNAL_DB_URL secret (I will store this in Cloud tab Secrets). The function should: 1) Accept POST requests with a JSON body containing 'table' and optional 'limit' parameters. 2) Validate the table name against a whitelist of allowed tables: products, categories, orders. 3) Execute a safe parameterized SELECT query. 4) Return the results as JSON. 5) Handle errors with proper logging. Also update my frontend component to call this Edge Function using supabase.functions.invoke() instead of connecting to the database directly.
Frequently asked questions
Can I connect to an external database directly from my Lovable frontend?
No. Lovable projects run in the browser, and browsers cannot make raw TCP connections to databases. Even if they could, putting a connection string in frontend code exposes it to every user. Always use a Supabase Edge Function as a proxy to connect to external databases from the server side.
Where do I store my database connection string in Lovable?
Store it in Cloud tab, then Secrets. Click the + button next to Preview, open the Cloud tab, navigate to Secrets, and add a new secret with your connection string. Secrets are encrypted and automatically injected into Edge Functions. Never store database credentials in VITE_ variables.
How do I connect to MySQL or MongoDB from a Lovable project?
The same proxy pattern applies. Create a Supabase Edge Function that uses a Deno-compatible MySQL or MongoDB client library. Store the connection string in Secrets. The Edge Function connects to your database server-side, and the frontend calls the function using supabase.functions.invoke().
Is it safe to use VITE_ variables for database credentials?
No. Variables prefixed with VITE_ are embedded at build time and shipped to every browser that loads your app. Anyone can open browser developer tools and see these values. Use Lovable Secrets instead — they are only accessible from server-side Edge Functions.
How do I prevent SQL injection in my Edge Function?
Always use parameterized queries where user input is passed as parameters, not interpolated into the SQL string. Additionally, whitelist allowed table names and column names instead of accepting arbitrary input. Limit query results with a maximum row cap.
What if I can't set up the database proxy myself?
Setting up a secure database proxy involves Edge Functions, Secrets, input validation, and error handling. If this is beyond your comfort level, RapidDev's engineers can build the complete proxy architecture for your Lovable project, including connection pooling and security hardening.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation