Skip to content

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():

  1. To aggregate_id = scan_id — so the scan-detail feed stays current.
  2. To aggregate_id = repository_id (with scan_id added 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:

  1. Receives the raw BugRecord list from the engine event payload
  2. Normalizes them into FindingRecord domain models
  3. Upserts each finding (creating new records or updating existing ones if the engine reports the same finding twice)
  4. 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 logProjectScanLogEventUseCase + 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:

  1. Live UI — the frontend polls GET /api/repositories/:repo_id/events and shows a real-time feed while the scan runs.
  2. Debug log — after a scan finishes, operators can read the full domain event log (or download vega-core-events.jsonl from artifacts) to understand what Codex analyzed and what it found.
  3. Findings sourcefinding_updated events are the primary source of truth for security findings. NormalizeAndUpsertFindingsUseCase processes each one and upserts a FindingRecord.

Debugging event issues

Scan runs but the UI doesn't update:

  1. Check that vega-core actually emitted events — look at the domain_events table for the scan's aggregate_id, or check vega-core-events.jsonl in the scan artifacts.
  2. Check EngineEventSink in adapters/engine/vega_core/ — did it successfully map the raw vega-core events to DomainEvents?
  3. Check EventStore — were events persisted? In Postgres: SELECT * FROM domain_events WHERE aggregate_id = '<scan_id>' ORDER BY sequence;
  4. Check the frontend — is it polling the right scan context, and are filters hiding events?

Events appear but no findings show up:

  1. Confirm vega-core emitted finding_updated events (check vega-core-events.jsonl or domain_events table).
  2. Check NormalizeAndUpsertFindingsUseCase in application/findings/ — look for errors in the scan runner logs.
  3. Check FindingStore — query the findings table directly: SELECT payload FROM findings WHERE payload->>'scan_id' = '<scan_id>';
  4. Check the frontend findings page filters — severity or status filters might be hiding results.