CloudSignal Docs
GuidesAccess Control (ACL)

Topic patterns

Learn to use wildcards and patterns in MQTT topic strings for ACL rules.

Topic patterns let you match many topics with a single ACL rule. Use this guide to understand + and # wildcards and write rules that scale across users without one rule per topic.

MQTT topic structure

Topics are hierarchical strings separated by /:

agents/team-alpha/agent-001/state
   |        |          |       |
   |        |          |       L Field type
   |        |          L Agent ID
   |        L Group
   L Category

Wildcards

MQTT defines two wildcards for topic matching.

Single-level wildcard: +

Matches exactly one level in the topic hierarchy.

Pattern:  agents/+/state

Matches:
  agents/agent-001/state
  agents/agent-002/state
  agents/abc123/state

Does NOT match:
  agents/state                       (missing a level)
  agents/team-a/agent-001/state      (too many levels)

Multi-level wildcard: #

Matches zero or more levels. Must be the last character.

Pattern:  agents/#

Matches:
  agents
  agents/state
  agents/agent-001/state
  agents/team-a/agent-001/inbox/task-42

Does NOT match:
  rooms/room-9   (different root)

Pattern examples

Match all topics under a prefix

agents/#

Matches everything under "agents":
  agents/agent-001/state
  agents/agent-001/inbox
  agents/team-a/agent-002/state

Match specific level structure

teams/+/agents/+/inbox

Matches:
  teams/alpha/agents/001/inbox
  teams/beta/agents/lead/inbox

Does NOT match:
  teams/alpha/inbox  (wrong structure)

Match all fields for any agent

teams/+/agents/+/#

Matches:
  teams/alpha/agents/001/state
  teams/alpha/agents/001/inbox
  teams/alpha/agents/001/logs/debug

Combine multiple + wildcards

+/+/state

Matches:
  agents/agent-001/state
  rooms/room-9/state
  anything/anything/state

Username substitution

CloudSignal supports special variables in topic patterns.

%u username substitution

Replaced with the connecting user's username:

User: agent-001
Pattern: agents/%u/inbox

Resolves to: agents/agent-001/inbox

This creates dynamic, per-user topic access:

ACL rule:
  User:   agent-%
  Topic:  agents/%u/#
  Access: pubsub

Result:
  agent-001 can access agents/agent-001/#
  agent-002 can access agents/agent-002/#
  (each agent isolated to its own namespace)

%c client ID substitution

Replaced with the MQTT client ID:

Client ID: mobile-app-user-123
Pattern: clients/%c/inbox

Resolves to: clients/mobile-app-user-123/inbox

Username (%u) is generally more predictable since you control it in the dashboard. Client ID (%c) is set by the connecting client.

Wildcard rules

Placement rules

PatternValid?Notes
agents/+/stateYes+ between levels
agents/#Yes# at end
agents/+/#YesCombine both
agents#No# must follow /
agents/state+No+ must be entire level
agents/#/stateNoNothing after #

Examples of invalid patterns

agents/state+ate    # + must be entire level
agents/#/inbox      # # must be last
agents+             # + must follow /
#agents             # # must follow /

Common patterns reference

Agent isolation

Each agent can only access its own topics:

User:  agent-%
Topic: agents/%u/#

Hub and spoke

Agents publish, central service subscribes:

# Agents
User:  agent-%
Topic: agents/%u/state
Access: publish

# Backend
User:  backend
Topic: agents/#
Access: subscribe

Broadcast to all agents

Service publishes to all agents:

# Agents listen
User:  agent-%
Topic: agents/broadcast
Access: subscribe

# Backend broadcasts
User:  backend
Topic: agents/broadcast
Access: publish

Multi-tenant isolation

Each customer in their own namespace:

User:  customer-a-%
Topic: customers/customer-a/#

User:  customer-b-%
Topic: customers/customer-b/#

Debugging patterns

Test your patterns

Before creating rules, verify patterns match expected topics:

Pattern: agents/+/state/#

Test topics:
  agents/agent-001/state/load   -> MATCH
  agents/agent-001/state        -> MATCH
  agents/agent-001/inbox        -> NO MATCH (state vs inbox)
  agents/state                  -> NO MATCH (missing level)

Check rule order

CloudSignal evaluates rules until a match is found. More specific rules should come first:

1. agent-001 -> agents/agent-001/config -> subscribe  (specific)
2. agent-%   -> agents/%u/state         -> publish    (general)

Next steps

On this page