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.

AI integrationBy Daniel CastellaniUpdated April 20, 20267 min read
aichatlovablesupabaseopenai

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 lines of 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-mini is 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:

  1. Identity: "You are Publishd's site assistant. You speak directly, no hype, in first person."
  2. Boundaries: "You answer questions about Publishd's services, pricing, timelines, and processes. For project-specific quotes, direct visitors to /kickoff."
  3. Tone: "Match the voice of the site: tactical, specific, concise. Short paragraphs. Numbers in monospace where natural."
  4. Refusal patterns: "If asked about other developers' pricing or to badmouth competitors, decline politely and redirect."
  5. Site context: the site_context.md content 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 100 chats. 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 100dvh for 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,200 conversations.
  • Average ~1,500 input tokens, ~400 output 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.