# How to Add Customer Support Chat to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

A support chat widget needs five pieces: a fixed-position launcher with unread badge, a Supabase Realtime channel per conversation, a messages table with sender role tracking, an agent inbox dashboard, and typing indicators via Broadcast — never DB writes. Ship it with Lovable in 4-10 hours for $0-25/month. Optionally add a Claude API bot for instant first-response triage before routing to a human agent.

## What a Customer Support Chat Feature Actually Requires

A support chat widget is more than a chat interface — it requires three distinct surfaces working together: the user-facing widget (embedded on every page, always visible), the Supabase Realtime backend (delivering messages and typing indicators in milliseconds), and the agent inbox dashboard (a separate route where support staff see and respond to all open conversations). The trickiest architectural decision is where to put the typing indicator: it must use Supabase Realtime Broadcast, never a database INSERT or UPDATE, or it will trigger hundreds of Supabase writes per minute per active conversation and spike your usage bill. Add an optional Anthropic Claude API bot layer for instant first-response that handles common questions before routing to a human.

## Anatomy of the Feature

Seven components. The chat widget and Realtime messaging generate well with AI tools. The agent inbox Realtime authorization and typing-via-Broadcast pattern are where builds consistently break in production.

- **Chat widget launcher** (ui): Fixed-position button in the bottom-right corner using shadcn/ui Button (circular, with a message icon) and Radix UI Popover or Sheet for the chat panel. Shows an unread message count badge that updates in real time via Supabase Realtime. Opens to full chat panel with Framer Motion slide-in animation.
- **Real-time messaging** (backend): Supabase Realtime Postgres Changes subscription filtered by conversation_id delivers new messages to both parties as soon as they are INSERTed. Typing indicators use Supabase Realtime Broadcast (a separate ephemeral channel per conversation) — no database write is needed for typing events.
- **Conversation and message storage** (data): Two Supabase tables: conversations (id, user_id, agent_id, status enum, channel, created_at, updated_at) and messages (id, conversation_id, sender_id, sender_role enum[user/agent/bot], content, attachment_url, read_at, created_at). A sequence_number column on messages prevents ordering issues when rapid sends share the same millisecond timestamp.
- **Agent inbox dashboard** (ui): Separate /admin/support route listing all open conversations with unread counts, last message preview, and user identifier. Agents can assign conversations to themselves, see conversations assigned to others, and filter by status. New conversation alerts arrive via Supabase Realtime Postgres Changes on the conversations table.
- **File attachment handling** (service): Supabase Storage bucket named 'support-attachments' with a file type whitelist (jpg, png, gif, pdf, max 5MB). The upload function generates a signed URL after upload and stores it in messages.attachment_url. Inline image preview renders the signed URL in an img tag inside the message bubble.
- **AI pre-screening bot layer (optional)** (service): Supabase Edge Function triggered on the first user message in a new conversation calls the Anthropic Claude API with a system prompt containing your FAQ and product knowledge. Returns { answered: boolean, response: string, confidence: number }. If confidence >= 0.8, inserts an automatic bot reply (sender_role = 'bot') before routing to a human. Reduces human agent workload by 30-60% for common questions.
- **Push notification for new messages** (service): Web Push API (via the browser Notifications API + a service worker) notifies desktop agents when a new conversation arrives while the dashboard tab is not focused. Firebase Cloud Messaging (FCM) handles push notifications for mobile apps built with FlutterFlow.

## Data model

Three tables: conversations, messages, and agents. Run this in the Supabase SQL Editor (Dashboard → SQL Editor):

