Topics, Spaces, Groups, and Scope Reference
This reference explains message addressing and permission boundaries. For protocol connections, credential creation, and copyable test commands, see the Resource and Protocol Integration Guide.
1. Four-layer access model
| Layer | Configured at | Example | Purpose |
|---|---|---|---|
| Credential scope | Console Credentials | spaces=['production'], groups=false, capabilities=['data'] | Maximum authority of the key |
| Connection location | MQTT username @… suffix | Application keys use @production, @alliance:shared | Domain and space entered by this connection |
| Topic filter | MQTT subscription or publish topic | plant/+/state, $space/production/# | Business messages visible inside the allowed location |
| Delivery range | Ordinary topic or $earth/deliver/… | Broadcast or directed resource | Who receives the message |
A later layer cannot expand an earlier one. Replacing # with $space/#, or writing another space into a topic, cannot bypass credential scope.
2. space: isolation inside one workspace
- is the built-in default space in every domain. You may add spaces such as production, testing, or team-a. A space isolates messages and permissions without changing your business topic format.
| Connection username | Primary space | Physical location of ordinary telemetry/state |
|---|---|---|
acme | - | acme/-/telemetry/state (clients do not write this physical path) |
acme@production | production | acme/production/telemetry/state |
acme@-,production | - | Default publishes still enter - |
acme@* | First space allowed by scope | Expanded from the allowed list |
The full forms in this table apply to resource keys and remain compatible with application keys. Because an application key is already bound to its workspace, it may omit acme: use "" for the default space, @production for a named space, or @-,production for multiple spaces.
Multi-space rules:
| Client form | Result |
|---|---|
# | Ordinary topics in the primary space only |
$space/production/# | Only production |
$space/# | Every space allowed by the credential |
Publish $space/production/telemetry/state | Publishes to production; the space must be present in the username and scope |
Publish $space/+/… | Rejected; publish operations cannot use wildcards |
Space example
An application key is limited to production:
mosquitto_sub -h '<MQTT_HOST>' -p '<MQTT_PORT>' --tls-use-os-certs \
-u '@production' -P '<DATA_ETK>' -t '#' -vThis subscription does not receive plant/a/state published in the default space. Connect with acme@-, or grant both spaces and subscribe to $space/#.
3. group: a shared domain across workspaces
A group is a message domain shared by multiple workspaces. It is independent from a same-named workspace space: acme@alliance:shared enters group alliance, space shared; it does not enter acme's own shared space.
| Requirement | Resource key | Application key |
|---|---|---|
| May enter a group | No | Yes |
| Workspace group membership | Not applicable | Required |
Key groups scope | Not applicable | Must be true or include the group |
| Username | Unsupported | @group: or @group:space; tenant@… remains compatible |
Connection example:
@alliance:sharedThe colon is required. @alliance means space alliance inside the key's workspace, not group alliance.
Business topics inside the group remain yours:
project/boiler-7/state
project/boiler-7/commandMembers in the same group space can subscribe to one another. A workspace that is not a member cannot enter through the username even if it knows the topic.
4. scope: the application credential ceiling
Application credential scope has three independent dimensions:
| Field | Example | Meaning |
|---|---|---|
spaces | ['-', 'production'] | Workspace spaces the key may enter; '*' means every space in the domain where the key was created |
groups | false, ['alliance'], true | Shared groups the key may enter |
capabilities | ['data'], ['data','manage'] | data sends/receives; manage creates resources, spaces, and batch registrations |
Scope and username examples:
| Key scope | Username or operation | Result |
|---|---|---|
spaces=['production'] | @production | Allowed |
spaces=['production'] | @- | Rejected |
groups=false | @alliance:shared | Rejected |
groups=['alliance'] | @alliance:shared | Allowed only if the workspace is a member and the space exists |
capabilities=['data'] | POST /v1/resources | 403 capability_required |
capabilities=['data','manage'] | POST /v1/resources | Allowed; returns one erk_… |
A resource record also has its own space authorization. Even when an application key may enter production, a resource using acme@production must have been granted that space when registered. New resource keys carry resource identity; username keeps only the workspace ID.
5. Ordinary topics and delivery range
An ordinary topic does not include workspace, resource, or space:
plant/boiler-7/stateSubscribers in the same space see the original topic. A publisher also receives its own broadcast when subscribed to it.
| Form | Range | Durable event recorded? |
|---|---|---|
plant/boiler-7/state | Every matching subscriber in the current space | Yes |
$earth/deliver/resource:boiler-7/cmd/restart | One resource publisher | Yes; resumable |
$earth/message | Resource default command topic | Receives directed commands |
$space/production/plant/boiler-7/state | One authorized space | Yes |
Use ordinary broadcasts for state, telemetry, and events; use directed delivery for commands. A device ID is not a security boundary. Credentials and connection location provide that boundary.
6. $earth and $space topics
| Reserved topic | Client action | Meaning |
|---|---|---|
$earth/events/# | subscribe | Full event envelopes with eventId, sequence, and publisher |
$earth/errors/# | subscribe | Errors caused by the current client |
$earth/resume | publish | Body: {"afterSequence":12,"limit":100} |
$earth/resumed | subscribe | Each resume's outcome: how many were replayed, where to carry on, or why none could be |
$earth/message | subscribe | Default command topic |
$earth/deliver/{publisherId}/{topic} | publish | Deliver only to one publisher |
$space/{space}/{topic} | publish / subscribe | Address one authorized space explicitly |
$space/+/{topic} | subscribe | Subscribe to the same topic in all authorized spaces |
$space/# | subscribe | Subscribe to all topics in all authorized spaces |
An ordinary # subscription does not match a first topic level beginning with $; subscribe explicitly. Unknown $… publishes are rejected. Quote $earth and $space with single quotes in shell commands.
7. Example: production and testing isolation
Create production and testing. A production device uses:
username = acme@production
password = RESOURCE_ERK
clientId = earthmqtt-common-clientid
publish = plant/sensor-01/stateA test dashboard key has spaces=['testing']:
username = @testing
password = TESTING_ETK
subscribe = plant/#The test dashboard cannot receive production messages. For a combined dashboard, create a key containing both spaces, connect as @-,production, and subscribe to $space/#.
8. Example: shared project
Workspaces acme and partner are members of group alliance, which has space shared. Both application keys allow groups=['alliance']:
# acme subscribes
mosquitto_sub -h '<MQTT_HOST>' -p '<MQTT_PORT>' --tls-use-os-certs \
-u '@alliance:shared' -P '<ACME_ETK>' -t 'project/#' -v
# partner publishes
mosquitto_pub -h '<MQTT_HOST>' -p '<MQTT_PORT>' --tls-use-os-certs \
-u '@alliance:shared' -P '<PARTNER_ETK>' \
-t 'project/progress' -m '{"percent":60}'acme receives project/progress. @shared, without a group name and colon, is a different path and cannot access the group. Although both sides use the same username, each application key still selects its own workspace.
9. Example: offline commands and ordering
After a resource subscribes to $earth/message or its command topic, it stores the highest successfully processed sequence. Commands issued while it is offline remain in the reliable event stream. After reconnecting and subscribing:
mosquitto_pub -h '<MQTT_HOST>' -p '<MQTT_PORT>' --tls-use-os-certs \
-i 'earthmqtt-common-clientid' -u 'acme' -P '<RESOURCE_ERK>' -q 1 \
-t '$earth/resume' -m '{"afterSequence":12}'The server replays retained directed commands with a sequence greater than 12 and adds "replay":true. If a client already processed through 20 but asks for afterSequence:0, it may receive still-retained messages through 20 again; the client must use its own deduplication record to avoid repeating actions.
Each resume replays at most limit messages (default 100, at most 500) and scans at most 2,560 events. A device's own publishes share the stream with what it is sent, and the scan steps over them. After the replays, the server answers on $earth/resumed (subscribe to it first):
{ "afterSequence": 12, "replayed": 3, "cursor": 2575, "more": true }While more is true, resume again with cursor as afterSequence (it is how far the scan got, not the last command's sequence) until more is false; a long run of the device's own reports then never stalls the catch-up. When nothing can be replayed:
{ "afterSequence": 12, "replayed": 0, "snapshotRequired": true, "reason": "retention-gap", "earliestSequence": 4000, "latestSequence": 5210 }retention-gap means part of what followed 12 is past retention and cannot be replayed; resuming from earliestSequence - 1 gets what is still kept. cursor-ahead means the requested sequence is beyond the end of the stream (for example a client holding stale state); start again from latestSequence.