Schema Versioning
Events are permanent records. Once an event is in the ledger, its payload schema is part of your permanent history. Changing payload fields affects not just new events but also existing events that projections and queries may reference.
Causet’s compiler validates all cross-references at build time — if a projection references event.some_field and that field is removed from the event’s payload, the build fails. This is intentional.
What is the IR
The IR (Intermediate Representation) is the compiled output of your .causet files. It has two artifacts:
causet.runtime.json— loaded by the runtime for rule evaluationcauset.projections.json— loaded by the projection worker and query service for DDL and query execution
Each release produces a unique IR version. The active IR version per fork is stored in the control plane. Deploying a new release updates the active IR version.
Additive changes are safe
The following changes are safe to make without a projection rebuild:
- Add a new optional payload field to an event. Existing ledger events don’t have this field — projections that derive from it will get
nullfor historical events. If that’s acceptable, add it and deploy. - Add a new projection. Deploy the new IR. The projection worker creates the table and populates it via Kafka. Backfill historical data by resetting the consumer offset.
- Add a new query. Deploy the new IR. No schema changes needed — queries are compiled SQL templates, not DDL.
- Add a column to an existing projection. Deploy the new IR. The projection worker applies the
ALTER TABLE ADD COLUMNDDL. Existing rows get the column default. Backfill by rebuilding the projection. - Add a new optional input field to an action. Existing callers continue to work without the field.
Breaking changes
The following changes require explicit migration steps:
Removing or renaming an event payload field
# BEFORE
events:
TICKET_PURCHASED:
payload:
ticket_id: string
show_id: string
price_cents: int # ← removing this
# AFTER
events:
TICKET_PURCHASED:
payload:
ticket_id: string
show_id: string
amount: int # ← renamedIf any projection has derive: { price: event.price_cents }, the compiler will catch the dangling reference on the next build. You must update all projection derive: mappings before the build succeeds.
After updating projections: deploy, then rebuild affected projections (historical events don’t have amount, they have price_cents — decide how to handle the gap).
Changing a payload field type
Changing a field from int to string affects:
- Expressions that compare or compute with the field
- Projection columns derived from the field (column type change)
- Queries that filter or aggregate on the field
Update the event declaration, all dependent projections (including their fields: types), and all dependent queries. Deploy the new IR. Apply DDL manually if needed. Rebuild affected projections.
Changing a projection column type
# BEFORE
fields:
price_cents: TEXT # ← was TEXT (bad)
# AFTER
fields:
price_cents: BIGINT # ← corrected to BIGINTThe projection worker applies column type DDL on deploy. Large tables may need a planned migration and rebuild. See Replay and Defining Projections.
Changing a projection’s primary key
This is a destructive change. The existing table’s primary key constraint must be dropped and recreated. The safest approach:
- Create a new projection with the new primary key (different table name).
- Deploy the new IR with both old and new projections active.
- Rebuild the new projection from history.
- Migrate application queries to the new projection.
- Remove the old projection in a subsequent release.
IR versioning workflow
- Modify
.causetfiles. - Compile — the compiler validates all cross-references. Fix any errors before proceeding.
- Upload artifacts (
causet.runtime.json+causet.projections.json) to the platform. - Create a release from the uploaded artifacts.
- Deploy the release to a fork — this updates the active IR version for that fork.
- Apply DDL — the projection worker applies any DDL changes declared in the new IR on startup.
- Rebuild projections if needed (new projection, column type change, derive expression change).
Active IR version per fork
Different forks can run different IR versions simultaneously. This enables:
- Staging and production on different releases.
- Canary releases — deploy to a test fork first.
- Safe rollback — revert the active IR version without re-deploying the service.
The runtime and query service resolve the active IR version for each fork on every request (with caching). Rolling back a release is instant — no service restart required.
Rollback
Rolling back a release reverts the active IR version. Effect on DDL:
- Columns added in the rolled-back release are not removed. DDL changes are applied additively. Rolling back does not drop columns or tables.
- Queries revert to the previous compiled SQL templates. If the rolled-back release added a new query, that query is no longer available to callers.
- Rules revert to the previous version. Intents are processed with the rolled-back rule set.
If a rollback is needed due to a schema issue (e.g. a column type change that caused errors), fix the DDL manually or tolerate the extra column until the issue is resolved in a forward release.
Compiler validation
The compiler validates:
- All
entity_exprreferences point to declared event payload fields. - All projection
derive:references point to declared event payload fields. - All query
from:andjoin:references point to declared projections. - All action
emitreferences point to declared events. - All listener
on:references point to declared events. - Reserved field names (
type,ts,entity_id) are not used in event payloads.
If any validation fails, the build fails — no artifacts are produced. This prevents deploying a broken IR.
Production notes
Treat event payloads as public APIs. Downstream services, projections, and queries all depend on event payload structure. Additive changes are safe; removals and renames are breaking changes.
Version your event types for major changes. If you need to fundamentally change an event’s payload shape, create a new event type (e.g. TICKET_PURCHASED_V2) rather than modifying TICKET_PURCHASED. Run both in parallel during transition. This is more verbose but eliminates the historical data gap problem.
Document payload field deprecations before removal. Remove a field in two steps: mark it as deprecated in comments (Causet DSL supports # comments), then remove it in the next release after confirming no projections or queries reference it.
See also
- Defining Projections — projection schema and
fields: - Best Practices — safe evolution patterns
- Replay — rebuilding projections after schema changes
- Events — declaring event payloads