Feature spec
IntermediateCategory
communication-social
Build with AI
4-10 hours with Lovable or V0
Custom build
1-2 weeks custom dev
Running cost
$0-25/mo up to 100 users · $100-300/mo at 10K users
Works on
Everything it takes to ship Customer Support Chat — parts, prompts, and real costs.
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.
What users consider table stakes in 2026
- Chat bubble visible on every page of the app without blocking any content — launcher button in the bottom-right corner is the 2026 standard position
- Instant message delivery with a typing indicator (animated dots) showing when the agent is composing a response
- Conversation history persists across sessions so users who close and reopen the chat see their previous messages
- File and screenshot attachment support with inline image preview in the chat thread
- Agent status indicator (online/away/offline) visible to users so they know response time expectations before starting a conversation
- Mobile-responsive slide-up chat panel that takes full height on small screens without horizontal overflow
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
UIFixed-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.
Note: Set z-index to 9999 on the launcher container. Lovable and V0 default to z-50 which places the widget under app modals and dropdown menus — test specifically against any full-screen overlay in your app.
Real-time messaging
BackendSupabase 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.
Note: Never implement typing indicators as DB UPDATE or INSERT operations. On a 100-message/minute conversation with 2-second typing debounce, a DB-based typing indicator generates 50+ unnecessary writes per minute and triggers Supabase usage spikes.
Conversation and message storage
DataTwo 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.
Note: Add a sequence_number column with a Supabase sequence generator (CREATE SEQUENCE message_seq; DEFAULT nextval('message_seq')). Order messages by (created_at, sequence_number) not just created_at — millisecond timestamp collisions are common under rapid typing.
Agent inbox dashboard
UISeparate /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.
Note: Use Supabase Realtime Authorization (RLS filtering for subscriptions) on the messages subscription — without it, all agents receive all messages across all conversations, which is a security and privacy problem.
File attachment handling
ServiceSupabase 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.
Note: Configure the bucket with restricted public access — attachments should only be accessible to the conversation's user and assigned agent via Supabase Storage RLS policies. Test file upload only on the published URL in Lovable — Storage signed URL generation doesn't work in the preview sandbox.
AI pre-screening bot layer (optional)
ServiceSupabase 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.
Note: Use claude-haiku-4-5 for the triage bot to keep latency under 1 second and cost per message under $0.001. Escalate to a human agent if confidence < 0.8 or if the user replies 'I need to speak to a person'.
Push notification for new messages
ServiceWeb 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.
Note: Always include a fallback: if the agent does not have notifications enabled, the inbox badge count serves as the secondary signal. Implement agent status (online/away/offline) as a presence indicator using Supabase Realtime Presence on the admin route.
The data model
Three tables: conversations, messages, and agents. Run this in the Supabase SQL Editor (Dashboard → SQL Editor):
1create sequence public.message_seq;23create type public.conversation_status as enum ('open', 'assigned', 'closed');4create type public.sender_role as enum ('user', 'agent', 'bot');5create type public.agent_status as enum ('online', 'away', 'offline');67create table public.conversations (8 id uuid primary key default gen_random_uuid(),9 user_id uuid references auth.users(id) on delete cascade not null,10 agent_id uuid references auth.users(id) on delete set null,11 status conversation_status not null default 'open',12 channel text not null default 'in_app',13 created_at timestamptz not null default now(),14 updated_at timestamptz not null default now()15);1617create table public.messages (18 id uuid primary key default gen_random_uuid(),19 conversation_id uuid references public.conversations(id) on delete cascade not null,20 sender_id uuid references auth.users(id) on delete set null,21 sender_role sender_role not null,22 content text,23 attachment_url text,24 read_at timestamptz,25 sequence_number bigint not null default nextval('public.message_seq'),26 created_at timestamptz not null default now(),27 constraint messages_content_or_attachment check (content is not null or attachment_url is not null)28);2930create table public.agents (31 id uuid primary key default gen_random_uuid(),32 user_id uuid references auth.users(id) on delete cascade not null unique,33 display_name text not null,34 status agent_status not null default 'offline',35 updated_at timestamptz not null default now()36);3738alter table public.conversations enable row level security;39alter table public.messages enable row level security;40alter table public.agents enable row level security;4142create policy "Users see only their own conversations"43 on public.conversations for select44 using (auth.uid() = user_id);4546create policy "Users can create their own conversations"47 on public.conversations for insert48 with check (auth.uid() = user_id);4950create policy "Agents see all conversations"51 on public.conversations for select52 using (exists (select 1 from public.agents where user_id = auth.uid()));5354create policy "Agents can update conversation status and assignment"55 on public.conversations for update56 using (exists (select 1 from public.agents where user_id = auth.uid()));5758create policy "Users see messages in their own conversations"59 on public.messages for select60 using (61 exists (62 select 1 from public.conversations63 where id = conversation_id and user_id = auth.uid()64 )65 );6667create policy "Agents see all messages"68 on public.messages for select69 using (exists (select 1 from public.agents where user_id = auth.uid()));7071create policy "Users can insert messages in their own conversations"72 on public.messages for insert73 with check (74 auth.uid() = sender_id75 and sender_role = 'user'76 and exists (77 select 1 from public.conversations78 where id = conversation_id and user_id = auth.uid() and status != 'closed'79 )80 );8182create policy "Agents can insert messages"83 on public.messages for insert84 with check (85 auth.uid() = sender_id86 and sender_role = 'agent'87 and exists (select 1 from public.agents where user_id = auth.uid())88 );8990create policy "Users can update read_at on messages in their conversations"91 on public.messages for update92 using (93 exists (94 select 1 from public.conversations95 where id = conversation_id and user_id = auth.uid()96 )97 );9899create policy "Anyone can read agent status"100 on public.agents for select101 using (true);102103create index messages_conversation_id_idx104 on public.messages (conversation_id, created_at asc, sequence_number asc);105106create index conversations_user_id_idx107 on public.conversations (user_id, created_at desc);108109create index conversations_status_idx110 on public.conversations (status) where status != 'closed';Heads up: 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 it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
The right path when support chat is a core product function with SLA tracking, multi-agent teams, CRM integration, or omnichannel requirements.
Step by step
- 1Implement 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
- 2Integrate with HubSpot, Salesforce, or Zendesk via their APIs to sync conversation history and create support tickets from chats automatically
- 3Build omnichannel routing: receive emails via Resend inbound, WhatsApp via Twilio, SMS via Twilio, and in-app messages all into a single unified agent inbox
- 4Add 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
Where this path bites
- 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
Third-party services you'll need
Core chat (Supabase Realtime + Storage) runs on the free tier. Costs appear with the AI triage bot, high-volume Realtime connections, and mobile push notifications at scale:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Database (conversations + messages), Realtime (live chat + typing), Storage (file attachments) | Free — 500 concurrent connections, 1GB storage | Pro $25/mo — 10,000 concurrent connections, 100GB storage |
| Anthropic Claude API | AI first-response bot classifying intent and answering common questions before human routing | No free tier — pay per token | Haiku: $1/M input tokens, $5/M output tokens (approx) |
| Firebase Cloud Messaging | Push notifications for mobile agents when a new conversation or message arrives | Free (unlimited push notifications) | Free up to high volume |
| Web Push API | Browser push notifications for desktop agents when dashboard tab is not focused | Browser-native, no service cost | Free |
| Twilio | SMS fallback for offline users who have not seen their response in-app | Trial credits only | approx $0.0075/SMS (approx) |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier covers messaging and storage at this scale. Claude API bot costs approximately $5-10/mo at 100 conversations with typical FAQ queries. FCM is free.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Typing indicator spikes Supabase usage to 100x normal
Symptom: 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
Symptom: 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
Symptom: 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
Symptom: 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
Symptom: 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
When You Need Custom Development
AI-assisted builds handle support chat for small teams well. Move to custom development when:
- SLA tracking, first-response-time reporting, and agent performance dashboards are required for an operations team — these analytics layers are not generated reliably by AI tools
- CRM integration (HubSpot, Salesforce, Zendesk) is needed to sync conversation history, auto-create tickets, and link chats to customer records
- Omnichannel support from a single inbox — receiving emails, WhatsApp messages, SMS, and in-app chats in one unified queue — requires custom routing logic
- HIPAA, SOC 2, or GDPR compliance requires audit logs of all message access, data retention controls, and encryption at rest with customer-managed keys
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
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.
Need this feature production-ready?
RapidDev builds customer support chat into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.