Skip to content

Routing

The mark of a healthy event architecture: producers don’t know consumers exist. A producer publishes what happened, annotated with attributes; the platform decides who receives it. In Vulkan, that decision is a binding — a stored predicate evaluated at fan-out time.

Publish with a routing key:

stream.Publish(ctx, &OrderCreated{...},
vulkan.WithRoutingKey("orders.eu.created"))

Bind groups with wildcard patterns (* matches one segment, > matches the rest):

// Receipts care about every created order, anywhere.
vulkan.Bind(client, "email-receipts", "orders.*.created")
// The EU compliance service wants everything EU, any event type.
vulkan.Bind(client, "eu-compliance", "orders.eu.>")
// US warehouse only ships US orders.
vulkan.Bind(client, "us-warehouse", "orders.us.>")

Publish orders.eu.createdemail-receipts and eu-compliance get delivery rows; us-warehouse hears nothing. The producer changed zero lines when eu-compliance came online last month.

When routing depends on message content rather than a hierarchy, match on headers:

stream.Publish(ctx, &OrderCreated{...},
vulkan.WithHeaders(vulkan.H{"tier": "enterprise", "amount_band": "high"}))
// Concierge team only sees high-value enterprise orders.
vulkan.BindHeaders(client, "concierge-review",
vulkan.H{"tier": "enterprise", "amount_band": "high"})

Under the hood this is JSONB containment (headers @> pattern) — indexed, fast, and visible in EXPLAIN like everything else in Vulkan.

A binding could in principle be checked at publish time or at claim time. Vulkan evaluates it at fan-out — when delivery rows materialize — which buys a property the broker world can’t match:

RabbitMQ made routing famous with exchanges; NATS with subject wildcards. Vulkan’s version: an exchange is just a table of predicates, and a predicate over retained facts beats a predicate over fly-by messages.

-- Your entire routing topology. No management UI required (but Cloud has one).
SELECT consumer_group, pattern FROM vulkan.bindings ORDER BY 1;