The Inbox is your lead-and-takeover console. /app/inbox
lists every captured lead; opening one shows the full conversation
transcript that produced it and lets you jump in as a human operator
to continue the thread.
/app/inbox shows leads on the left, sorted by recency.
Click one and the end-hand pane shows the conversation that
produced it: visitor messages on one side, agent replies on the
other, human-agent messages from past takeovers in their own role
(user / assistant / human-agent).
The Status column is an inline select — change it in place to move a lead through new → qualified → contacted → won / lost. The list also auto-refetches when the tab regains focus or you navigate back from a conversation thread, so a status edit you made elsewhere shows up without a manual reload.
The conversations index at /app/conversations covers
every conversation regardless of whether it produced a lead — useful
for spelunking past sessions that didn't convert. From there you can
open a conversation and use the same takeover controls.
Every open conversation subscribes to its private Reverb channel
(conversation.{id}) for real-time updates. New messages
appear without polling; the takeover state propagates to both the
visitor's widget and any other operator looking at the same thread.
Click Take over. A few things happen:
claimed_by_user_id + claimed_at set to you (route: POST /app/conversations/{conversation}/claim).conversation.claimed Reverb event fires on the conversation's private channel — the visitor's widget shows "Human is here" in the chat header.human-agent role message via POST /app/conversations/{conversation}/reply.
Click Hand back to bot to release (route:
POST /app/conversations/{conversation}/release). The next
visitor message goes through the RAG pipeline again. The visitor's
chat header flips back to the agent's persona. Useful when:
Leads come in two ways:
/app/inbox.lead.captured outgoing webhook so you can fan it into your CRM. See Outgoing webhooks.
The admin shell polls GET /app/leads/feed every 30
seconds for newly captured leads in the workspace and surfaces each
one as a sonner toast in the bottom-right of every page in the
admin SPA. Click the toast to jump straight to the inbox row.
The bell button in the top header asks the browser for native notification permission. Once granted, every new lead also fires an OS-level notification so workspace members get pinged on tabs that aren't focused. Permission is per-domain — denying it once can only be reversed from your browser's settings.
Polling pauses on hidden tabs to keep idle dashboards from burning
HTTP. The cursor lives in sessionStorage so opening a
second tab doesn't double-toast already-seen leads.
Every captured lead fans out to every workspace owner and admin
over email. The notification (App\Notifications\NewLeadCaptured)
is queued — the visitor's HTTP request never waits on SMTP, so a
slow mailer cannot slow lead capture or chat.
Two requirements for the email to actually arrive:
database driver — make sure
php artisan queue:work --queue=default runs on a
process supervisor (the in-cluster worker takes care of this on
Laravel Cloud). Without it, queued notifications pile up in
jobs and never send.
MAIL_MAILER + matching credentials are configured
in .env. The default is log — fine for
dev but no actual email is sent. Switch to smtp /
resend / postmark in production and
verify with php artisan tinker --execute 'Mail::raw("ping",fn($m)=>$m->to("you@example.com")->subject("test"));'.
Recipients are filtered down to workspace_users.role IN
('owner', 'admin') with accepted_at IS NOT NULL.
Pending invites and viewers do not receive lead emails. The footer
of the email reflects your white-labelled site title (set in
Settings → System).
A new Conversation row is written every time a visitor
loads the widget for the first time in 24 hours. Visitors that drive
by without typing still produce a row, which means
/app/conversations can pile up with empty sessions over
time. Two affordances keep it manageable:
nullOnDelete) so aggregate stats stay intact.
Conversation protects
cross-tenant ID smuggling — IDs from other workspaces are
silently dropped server-side.
Viewers and Editors do not see delete affordances; the
ConversationPolicy::delete +
WorkspacePolicy::bulkDeleteConversations checks require
Admin or Owner. Deletion is irreversible — there is no soft-delete
trail and the audit log does not yet capture conversation removals.
Privileged actions on conversations (claim, release) write rows to
the audit_logs table for forensic traceability. There's
no UI page for browsing them in v1; query the table directly when
you need to investigate.