Appearance
Inbox
The Lubb Desk module is how the ERP works with the customer chat/helpdesk inbox — which runs on the Lubb Desk server, a separate helpdesk platform on its own host, not inside the ERP itself. It gives every user a per-person "My Chats" tab carrying their own assigned queue with the live inbox embedded below it, signed in as them; surfaces the whole inbox's health as a "Chat" tab; and mirrors conversations/messages locally so they show up in a Customer or CRM Contact's interaction timeline.
The inbox is a dashboard tab, not a sidebar page — there is no Chat section in the left menu. (It briefly had one, at /chats, on 2026-08-06 before moving onto the dashboard.)
The embed and the cards above it are deliberately scoped differently. The embedded inbox always signs in whoever is viewing, while the My Chats cards are scoped to the person whose dashboard tab is open — so a superuser can read another agent's queue without the embed below it turning into their own inbox. That is why the embed renders only on your own My Chats tab.
Every organization has its own Lubb Desk account (since 2026-08-31). Lubb Desk isolates by account and by nothing else — inside one, every agent reads every conversation, contact and report — so the module follows the org selected in the switcher at the top of the sidebar: the Chat tab's figures, My Chats, the inbox embed, the Agents and Inboxes grids and the mirrored timeline are all that org's. An agent who genuinely works two orgs is a member of both accounts and moves between them from Lubb Desk's own account dropdown, which is a grant made per person rather than one inbox everybody shares. An org with no account assigned reads as "not configured" — there is deliberately no site-wide account left to fall back to.
What you can do
- Answer chats without leaving the ERP — the "My Chats" dashboard tab embeds the live inbox under your cards, signed in as you via an agent SSO hand-off, so you work your queue in place. If the Lubb Desk server's headers don't permit framing, the tab hands you a signed-in link that opens Lubb Desk in its own tab instead.
- Know when a chat arrives — the My Chats tab shows a red dot the moment new activity lands on a conversation assigned to you, while you're on any other dashboard tab. It clears when you open the tab.
- Dashboard "My Chats" tab — one person's own assigned conversations: cards counting their open, pending and total assigned, plus a list of the active queue linking into Lubb Desk. Assignable per-person in Dashboard settings.
- Dashboard "Chat" tab — see live cards for open conversations, unassigned conversations, new conversations and resolutions over the selected period, average first-response time and average resolution time, incoming/outgoing message counts, and total contacts.
- Spot dropped conversations at a glance — the Open and Unassigned cards render in red the moment anything is waiting, so an unattended chat is visible without opening the tab.
- Track service-level trends — a "new conversations per day" line chart, and first-response/resolution-time cards graded green/amber/red against built-in targets (first response within 60 minutes, resolution within 24 hours).
- Jump straight into Lubb Desk — every card and the recent-conversations list link out to the matching view in Lubb Desk (inbox, reports, or contacts).
- Browse recent conversations — a "Recent conversations" list of the 10 most recently active conversations, showing who's writing, a snippet of their last message, and status.
- See chat history on a Customer/Contact record — the Customer drawer's Interactions tab, and the CRM Contact/Deal drawers' Interactions tab, include Lubb Desk messages merged in with calls and emails, newest first.
- Link a Lubb Desk contact to a Customer or CRM Contact manually — when the automatic email/phone match misses (or matched the wrong record), link a Lubb Desk contact ID to the correct Customer or Contact by hand.
- Configure the Lubb Desk connection — set the Lubb Desk URL and Platform App token on LubbDesk → Settings (or the Remote Connections grid / environment fallback), and fetch the webhook URL/secret to paste into Lubb Desk's own webhook settings.
- Give each organization its own account — LubbDesk → Accounts lists every org on the site with the Lubb Desk account it owns, and assigns or disconnects one. An account id already held by another org is refused rather than duplicated.
Common tasks
Check today's chat volume on the dashboard Open the ERP dashboard and switch to the "Chat" tab. The cards show the current open/unassigned counts plus new conversations, resolutions, and message volumes for the selected reporting window (days).
Investigate an unassigned or slow-response conversation From the Chat tab, click the Open or Unassigned card (or an item in the recent-conversations list) to jump straight into that inbox view in Lubb Desk, where the conversation can be picked up or reassigned.
Review response-time trends Look at the "Avg first response" and "Avg resolution" cards — they're colour-graded (green/amber/red) against the built-in targets — and click through to Lubb Desk's Reports Overview for the full breakdown, or follow the "New conversations per day" chart for volume trends.
See a customer's full chat history Open a Customer record and go to its Interactions tab (or a CRM Contact/Deal's Interactions tab) — Lubb Desk messages for that person appear inline with their calls and emails, matched automatically by email/phone.
Link a conversation to the right Customer/Contact manually If a Lubb Desk conversation didn't auto-match (or matched the wrong person), find the Lubb Desk contact ID in Lubb Desk and use the manual link action on the correct Customer or CRM Contact to attach it — this also backfills any of that contact's existing conversation rows.
Connect an organization to its Lubb Desk account Open LubbDesk → Accounts, select the org, and use Assign Account with its Lubb Desk account ID (and, if it should hold its own credential rather than the install's, an API access token). The first assignment on a site retires the legacy site-wide account for everyone, so on a multi-org site assign the rest before anybody notices their Chat tab has gone quiet.
Set up the Lubb Desk webhook (one-time, per site) Fetch the webhook URL and secret for this site, then paste them into Lubb Desk under Settings → Integrations → Webhooks, subscribed to Conversation Created/Updated, Message Created and Contact Updated. A secret is auto-generated the first time this is fetched if none exists yet.
Data model
Lubb Desk itself stays the system of record; these tables are a local, queryable mirror fed by the webhook, plus link columns on existing tables. Everything mirrored is scoped to the organization that owns the Lubb Desk account it arrived on, and both mirror tables are keyed on (tenant_id, <Lubb Desk id>) — Lubb Desk numbers conversations and messages within an account, so the id alone stopped being an identity the moment a second account existed.
| Table / column | Purpose |
|---|---|
chatwoot_conversations | One row per Lubb Desk conversation — inbox, channel (website/whatsapp/email/etc.), status, assignee, a snapshot of the contact's name/email/phone, and the matched customer_id/contact_id (either may be null until matched). |
chatwoot_conversations.tenant_id | The org that owns the account this conversation came from. NULL means it was mirrored before the split; app.scripts.split_chatwoot_shared_account_2026_08_31 claims those for the org that takes the shared account. |
chatwoot_messages | One row per Lubb Desk message on a mirrored conversation — direction (incoming/outgoing/note), content, sender name, and Lubb Desk's own sent-at timestamp. Carries its conversation's tenant_id so every read can filter without a join. |
acc_customer.chatwoot_contact_id + .chatwoot_account_id | The Lubb Desk contact this Customer is linked to, and which account that id belongs to. The pair is the link: contact ids repeat across accounts, and acc_customer itself is site-wide (the books have no tenant column — each business gets its own schema instead). |
crm_contacts.chatwoot_contact_id + .chatwoot_account_id | The same for a CRM Contact, which is additionally tenant-scoped in its own right. |
Configuration & integrations
- The module talks to a single external Lubb Desk installation (one Rails install on 156.0.96.100; the per-tenant
<slug>.chat.lubb.co.zanames are aliases of it, not separate deployments) over its REST API, authenticated with a user API access token belonging to an account administrator (Lubb Desk's v2 reports endpoints are admin-only), sent as theapi_access_tokenheader. - The install is site-wide; the account is per organization. URL, Platform App token and webhook secret describe the install and are stored once on the
Lubb DeskRemote Connections row (falling back toCHATWOOT_URL/CHATWOOT_PLATFORM_TOKEN/CHATWOOT_WEBHOOK_SECRET). The account ID — the only thing Lubb Desk actually isolates on — is held per org on that same row and edited on LubbDesk → Accounts. An org may also hold its own API token; otherwise the install's administrator token is used, addressed to that org's account id. - A site that has assigned no accounts still resolves the legacy site-level
account_id/token, so nothing changed until somebody splits. The first assignment retires that fallback for every org on the site — going dark is a support ticket, falling back into another org's inbox is a breach. - Inbound sync runs the other way: Lubb Desk pushes events (Conversation Created/Updated, Message Created and Contact Updated) to this site's webhook receiver, gated by a shared
?secret=query parameter (Lubb Desk has no built-in webhook signing). One URL serves every org — the account ID inside each payload is what decides whose mirror it lands in, and an event for an account no org has claimed is discarded rather than guessed at. The webhook URL and secret are fetched from the API and pasted into Lubb Desk's own webhook settings; the secret is auto-provisioned on first fetch if not already set. - Incoming Lubb Desk contacts are auto-matched to an existing Customer or CRM Contact by email or phone (never creates a new record) and the match is cached on
chatwoot_contact_id+chatwoot_account_idso later events for the same contact skip straight to it. The CRM half of the match is narrowed to the owning org; the Customer half is site-wide, as the books are, and is kept honest by every read of the mirror being scoped instead. - One thing the split gives up: the dashboard's yesterday-vs-today movement badge on ruled Chat cards.
dashboard_card_snapshotsis keyed(site, category, days, card_key, date)with no room for an org, so on a split site nothing is recorded rather than one org's numbers under the site's name. The Chat tab itself is unaffected — it reads live.
API reference
| Method & Path | Purpose |
|---|---|
POST /webhooks/chatwoot?secret=… | Webhook receiver — ingests message_created, conversation_created/conversation_updated, and contact_created/contact_updated events from Lubb Desk; unauthenticated, gated by the shared secret. The account id in the payload selects the owning org; an event for an unclaimed account is discarded. Always returns 200 on anything accepted or intentionally ignored. |
GET /chatwoot/webhook-info | Returns the webhook URL and secret to paste into Lubb Desk's settings, auto-provisioning a secret on first call. |
GET /chatwoot/launch | Mints a single-use agent SSO URL into the caller's active org's Lubb Desk account, and reports whether Lubb Desk's live headers permit embedding it. Powers the My Chats tab's embedded inbox; degrades to a reason code (not_configured, unreachable, not_an_agent, no_sso, sso_failed) plus a fallback link rather than erroring. |
GET /erp/my-chats | The My Chats tab's cards and assigned-conversation list for one person, within the active org's account. user_id scopes it to another user (superuser only). |
GET /erp/my-chats/signal | The cheap poll behind the Lubb Desk tab's "new chat" dot — newest assigned-to-me activity timestamp plus unread count, always for the caller. Reads one page of conversations rather than the full sweep, because every signed-in user polls it continuously. |
POST /customers/{customer_id}/chatwoot-link | Manually link a Lubb Desk contact ID to an AccCustomer, and backfill any of that contact's existing conversation rows. |
POST /crm/contacts/{contact_id}/chatwoot-link | Manually link a Lubb Desk contact ID to a CRM Contact, and backfill any of that contact's existing conversation rows. |
GET /customers/{customer_id}/interactions | Merged interaction timeline (calls, emails, Lubb Desk messages) for a Customer. |
GET /erp/sites/{site}/stats | Powers the dashboard tabs generally; for the "chat" category this returns the Chat tab's cards, chart, and recent-conversations items, sourced live from Lubb Desk's API rather than the local mirror tables. |
There is no dedicated grid CRUD surface for chatwoot_conversations / chatwoot_messages — they're written only by the webhook receiver and read via the interaction-timeline endpoints above, not exposed as an editable admin grid.