Event Flow
Events are the mechanism Vega uses to bridge the gap between a long-running background process (the scan) and the user-facing dashboard. Because scans can take minutes to run, the backend doesn't make the user wait for a single HTTP response — instead, it streams incremental updates as domain events.
How events work
flowchart LR
Codex[Codex CLI subprocess]
VC[vega-core\norchestrator/stages]
Sink[EngineEventSink\nadapters/engine/vega_core]
ES[EventStore port\nPostgresEventStore / JsonEventStore]
UC[NormalizeAndUpsertFindingsUseCase]
FS[FindingStore port]
API["Events API\nGET /api/repositories/:id/events"]
UI[Dashboard\nLive scan feed]
Codex -->|stdout JSON stream| VC
VC -->|VegaCoreEvent objects| Sink
Sink -->|DomainEvent| ES
Sink -->|finding_updated| UC
UC -->|upsert FindingRecord| FS
ES -->|GET domain events| API
API -->|JSON array| UI
Dual-aggregate publishing
Every backend lifecycle event (scan_running, scan_completed, etc.) is published twice by ExecuteScanUseCase._publish_scan_event():
- To
aggregate_id = scan_id— so the scan-detail feed stays current. - To
aggregate_id = repository_id(withscan_idadded to the payload) — so the repository-level event feed can track all scans without polling each individually.
Engine events (emitted by vega-core via EngineEventSink) are also stored under aggregate_id = scan_id.
The event store
Events flow into EventStore (a port implemented by PostgresEventStore in production or JsonEventStore locally). The event store is append-only — events are never modified or deleted. Each event has a monotonically increasing sequence number per aggregate_id, which enables the frontend to poll for events since a known sequence position.
Domain events are stored in the domain_events table (added in migration 022).
The finding store
finding_updated engine events are separately processed by NormalizeAndUpsertFindingsUseCase. This use case:
- Receives the raw
BugRecordlist from the engine event payload - Normalizes them into
FindingRecorddomain models - Upserts each finding (creating new records or updating existing ones if the engine reports the same finding twice)
- Optionally enqueues the finding for verification
Findings are durable records separate from the event log. The event log is for live progress; findings are for structured review and triage.
Event types
Backend-emitted (published by ExecuteScanUseCase):
| Event type | What it means | Aggregate |
|---|---|---|
scan_running |
Scan transitioned to running state | scan + repo |
scan_completed |
All phases finished successfully | scan + repo |
scan_failed |
Scan failed with an exception | scan + repo |
planning_artifact_reused |
Cached planning artifact reused | scan + repo |
scan_runner_started |
ECS runner task launched for verification shard | scan |
Engine-emitted (called by vega-core through EngineEventSink, stored under aggregate_id = scan_id):
| Event type | What it means | Stored as finding? |
|---|---|---|
scan_started |
vega-core has started the audit loop | No |
scan_progress |
Status update (stage name, component N of M) | No |
scan_log |
Debug log line from vega-core or Codex | No |
finding_updated |
One or more security issues discovered or updated | Yes |
component_worker_started |
A component audit worker has started | No |
component_worker_completed |
A component audit worker finished successfully | No |
component_worker_failed |
A component audit worker encountered an error | No |
scan_completed |
vega-core audit loop finished | No |
scan_failed |
vega-core encountered an unrecoverable error | No |
scan_cancelled |
Scan was cancelled cooperatively | No |
finding_verified |
Verification result for one finding | Updates finding record |
stage_started / stage_completed |
Stage-level progress bookmarks (used for plan cache replay) | No |
Read models for the dashboard
For operator visibility, two additional read models are materialized from the event stream:
Activity log — ProjectScanLogEventUseCase + MaterializeScanLogArtifactsUseCase build a structured activity log artifact that contains the sequence of scan events in a human-readable format. Accessible via GET /api/artifacts/scans/:id/activity-log.
Worker components log — tracks per-component worker status (started / completed / failed for each component). Accessible via GET /api/artifacts/scans/:id/worker-components-log.
Why this design?
The event stream serves three purposes:
- Live UI — the frontend polls
GET /api/repositories/:repo_id/eventsand shows a real-time feed while the scan runs. - Debug log — after a scan finishes, operators can read the full domain event log (or download
vega-core-events.jsonlfrom artifacts) to understand what Codex analyzed and what it found. - Findings source —
finding_updatedevents are the primary source of truth for security findings.NormalizeAndUpsertFindingsUseCaseprocesses each one and upserts aFindingRecord.
Debugging event issues
Scan runs but the UI doesn't update:
- Check that vega-core actually emitted events — look at the
domain_eventstable for the scan'saggregate_id, or checkvega-core-events.jsonlin the scan artifacts. - Check
EngineEventSinkinadapters/engine/vega_core/— did it successfully map the raw vega-core events toDomainEvents? - Check
EventStore— were events persisted? In Postgres:SELECT * FROM domain_events WHERE aggregate_id = '<scan_id>' ORDER BY sequence; - Check the frontend — is it polling the right scan context, and are filters hiding events?
Events appear but no findings show up:
- Confirm vega-core emitted
finding_updatedevents (checkvega-core-events.jsonlordomain_eventstable). - Check
NormalizeAndUpsertFindingsUseCaseinapplication/findings/— look for errors in the scan runner logs. - Check
FindingStore— query the findings table directly:SELECT payload FROM findings WHERE payload->>'scan_id' = '<scan_id>'; - Check the frontend findings page filters — severity or status filters might be hiding results.