```sql
create sequence public.message_seq;

create type public.conversation_status as enum ('open', 'assigned', 'closed');
create type public.sender_role as enum ('user', 'agent', 'bot');
create type public.agent_status as enum ('online', 'away', 'offline');

create table public.conversations (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  agent_id uuid references auth.users(id) on delete set null,
  status conversation_status not null default 'open',
  channel text not null default 'in_app',
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table public.messages (
  id uuid primary key default gen_random_uuid(),
  conversation_id uuid references public.conversations(id) on delete cascade not null,
  sender_id uuid references auth.users(id) on delete set null,
  sender_role sender_role not null,
  content text,
  attachment_url text,
  read_at timestamptz,
  sequence_number bigint not null default nextval('public.message_seq'),
  created_at timestamptz not null default now(),
  constraint messages_content_or_attachment check (content is not null or attachment_url is not null)
);

create table public.agents (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null unique,
  display_name text not null,
  status agent_status not null default 'offline',
  updated_at timestamptz not null default now()
);

alter table public.conversations enable row level security;
alter table public.messages enable row level security;
alter table public.agents enable row level security;

create policy "Users see only their own conversations"
  on public.conversations for select
  using (auth.uid() = user_id);

create policy "Users can create their own conversations"
  on public.conversations for insert
  with check (auth.uid() = user_id);

create policy "Agents see all conversations"
  on public.conversations for select
  using (exists (select 1 from public.agents where user_id = auth.uid()));

create policy "Agents can update conversation status and assignment"
  on public.conversations for update
  using (exists (select 1 from public.agents where user_id = auth.uid()));

create policy "Users see messages in their own conversations"
  on public.messages for select
  using (
    exists (
      select 1 from public.conversations
      where id = conversation_id and user_id = auth.uid()
    )
  );

create policy "Agents see all messages"
  on public.messages for select
  using (exists (select 1 from public.agents where user_id = auth.uid()));

create policy "Users can insert messages in their own conversations"
  on public.messages for insert
  with check (
    auth.uid() = sender_id
    and sender_role = 'user'
    and exists (
      select 1 from public.conversations
      where id = conversation_id and user_id = auth.uid() and status != 'closed'
    )
  );

create policy "Agents can insert messages"
  on public.messages for insert
  with check (
    auth.uid() = sender_id
    and sender_role = 'agent'
    and exists (select 1 from public.agents where user_id = auth.uid())
  );

create policy "Users can update read_at on messages in their conversations"
  on public.messages for update
  using (
    exists (
      select 1 from public.conversations
      where id = conversation_id and user_id = auth.uid()
    )
  );

create policy "Anyone can read agent status"
  on public.agents for select
  using (true);

create index messages_conversation_id_idx
  on public.messages (conversation_id, created_at asc, sequence_number asc);

create index conversations_user_id_idx
  on public.conversations (user_id, created_at desc);

create index conversations_status_idx
  on public.conversations (status) where status != 'closed';
```

The message_seq sequence ensures correct ordering when two messages are inserted within the same millisecond — order by (created_at, sequence_number) not just created_at. The agents table controls who can see all conversations; anyone not in the agents table sees only their own.

## Build paths

### Lovable — fit 4/10, 5-8 hours

Supabase Realtime and Edge Functions are Lovable's native stack — the chat widget and admin inbox generate reliably. The AI bot layer via Lovable's built-in Anthropic connector requires one follow-up prompt to configure correctly.

1. Create a Lovable project with Lovable Cloud so Supabase auth and the conversations/messages schema are provisioned automatically
2. Paste the prompt below; Lovable will generate the chat widget launcher, message thread component, and the admin inbox dashboard
3. After generation, open the generated typing indicator code and verify it uses Supabase Realtime Broadcast channel (supabase.channel('typing-{conversationId}').send()) — not a DB UPDATE or INSERT
4. Go to Lovable Cloud tab → Storage → create a 'support-attachments' bucket; test file upload only on the published URL
5. Publish and test: open two browser windows — one as user, one as agent — and verify messages appear in real time on both sides

Starter prompt:

```
Build a customer support chat feature. Schema: conversations table (id, user_id, agent_id, status enum[open/assigned/closed], channel, created_at, updated_at) and messages table (id, conversation_id, sender_id, sender_role enum[user/agent/bot], content, attachment_url, read_at, sequence_number bigint using a Supabase sequence, created_at). Agents table (id, user_id, display_name, status enum[online/away/offline]). RLS: users see only their own conversations and messages; authenticated agents (users in agents table) see all conversations and all messages. Chat widget: fixed-position launcher button (bottom-right, z-index 9999) with unread message count badge; clicking opens a Sheet or Popover panel; shows the active conversation thread; messages ordered by (created_at, sequence_number). Message input: textarea, send button, file attachment upload to Supabase Storage 'support-attachments' bucket (jpg/png/pdf max 5MB), file type validation. Typing indicator: use Supabase Realtime Broadcast channel 'typing-{conversationId}' — NEVER use a DB INSERT or UPDATE for typing events; show animated dots when a typing event is received; stop after 3 seconds of no event. Read receipts: UPDATE messages.read_at = now() when the agent views the message; show read receipt tick in the user's chat UI. Auto-scroll to latest message on new INSERT. Offline queue: if Realtime disconnects, store outgoing messages in a localStorage queue and flush on reconnect. New conversation auto-created on first message with status = 'open'. Agent inbox dashboard at /admin/support: lists all open conversations with last message preview and unread count; clicking opens the conversation thread; agent can assign conversation to themselves (sets status = 'assigned', agent_id = auth.uid()); close button sets status = 'closed'. Agent status badge showing online/away/offline. Optional AI triage: on first user message, call a Supabase Edge Function that calls Anthropic claude-haiku-4-5 with a system prompt listing common FAQs; if confidence >= 0.8, auto-reply with sender_role = 'bot'; always give user option to escalate to human.
```

