Channels
Channels control who receives which messages. Every channel uses the format {tenant}.{suffix} — the tenant prefix is validated at the gateway to enforce tenant isolation.
Channel Access Validation
The gateway validates channels in two stages:
Stage 1 — Tenant validation (always applies):
The channel must start with {tenant_id}. matching the authenticated tenant (from JWT tenant_id claim or API key). Channels with the wrong tenant prefix are silently filtered on subscribe, or rejected with forbidden on publish.
Stage 2 — Pattern matching: The gateway strips the tenant prefix and matches the remaining suffix against permission patterns. Channels not matching any pattern are denied.
Channel Types
Public Channels
Any connection can subscribe — no specific JWT claims required. Used for broadcast data like announcements or general chat.
demo.general.chat
demo.announce.updates
demo.lobby.messages
Pattern example: *.chat, announce.*
User-Scoped Channels
Only the user whose JWT sub claim matches the {principal} placeholder can subscribe. Used for private messages and notifications.
demo.inbox.{principal} → demo.inbox.user-123 (only sub=user-123 can subscribe)
demo.dm.{principal} → demo.dm.user-456 (only sub=user-456 can subscribe)
The gateway resolves {principal} from the JWT sub claim at connection time.
Group-Scoped Channels
Only users whose JWT groups claim includes the matching group can subscribe. Used for team or role-based messaging.
demo.rooms.{group_id} → demo.rooms.engineering (only users with "engineering" in groups)
demo.rooms.{group_id} → demo.rooms.support (only users with "support" in groups)
The gateway resolves {group_id} from the JWT groups claim.
API-key-only connections (no JWT) can only subscribe to public channels — user-scoped and group-scoped patterns require JWT claims.
Per-Tenant Channel Rules
Channel authorization is provisioning-only: per-tenant channel rules, configured via the provisioning API and streamed to the gateway, are the sole authorization source for every transport (WebSocket, SSE, Web Push, REST publish). There is no static pattern configuration and no fallback rule set.
A tenant with no channel rules configured is denied every subscribe and publish. Setting channel rules is a required step after creating a tenant — sukko up seeds permissive rules for its demo tenant automatically.
Configure rules via the provisioning API:
curl -X PUT http://localhost:8080/api/v1/tenants/demo/channel-rules \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"public": ["*.chat", "announce.*"],
"default": ["*.chat", "inbox.{principal}"],
"group_mappings": {
"engineering": ["rooms.engineering", "rooms.{group_id}"],
"support": ["rooms.support", "tickets.*"]
},
"publish_public": ["*.chat"],
"publish_default": ["*.chat"],
"publish_group_mappings": {
"engineering": ["rooms.engineering"],
"support": ["rooms.support", "tickets.*"]
}
}'
Patterns support wildcards (*) and placeholders such as {principal} — see Pattern Syntax for the full list.
Rule Fields
| Field | Description |
|---|---|
public | Subscribe patterns for all connections |
default | Fallback subscribe patterns when no group matches |
group_mappings | Group name to subscribe patterns mapping |
publish_public | Publish patterns for all connections |
publish_default | Fallback publish patterns when no group matches |
publish_group_mappings | Group name to publish patterns mapping |
Rules are side-specific and may be asymmetric: subscribe operations check only the subscribe-side fields, publish operations only the publish-side fields. An empty side denies all operations on that side — a tenant with subscribe rules but no publish_* rules cannot publish anything.
If the gateway has not yet received its initial rules snapshot from provisioning (cold start), it fails closed: all channel operations are denied and readiness (/ready) is withheld until the snapshot is applied.
Pattern Syntax
Wildcards:
- In channel permission patterns,
*matches a single segment (no dots):general.*matchesgeneral.chatbut notgeneral.chat.v2 - In routing rule patterns,
*matches any sequence including dots:rooms.*matchesrooms.general.messages
Placeholders (resolved against JWT claims at authorization time):
{principal},{sub},{user_id},{app_id}— resolve to the JWTsubclaim{tenant_id},{tenant}— resolve to the authenticated tenant{group_id}— used insidegroup_mappingsvalues; resolves to the mapped group name
A pattern whose placeholder cannot be resolved (API-key-only connection with no JWT claims, or an empty claim) is dropped for that connection — it can never match.
Publish Authorization
Publishing is controlled by the publish_public, publish_default, and publish_group_mappings rules. If a client publishes to a channel they're not authorized for, the gateway rejects it with a forbidden error.
Published messages are routed to backend topics via topic routing rules configured per-tenant (Kafka backend). If the tenant has no routing rules, or has rules but none match the channel, the publish is rejected — never silently dropped or dead-lettered. See Unroutable Publishes for the exact responses.
Topic Routing Rules
Every tenant must configure routing rules that map channel patterns to backend Kafka topic suffixes. Without routing rules, all client publishes are rejected (secure by default). Routing rules gate client and REST publishing only — ingesting via the tenant's automatically provisioned default topic needs no rules on any edition (see Which Topics Sukko Consumes).
Rules are evaluated in ascending priority order — the rule with the lowest priority value is checked first (first match wins). Add a catch-all rule at priority 100 and tenant-specific rules at 0–99 to override it.
# Add a specific rule (lower priority = checked first)
curl -X POST https://gateway.example.com/api/v1/tenants/demo/routing-rules \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"rule": {"pattern": "trades.**", "topics": ["trades"], "priority": 0}
}'
# Add a catch-all rule
curl -X POST https://gateway.example.com/api/v1/tenants/demo/routing-rules \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"rule": {"pattern": "**", "topics": ["default"], "priority": 100}
}'
Or replace all rules atomically with PUT:
curl -X PUT https://gateway.example.com/api/v1/tenants/demo/routing-rules \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"rules": [
{"pattern": "trades.**", "topics": ["trades"], "priority": 0},
{"pattern": "rooms.**", "topics": ["rooms"], "priority": 10},
{"pattern": "**", "topics": ["default"], "priority": 100}
]
}'
| Field | Description |
|---|---|
pattern | Channel suffix pattern — see Wildcard Semantics below |
topics | One or more topic suffixes to route matching channels to — see Fan-Out |
priority | Evaluation order (ascending, first match wins). Must be unique per tenant. |
Wildcard Semantics
| Pattern | Matches |
|---|---|
** | Any channel suffix (any number of dot-separated segments) |
trades.** | trades.BTC-USD, trades.spot.BTC-USD, trades.futures.ETH.weekly |
rooms.*.presence | rooms.engineering.presence, but NOT rooms.a.b.presence |
* submitted via the API is accepted and normalized to ** on storage — GET responses always return the normalized ** form. For example, submitting "pattern": "trades.*" is stored and returned as "pattern": "trades.**".
This means two rules with patterns trades.* and trades.** are treated as duplicates — the second POST will return 409 ROUTING_RULE_DUPLICATE_PATTERN.
Fan-Out
The topics array enables routing a channel to multiple Kafka topics simultaneously. All listed topics receive the message in parallel — this is useful for audit logging, analytics, or multi-consumer architectures.
{"pattern": "trades.**", "topics": ["trades", "audit-log"], "priority": 0}
Publishing to demo.trades.BTC-USD routes to both prod.demo.trades and prod.demo.audit-log.
Each value in topics is a topic suffix (not a full topic name). The full Kafka topic name is constructed as:
{namespace}.{tenant_id}.{suffix}
For example, with namespace prod and tenant demo, suffix trades becomes topic prod.demo.trades.
Unroutable Publishes
A publish that cannot be routed to a topic is rejected — never silently dropped or dead-lettered. Because REST publish is synchronous, the publisher receives the rejection immediately instead of a misleading 200 for a message that would never reach subscribers. Two cases, both rejected:
| Case | REST response | WebSocket error code |
|---|---|---|
| Tenant has no routing rules provisioned | 409 PUBLISH_NOT_ROUTABLE | no_routing_rules |
| Tenant has rules but none match the channel | 409 PUBLISH_NOT_ROUTABLE | no_matching_route |
Resolve either by provisioning a routing rule whose pattern matches the channel — add a catch-all ** rule to accept every channel suffix. This applies to the Kafka backend only; the direct backend has no routing rules and never returns these errors. Note that routing rules are a Pro feature: on a Community deployment running the kafka backend, client and REST publishes are always rejected as unroutable — there, messages enter via your own Kafka producers (the ingest path), or you use the direct backend for client publishing.
Infrastructure topics
The default and dead-letter topics are created automatically when a tenant is provisioned. If a topic fails to create during tenant setup, the error is logged at Error level, provisioning_reactivate_topic_provision_errors_total is incremented, and reactivation continues. To create these topics for tenants provisioned before they existed, run migrate-dlq.
Next Steps
- Authentication — JWT claims that drive channel access
- Multi-Tenancy — Per-tenant channel isolation
- REST API Reference — Channel rules API endpoints