- Diagrams
- Flows
- Entity Stream Flow
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 kind | Topic | Read by |
|---|---|---|
OutboxKindEntity | entity:{entityType}.{entityId}.{event} | Entity stream subscriptions |
OutboxKindEvents | events:{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.
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.