Bidi Operations Runbook
v0.78.0 — BIDIOPS-DOC-01
This runbook covers day-two operations for the bidirectional integration (bidi) subsystem introduced in v0.77.0 and v0.78.0. It assumes the reader has completed the pg-tide relay guide.
Queue draining procedure
When a relay goes down or pg_tide delivery stalls:
-
Check the queue depth for each subscription:
SELECT subscription_name, outbox_depth, outbox_oldest_age, dead_letter_count, pg_tide_paused FROM pg_ripple.bidi_status(); -
Pause delivery if you need a maintenance window:
SELECT tide.relay_disable('<pipeline_name>');bidi_status()will showpg_tide_paused = truewhile paused. New outbox events continue to accumulate. -
Resolve the relay issue, then resume:
SELECT tide.relay_enable('<pipeline_name>'); -
Monitor drain progress until
outbox_depthreturns to 0:SELECT subscription_name, outbox_depth FROM pg_ripple.bidi_status(); -
Inspect dead-letter events if any accumulated during the pause:
SELECT * FROM pg_ripple.list_dead_letters('<subscription_name>');
Token rotation procedure
Per-subscription bearer tokens should be rotated on a schedule (e.g., every 90 days) or immediately after a suspected breach.
-
Register the new token while the old one is still active:
SELECT pg_ripple.register_subscription_token( '<subscription_name>', ARRAY['linkback','divergence','abandon'], 'crm-relay-2026-Q3' );Store the returned raw token securely (it is shown only once).
-
Distribute the new token to the relay(s). Allow a brief overlap period (≥ token cache TTL, 60 seconds).
-
Revoke the old token once
last_used_atconfirms relay traffic has switched:SELECT pg_ripple.revoke_subscription_token(<old_token_hash_bytea>); -
Verify only the new token is active:
SELECT label, scopes, last_used_at, revoked_at FROM pg_ripple.list_subscription_tokens('<subscription_name>');
Redaction policy guidance
- Mark PII and secret-bearing predicates with
"@redact": truein the subscription's JSON-LD frame. - Redacted predicates render as
{"@redacted": true}in the standard outbox. - For compliance pipelines that need cleartext, configure a separate subscription with an unredacted outbox table and grant access only to the elevated relay's credentials (standard PostgreSQL GRANTs).
- The frame is the only source of redaction truth — there is no additional allow-list.
- See Redaction pattern below.
Schema-evolution rollout playbook
When you need to change the subscription frame, IRI template, or exclude list:
-
Frame change (add/remove predicates):
SELECT pg_ripple.alter_subscription( '<name>', frame_change_policy => 'new_events_only' ); -- Then update the subscription's frame column separately.Queued outbox rows drain with the old frame; new events use the updated frame.
-
IRI template or match-pattern change:
SELECT pg_ripple.alter_subscription( '<name>', iri_change_policy => 'new_events_only' );Already-rendered outbox IRIs are not retroactively rewritten.
For broken templates, pause the pg_tide relay pipeline, drop/requeue affected rows if needed, and record the action in_pg_ripple.subscription_schema_changes. -
Exclude-graphs change:
SELECT pg_ripple.alter_subscription( '<name>', exclude_change_policy => 'new_events_only' );All changes are recorded automatically in
_pg_ripple.subscription_schema_changes:SELECT * FROM _pg_ripple.subscription_schema_changes WHERE subscription_name = '<name>' ORDER BY changed_at DESC;
Reconciliation playbook
When a relay reports CAS divergence (actual values differ from base):
-
Enqueue the divergence (the relay calls this automatically):
SELECT pg_ripple.reconciliation_enqueue( '<event_id>'::uuid, '{"ex:phone": {"actual": "+1-555-0100", "base": "+1-555-0200", "after": "+1-555-0300"}}'::jsonb ); -
Pull the next item for review:
SELECT * FROM pg_ripple.reconciliation_next('<subscription_name>'); -
Choose a resolution action:
Action When to use accept_externalExternal system is the authority; ingest its actual values force_internalHub is the authority; re-emit event unconditionally merge_via_owl_sameAsDivergence reveals a duplicate subject; assert owl:sameAsdead_letterCannot resolve now; move to dead-letter for later review SELECT pg_ripple.reconciliation_resolve( <reconciliation_id>, 'accept_external', 'Confirmed with CRM team: external value is correct' ); -
Monitor open items:
SELECT subscription_name, reconciliation_open FROM pg_ripple.bidi_status() WHERE reconciliation_open > 0;
Chaos-test interpretation
The bidi chaos test (tests/stress/bidi_chaos.sh) runs the following smoke scenarios:
- abandon_linkback idempotency — calling twice should not error.
- purge_event_audit zero-row safety — no error when table is empty.
- reconciliation enqueue/resolve cycle — end-to-end round-trip.
- bidi_health valid status — always returns one of:
healthy|degraded|paused|failing. - token registration/revocation — register, then revoke.
If any test fails, the script exits with a non-zero code and identifies the failing case.
Pattern: per-subscription auth
Four-token deployment example (two relays × two scope sets):
-- CRM relay: full bidi scopes.
SELECT pg_ripple.register_subscription_token(
'crm_relay',
ARRAY['linkback','divergence','abandon','outbox_read'],
'crm-relay-prod'
);
-- ERP relay: outbox-read only.
SELECT pg_ripple.register_subscription_token(
'erp_relay',
ARRAY['outbox_read'],
'erp-relay-prod'
);
-- Operator admin token: registered in _pg_ripple.admin_tokens (not shown here).
HTTP endpoints enforce scope checks: POST /subscriptions/crm_relay/events/{id}/linkback requires linkback scope for the crm_relay subscription.
Pattern: redaction with elevated subscription
Standard relay (redacted, normal consumers):
{
"@context": { "ex": "https://example.com/ns#" },
"@type": "ex:Contact",
"ex:name": {},
"ex:phone": { "@redact": true },
"ex:email": {},
"ex:taxId": { "@redact": true }
}
Compliance relay (unredacted, elevated access):
- Register a separate subscription
crm_relay_auditpointing tocrm_relay_outbox_unredacted. - The frame omits
"@redact": truefor all predicates. - Grant
SELECToncrm_relay_outbox_unredactedonly to the compliance relay's database role. - Register a separate token with
outbox_readscope forcrm_relay_audit.
event_audit records action = 'emit_unredacted' for bridge-writer rows in the unredacted outbox when optional emit auditing is enabled (controlled by pg_ripple.audit_log_enabled).