Substrate subscriptions deliver Neotoma write-path events to external consumers without polling. Available since v0.12.0. Each subscription is a first-class subscription entity, so its filters, history, and provenance live in the same SQLite + reducer model as any other Neotoma record.
Use subscriptions when:
An external workflow needs to react when a new
taskorissuelands.A peer Neotoma instance should mirror a subset of entity types (the
peer sync
bridge runs on top of subscriptions).
A live UI or third-party dashboard wants a streaming activity feed.
Subscriptions are bounded by design: per-user caps, webhook circuit breakers, and a process-wide SSE ring buffer keep deliveries from overwhelming receivers.
MCP tools
subscribe({ entity_types?, entity_ids?, event_types?, delivery_method, webhook_url?, webhook_secret?, sync_peer_id? })— at least one ofentity_types,entity_ids, orevent_typesmust be non-empty (there is no firehose mode).delivery_methodiswebhookorsse.unsubscribe({ subscription_id })— removes a subscription.list_subscriptions({ active_only? })— enumerates subscriptions owned by the caller.get_subscription_status({ subscription_id })— returns the currentsubscriptionsnapshot, includingconsecutive_failures,max_failures, and theactiveflag (flipped tofalseby the circuit breaker aftermax_failuresconsecutive errors).
HTTP / SSE surface
POST /subscribe— same shape as the MCP tool.POST /unsubscribe—{ subscription_id }.GET /events/stream?subscription_id=<id>— Server-Sent Events stream for an SSE-mode subscription. Frame format:id: <ring_id>\nevent: <event_type>\ndata: <json>\n\n. Clients reconnect withLast-Event-ID: <ring_id>to replay buffered events.
Webhook contract
Environment
NEOTOMA_MAX_SUBSCRIPTIONS_PER_USER— soft cap on active subscriptions per user (default 50).NEOTOMA_SSE_EVENT_BUFFER— ring buffer capacity for the SSE hub (clamped 100–10000, default 1000).NEOTOMA_DEBUG_SUBSTRATE_EVENTS— debug logging on the underlying event bus.
Example
Subscribe to webhook delivery for new issue and task writes:
curl -X POST http://localhost:3080/subscribe \
-H "Authorization: Bearer $NEOTOMA_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"entity_types": ["issue", "task"],
"delivery_method": "webhook",
"webhook_url": "https://hooks.example.com/neotoma"
}'
Stream the same filter as Server-Sent Events:
curl -N \
-H "Authorization: Bearer $NEOTOMA_BEARER_TOKEN" \
"http://localhost:3080/events/stream?subscription_id=<id>"
Loop prevention with peer sync
When a subscription is configured with sync_peer_id = "<peer_id>", the bridge:
- Skips events whose
source_peer_idequals the subscription'ssync_peer_id(those came from that peer; sending them back would loop). - Routes matching deliveries through the peer-sync envelope, not the generic webhook queue, so they are signed and verified per the peer's
auth_method.
See the peer sync reference for full peer wiring.
Full reference
docs/subsystems/subscriptions.md covers the subscription schema, the matcher (subscriptionMatchesEvent), the bridge (subscription_bridge.ts), the SSE hub (sse_hub.ts), the webhook delivery worker (webhook_delivery.ts), and the boot wiring in install_subscription_bridge.ts.
See peer sync, API reference, and MCP reference.