BEAM Store Contract¶
Purpose¶
This document captures the current Phase 3 contract for the Elixir event store in beam/apps/dg_store.
The Python semantic reference remains authoritative. The BEAM store is considered correct only insofar as it preserves the observable behavior frozen in:
docs/reference/EVENT_ENVELOPE_CONTRACT.mddocs/reference/APPEND_SEMANTICS.mddocs/reference/STORAGE_BACKEND_EXPECTATIONS.mddocs/reference/QUERY_AND_ORDERING_INVARIANTS.mddocs/reference/SEMANTIC_PARITY_POLICY.md
Scope¶
Phase 3 covers:
- append-only persistence into Postgres
- idempotency enforcement
trace_seqmonotonicity enforcement- post-
TraceFinishedwrite locking - deterministic read ordering
- batch iteration for replay and projector catch-up
- projection-cursor storage for Phase 4 handoff
- store telemetry and domain-level error mapping
Phase 3 does not yet cover:
- BEAM-native projection tables
- BEAM-native projection digests
- BEAM-native graph, summary, or precedent query surfaces
Those remain Phase 4 work.
Tables¶
dg_event_log¶
Authoritative append-only event table.
Columns:
log_seq: global append ordertenant_id: tenant partition keyevent_id: caller-supplied event idtrace_id: trace membership keytrace_seq: per-trace sequence numberevent_type: frozen event vocabularyoccurred_at: validated RFC3339 timestamp string preserved from the callerrecorded_at: normalized RFC3339 timestamp string assigned at storage timeproducer_id: producer scope for idempotencysource_system,source_subsystemactor_type,actor_idcorrelation_id,causation_event_ididempotency_keyschema_versionpayload_json: canonical JSON stringpayload_hash: SHA-256 over canonical payload JSONtags_json: canonical JSON string for tags
Indexes and constraints:
- unique
event_id - unique
(tenant_id, trace_id, trace_seq) - unique
(tenant_id, producer_id, idempotency_key) - index
(tenant_id, trace_id, log_seq) - index
(tenant_id, event_type, log_seq) - index
(tenant_id, trace_id, event_type) - check constraints for non-negative
trace_seq, positiveschema_version, and validevent_type - insert trigger for first-event rules, monotonic sequencing, and
TraceFinishedlocking
dg_projection_cursors¶
Reserved Phase 4 handoff table used to track projection catch-up state.
Columns:
tenant_idprojection_namelast_log_sequpdated_at
This table exists now so projector work can build on a stable cursor surface without revisiting migration shape.
Append Contract¶
DecisionGraph.Store.append_event/2 performs the following logical pipeline:
- normalize the incoming Elixir
EventEnvelope - validate required envelope fields, payload shape, idempotency key, tags, actor, source, and PII rules
- canonicalize the payload and compute
payload_hash - canonicalize tags
- preserve the caller-provided
occurred_atstring after validation and assign normalizedrecorded_at - acquire a per-trace advisory transaction lock
- check for idempotent reuse under
(tenant_id, producer_id, idempotency_key) - if a prior event exists, validate reuse metadata parity while intentionally excluding
trace_seq - otherwise enforce next expected
trace_seqand post-finish locking - insert and return
StoredEvent
Observable guarantees:
- append-only persistence
- idempotent retries return the original stored event
- metadata drift on idempotent reuse raises
:idempotency_conflict - sequence drift raises
:event_sequence_invalid - post-finish writes raise
:conflict - storage failures are surfaced as
DecisionGraph.Error
Read Contract¶
Current read APIs in DecisionGraph.Store:
get_trace_events/2list_events/1iter_event_batches/1get_last_log_seq/1is_trace_finished/2get_next_trace_seq/2get_projection_cursor/2put_projection_cursor/3list_projection_cursors/1
Ordering guarantees:
get_trace_events/2returnstrace_seq ASClist_events/1returnslog_seq ASCiter_event_batches/1yields globally ordered, non-overlappinglog_seqbatches
Filters currently supported:
tenant_idtrace_idevent_typesince_log_sequntil_log_seqsince_occurred_atuntil_occurred_atlimit
Bounds are validated before query execution:
since_log_seq <= until_log_seqsince_occurred_at <= until_occurred_at- positive
limitandbatch_size
Telemetry and Errors¶
Phase 3 emits:
[:store, :append, :stop][:store, :append, :exception][:store, :idempotency, :reuse][:store, :read_batch, :stop]
Metadata fields currently attached where applicable:
tenant_idtrace_idevent_typeproducer_idrequest_iderror_code
Stable domain-level error categories:
:invalid_argument:schema_violation:pii_policy_violation:idempotency_conflict:event_sequence_invalid:conflict:storage
Local Dev vs Production Notes¶
Accepted environment differences in Phase 3:
- local dev and test use Docker-hosted Postgres on
localhost - test uses
Ecto.Adapters.SQL.Sandbox - local benchmark guidance prefers
MIX_ENV=testfor quieter logs and isolated storage - production is expected to use managed Postgres, stronger connection tuning, and observability aggregation beyond the local defaults
These are operational differences only. They must not change semantic outcomes.
Accepted Phase 3 Limitations¶
- The BEAM runtime persists only the event log and projection cursors; projection materialization remains Phase 4 work.
- Benchmark numbers are baseline local measurements, not release SLOs.
- The store currently preserves caller-provided
occurred_atstrings and normalizesrecorded_atso parity-sensitive projection rows and digests stay byte-stable with the Python reference. A future switch to native Postgres time types is allowed only if observable semantics remain unchanged.