Communications $99.99 / mo Updated 2026-04-24 VERSION 2 · NEW

Internal Messaging — Team Chat & Broadcast Guide

V2 is a code-level rebuild on the existing 6-table schema. Every send now runs through a MessagingEngine class that validates recipients against the sender's tenant, rate-limits abuse, caps body length, and sanitizes error messages before they reach the client. Same UI, hardened internals.

Overview

The Internal Messaging module provides 1:1 direct messages, group channels, and tenant-admin broadcasts for agents and supervisors inside the CRM. Built on a 6-table schema with strict tenant isolation, per-agent rate limits, and sanitized error responses — designed for multi-tenant SaaS from the ground up.

A single MessagingEngine class consolidates validation, rate limiting, and error sanitization for every write path. The JSON API is a thin wrapper that defers to the engine — every send, broadcast, group join, and read receipt runs through the same security guarantees.

What's new in V2

Cross-tenant recipient validation

Every send runs through MessagingEngine, which looks up the recipient_id with a WHERE tenant_id = clause before the INSERT. If the recipient isn't in the sender's tenant, the send is rejected with an error string. Cross-tenant sends are impossible by construction.

Sanitized error responses

Database errors are logged server-side via error_log() and a generic "Message could not be sent — please try again." string is returned to the client. No DB schema, query fragments, or server paths leak through the JSON response.

Per-agent rate limits

Each individual agent has their own quota of 30 messages/minute and 500/hour — quotas are independent, not a tenant-wide cap, so a 100-person team can collectively send up to 3,000 messages/minute. Counted against the internal_messages table by (sender_id, tenant_id). Admins can see who's near the cap in the state viewer.

Body + subject length caps

32KB body, 255-char subject. Truncated via mb_substr so multi-byte characters (emoji, CJK) count correctly. The underlying TEXT column accepts 64KB; the engine cap protects against accidental megabyte-paste loads.

Broadcast role gate

Server-side enforcement: only role_id ∈ {1,2,3} can send broadcasts. A regular agent hitting the broadcast endpoint directly gets a clear "Only tenant admins can send broadcasts" error. The UI hides the broadcast composer for non-admins as well.

State viewer for supervisor oversight

New admin page at /admin/view-messaging-state.php shows tenant stats, top senders in the last 24h with rate-signal chips, recent messages, recent broadcasts, and active groups. Per the browser-only QA workflow — no MySQL CLI needed for any ops or audit task.

Tenant isolation

Multi-tenant messaging has one cardinal rule: a user in Tenant A must never be able to message a user in Tenant B, intentionally or otherwise. V2 enforces this in three places:

  • Before every direct-message INSERT: MessagingEngine::isTenantUser($recipientId) runs a SELECT 1 FROM users WHERE id = ? AND tenant_id = ? AND status = 'active'. If no row, the send returns "Recipient not found" and no message row is written.
  • Before every group-message INSERT: isTenantGroup($groupId) verifies the group exists in this tenant AND isGroupMember($groupId) verifies the sender is in the group.
  • Before every broadcast: every group ID in the target list must pass isTenantGroup(). Broadcasts to "all" automatically scope to the sender's tenant via the recipient_type column.

These checks are in the engine class, not the API handler — so any caller (API, cron job, future webhook consumer) gets the same protections.

Rate limits

Default caps are defined as class constants on MessagingEngine:

const RATE_PER_MINUTE = 30;
const RATE_PER_HOUR   = 500;

The check runs before every direct or group send. It counts the sender's messages over the last 60 seconds and 3600 seconds in internal_messages, filtered by sender_id and tenant_id. If either cap is exceeded, the send returns a clear error and no row is inserted.

The check is fail-open — if the rate query itself errors (e.g. brief DB issue), messages are still allowed through. This is a deliberate trade: we prefer to under-block than over-block. Real abuse will hit the cap anyway on the very next message once the DB is healthy.

Broadcasts don't currently rate-limit (they're admin-only and a different abuse profile). V2.1 may add a per-tenant broadcast throttle.

Groups & broadcasts

Groups

Any user can create a group. The creator becomes group admin automatically. Members can be added on creation or via join_group later. Each group lives in exactly one tenant — message_groups.tenant_id is a real FK.

Group messages use internal_messages.recipient_group_id (nullable). Only members can post. The same rate limits apply as for direct messages.

Broadcasts

Broadcasts are tenant-admin-only (role_id 1, 2, or 3). Two targeting modes:

  • All: every active user in the tenant sees it in their broadcast feed.
  • Groups: supply a list of group IDs — only members of those groups see it.

Read tracking uses the broadcast_read_status table (unique on broadcast_id + user_id). The state viewer shows read counts so admins know their announcement actually landed.

The send_email_offline flag exists on broadcast_messages but no email integration is wired up in V2 — it's available for a future V2.1 feature.

Soft delete model

Every message row has two boolean flags: is_deleted_sender and is_deleted_recipient. "Deleting" a message only flips YOUR side's flag — the counterparty still sees it.

