Spec-X7-Subscriptions
What this spec defines
Push-style notification for changes to scopes or entities. Useful for webhook consumers and agent wake-ups, but the primitive expands the authorization boundary because event delivery can leak information after a caller loses access.
Status: Experimental. This material is deferred from the supported v0.9.0aN surface and must pass ADR-008 gates before reintroduction.
Source material: pre-reset §20.5 subscription material, extracted from the archived recall-and-graph design.
Subscriptions remain experimental until delivery-time authorization, replay, and operator runbook requirements are validated.
Route
The subscription API follows standard REST conventions: POST to create, GET to list or inspect, DELETE to cancel. Subscription state is server-managed: the node tracks cursors internally and delivers events through the configured change mechanism.
POST /v1/subscriptions
GET /v1/subscriptions
GET /v1/subscriptions/:id
DELETE /v1/subscriptions/:id
Request shape
A subscription request binds a target (either a scope or a specific entity) to a change-notification mechanism. The on_change field selects between webhook for HTTP-based push delivery and wake for Paperclip-integrated agent wake-ups. The event_filter array lets callers subscribe to a subset of event types, reducing noise for consumers that only care about specific lifecycle events. The idempotency_key ensures that retried creation requests do not produce duplicate subscriptions.
{
"target": "scope:global" | "entity:{entity_uri}",
"on_change": "webhook" | "wake",
"webhook_url": "https://example.com/hook",
"wake_agent_id": "{uuid}",
"event_filter": [
"fact_assert",
"fact_retract",
"contradiction_detected",
"card_refreshed"
],
"idempotency_key": "{opaque string}",
"scope": "{scope_id}"
}
targetscope: prefixed scope identifier or an entity: prefixed entity URI.on_change=webhookwebhook_url through HTTP POST. webhook_url MUST use HTTPS.on_change=wakewake_not_supported.idempotency_key if provided, or matched by structural equality if not. A duplicate POST MUST return 200 with the existing subscription.Event shape
Events are delivered as JSON payloads with the following structure:
{
"subscription_id": "{uuid}",
"event_id": "{uuid}",
"event_type": "fact_assert" | "fact_retract" | "contradiction_detected" | "card_refreshed",
"entity": "{entity_uri}",
"scope": "{scope_id}",
"fact_id": "{uuid}",
"hlc": "{hlc}",
"payload": {},
"idempotency_key": "{event_id}"
}
The idempotency_key in the delivery envelope MUST equal event_id.
Subscribers MUST treat deliveries with the same idempotency_key as
duplicates and discard them after first processing.
Delivery guarantees
At-least-once
Implementations MUST deliver each event at least once.
Exponential backoff
Retry failed webhook deliveries with exponential backoff (initial 1 s, max 10 attempts, cap 300 s).
5xx / timeout
Triggers retry. Webhook timeout is 10 seconds.
410 Gone
Permanently removes the subscription.
Replay window
STIGMEM_SUBSCRIPTION_REPLAY_S default 3600 s. Subscribers MAY replay via GET /v1/subscriptions/:id/events?after={event_id}.
Beyond window
Events outside the replay window are not recoverable. Nodes SHOULD expose this limit as replay_window_s.
Auth and scoping
Subscription creation MUST require the caller to hold a capability token with verb subscribe on the target scope or entity URI. Tokens MUST be validated before any subscription is persisted.
The primary security concern is cross-garden leakage through event streams.
A subscription's event stream MUST NOT leak facts from garden-scoped entities to callers without garden read access.
At each event delivery, implementations MUST perform the following checks in order:
- Token revocation check: verify the subscriber's capability token is not revoked. A revoked token MUST be treated the same as access revocation below.
- Garden ACL check: re-evaluate the caller's garden ACL against the event's target entity/scope, not just at subscription creation time.
If either check fails, event content MUST NOT be populated or delivered. The event record MAY be queued internally before these checks to honor at-least-once delivery semantics, but event content MUST be withheld from the subscriber until both checks pass at delivery time. If the caller's access or token has been revoked since subscription creation, delivery MUST be silently dropped and the subscription MUST be automatically cancelled with event type subscription_cancelled_access_revoked.
Storage
Implementations that enable this experimental feature persist subscription state and a replay buffer with dedicated tables. The replay buffer supports at-least-once delivery and short-window event replay without storing events in the core fact table.
CREATE TABLE IF NOT EXISTS subscriptions (
id TEXT PRIMARY KEY,
target TEXT NOT NULL,
on_change TEXT NOT NULL CHECK (on_change IN ('webhook', 'wake')),
webhook_url TEXT,
wake_agent_id TEXT,
event_filter TEXT NOT NULL DEFAULT '["fact_assert","fact_retract"]',
scope TEXT NOT NULL,
idempotency_key TEXT,
created_at INTEGER NOT NULL,
last_event_at INTEGER,
cancelled_at INTEGER,
replay_window_s INTEGER NOT NULL DEFAULT 3600
);
CREATE INDEX IF NOT EXISTS idx_subscriptions_target
ON subscriptions (target, scope);
CREATE TABLE IF NOT EXISTS subscription_events (
id TEXT PRIMARY KEY,
subscription_id TEXT NOT NULL REFERENCES subscriptions(id),
event_type TEXT NOT NULL,
entity TEXT,
fact_id TEXT,
hlc TEXT,
payload TEXT,
delivered_at INTEGER,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sub_events_sub_id
ON subscription_events (subscription_id, created_at DESC);
Error reference
subscription_not_foundwake_not_supportedon_change="wake" on a non-Paperclip deployment.