Permissions model
Tokens do two things:
- Authenticate the client (prove “who you are”).
- Authorize the client (what tenants and channels you can use).
tenant_grants
Permissions are described as a list of grants:
- tenant_grants: a list of rules
- tenant_ids: exact tenant names the grant applies to
- allow_channels_pub: which channels are allowed for publish
- allow_channels_sub: which subscription patterns the client is allowed to send
One token may have several grants (different tenant sets and different channel sets).
Full JSON shape and examples are in Client configuration.
Publish rules (allow_channels_pub) – pub-token rules
Publish rules use pub-token rules – a syntax similar to sub-token rules but without ? and *. A pub-token rule describes which concrete channels the client is allowed to publish to.
A pub-token rule is a dot-separated list of segments. Each segment is one of:
| Segment | Meaning |
|---|---|
store | Exact literal. The channel must have this exact segment. |
(a|b|prefix*) | Alternatives. The channel segment must match at least one variant. Prefix variants like b* match any segment starting with b. |
# | Tail (0+). Only as the last segment. Allows any number of additional channel segments (including zero). |
> | Tail (1+). Only as the last segment. Allows one or more additional channel segments. |
Not allowed in publish rules: ? (any literal) and * (wildcard). These are only available in subscribe rules.
Matching
When a client publishes to a channel, the channel name is always a concrete string (no wildcards). The server checks each segment of the channel name against the pub-token rule:
| Rule segment | Channel segment must be |
|---|---|
Literal "store" | Exactly "store" |
Alternatives (eu|us|a*) | A value matching at least one variant |
# (tail) | 0 or more additional segments |
> (tail) | 1 or more additional segments |
Examples
Exact rule: store.sell.status – only store.sell.status is allowed.
Prefix tree: store.sell.# – allows store.sell, store.sell.status, store.sell.status.v2, etc.
Alternatives: orders.(eu|us|a*).#
| Channel | Allowed? | Why |
|---|---|---|
orders.eu | yes | matches eu |
orders.us.west | yes | matches us, tail allowed |
orders.asia.east | yes | matches a* (starts with a) |
orders.ru | no | not in alternatives |
Tail > vs #:
events.#allows:events,events.click,events.click.v2events.>allows:events.click,events.click.v2but notevents
Invalid publish rules (rejected at token creation)
store.?.status–?not allowed in publish rulesstore.*.status–*not allowed in publish rulesstore.#.status–#not at endstore..sell– empty segment
Limits
Same as subscribe rules: max 32 segments, max 128 bytes per segment, max 16 alternatives.
Subscribe rules (allow_channels_sub) – sub-token rules
Subscribe rules use a richer syntax called sub-token rules. A sub-token rule does not describe which channels are available – it describes which subscription patterns the client is allowed to send in SUBSCRIBE(...).
This protects against clients sending overly broad patterns (like a.*.c.#) even when the intersection with real permissions would be non-empty.
Syntax
A sub-token rule is a dot-separated list of segments. Each segment is one of:
| Segment | Meaning |
|---|---|
store | Exact literal. The subscription must have this exact segment. |
? | Any literal segment. The client can use any concrete value (de, fi, v2) but not *, #, >. |
* | Literal or wildcard. The client can use a concrete value or * at this position. |
(a|b|prefix*) | Alternatives. The subscription must have a literal matching at least one variant. Prefix variants like b* match any literal starting with b. |
# | Tail (0+). Only as the last segment. Allows any number of additional subscription segments (including zero). |
> | Tail (1+). Only as the last segment. Allows one or more additional subscription segments. |
Matching rules
When a client sends SUBSCRIBE(pattern), the server checks each sub-token rule against the subscription pattern segment by segment:
| Rule segment | Subscription segment must be |
|---|---|
Literal "store" | Exactly "store" |
? | Any literal (not *, #, >) |
* | Any literal or * |
Alternatives (a|b|b*) | A literal matching at least one variant |
# (tail) | 0 or more valid subscription segments |
> (tail) | 1 or more valid subscription segments |
In the tail region (# or >), each remaining subscription segment can be a literal, *, or #/> (only as the last segment of the subscription).
Examples
Strict branch: store.sell.#
| Subscription | Allowed? | Why |
|---|---|---|
store.sell | yes | tail is empty, # allows 0+ |
store.sell.status | yes | 1 tail segment |
store.sell.* | yes | * is valid in tail |
store.sell.# | yes | # at end of subscription |
store.* | no | position 2 must be literal sell |
store.buy.status | no | buy != sell |
Any-literal position: store.?.status.#
| Subscription | Allowed? | Why |
|---|---|---|
store.fi.status | yes | fi is a literal, matches ? |
store.de.status.v2 | yes | tail allowed |
store.*.status | no | ? does not accept * |
Wildcard position: store.*.status
| Subscription | Allowed? | Why |
|---|---|---|
store.*.status | yes | * accepts * |
store.fi.status | yes | * accepts literal |
store.fi.status.v2 | no | no tail allowed |
Alternatives: store.(sell|bay|b*).#
| Subscription | Allowed? | Why |
|---|---|---|
store.sell | yes | matches sell |
store.buy.status.v2 | yes | matches b*, tail allowed |
store.bag.> | yes | matches b*, tail allowed |
store.pay.status | no | pay not in alternatives |
Tail # vs >:
a.b.#allows:a.b,a.b.c,a.b.c.da.b.>allows:a.b.c,a.b.c.dbut nota.b
Invalid syntax (rejected at token creation)
store.sell|bay.status–|outside parentheses (usestore.(sell|bay).status)store.(sell.status|buy).#– multi-segment inside alternatives (use two separate rules)store.#.status–#not at endstore..sell– empty segment
Limits
- Maximum segments per rule: 32
- Maximum segment length: 128 bytes
- Maximum alternatives inside
(...): 16
Allow vs deny
The base token model is allow-list: if a tenant/channel is not in the token, it is not allowed.
If your deployment supports explicit deny rules, keep them rare and use them only to carve out exceptions from a broad allow-root (example: allow orders.# but deny orders.internal.#). If deny is not supported, enforce exclusions by splitting roots (example: use separate roots pub.# vs sys.#).