Learn how to build a scalable secure Directory service with Lovable step by step including setup schema design access control and deployment tips.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Build a simple Directory service in Lovable that stores people (name, title, email, phone) in Supabase and gives a CRUD UI in your app — no terminal required. You’ll add a small client-side API (uses Supabase REST), create the UI pages, set two Secrets (SUPABASE_URL, SUPABASE_ANON\_KEY) in Lovable Cloud, test in Preview, and Publish. Table creation in Supabase is done in the Supabase dashboard (outside Lovable).
Directory service: a page that lists directory entries, lets you add, edit, delete and search entries. Data lives in a Supabase table. Lovable will create client-side code that talks to Supabase REST endpoints using the anon key stored as a Lovable Secret.
We will use Chat Mode edits to create files (no terminal). Add Secrets via Lovable Cloud Secrets UI. Use Preview to run and test the UI. If you later need DB schema changes or migrations, do that in the Supabase dashboard (outside Lovable). If you require advanced server code, export to GitHub from Lovable and continue locally.
Prompt 1 — Create Supabase REST helper
Goal: create a small helper that calls Supabase REST endpoints using Secrets.
Exact files to create/modify:
Acceptance criteria: done when file exists and exports listDirectory, createEntry, updateEntry, deleteEntry functions.
Secrets/integration: uses process.env.SUPABASE_URL and process.env.SUPABASE_ANON\_KEY (Lovable Secrets will inject these at runtime).
// src/lib/supabaseRest.ts
// helper using Supabase REST API (no extra deps)
const url = process.env.SUPABASE_URL!;
const anon = process.env.SUPABASE_ANON_KEY!;
async function request(path: string, opts: any = {}) {
const res = await fetch(`${url}/rest/v1/${path}`, {
headers: {
'apikey': anon,
'Authorization': `Bearer ${anon}`,
'Content-Type': 'application/json',
'Prefer': 'return=representation'
},
...opts
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function listDirectory(q = '') {
// simple text search on name and email
const filter = q ? `?or=(name.ilike.*${encodeURIComponent('%'+q+'%')}*,email.ilike.*${encodeURIComponent('%'+q+'%')}*)` : '';
return request(`directory${filter}`, { method: 'GET' });
}
export async function createEntry(row: any) {
return request('directory', { method: 'POST', body: JSON.stringify(row) });
}
export async function updateEntry(id: number, row: any) {
return request(`directory?id=eq.${id}`, { method: 'PATCH', body: JSON.stringify(row) });
}
export async function deleteEntry(id: number) {
return request(`directory?id=eq.${id}`, { method: 'DELETE' });
}
Prompt 2 — Create Directory page UI
Goal: add a page that lists entries, supports add/edit/delete/search.
Exact files to create/modify:
Acceptance criteria: page renders with a list, a form to add, buttons to edit/delete, and search input that filters list.
// src/pages/directory.tsx
import React, {useEffect, useState} from 'react';
import { listDirectory, createEntry, updateEntry, deleteEntry } from '../lib/supabaseRest';
export default function DirectoryPage() {
const [rows, setRows] = useState([]);
const [q, setQ] = useState('');
const [form, setForm] = useState({id:0,name:'',title:'',email:'',phone:''});
async function refresh() {
try {
const data = await listDirectory(q);
setRows(data);
} catch(e){ console.error(e); }
}
useEffect(()=>{ refresh() }, [q]);
async function onSave(e:any){
e.preventDefault();
try {
if (form.id) {
await updateEntry(form.id, form);
} else {
await createEntry(form);
}
setForm({id:0,name:'',title:'',email:'',phone:''});
await refresh();
} catch(e){ console.error(e); }
}
async function onDelete(id:number){
if(!confirm('Delete?')) return;
await deleteEntry(id);
refresh();
}
function startEdit(r:any){ setForm(r); }
return (
<div style={{padding:20}}>
<h2>Directory</h2>
<input placeholder="Search" value={q} onChange={e=>setQ(e.target.value)} />
<ul>
{rows.map((r:any)=>(
<li key={r.id}>
<b>{r.name}</b> — {r.title} — {r.email} — {r.phone}
<button onClick={()=>startEdit(r)}>Edit</button>
<button onClick={()=>onDelete(r.id)}>Delete</button>
</li>
))}
</ul>
<form onSubmit={onSave}>
<h3>{form.id ? 'Edit' : 'Add'}</h3>
<input placeholder="Name" value={form.name} onChange={e=>setForm({...form,name:e.target.value})} />
<input placeholder="Title" value={form.title} onChange={e=>setForm({...form,title:e.target.value})} />
<input placeholder="Email" value={form.email} onChange={e=>setForm({...form,email:e.target.value})} />
<input placeholder="Phone" value={form.phone} onChange={e=>setForm({...form,phone:e.target.value})} />
<button type="submit">Save</button>
</form>
</div>
);
}
Prompt 3 — Add Secrets in Lovable Cloud and test
Goal: configure Secrets so Preview can call Supabase.
Exact steps (Lovable UI):
Acceptance criteria: Secrets saved; Preview requests succeed (no 401).
Run this SQL in the Supabase dashboard SQL editor (not in Lovable):
-- create table in Supabase dashboard SQL editor
create table public.directory (
id serial primary key,
name text,
title text,
email text,
phone text
);
Use Lovable's Publish button. Publishing will use the same Secrets from Lovable Cloud. No terminal required. If you later need server-only code or migrations, export to GitHub from Lovable and run CLI tools locally (this is outside Lovable).
This prompt helps an AI assistant understand your setup and guide to build the feature
This prompt helps an AI assistant understand your setup and guide to build the feature
This prompt helps an AI assistant understand your setup and guide to build the feature

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Build the directory as a schema-first service: use AI code generators to create structured listing drafts, validate them server-side, store canonical data in a real DB (Supabase), secure API keys in Lovable Secrets, test with Lovable Preview and human review before Publish, and sync to GitHub when you need CLI-level changes. Keep AI outputs deterministic (low temp, schema or function-calling), add rate-limit/backoff, and always include human-in-the-loop moderation for writes.
Keep responsibilities separate:
// Express-like handler for a serverless/API route
import fetch from "node-fetch";
import { createClient } from "@supabase/supabase-js";
// // These values should come from Lovable Secrets / env
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_KEY = process.env.SUPABASE_KEY;
const OPENAI_KEY = process.env.OPENAI_API_KEY;
const OPENAI_MODEL = process.env.OPENAI_MODEL || "gpt-4";
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
export default async function handler(req, res) {
// // req.body.prompt: user input or base info
const prompt = req.body.prompt || "Local coffee shop with pastries";
// // Ask AI for strict JSON matching schema
const system = `You must output ONLY valid JSON matching the schema:
{ "name": string, "slug": string, "category": string, "description": string, "latitude": number|null, "longitude": number|null, "contact": { "email": string|null, "phone": string|null } }`;
const ua = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${OPENAI_KEY}` },
body: JSON.stringify({
model: OPENAI_MODEL,
messages: [{ role: "system", content: system }, { role: "user", content: prompt }],
temperature: 0.0
})
});
const data = await ua.json();
// // Extract text and parse JSON (be defensive)
const text = data.choices?.[0]?.message?.content || "";
let parsed;
try {
parsed = JSON.parse(text);
} catch (e) {
return res.status(500).json({ error: "AI returned non-JSON", raw: text });
}
// // Basic validation
if (!parsed.name || !parsed.slug) return res.status(400).json({ error: "missing fields" });
// // Persist to Supabase
const { data: row, error } = await supabase.from("listings").insert([parsed]).select().single();
if (error) return res.status(500).json({ error: error.message });
res.json({ listing: row });
}
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.