Chat
Real-time direct messages and group chats with @mentions, presence, and search.
MangoApps
Overview
Pusher-powered real-time chat with one-to-one DMs, group rooms, @mentions feeding the existing Notification system, per-room presence, and PostgreSQL full-text search. Built on the same business-scoping and authentication patterns as the rest of the platform — every message is searchable, auditable, and tenant-isolated.
Highlights
Capabilities
Conversations
-
One-to-one direct messages Auto-deduped per pair — starting a chat with someone you already DM'd opens the existing room.
-
Group rooms with role-based admin/member Creator becomes room admin; admins add/remove members, members can leave (sole-admin guard prevents orphaned rooms).
-
Public business channels `channel` is a first-class room_type alongside `direct` and `group`; member management mirrors groups today.
-
Threaded replies Flat one-level threads via an offcanvas panel; replies fan out over the parent room's Pusher channel.
-
Message edit and delete Edit re-flows the FTS vector and (when enabled) the semantic embedding; delete is soft (deleted_at) so audit trails survive.
Real-time Delivery
-
Sub-second message fan-out via Pusher Channels CreateMessageJob persists then triggers `message:created` on `private-room-<id>`.
-
Per-room presence (Pusher presence channels) `presence-room-<id>` authorized through the same business + room-membership gate; payload drives the avatar strip.
-
Typing indicators Client-event over the room's Pusher channel; admin-toggleable per business.
-
Global online status (cache-based heartbeat) `PresenceService.heartbeat!` keeps a per-business cache; `/presence/online` returns who is up in a single batch.
Notifications & Discovery
-
Unread message counts per room UpdateUnreadCountsJob pushes `inbox:unread-changed` to the per-user-per-business inbox channel.
-
Read receipts BroadcastReadReceiptJob fires `message:read` after the DB write commits; admin-toggleable; suppressed in channels.
-
@mention notifications via Notification NotifyMentionedUsersJob inserts platform Notification rows so the bell badge / Action Cable fan-out is reused.
-
Dedicated Mentions inbox Reverse-chronological feed of every chat mention with cursor pagination and jump-to-message.
-
PostgreSQL full-text search `search_vector` (tsvector + GIN index) written before_save; results highlight matches with `<mark>` tags.
-
Semantic search via pgvector Admin-gated (semantic_search_enabled). EmbedMessageJob keeps row embeddings fresh on create + edit; BackfillEmbeddingsJob seeds history.
Files & Reactions
-
File attachments via Active Storage Drag-and-drop or paperclip upload; up to 10 files per message; size + extension + MIME blocklist enforced server-side.
-
Emoji reactions on messages Toggleable from the 10-emoji default set via Platform::Reactable; BroadcastReactionJob pushes `message:reacted` to all subscribers.
Analytics & Insights
-
Admin analytics dashboard Adoption %, DAU/MAU stickiness, cross-department %, and mention response rate — admin_or_above? or chat_app_admin? gated.
-
CSV export of analytics Single file with summary metrics, 30-day DAU, 12-week adoption trend, and top department-pair message volumes.
-
Personal activity dashboard Active conversations, mention response time vs. team median, messaging-hours heatmap, top conversations.
Clients & API
-
Mobile web (iOS & Android browsers) Dedicated `/m/apps/chat` controllers and views — room list, room view, mentions, send.
-
Desktop client (Mac & Windows) MangoMessenger Electron app reuses the Pusher transport and JSON API; download surfaced from the chat dashboard.
-
REST API for every web feature `/api/v1/chat/{rooms,messages,members,threads,reactions,search,mentions,presence,settings,pusher}` — Bearer-token or session auth.
Admin Controls
-
Toggle direct messages globally
-
Toggle group chats globally Also gates channel creation today.
-
Toggle file uploads globally Hides the paperclip button when off; server-side validation refuses uploads either way.
-
Toggle typing indicators
-
Toggle read receipts
-
Toggle semantic search Off by default — flipping on requires a one-time embeddings backfill via BackfillEmbeddingsJob.
-
Configurable max attachment size Per-file MB cap, validated in the Message model on every save.
-
Configurable retention policy Auto-purge ships with pg_partman partitioning in Phase 4.
-
Per-business feature flags
Limits & Specs
-
Real-time transport: Pusher Channels (Premium / Growth tier)
-
Message storage: PostgreSQL (single non-partitioned table — partitioned at ~100M rows)
-
Default retention: Forever (auto-purge ships with Phase 4 partitioning)
-
Default max attachment size: 25 MB per file (admin-configurable, 1–500 MB)
-
Max attachments per message: 10
-
Reaction emoji set: 10 platform-default emojis (Platform::Reaction::DEFAULT_EMOJI_SET)
-
Mobile support: iOS & Android via mobile web; native via the same Pusher endpoint
-
Pricing: Included with MangoApps Workforce
Use cases
Resources
FAQ
Pusher Channels handles fan-out. The browser subscribes to a per-room private channel; Rails authorizes the subscription against business and room membership before signing the auth response. Four background jobs trigger the four event types: `message:created` (CreateMessageJob), `message:read` (BroadcastReadReceiptJob), `message:reacted` (BroadcastReactionJob), and `inbox:unread-changed` (UpdateUnreadCountsJob).
Messages still persist to PostgreSQL via the standard HTTP request/response — CRUD never depends on Pusher. Real-time delivery, typing indicators, presence, and read receipts degrade gracefully; a refresh shows the missed messages. The Pusher auth endpoint returns a clean 503 (`pusher_unconfigured`) when env vars are blank so the JS client surfaces a friendly error rather than an opaque 500.
Yes. Pusher's Premium and Growth tiers handle 10k+ concurrent connections per cluster. The Rails layer only handles auth at subscription time, not the WebSocket itself, so it scales independently. Message storage is a single non-partitioned table today; pg_partman partitioning is planned at ~100M rows.
Three layers, all run inside the Pusher auth endpoint before signing: (1) channel-name regex (`private-room-`, `presence-room-`, `private-user-inbox--business-`), (2) business membership of current_user, and (3) room membership inside that business. Pusher itself never touches business data, and channel-derived ids are only ever used to look up records — server state is the source of truth.
Read receipts (`Seen by …`) appear on sent messages in DMs and group rooms; channels are intentionally excluded. The admin Settings page has a single toggle (Realtime → Read receipts); when off, BroadcastReadReceiptJob short-circuits and the UI hides the indicator on the next render.
Keyword (PostgreSQL full-text) search is on by default and covers most needs. Semantic search adds a "find a message about…" mode powered by pgvector embeddings — useful when you want to search by meaning rather than exact words. It is admin-gated (off by default) because flipping it on requires a one-time embedding backfill via BackfillEmbeddingsJob; ongoing inserts and edits keep their embeddings fresh automatically.
Up to 10 files per message, with a 25 MB per-file default cap that admins can tune from 1 to 500 MB. Executable / scriptable file types are server-side blocked by both extension and MIME type as a defense-in-depth measure — the composer rejects them client-side too, but the Message model is the source of truth.
Yes. Admins (or per-app `chat_app_admin?`) see four widgets at `/apps/chat/analytics`: Adoption (12-week trend), DAU/MAU stickiness (30-day), Cross-department communication (% of messages crossing departments, plus the top dept-pair breakdown), and Mention response rate (1h / 4h / 24h with median response time). All four are CSV-exportable in a single file.