Skip to content

event-publisher

Status: Current | Last reviewed: 2026-08-12

Publishes integration firewall changes out of Postgres to the cloud's own event bus, so that whatever owns security groups / firewall rules in that account can react when a CCaaS or SIP integration's CIDR list changes.

It is not a general-purpose event fan-out. It listens on exactly one channel and cares about exactly one kind of change.

Tier 2: a regression stops firewall rules from following a CIDR edit in one cloud. Loud for that customer, invisible to everyone else.

Source

apps/event-publisher

Runtime

Aspect Value
Language Go 1.25
Port 8080 in-cluster (HTTP_ADDR); /health, /healthz, /readyz, /info
Sinks AWS EventBridge, GCP Pub/Sub, Azure Event Grid
Listen channel integration_firewall_events (PG_LISTEN_CHANNEL)

Test files: 0. See Service tiers.

What produces the events

The integrations table carries an AFTER INSERT OR UPDATE OR DELETE trigger, integration_firewall_change_trigger → notify_integration_change(). On a qualifying change it does two things in the same transaction:

  1. Inserts a row into integration_events with processing_status = 'pending' — the durable outbox.
  2. Fires pg_notify('integration_firewall_events', …) — the low-latency nudge.

It only qualifies for type_id IN ('genesys', 'webex', 'asterisk', 'kamailio', 'janus', 'sip_trunk'), and an UPDATE is dropped outright unless extract_cidrs_from_config() actually returns a different list. Editing a Genesys integration's client secret emits nothing; editing its CIDR allow-list emits cidr_changed.

What it does

  • LISTENs on the channel and handles each NOTIFY as it arrives.
  • Independently polls get_pending_firewall_events($BATCH_SIZE) every PG_POLL_INTERVAL (default 30 s) to drain the outbox — this is what makes a restart or a missed notification recoverable, since LISTEN state does not survive a dropped connection.
  • Wraps each event as a CloudEvent and publishes to every configured cloud concurrently. CLOUD_PROVIDER is an allow-list (aws, gcp, azure, a comma-separated subset, or all — and all is the default), not a single-target selector.
  • Skips a sink whose failure count has crossed CIRCUIT_BREAKER_THRESHOLD (default 5) until CIRCUIT_BREAKER_TIMEOUT (default 30 s) elapses.
  • On success, batches the ids into mark_events_published(). On failure, calls complete_event(id, false, err), which sets failed and increments retry_count; get_pending_firewall_events stops re-serving a row at retry_count >= 5.

A cloud with no credentials configured is not an error — the publisher logs a warning at startup and, if no sink initializes, logs every event and acks it. A misconfigured deploy therefore looks healthy and drains its outbox to /dev/null.

Known: a once-failed event republishes on every poll

get_pending_firewall_events() serves rows with processing_status IN ('pending', 'failed'), but mark_events_published() only updates rows WHERE processing_status = 'pending'. So an event that fails once and then succeeds is never marked published: it stays failed with its retry_count frozen (complete_event only increments on the failure path), and every 30 s poll re-fetches and re-publishes it. The retry ceiling never catches it, because that ceiling counts failures.

Consequence: a transient EventBridge blip turns one CIDR change into an unbounded stream of duplicate events. Consumers of this bus must be idempotent regardless — but this is a bug, not a design. Fixing it means either having the success path call complete_event(id, true) or widening mark_events_published's WHERE to include 'failed'.

Dependencies

Service Purpose
Postgres LISTEN/NOTIFY + the integration_events outbox
Cloud event bus One or more of: AWS EventBridge, GCP Pub/Sub, Azure Event Grid

Vanguard

It does not run in Vanguard. There is no .devcontainer/Procfile entry and no overmind process for it, because there is no cloud event bus to publish to. The trigger still fires on every qualifying integrations write, so integration_events accumulates pending rows locally — harmless, and a useful place to check that the producing half works:

SELECT event_type, integration_type, processing_status, created_at
FROM integration_events ORDER BY created_at DESC LIMIT 10;

To exercise the service itself, run it against the local DB with no cloud configured; it will log each CloudEvent and ack it.

Production

Helm chart at charts/event-publisher — a base-app wrapper, one replica, CLOUD_PROVIDER and the per-cloud env commented out for the deployer to fill in. It is not a subchart of charts/trueserve-stack, so it is installed deliberately or not at all.

One replica is the intended shape. get_pending_firewall_events uses FOR UPDATE SKIP LOCKED, but the lock is released the moment the wrapping statement commits, so it does not actually serialize two concurrent pollers — don't scale this up expecting exactly-once.

See also