Skip to main content

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.

Deny-all by default

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​

FieldDescription
publicSubscribe patterns for all connections
defaultFallback subscribe patterns when no group matches
group_mappingsGroup name to subscribe patterns mapping
publish_publicPublish patterns for all connections
publish_defaultFallback publish patterns when no group matches
publish_group_mappingsGroup 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.* matches general.chat but not general.chat.v2
  • In routing rule patterns, * matches any sequence including dots: rooms.* matches rooms.general.messages

Placeholders (resolved against JWT claims at authorization time):

  • {principal}, {sub}, {user_id}, {app_id} — resolve to the JWT sub claim
  • {tenant_id}, {tenant} — resolve to the authenticated tenant
  • {group_id} — used inside group_mappings values; 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}
]
}'
FieldDescription
patternChannel suffix pattern — see Wildcard Semantics below
topicsOne or more topic suffixes to route matching channels to — see Fan-Out
priorityEvaluation order (ascending, first match wins). Must be unique per tenant.

Wildcard Semantics​

PatternMatches
**Any channel suffix (any number of dot-separated segments)
trades.**trades.BTC-USD, trades.spot.BTC-USD, trades.futures.ETH.weekly
rooms.*.presencerooms.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:

CaseREST responseWebSocket error code
Tenant has no routing rules provisioned409 PUBLISH_NOT_ROUTABLEno_routing_rules
Tenant has rules but none match the channel409 PUBLISH_NOT_ROUTABLEno_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​