Skip to content

Entity Stream Flow

Entity streams store domain events per entity with optimistic concurrency. EntityStreamService/AppendEvent writes a row to the events table carrying entity_id ("order:123"), entity_type, the next entity_version, and the JSON payload. Internal function history uses the same machinery under an ironflow:fn:{id} entity ID.

Entity data lives only in the database. NATS carries notifications, and an append writes two outbox rows in the same transaction as the event — not one:

Outbox kindTopicRead by
OutboxKindEntityentity:{entityType}.{entityId}.{event}Entity stream subscriptions
OutboxKindEventsevents:{name}Projections

The entity row is written with entityVersion: 0 as a placeholder and carries PatchEntityVersion, because the real version is not known until CreateEntityEvent computes it inside the transaction. Both rows ride that transaction, so a rejected append leaves no orphan NATS message.

Ironflow event fan-out

There is one more channel that does not ride the outbox. After the append commits, publishStreamAppended fires system.stream.{entityId}.appended (#1730) for the Ironflow Desktop streams channel. It is fire-and-forget in both directions: a failed frame never fails the append, and it is neither retried nor dead-lettered — a dropped frame is simply gone. Treat those frames as a hint over the streams table, never as a ledger to hold state from.

Optimistic concurrency is enforced by a unique constraint on (environment_id, entity_id, entity_version). A version conflict returns ErrVersionConflict and writes nothing — read the stream again and retry.

Entity events also trigger workflow function matching: if a function’s trigger matches the event name, runs are created and dispatched exactly as they are for emit().

The write path, upcasters, and the projection read path are drawn on the Event Sourcing Flow diagram.