CloudSignal Docs
Core Concepts

Pub/Sub & topics

How CloudSignal uses MQTT's topic-based publish/subscribe model - with wildcards, shared subscriptions, and ACL enforcement - to route messages exactly where they need to go.

Publish/subscribe is the foundation of how messages move through CloudSignal. Instead of clients sending messages directly to each other, they publish to topics and subscribe to topics. The broker handles all routing. This decoupling is what makes MQTT-based messaging scalable - publishers do not need to know who is listening, and subscribers do not need to know who is publishing.

Combined with CloudSignal's access lists, this gives you both flexible message distribution and fine-grained control over who can see what.

How topics work

An MQTT topic is a UTF-8 string organized in a hierarchy separated by forward slashes. Each segment between slashes is a topic level:

agents/agent-42/state

Here agents, agent-42, and state are three topic levels.

Topics are not pre-created. When a client publishes to a topic, it exists. When nothing is published to it, it simply is not active. There is no schema, no configuration, and no provisioning step.

Topic design patterns

Good topic structure makes your system easier to reason about and your access control rules simpler:

Pattern: {category}/{identifier}/{data-type}

Examples:
  agents/agent-42/state
  agents/agent-42/inbox
  chat/conv-7/messages
  presence/room-9
  tasks/task-3/progress
  ai/session-12/response

A consistent hierarchy means you can write broad ACL rules using wildcards and narrow ones using specific paths.

Topic subscriptions

Subscribers tell the broker which topics they want to receive messages from. MQTT provides three subscription methods:

Exact subscription

Subscribe to a specific topic. You receive only messages published to that exact path.

Subscribe: agents/agent-01/state

Receives:  agents/agent-01/state              ✓
           agents/agent-01/inbox              ✗
           agents/agent-02/state              ✗

Single-level wildcard (+)

The + wildcard matches exactly one topic level. Use it when you want messages from multiple entities at the same level.

Subscribe: agents/+/state

Receives:  agents/agent-01/state              ✓
           agents/agent-02/state              ✓
           agents/agent-99/state              ✓
           agents/agent-01/inbox              ✗  (wrong leaf)
           agents/agent-01/sub/state          ✗  (too many levels)

+ can appear at any position and multiple times:

Subscribe: +/+/messages

Receives:  chat/conv-7/messages               ✓
           rooms/kitchen/messages             ✓
           teams/eng/messages                 ✓

Multi-level wildcard (#)

The # wildcard matches zero or more topic levels. It must be the last character in the subscription.

Subscribe: agents/#

Receives:  agents                             ✓
           agents/agent-01                    ✓
           agents/agent-01/state              ✓
           agents/agent-01/tasks/raw/v2       ✓

# is powerful but broad. Use it when you genuinely need everything under a topic prefix - monitoring dashboards, debug logging, or admin tools.

Subscribing to # (all topics) is possible but should be reserved for administrative or debugging purposes. In production, always scope subscriptions to the narrowest pattern your client needs.

Shared subscriptions

Standard MQTT subscriptions deliver every message to every subscriber. If 10 clients subscribe to events/#, all 10 receive every message. This is correct for fan-out scenarios (dashboards, notifications) but wasteful for work-queue scenarios where you want to distribute processing across workers.

Shared subscriptions solve this by distributing messages across a group of subscribers - each message goes to exactly one subscriber in the group.

Standard subscription (fan-out)
Publisher
PUBLISH
Broker
Subscriber A
Subscriber B
Subscriber C

Shared subscription syntax

Shared subscriptions use the $share/{group-name}/{topic} prefix. For example, $share/workers/jobs/# is composed of three parts:

  • $share is the shared-subscription prefix
  • workers is the group name (you choose it)
  • jobs/# is the underlying topic filter

All subscribers using the same group name share the load. Different group names receive independent copies:

$share/team-a/events/#   ──▶  Team A workers (load-balanced among themselves)
$share/team-b/events/#   ──▶  Team B workers (load-balanced among themselves)
events/#                  ──▶  Standard subscriber (receives everything)

When to use shared subscriptions

ScenarioSubscription Type
Dashboard showing all eventsStandard (events/#)
Worker pool processing jobsShared ($share/workers/jobs/#)
Multiple instances of same serviceShared ($share/api/commands/#)
Notification broadcast to all usersStandard (notifications/#)
Log ingestion across serversShared ($share/ingest/logs/#)

Shared subscriptions let you scale message processing horizontally. Add more subscribers to the group to increase throughput. Remove them to scale down. The broker handles rebalancing automatically.

ACL-enforced distribution

Topic subscriptions and access lists work together. A client can only subscribe to topics its access list rules allow. The broker checks permissions on every SUBSCRIBE and PUBLISH operation.

ACL Rules:
  ALLOW subscribe  users/{username}/inbox/#
  DENY  subscribe  #

User "alice" subscribes:
  users/alice/inbox/#         ✓  (allowed - {username} resolves to "alice")
  users/bob/inbox/#           ✗  (denied - alice is not bob)
  agents/#                    ✗  (denied - no matching allow rule)

This enforcement happens at the broker level, not in application code. You cannot bypass it by connecting with a different client library or constructing custom packets. The broker evaluates ACL rules before delivering any message.

Why this matters

Many messaging systems implement access control as middleware - a proxy layer that filters messages before or after delivery. This approach has gaps:

  • Clients may receive messages briefly before the filter kicks in
  • Direct connections (bypassing the proxy) skip the filter entirely
  • Authorization and message routing are separate systems with separate failure modes

In CloudSignal, access control and message routing are the same system. The broker does not route a message to a subscriber unless the subscriber has permission to receive it. There is no gap, no race condition, and no way to bypass it.

Topic-based architecture

MQTT topics give you a natural way to structure your entire application's messaging. A typical tree might look like this:

  • users/
    • {user_id}/
      • inbox - personal messages
      • notifications - alerts and updates
      • presence - online/offline status (retained)
    • broadcast - system-wide announcements
  • agents/
    • {agent_id}/
      • state - latest agent status (retained)
      • inbox - tasks routed to this agent
      • responses - results streamed out
    • coordination - multi-agent orchestration
  • chat/
    • {conversation_id}/
      • messages - chat events
      • typing - typing indicators
  • ai/
    • {session_id}/
      • response - LLM token streaming
      • tools - tool-call requests and results
  • system/
    • maintenance - planned downtime (retained)
    • metrics - platform health

Each branch of this tree maps to a set of ACL rules, a set of subscribers, and a clear data flow. New features add new branches without affecting existing ones.


Next steps

On this page