Why Zitadel's Next Generation Is No Longer Event-Sourced

Event sourcing was an important part of Zitadel. We said so in January. But we built the next generation without it. The result is a simpler storage model and a more focused approach to auditing.

In January, we published Relational Core, Event-Driven Soul. The plan was to move read-heavy paths to relational tables while still creating an event for every change. Those events would preserve auditability, time-travel forensics, and event-driven integrations.

The core of that plan remains. Nextgen still records events when the system changes. What changed is the definition and role of those events.

In zitadel/nextgen, relational tables are the only source of truth. The system reads and writes entities through typed statements for each supported database (ADR 028). It does not replay events to determine the current state of a user, session, or project.

Nextgen uses wide events for auditing (ADR 048). They continue the original idea of writing an event when something changes, but their payload is different. They contain more context about each action. Nextgen also adds events for actions such as API requests.

The system does not replay these events to rebuild current state. That is the main difference from event sourcing. Events remain important for security teams, auditing, and integrations, but relational tables hold the current state.

This post explains why we changed direction, what replaces event sourcing, and what we give up in return.

What event sourcing and CQRS gave Zitadel

First, two definitions.

Event sourcing stores every state change as an immutable event. The application rebuilds the current state by replaying those events. The event log is not only a record of the state. It is the source of truth. Martin Fowler's definition provides a useful introduction.

CQRS, or Command Query Responsibility Segregation, separates the model used for writes from the model used for reads. Commands change the system and produce events. Queries read from projections built from those events. Event sourcing and CQRS are not the same, but they are often used together. See Greg Young's CQRS documents or Fowler's overview for more detail.
This architecture gave Zitadel several benefits. Every state change produced an event by design. Developers did not need to remember to create a separate audit entry. The audit trail followed directly from the way the system stored data.

It also allowed the system to reconstruct state at an earlier point in time. Other systems could subscribe to the event stream and react to changes. That was a sound design, and it served Zitadel for years.

What it cost

The event log was accurate, but it was not shaped for the questions applications ask most often.

Determining the current state required replaying events or reading a projection. Even a lookup such as “Which users does this administrator manage?” could require complex queries.

The architecture also introduced eventual consistency and custom projection management. Contributors had to understand both the write model and the read model. Operators had to manage the delay and failure cases between them.

These costs make sense when read and write models differ enough to justify two representations. For Zitadel's core entities, they often did not. Users, sessions, tokens, and projects usually have a similar shape when they are written and read. Maintaining a separate projection model added complexity without enough benefit.

The January plan and what we built

The January plan kept events alongside relational state. Each write would update a relational table and create an event in the same transaction. We described this as an evolution of the architecture, not a replacement. That core idea still applies.

What changed is the role and payload of the events. Instead of using domain events to rebuild application state, nextgen uses wider events designed for auditing and security analysis. These events can include more context and cover actions beyond entity changes, such as API requests.

Entity persistence now uses relational statements such as CreateProject, GetProjectByID, and ListProjects. These statements are implemented for PostgreSQL, Spanner, and SQLite.

We also removed an earlier CRUD and repository layer from nextgen. The goal is one clear storage layer based on typed statements, not several permanent paths.

The key distinction is simple:

  • Nextgen still record events when changes happen
  • Nextgen does not use events to calculate current state.

It is therefore not event sourcing in the traditional sense. It is a relational system that still emits events for auditing, security, and integrations.

Why relational storage fits nextgen better

Nextgen reads and writes the same relational tables. A successful write is available to the next read without waiting for a projection.

This makes the system easier to operate and review. The team chose hand-written SQL for each database dialect because the behavior is explicit. A reviewer can see what a statement does instead of inferring it through a generic repository layer (ADR 028).

The same model supports PostgreSQL and Spanner for production, and SQLite for local development. SQLite uses a pure Go driver, so contributors do not need a C toolchain or a separate embedded PostgreSQL process.

This is not a general argument against CQRS. It remains useful when read and write models need very different structures. Financial ledgers and systems with complex reconciliation are good examples. Our conclusion is narrower: Zitadel's core identity data did not benefit enough to justify the ongoing complexity.

How the audit trail changes

