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¶
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:
- Inserts a row into
integration_eventswithprocessing_status = 'pending'— the durable outbox. - 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 eachNOTIFYas it arrives.- Independently polls
get_pending_firewall_events($BATCH_SIZE)everyPG_POLL_INTERVAL(default 30 s) to drain the outbox — this is what makes a restart or a missed notification recoverable, sinceLISTENstate does not survive a dropped connection. - Wraps each event as a CloudEvent and publishes to every configured cloud
concurrently.
CLOUD_PROVIDERis an allow-list (aws,gcp,azure, a comma-separated subset, orall— andallis the default), not a single-target selector. - Skips a sink whose failure count has crossed
CIRCUIT_BREAKER_THRESHOLD(default 5) untilCIRCUIT_BREAKER_TIMEOUT(default 30 s) elapses. - On success, batches the ids into
mark_events_published(). On failure, callscomplete_event(id, false, err), which setsfailedand incrementsretry_count;get_pending_firewall_eventsstops re-serving a row atretry_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.