You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Let a host app give each tenant/customer org its own private support desk — its own visitors, its own inbox, its own agents — without the gem taking a foreign key into the host's models. Each tenant's members (visitors, signed-in or anonymous) talk to that tenant's admins (agents); no tenant can see another's threads, inbox, or notifications. It stays strictly visitor↔team — tenancy just partitions which team and which visitors belong together (no group chat, no member-to-member chat).
Targeted for a later release (v2/v3) if there's demand — not urgent. Single-tenant apps must keep working with zero config.
Sibling reference implementations already shipped with this exact recipe: yshmarov/testimonials#3 (v0.5.0) and yshmarov/ideasbugs#2 (v0.6.0). livechat is deliberately not a copy-paste of those — see "What makes livechat different" below.
Same loose-coupling rule as the rest of the gem — visitor/agent identity is already loose strings (visitor_id, visitor_token, agent_id, agent_label; no belongs_to :user). Multi-tenancy follows suit.
Foundation:
One nullable column tenant (string) on livechat_conversations. nil = the single global desk (today's behavior, unchanged). Messages inherit the tenant via their conversation.
A request-resolver hook shaped exactly like current_user / authorize_agent:
The key is opaque and host-defined: a GlobalID, an org id, a subdomain, a slug — the engine never needs to know what an Organization is.
Flow (uniform):
A conversation is stamped with the resolved tenant when it starts.
Conversation.for_visitor(...) scopes by tenant, so "one open thread per visitor" becomes one per visitor, per tenant. Guest→signed-in claim! is scoped so a guest thread is only adopted within the same tenant.
The inbox (/livechat) scopes every query to the current tenant — list, counts, the set_conversation finder (a cross-tenant id 404s), poll, index_poll.
Attachment serving already gates on owns-conversation-or-agent — add tenant to the finder.
Optional acts_as sugar (a veneer over the string, not a new coupling):
classOrganization < ApplicationRecordhas_livechat# def conversations = Livechat.for(self) -> where(tenant: to_gid.to_s)endorganization.conversations.open# normal AR relation
Apps that want organization.conversations include the concern; apps that don't still get full multi-tenancy through the resolver alone. Foundation = the string; concern = sugar.
Why string+resolver over polymorphic
No FK into host tables; no owner_type = "Organization" class-name coupling (rename-safe).
Host defines what the key means (gid/id/subdomain/slug).
Consistent with the gem's existing identity primitive: opaque strings resolved from the request by host lambdas.
The visitor write path is the widget; the tenant is resolved server-side from the request (same as visitor_id), never trusted from the client.
What makes livechat different from the testimonials/ideasbugs recipe
Those are one-way collection tools; livechat is a live two-sided conversation with realtime and notifications. Three things are genuinely new here — the ~20% that isn't mechanical:
Notification routing becomes tenant-aware (the real "decision point File attachments in conversations #1"). Today config.agent_emails is one global list — "email the support team." Per-tenant, the recipients depend on the conversation's tenant (Org A's admins, not everyone). So agent_emails should be able to receive the conversation/tenant, e.g. ->(conversation) { ... }, instead of a flat array. This is the analog of "tenant-aware authorization" in the sibling issues, and the one place needing real care. mailer_from may also want to be tenant-aware.
The realtime inbox stream must be partitioned per tenant. The per-conversation Action Cable stream (livechat:conversation:<id>) and its signed token are already isolated. But the shared livechat:inbox stream and the index_poll token would leak cross-tenant activity — an Org A agent would get "new message" nudges from Org B. So the inbox stream becomes livechat:inbox:<tenant> and the poll token is computed per tenant. (Polling stays the fallback, unchanged otherwise.)
"Who is the visitor" is load-bearing. livechat supports both signed-in (visitor_id) and anonymous (visitor_token cookie) visitors; both must work under tenancy. An anonymous guest's tenant comes from the request context (subdomain/page). Decide and document the B2B2C vs internal-workspace framing — mechanically identical (the resolver just returns the tenant key), but the examples/docs should pick one.
Decision points to resolve during implementation
Tenant-aware agent authorization + notifications.authorize_agent becomes "this admin, this tenant"; the inbox scoping enforces isolation, but agent_emails (and possibly mailer_from) must resolve per tenant. This is the heart of the work.
Tenant key convention. Recommend GlobalID (to_gid.to_s) as the documented default, accept any host string. Same as the siblings.
Realtime: namespace the inbox stream + poll token per tenant; per-conversation stream is already isolated.
Attachment/blob access already goes through the owns-conversation-or-agent gate — extend the finder to respect tenant.
for_visitor / claim! / guest cookie scoping — one thread per visitor per tenant; guest adoption stays within-tenant.
No polymorphic FK, no host model requirement (the concern is optional).
No group chat, no visitor-to-visitor chat — still strictly visitor↔team, just partitioned.
Not changing the two-party model or the polling-first transport.
Scope
Mirror the testimonials/ideasbugs reference implementations (tenant column + config.tenant resolver + optional has_livechat concern + tenant-aware auth), then handle the three livechat-specific concerns above (tenant-aware notifications, per-tenant inbox realtime stream, guest/visitor identity). Its own focused release, not folded into unrelated work. Gate on real demand.
Goal
Let a host app give each tenant/customer org its own private support desk — its own visitors, its own inbox, its own agents — without the gem taking a foreign key into the host's models. Each tenant's members (visitors, signed-in or anonymous) talk to that tenant's admins (agents); no tenant can see another's threads, inbox, or notifications. It stays strictly visitor↔team — tenancy just partitions which team and which visitors belong together (no group chat, no member-to-member chat).
Targeted for a later release (v2/v3) if there's demand — not urgent. Single-tenant apps must keep working with zero config.
Sibling reference implementations already shipped with this exact recipe: yshmarov/testimonials#3 (v0.5.0) and yshmarov/ideasbugs#2 (v0.6.0). livechat is deliberately not a copy-paste of those — see "What makes livechat different" below.
Design: opaque
tenantstring + aconfig.tenantresolver (not polymorphicacts_as)Same loose-coupling rule as the rest of the gem — visitor/agent identity is already loose strings (
visitor_id,visitor_token,agent_id,agent_label; nobelongs_to :user). Multi-tenancy follows suit.Foundation:
tenant(string) onlivechat_conversations.nil= the single global desk (today's behavior, unchanged). Messages inherit the tenant via their conversation.current_user/authorize_agent:Flow (uniform):
Conversation.for_visitor(...)scopes by tenant, so "one open thread per visitor" becomes one per visitor, per tenant. Guest→signed-inclaim!is scoped so a guest thread is only adopted within the same tenant./livechat) scopes every query to the current tenant — list, counts, theset_conversationfinder (a cross-tenant id 404s),poll,index_poll.Optional
acts_assugar (a veneer over the string, not a new coupling):Apps that want
organization.conversationsinclude the concern; apps that don't still get full multi-tenancy through the resolver alone. Foundation = the string; concern = sugar.Why string+resolver over polymorphic
owner_type = "Organization"class-name coupling (rename-safe).visitor_id), never trusted from the client.What makes livechat different from the testimonials/ideasbugs recipe
Those are one-way collection tools; livechat is a live two-sided conversation with realtime and notifications. Three things are genuinely new here — the ~20% that isn't mechanical:
Notification routing becomes tenant-aware (the real "decision point File attachments in conversations #1"). Today
config.agent_emailsis one global list — "email the support team." Per-tenant, the recipients depend on the conversation's tenant (Org A's admins, not everyone). Soagent_emailsshould be able to receive the conversation/tenant, e.g.->(conversation) { ... }, instead of a flat array. This is the analog of "tenant-aware authorization" in the sibling issues, and the one place needing real care.mailer_frommay also want to be tenant-aware.The realtime inbox stream must be partitioned per tenant. The per-conversation Action Cable stream (
livechat:conversation:<id>) and its signed token are already isolated. But the sharedlivechat:inboxstream and theindex_polltoken would leak cross-tenant activity — an Org A agent would get "new message" nudges from Org B. So the inbox stream becomeslivechat:inbox:<tenant>and the poll token is computed per tenant. (Polling stays the fallback, unchanged otherwise.)"Who is the visitor" is load-bearing. livechat supports both signed-in (
visitor_id) and anonymous (visitor_tokencookie) visitors; both must work under tenancy. An anonymous guest's tenant comes from the request context (subdomain/page). Decide and document the B2B2C vs internal-workspace framing — mechanically identical (the resolver just returns the tenant key), but the examples/docs should pick one.Decision points to resolve during implementation
authorize_agentbecomes "this admin, this tenant"; the inbox scoping enforces isolation, butagent_emails(and possiblymailer_from) must resolve per tenant. This is the heart of the work.to_gid.to_s) as the documented default, accept any host string. Same as the siblings.for_visitor/claim!/ guest cookie scoping — one thread per visitor per tenant; guest adoption stays within-tenant.livechat:tenant).niltenant = today.Non-goals
Scope
Mirror the testimonials/ideasbugs reference implementations (
tenantcolumn +config.tenantresolver + optionalhas_livechatconcern + tenant-aware auth), then handle the three livechat-specific concerns above (tenant-aware notifications, per-tenant inbox realtime stream, guest/visitor identity). Its own focused release, not folded into unrelated work. Gate on real demand.