Moving state to relational tables changes how the audit trail works. Nextgen uses the concept of wide events (ADR 048).

A wide event is one rich, structured record for a single action. It contains who performed the action, what happened, how and where it happened, and when it happened. This information is stored in one flat row, so common audit queries do not require several joins. It gives security teams the context they need to investigate activity and understand how a change happened.

The approach is based on the structured-event model Charity Majors described in 2019 and is adapted to Zitadel's tenancy model in ADR 048.

The main difference from event sourcing is that wide events are never replayed. Relational tables determine what a user or session looks like now. Wide events help answer questions such as: Which AI agent changed this user's data, through which delegation, and when?

The proposed design has two event paths with different guarantees:

  • Events for authentication, sessions, administration, and entity changes are written in the same database transaction as the state change.
  • General request telemetry is batched and best-effort. A crash before the batch is written can cause some telemetry to be lost.

This distinction matters for audit and compliance use cases.

The team also considered database triggers and change data capture (CDC). Both were rejected. Spanner does not support triggers. CDC can show that a row changed, but it usually cannot explain why. That would turn a clear event such as user.deactivated into a generic row change.

One important status note: the relational storage layer in ADR 028 is implemented. The wide-events design in ADR 048 is still proposed. It has not shipped yet.

What changes and what we give up

The new model has clear tradeoffs.

Time travel

Nextgen cannot replay an event log to reconstruct the exact state at any point in the past. It can provide a queryable record of what happened and when, but that is not the same capability.

Event subscriptions

Users will still be able to consume events in some form. The exact subscription model has not been finalized. The proposed API in ADR 049 describes access, retention, and export rules, but these details may still change.

The goal is to keep events available for security, auditing, and integrations. They will not act as the source of truth for downstream projections in the same way as the classic event store.

In short, we are separating responsibilities that were previously handled by one mechanism:

  • relational tables for current state
  • wide events for auditing, security analysis, and integrations

The exact integration surface is still being defined.

A concrete example

With wide events, a forensic query could look like this:

SELECT * FROM events
WHERE project_id = $1 AND client_id = 'agent_copilot'
ORDER BY created_at, id;

This is a direct scan of a table designed for that question. An index on (project_id, created_at, id) supports the query. No projection is required.

Where nextgen stands today

Implemented: Relational storage for PostgreSQL, Spanner, and SQLite. The historical event store and the earlier repository code are no longer part of the nextgen storage path (ADR 028).

Still being finalized:: The full wide-events model (ADR 048), including how users subscribe to events and the retention and export contract described in ADR 049.

Frequently asked questions

Why did Zitadel move away from event sourcing?

Zitadel is moving away from using events as the source of truth. Event replay and projections added complexity, eventual consistency, and custom projection management to current-state reads. For Zitadel's core identity data, those costs were higher than the benefits. Nextgen still creates events, but uses them for auditing, security, and integrations instead of rebuilding current state.

Is nextgen still auditable without event sourcing?

Yes. Wide events record one structured entry per action, including who, what, how, where, and when. Important events are written with the state change. The model also covers activity such as API requests. This gives security teams a detailed record for investigations and compliance. Some details of the design are still being finalized.

What is the difference between an event store and a wide-events audit log?

An event store is the source of truth. The application replays its events to calculate current state.

A wide-events audit log records changes made to a separate source of truth. In nextgen, that source of truth is the relational database. The application does not read wide events to determine current state.

Can nextgen still support time-travel debugging?

Not in the classic event-sourcing sense. The audit log can show what happened and when, but it cannot reconstruct any past state on demand.

Does this mean CQRS is a bad pattern?

No. CQRS is useful when read and write models differ enough to require separate representations. Our decision applies to Zitadel's core identity and access-control data, where the benefit did not justify the added complexity.

Terms used in this post

  • Event sourcing: State is calculated by replaying an immutable event log. The log is the source of truth.
  • CQRS: The system uses separate models for writes and reads.
  • Projection: A read-focused view created from events.
  • Wide event: A structured record that contains the full context of one action. It is designed for queries, not replay.
  • Source of truth: The data store the application trusts for current state. In nextgen, this is the relational database.

Sources and further reading

Liked it? Share it!