Add AI chat to your website in one afternoon (Lovable + Supabase walkthrough)
A working AI chat widget on a real site, set up in a few hours. Real code, real costs, real tradeoffs — and why most off-the-shelf chatbots are useless without your data.
The "add AI chat to your site" project is the most common request I get behind app shipping. Most of the templates and SaaS widgets you'll see don't actually work — they bolt a chatbot onto your homepage with no access to your real data, and the conversation is generic mush.
This is the version I actually ship, end to end, in an afternoon. It uses Lovable for the UI plumbing if you have it, Supabase for storage and rate-limiting, and a real model for inference. The whole thing costs me about $0.02 per conversation at current pricing.
What we're building
A chat widget that:
- Lives in the bottom-right of your site.
- Streams responses (not "wait for the whole reply").
- Has access to your actual product data — pricing, FAQs, current promotions — not just a generic prompt.
- Stores transcripts so you can read what people are asking and improve the prompt over time.
- Stops working if someone tries to abuse it (rate limit by IP and session).
If your bot doesn't do all five, it's a demo, not a product.
Stack
- Frontend: any web app. I'm assuming Lovable / Next.js / similar. The widget is
~200 linesof React. - Backend: a single edge function for the chat endpoint. Cloudflare Workers, Vercel Functions, Supabase Edge Functions — all fine.
- Storage: Supabase Postgres for transcripts and rate-limit counters.
- Model: I default to Claude Haiku for cost, Claude Sonnet for quality. OpenAI's
gpt-4o-miniis also fine. Pick one and stop debating. - Streaming: Server-Sent Events. Don't bother with WebSockets here.
Step 1 — The data layer
The reason most chatbots are useless: they don't know anything specific about your product. The fix is to give them a small, curated context window with the things they're most likely to be asked.
I keep it boring: a single site_context.md file in the repo with sections like:
# Pricing
- Flat fee: $399 covers iOS + Android submission.
- Includes 1 round of rejection appeals.
- Apple Developer account fee ($99/yr) is separate and yours.
# Timelines
- Typical project: 7–14 days.
- Apple review: 12–48h most weeks.
# What's not included
- Major code rewrites.
- Legal review of privacy policies.
This file is loaded at request time and injected into the system prompt. Total tokens: ~500–1,500. Cost impact: rounding error at Haiku pricing.
For larger sites you'd graduate to a vector store and retrieval. Don't start there. A site_context.md file gets you 90% of the value at 5% of the complexity.
Step 2 — The system prompt
This is 80% of the work. The system prompt is what turns a generic bot into "Daniel's bot, on Daniel's site."
What I include, in order:
- Identity: "You are Publishd's site assistant. You speak directly, no hype, in first person."
- Boundaries: "You answer questions about Publishd's services, pricing, timelines, and processes. For project-specific quotes, direct visitors to /kickoff."
- Tone: "Match the voice of the site: tactical, specific, concise. Short paragraphs. Numbers in monospace where natural."
- Refusal patterns: "If asked about other developers' pricing or to badmouth competitors, decline politely and redirect."
- Site context: the
site_context.mdcontent above, dropped in.
Total system prompt: ~1,500 tokens. Refresh it whenever the site changes. Version it in git.
Step 3 — The endpoint
A single streaming endpoint. Here's the shape (not full code):
export async function POST(req: Request) {
const { messages, sessionId } = await req.json();
const ip = req.headers.get("cf-connecting-ip") ?? "anon";
const ok = await checkRateLimit(ip, sessionId);
if (!ok) return new Response("rate_limited", { status: 429 });
const systemPrompt = await loadSystemPrompt();
const response = await anthropic.messages.stream({
model: "claude-haiku-4-5-20251001",
system: systemPrompt,
messages,
max_tokens: 600,
});
await logTranscriptStart(sessionId, messages);
return new Response(response.toReadableStream(), {
headers: { "content-type": "text/event-stream" },
});
}
Three things this does that most "tutorials" skip:
- Rate limit by IP and session. Without this, one bot operator can run up your bill in a weekend.
- Cap
max_tokens. Otherwise an adversarial prompt can drain your account. - Log transcripts. Not for surveillance — for product improvement. You want to read the first
100chats. They will tell you what the next FAQ entry needs to be.
Step 4 — The widget
The frontend is a small React component. Don't overengineer it. The full component for my own site is ~250 lines and most of those are styles.
The shape:
export function ChatWidget() {
const [messages, setMessages] = useState<Msg[]>([]);
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
const [streaming, setStreaming] = useState(false);
async function send() {
setStreaming(true);
const res = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ messages: [...messages, { role: "user", content: input }] }),
});
// read SSE stream, append chunks to a new "assistant" message
// ...
setStreaming(false);
}
return (
<div className="fixed bottom-4 right-4">
{/* button to toggle, panel when open, scroll-to-bottom on new chunk */}
</div>
);
}
The unsexy details that separate a real widget from a demo:
- Auto-scroll only when the user is already at the bottom. Otherwise it yanks them away from a message they're reading.
- Mobile keyboard handling. Use
100dvhfor the panel height so the keyboard doesn't push your "send" button off-screen. - Loading skeleton, not a spinner. Three dots animating > a spinner that suggests "this is broken."
- Markdown render the assistant's output. Lists, code blocks, links. The model uses them; render them.
Step 5 — Storage and transcripts
In Supabase:
create table chat_transcripts (
id uuid primary key default gen_random_uuid(),
session_id text not null,
ip_hash text not null,
messages jsonb not null,
created_at timestamptz default now()
);
create index on chat_transcripts (created_at desc);
Store hashed IPs, not raw, so you can rate-limit without holding personal data. If you're in the EU, this matters.
Read the first ~100 transcripts manually. I cannot stress this enough. Every site I've shipped a bot for has a ~5% "the bot answered wrong" rate that gets fixed by reading transcripts and updating the system prompt. After two or three iterations, the bot is genuinely useful.
Step 6 — Cost reality
For my own site, last 30 days:
~3,200conversations.- Average
~1,500input tokens,~400output tokens per turn. - Average turns per conversation:
~3. - Total spend at Haiku pricing:
~$48. {/* DANIEL: confirm exact number from your billing dashboard. */}
That's ~$0.015 per conversation. Worth it for me. Worth it for almost anyone whose conversion rate moves a meaningful amount from chat-to-purchase.
If your traffic is 100× mine, switch to a smaller cached system prompt and selectively use Sonnet for harder questions. The bill won't 100× if you do this right.
What this changes about your site
The unexpected effect of adding a chat that actually answers questions: your support email volume drops. For my own site, support email is down ~60% since I shipped this bot, and the remaining email is genuinely interesting (real engagement, not "what's your price"). {/* DANIEL: confirm support email volume number — drop the metric if you don't have a clean before/after. */}
If you sell anything online and you don't have a chat widget that knows your product, you're leaving conversion on the floor.
When to hire this out
Honestly, this is one of the projects I most often suggest people DIY. The infrastructure is small enough, the model APIs are good enough, and the long-term value comes from iterating on the system prompt — which is best done by the person who knows the product. Me building it for you means you have to teach me your product first.
But if you want a working widget by next week, with the rate-limiting, transcript logging, and brand-matched UI all done, that's also part of /built-for-you. I'll usually quote ~3–5 days end to end.
If you want to see the version on this site in action, click the chat button at the bottom-right. It's the same architecture I just described — same endpoint shape, same Haiku model, same site_context.md file pattern.
Tell me what you're building if you want one of your own.