Limitations:

- Complex multi-agent inbox routing (round-robin, skill-based assignment) is not what Lovable generates reliably — expect manual work for teams of 5+ agents
- File attachments need manual Supabase Storage bucket creation in the Cloud tab; test uploads only after publishing
- The AI bot layer requires configuring Anthropic API key in Lovable Cloud tab → Secrets and a separate Edge Function — expect one follow-up prompt to get the confidence threshold logic correct

### V0 — fit 3/10, 6-10 hours

V0 generates clean chat UI components and Next.js API routes for message submission. Supabase Realtime subscription and the agent dashboard need 'use client' wrappers that V0 usually gets right but occasionally misses.

1. Prompt V0 with the spec below; it will generate the ChatWidget, MessageThread, and AgentInbox components
2. Open the Vars panel and add NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and ANTHROPIC_API_KEY
3. Run the SQL schema from af_data_model in your Supabase SQL Editor
4. Verify the typing indicator uses Supabase Broadcast (not a database write) in the generated code
5. Deploy to Vercel and test both user and agent views in separate browser sessions

Starter prompt:

```
Build a customer support chat feature in Next.js with Supabase. Component 1 (use client): ChatWidget — fixed-position launcher button (bottom-right, z-index 9999) with unread count badge from Supabase Realtime; Radix Sheet for chat panel; MessageThread showing messages ordered by (created_at, sequence_number); auto-scroll ref on new message; typing indicator using Supabase Broadcast channel 'typing-{conversationId}' (never a DB write); send message via Server Action; file upload to Supabase Storage with signed URL stored in messages.attachment_url; inline image preview for image attachments. Component 2 (use client): AgentInbox — lists all open conversations with Supabase Realtime Postgres Changes subscription; shows last message preview and unread count per conversation; conversation detail pane with message thread; assign button sets conversation status and agent_id via Server Action; close conversation button. Component 3: API route at /api/support/triage — receives first message content, calls Anthropic claude-haiku-4-5 API with FAQ system prompt, returns { answered: boolean, response: string, confidence: number }; if confidence >= 0.8, insert bot reply into messages table. Typing indicator: on input change, broadcast to 'typing-{conversationId}' with debounce 500ms; recipient shows animated dots for 3 seconds after last event. Read receipts: UPDATE messages.read_at when agent opens a conversation; display read tick in user chat. Message ordering guard: always ORDER BY created_at ASC, sequence_number ASC. Offline message queue in localStorage with flush on Realtime reconnect. RLS matching the conversations/messages/agents schema described above.
```

Limitations:

- V0 does not auto-provision Supabase — the conversations/messages/agents schema must be created manually in the Supabase SQL Editor
- The agent dashboard is a separate route that V0 generates well but does not deploy as a unified authenticated app — you need to add your existing auth middleware
- Server-Sent Events or Supabase Realtime subscription in a 'use client' component requires manual verification that V0 didn't place the subscription in a Server Component

### Flutterflow — fit 3/10, 1-2 days

Firebase Firestore real-time listeners work well for mobile chat; FlutterFlow's built-in chat templates provide a starting scaffold. Push notifications via Firebase Cloud Messaging are a native FlutterFlow action. The agent desktop dashboard is not buildable in FlutterFlow (mobile only).

1. Use FlutterFlow's built-in chat template as a starting point — it scaffolds message list and send input with Firestore real-time stream
2. Customize the message bubble design to include file attachment preview and sender role badges (user vs agent vs bot)
3. Add a Custom Action for typing indicator using Firestore realtime field updates with a debounced write (less ideal than Broadcast but workable in FlutterFlow)
4. Configure Firebase Cloud Messaging push notifications using FlutterFlow's native Push Notification action on new message events
5. For agent status (online/away/offline), update a Firestore agents collection document from the agent's mobile app

