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
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
Agent inbox
visitor-001
OpenTenant acme-sample
Status Open
03 · How it works
Three steps, and one wall that keeps tenants apart.
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.
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.
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.
