← All case files

Case file 013Live chat widget

The problem

“People land on our site with a question, and leave before anyone knows they were there.”

Solved

For
Small businesses answering their own website visitors
Built
A live chat MVP: embed snippet, agent inbox, one backend for many businesses
Stack
Vite widget · React admin · Firebase Auth, Firestore, rules, Functions
Result
Metric to confirm
Before: the message arrives, and nobody is there3D pixel film · made by me

01 · The pain

The visitor had a question. Nobody was there to hear it.

  • “Is anyone there?” typed into a contact form that answers in two days.
  • A visitor with a question and a card in their hand, closing the tab.
  • The one message that did come in seen hours later, after they had gone.

02 · The fix

A visitor on your site. An agent inbox on yours.

You play both sides. Write as the visitor and watch it land in the inbox. Reply or close the chat as the agent and watch the visitor’s side change.

Sample visitor site

Chat with us

Hi there! How can we help?

Agent inbox

visitor-001

Open

YouHi there! How can we help?

Tenant acme-sample

Status Open

03 · How it works

Three steps, and one wall that keeps tenants apart.

  1. Embed one snippet on any website

    One script tag with the business's ID. A chat bubble appears in the corner, and a visitor can write without signing up.

  2. Messages arrive in your agent inbox

    Each visitor gets one conversation. New messages appear on the agent dashboard as they are sent, with the latest message and whether the chat is still open.

  3. Each business sees only its own chats

    Every conversation carries the business it belongs to, and so does every agent's login. The database compares the two before it hands anything over.

The hard part

One database, many businesses, no leaks between them.

The widget runs in a stranger’s browser, so nothing it says can be trusted. The rule can’t live in the app; it has to live in the database.

An agent’s login carries their business, and a chat is only handed over when it matches. A visitor can read only their own conversation. Even an allowed edit is narrow: an agent can change the status and the last message, never which business the chat belongs to.

In code

where('tenantId', '==', tenantId) .where('visitorId', '==', visitorId) .limit(1)

In rules

allow read: if isTenantAgent(resource.data.tenantId)

In transactions

if (!conversation.exists() || conversation.get('tenantId') !== tenantId) throw new Error('Not found')

04 · What changed

Conversations became events someone acts on.

Businesses using it
—
Conversations handled
—
Time to first reply
—

Built as an MVP. Blanks: metrics to confirm.