Skip to content

Event Versioning

As your system evolves, event schemas change. Ironflow supports event versioning with upcaster functions that automatically transform old events to the latest schema when they’re consumed.

How It Works

Upcaster Chain — a stored v1 event is transformed through upcasters (v1→v2, v2→v3) so consumers always see the latest schema

Events are stored with their original version. When a consumer reads them, upcasters transform the data through each version step until it reaches the latest schema.

Two language-specific rules to keep in mind:

  • TypeScript — each defineEvent({ version: N, upcast }) registers the N-1 → N step. You do not specify the source version; it’s implicit.
  • Goupcasters.Register(name, from, to, fn) is explicit, but to must equal from + 1. The chain walks one step at a time; a registered 1 → 3 hop is unreachable.

Defining Versioned Events

import { defineEvent } from "@ironflow/core";
// Version 1 — original schema
const userCreatedV1 = defineEvent({
name: "user.created",
version: 1,
});
// Version 2 — added email field
const userCreatedV2 = defineEvent({
name: "user.created",
version: 2,
upcast: (v1Data) => ({
...v1Data,
email: v1Data.email ?? "unknown@example.com",
}),
});
// Version 3 — added role field
const userCreatedV3 = defineEvent({
name: "user.created",
version: 3,
upcast: (v2Data) => ({
...v2Data,
role: v2Data.role ?? "member",
}),
});

Registering Events

Register event definitions when setting up your serve handler or worker:

import { defineEvent, createEventDefinitionRegistry } from "@ironflow/core";
import { serve, createFunction } from "@ironflow/node";
const userCreatedV1 = defineEvent({ name: "user.created", version: 1 });
const userCreatedV2 = defineEvent({
name: "user.created",
version: 2,
upcast: (data) => ({ ...data, email: data.email ?? "unknown@example.com" }),
});
const registry = createEventDefinitionRegistry();
registry.register(userCreatedV1);
registry.register(userCreatedV2);
const handleUser = createFunction(
{
id: "handle-user",
triggers: [{ event: "user.created" }],
},
async ({ event }) => {
// event.data is always v2 shape — upcasted automatically
console.log(event.data.email); // always present
},
);
export default serve({
functions: [handleUser],
eventDefinitions: registry,
});

Emitting Versioned Events

Specify the version when emitting events:

// Emit a v2 event
await client.emit("user.created",
{ name: "Alice", email: "alice@example.com" },
{ version: 2 },
);

Events emitted without a version default to version 1.

Versioning with Entity Streams

Entity stream events also support versioning:

// Append a versioned event to an entity stream
await client.streams.append("user-123", {
name: "user.profile_updated",
data: { name: "Alice", email: "alice@example.com" },
entityType: "user",
}, {
expectedVersion: 2,
version: 2, // event schema version
});

When reading entity streams, events are returned with their original version. Upcasting is applied automatically inside push handlers (serve) and pull workers (createWorker) when an event triggers a function — not on the raw read path. Calling client.streams.read(...) returns event data as it was stored; if you need the latest schema there, run it through your registered upcasters yourself.

Best Practices

Always add, never remove

Add new fields with defaults. Never remove or rename fields in a new version — consumers on older code versions may still expect them.

// Good: add field with default
const v2 = defineEvent({
name: "order.created",
version: 2,
upcast: (data) => ({ ...data, currency: data.currency ?? "USD" }),
});
// Bad: remove field (breaks older consumers)
// upcast: (data) => { delete data.oldField; return data; }

One change per version

Keep each version bump focused on a single schema change. This makes the upcaster chain easier to understand and debug.

Version numbers are linear

Use simple incrementing integers (1, 2, 3). The upcaster chain runs sequentially from the stored version to the latest registered version.

Test your upcasters

// Verify the full chain works
const v1Data = { name: "Alice" };
// After v1→v2: { name: "Alice", email: "unknown@example.com" }
// After v2→v3: { name: "Alice", email: "unknown@example.com", role: "member" }

Event Schema Registry

Ironflow provides a server-side schema registry for storing JSON Schema definitions per event type and version. This enables schema validation, documentation, and discovery.

Registering Schemas

Terminal window
# Register a schema for order.created v1
curl -X POST http://localhost:9123/api/v1/events/schemas \
-H "Content-Type: application/json" \
-d '{
"event_name": "order.created",
"version": 1,
"schema_json": "{\"type\":\"object\",\"properties\":{\"orderId\":{\"type\":\"string\"},\"total\":{\"type\":\"number\"}},\"required\":[\"orderId\",\"total\"]}",
"description": "Order creation event"
}'

Querying Schemas

Terminal window
# List all schemas
curl http://localhost:9123/api/v1/events/schemas
# List schemas for a specific event
curl "http://localhost:9123/api/v1/events/schemas?event_name=order.created"
# Get latest version of a schema
curl http://localhost:9123/api/v1/events/schemas/order.created
# Get a specific version
curl http://localhost:9123/api/v1/events/schemas/order.created/1

Testing Upcasts

Test the upcaster chain for an event without processing real data:

Terminal window
curl -X POST http://localhost:9123/api/v1/events/upcast \
-H "Content-Type: application/json" \
-d '{
"event_name": "order.created",
"from_version": 1,
"to_version": 3,
"data": {"orderId": "123"}
}'

This returns the version chain that would be applied. Phase 1 returns schema metadata without actual data transformation — complex SDK-defined upcasters require a running worker.

Domain-Driven Design

Event versioning is a critical part of maintaining domain events in a DDD system — as your domain model evolves, upcasters ensure consumers always see the latest event schema. See Commands, Events & Reactions for how domain events fit into the broader DDD picture.