Limitations:

- The agent inbox dashboard cannot be built in FlutterFlow for desktop — agents need a separate web app for the support dashboard
- File upload widget requires a FlutterFlow marketplace extension (file_picker + Firebase Storage) and does not support inline image preview natively
- Typing indicator in FlutterFlow requires a Firestore write per keystroke (unlike Supabase Broadcast) — add a 500ms debounce in a Custom Action to avoid excessive writes

### Custom — fit 5/10, 1-2 weeks

The right path when support chat is a core product function with SLA tracking, multi-agent teams, CRM integration, or omnichannel requirements.

1. Implement SLA tracking: add a first_response_at column and compute response time per conversation; build an agent performance dashboard showing average response time, CSAT score, and resolution rate
2. Integrate with HubSpot, Salesforce, or Zendesk via their APIs to sync conversation history and create support tickets from chats automatically
3. Build omnichannel routing: receive emails via Resend inbound, WhatsApp via Twilio, SMS via Twilio, and in-app messages all into a single unified agent inbox
4. Add CSAT survey: on conversation close, send a one-click satisfaction rating via Resend email; store results in a csat_ratings table linked to the conversation

Limitations:

- Custom support chat with SLA, CRM sync, and omnichannel routing is a 4-6 week project — only justified when support operations are a significant business differentiator

## Gotchas

- **Typing indicator spikes Supabase usage to 100x normal** — AI tools default to implementing typing indicators as a DB UPDATE on every keypress — setting a typing_at column in the conversations table or inserting ephemeral 'typing' messages. In an active conversation with fast typists, this generates 1-3 DB writes per second per conversation. At 100 concurrent chats, that is 100-300 unnecessary writes per second that consume Supabase free tier quota within minutes. Fix: Use Supabase Realtime Broadcast for typing events — a fire-and-forget WebSocket message with zero DB writes. On the sender side: supabase.channel('typing-{id}').send({ type: 'broadcast', event: 'typing', payload: {} }). On the receiver side: subscribe to the same channel and show animated dots for 3 seconds after the last event.
- **Chat widget z-index conflict hides it under modals** — Lovable and V0 generate the fixed-position chat launcher with z-index: 50 (Tailwind z-50 class). Any modal, dialog, or toast notification in your app that uses z-index: 50 or higher renders on top of the chat widget. Users see a ghost button that appears to be unclickable when a modal is open. Fix: Set the chat widget container to z-index: 9999 in a custom CSS class. In Tailwind, add a z-[9999] utility class. Test specifically against every modal and slide-over in your app before launch.
- **Agents receive all users' messages due to missing Realtime RLS** — Adding a Supabase Realtime Postgres Changes subscription on the messages table without row-level filtering sends every inserted message from every user to every connected agent. In a multi-agent inbox with 50 concurrent conversations, each agent receives 50x the messages they need, causing performance issues and a data privacy problem. Fix: Enable Supabase Realtime Authorization by adding an RLS policy on the messages table that the Realtime subscription respects. Agents subscribe with a filter: .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', filter: 'conversation_id=eq.' + conversationId }, callback). Each agent should only subscribe to conversations assigned to them.
- **File attachment upload fails in Lovable preview** — Supabase Storage signed URL generation requires the Edge Function to be invoked from the published app domain. The Lovable preview sandbox runs under a different subdomain that is not authorized in the Supabase Storage CORS config, causing upload requests to fail with a CORS or 403 error. Fix: Create the Storage bucket and configure CORS in Supabase Dashboard (Storage → Policies → add your published domain). Test file upload only after clicking Publish in Lovable. Do not file bug reports about attachment failures observed in the preview panel.
- **Message ordering breaks under rapid sends** — Two messages inserted 1-5 milliseconds apart share the same created_at timestamp at millisecond precision. PostgreSQL returns rows with identical timestamps in an undefined order, causing the chat thread to randomly flip message order when a user types quickly or pastes a long message that gets split. Fix: Add a sequence_number column using a Supabase CREATE SEQUENCE object with DEFAULT nextval('your_seq'). Order messages by (created_at ASC, sequence_number ASC) in all queries. The sequence guarantees a strict ordering even for same-millisecond inserts.