This is deliberate: one user can't unilaterally erase evidence of a conversation from the other party. For compliance-retention use cases, delete_conversation flips every message's sender-side or recipient-side flag to 1 in a single batch, but the rows stay in the database. There's no hard-delete path through the UI.

For GDPR/legal-hold hard deletion, the operation has to go through a direct DB query that should be logged separately in the tenant's compliance system.

Quick start ≈ 10 minutes

  1. Activate the module at RubiMine. $99.99/mo, unlimited users.
  2. Sysadmin verifies schema at /admin/run-messaging-v2-migration.php. All 6 tables should show ✅.
  3. Agents start messaging from the CRM's Messaging tab — compose direct messages, create groups, invite members.
  4. Tenant admins send their first broadcast to introduce the feature. Pick "all users" + normal priority.
  5. Sysadmin checks the state viewer at /admin/view-messaging-state.php for activity + top-sender stats.

API endpoints

All endpoints on /crm/api/messaging.php. Session-authenticated, tenant-scoped. Action names preserved from V1 so the existing UI doesn't need changes.

Read

POST action=get_users              → tenant users + groups for recipient picker
POST action=get_conversations      → threaded 1:1 conversation list
POST action=get_messages            (user_id) → messages in a 1:1 thread
POST action=get_group_messages      (group_id)
POST action=get_group_members       (group_id)
POST action=get_groups             → all groups in tenant (+ is_member flag)
POST action=get_unread_count
POST action=get_broadcasts         → tenant broadcasts with is_read per-user
POST action=get_sent               → current user's sent items

Send / mark / delete

POST action=send_message            (recipient_id, subject?, body, priority?, is_group?)
POST action=send_group_message      (group_id, subject?, body, priority?)
POST action=send_broadcast          (subject, body, recipient_type=all|groups, recipient_groups[]?, send_email_offline?)
POST action=mark_read               (message_id)
POST action=mark_broadcast_read     (broadcast_id)
POST action=delete_message          (message_id) — sender-side OR recipient-side soft delete
POST action=delete_conversation     (user_id) — soft-delete your side of a thread

Groups

POST action=create_group            (name, description?, member_ids[]?)
POST action=join_group              (group_id)
POST action=leave_group             (group_id)

Known limitations (V2)

  • File attachments: schema is ready (message_attachments) but no upload endpoint. V2.1 roadmap. Paste links to shared-storage files in the body for now.
  • Email on offline: broadcast_messages.send_email_offline is stored but no mailer wiring. V2.1.
  • Scheduled broadcasts: scheduled_at column exists but no cron processor. Broadcasts send immediately when sent_at is set on INSERT.
  • Typing indicators / real-time delivery: no WebSocket layer. The UI polls. Adequate for most team sizes; V2.1 may add a WebSocket bridge for high-volume tenants.
  • Message threading: parent_message_id column exists for reply chains but the UI doesn't expose thread view yet. Replies show inline in the flat conversation.
  • Message export: no CSV export in the UI. Direct DB query works for e-discovery / compliance use cases.
  • Per-tenant rate limit override: caps are global constants. V2.1 may surface them as tenant settings for high-volume customers.

Frequently asked questions

How does cross-tenant validation work?
Every send runs isTenantUser() or isTenantGroup() with a WHERE tenant_id = ? clause before the INSERT. Recipients in other tenants are rejected at the engine layer — cross-tenant sends are impossible by construction.
What are the rate limits?
Per agent: 30 messages/min, 500/hour — each agent has their own quota, not tenant-wide. Counted against internal_messages by (sender_id, tenant_id). Hitting either cap returns an error; check fails open on DB issues so a brief hiccup doesn't block all messaging.
Are broadcasts restricted?
Yes. Only tenant-admin role (role_id 1/2/3). Server-side enforcement in sendBroadcast(); UI also hides the composer for non-admins.
What's the body length limit?
32KB body, 255-char subject. Enforced via mb_substr so unicode (emoji, CJK) counts correctly.
How does delete work?
Soft delete via is_deleted_sender / is_deleted_recipient flags. Your delete doesn't remove the counterparty's copy. Row stays in the database for audit / compliance.
What about file attachments?
Schema is ready but no upload endpoint in V2. V2.1 roadmap. Paste links to shared storage in the body for now.
How do I check for abuse?
State viewer shows top 10 senders in the last 24h with rate-signal chips (Normal / Heavy / Over cap). Anyone in the Over-cap column needs follow-up.
Can I export history?
Not via UI in V2 — direct DB SELECT works for e-discovery. V2.1 will ship an admin CSV export with date-range scoping.
How much does it cost?
$99.99 / month / tenant. Unlimited users, messages, groups, broadcasts. Rate limits are abuse prevention, not billing.

Ready to activate messaging?

Activate from RubiMine, run the migration check, and your team is chatting in 10 minutes. Already active? Hit the state viewer to see usage.