Skip to main content
Version: v0.9.0a2
Spec
Experimental feature
This feature is Experimental. Breaking changes may occur before it reaches Stable. See feature status definitions →

Spec-X7-Subscriptions

4 min readSpec contributor · Node operatorExperimental · future plugin line

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}"
}
Field
Constraint
Notes
target
required
MUST be either a scope: prefixed scope identifier or an entity: prefixed entity URI.
on_change=webhook
HTTPS only
Delivers events to webhook_url through HTTP POST. webhook_url MUST use HTTPS.
on_change=wake
Paperclip
Wakes the specified agent by triggering a Paperclip wake event on its assigned issue. Standalone deployments that do not support wake delivery MUST return HTTP 422 with error code wake_not_supported.
Scoping
authorized only
Subscriptions MUST be scoped to the caller's authorized garden or to the global scope if the caller has global read access.
Duplicates
dedup
Duplicate subscriptions MUST be deduplicated using 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:

  1. Token revocation check: verify the subscriber's capability token is not revoked. A revoked token MUST be treated the same as access revocation below.
  2. 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

HTTP
Error code
Condition
404
subscription_not_found
No subscription with given id exists.
422
wake_not_supported
on_change="wake" on a non-Paperclip deployment.