## Best practices

- Always use Supabase Realtime Broadcast for typing indicators — never a database write. One DB write per keypress per active conversation is a guaranteed way to exhaust free tier quotas
- Add a sequence_number column to the messages table and order by (created_at, sequence_number) to prevent message ordering inversions under rapid concurrent sends
- Set the chat widget container z-index to 9999 and explicitly test against every modal, slide-over, and toast notification in your app to confirm the launcher stays on top
- Implement the offline message queue in localStorage from the start: store outgoing messages if Supabase Realtime disconnects, and flush the queue on reconnect. Users on mobile with flaky connections will hit this regularly
- Add a conversation status machine (open → assigned → closed) with clear visual state in both the user widget and agent inbox — agents need to see at a glance which conversations need attention
- Cache common AI bot responses by question category to reduce Anthropic API calls: if 80% of first messages ask about pricing or shipping, pre-compute those answers and skip the API call
- Set a read receipt update debounce of 2 seconds when the agent scrolls through old messages — marking every message read individually as the agent scrolls generates excessive UPDATE calls

## Frequently asked questions

### Should I build my own support chat or use a third-party tool like Intercom?

Use Intercom or Crisp if support chat is a utility for your app, not a differentiator. Build your own when: you need the chat deeply integrated with your data model (e.g., showing the user's order history inside the chat), when pricing per seat becomes prohibitive at scale, or when your support workflow is custom enough that Intercom's limited customization creates friction.

### How do I add a support chat widget to every page of my app?

Place the ChatWidget component in your root layout file — in Next.js App Router this is app/layout.tsx, in Lovable it lives in a shared App.tsx or layout component. The widget renders as a fixed-position element that persists across route changes without remounting. Initialize the Supabase Realtime subscription once at mount in the widget root, not per-page.

### Can the chat widget handle file attachments?

Yes — upload files to a Supabase Storage bucket named 'support-attachments', store the signed URL in messages.attachment_url, and render inline image previews for image types. Set a 5MB file size limit enforced on the client (before upload) and a file type allowlist (jpg, png, pdf) enforced in the Supabase Storage bucket policy. Test file upload on the published URL, not in the Lovable preview.

### How do I notify support agents when a new message arrives?

Two signals work in tandem: the Supabase Realtime Postgres Changes subscription on the conversations table updates the unread count badge in the agent inbox in real time. For agents with the tab in the background, use the Web Push API (a service worker + Notifications permission) to send a browser push notification when a new conversation is inserted. For mobile agent apps, use Firebase Cloud Messaging.

### Can I add an AI bot that answers common questions first?

Yes — a Supabase Edge Function triggered on the first message in a new conversation calls the Anthropic Claude API (use claude-haiku-4-5 for speed and cost) with a system prompt containing your FAQ. Return a confidence score: if >= 0.8, insert an automatic bot reply with sender_role = 'bot' before notifying a human agent. Always show an 'escalate to human' button in the bot reply.

### How do I track whether customer issues were resolved?

Add a CSAT (Customer Satisfaction) survey: when an agent sets conversation status to 'closed', trigger a Supabase Edge Function that sends a short email via Resend with a 1-5 star rating link. Store the rating in a csat_ratings table linked to the conversation. Display average CSAT per agent and per month in the agent dashboard.

### Is Supabase Realtime fast enough for live support chat?

Yes for most support chat use cases. Supabase Realtime Postgres Changes typically delivers new messages within 50-200ms on the free tier. At 10,000 concurrent conversations you'll need Supabase Pro for the higher connection limit, and potentially a read replica to keep message history queries fast. For sub-50ms latency at massive scale, purpose-built services like Stream Chat are faster but at significantly higher cost.

### How do I build a separate agent inbox dashboard?

Create a /admin/support route protected by an agents table RLS check (only rows in the agents table can access agent-level data). The inbox page has two panels: a conversation list on the left (Supabase Realtime subscription on conversations filtered by status = 'open') and a message thread on the right (Supabase Realtime subscription filtered by conversation_id). Add an agents table with a user_id, display_name, and status column — manage agent membership in the Supabase Dashboard.

---

Source: https://www.rapidevelopers.com/app-features/customer-support-chat
© RapidDev — https://www.rapidevelopers.com/app-features/customer-support-chat
