Earth Resources

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

LayerConfigured atExamplePurpose
Credential scopeConsole Credentialsspaces=['production'], groups=false, capabilities=['data']Maximum authority of the key
Connection locationMQTT username @… suffixApplication keys use @production, @alliance:sharedDomain and space entered by this connection
Topic filterMQTT subscription or publish topicplant/+/state, $space/production/#Business messages visible inside the allowed location
Delivery rangeOrdinary topic or $earth/deliver/…Broadcast or directed resourceWho 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 usernamePrimary spacePhysical location of ordinary telemetry/state
acme-acme/-/telemetry/state (clients do not write this physical path)
acme@productionproductionacme/production/telemetry/state
acme@-,production-Default publishes still enter -
acme@*First space allowed by scopeExpanded 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 formResult
#Ordinary topics in the primary space only
$space/production/#Only production
$space/#Every space allowed by the credential
Publish $space/production/telemetry/statePublishes 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 '#' -v

This 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.

RequirementResource keyApplication key
May enter a groupNoYes
Workspace group membershipNot applicableRequired
Key groups scopeNot applicableMust be true or include the group
UsernameUnsupported@group: or @group:space; tenant@… remains compatible

Connection example:

@alliance:shared

The 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/command

Members 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:

FieldExampleMeaning
spaces['-', 'production']Workspace spaces the key may enter; '*' means every space in the domain where the key was created
groupsfalse, ['alliance'], trueShared groups the key may enter
capabilities['data'], ['data','manage']data sends/receives; manage creates resources, spaces, and batch registrations

Scope and username examples:

Key scopeUsername or operationResult
spaces=['production']@productionAllowed
spaces=['production']@-Rejected
groups=false@alliance:sharedRejected
groups=['alliance']@alliance:sharedAllowed only if the workspace is a member and the space exists
capabilities=['data']POST /v1/resources403 capability_required
capabilities=['data','manage']POST /v1/resourcesAllowed; 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/state

Subscribers in the same space see the original topic. A publisher also receives its own broadcast when subscribed to it.

FormRangeDurable event recorded?
plant/boiler-7/stateEvery matching subscriber in the current spaceYes
$earth/deliver/resource:boiler-7/cmd/restartOne resource publisherYes; resumable
$earth/messageResource default command topicReceives directed commands
$space/production/plant/boiler-7/stateOne authorized spaceYes

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 topicClient actionMeaning
$earth/events/#subscribeFull event envelopes with eventId, sequence, and publisher
$earth/errors/#subscribeErrors caused by the current client
$earth/resumepublishBody: {"afterSequence":12,"limit":100}
$earth/resumedsubscribeEach resume's outcome: how many were replayed, where to carry on, or why none could be
$earth/messagesubscribeDefault command topic
$earth/deliver/{publisherId}/{topic}publishDeliver only to one publisher
$space/{space}/{topic}publish / subscribeAddress one authorized space explicitly
$space/+/{topic}subscribeSubscribe to the same topic in all authorized spaces
$space/#subscribeSubscribe 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/state

A 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.