This is the abridged developer documentation for Deedbox # Deedbox > Event-source part of your app. Postgres or SQL Server. EF Core, Dapper, or neither. Deedbox is an MIT-licensed .NET library. It stores the events of one area of your app, such as orders or manuscripts, in your existing Postgres or SQL Server database. The rest of your app stays as it is. ```cs // Events: plain records. No marker interface, no base class. public record ItemAdded(string Sku, int Qty); public record CheckedOut(DateTimeOffset At); ``` ```cs // State: Initial and Evolve. Nothing else. public record Cart(ImmutableDictionary Items, bool IsCheckedOut) : IState { public static Cart Initial { get; } = new(ImmutableDictionary.Empty, false); public static Cart Evolve(Cart s, object e) => e switch { ItemAdded x => s with { Items = s.Items.SetItem(x.Sku, s.Items.GetValueOrDefault(x.Sku) + x.Qty) }, CheckedOut => s with { IsCheckedOut = true }, _ => s, }; } ``` ```cs // Load, decide, evolve and append in one transaction. var result = await store.Execute(cartId, cart => CartDecider.Add(cart, sku, qty)); // result.State is the new state; result.Version the new version; result.Events the appended envelopes. ``` Your app stays yours Deedbox tables live in their own schema. Appends share your connection and transaction. There is no mediator, no document database and no base class for your events. Correct under load No event is ever skipped. Global order is commit order. A torture suite checks this on both databases, with killed sessions and competing instances. Erasure built in Mark personal data with an attribute. Erasing a person makes their data unreadable everywhere, even inside long-lived shared streams. Operable A CLI, an admin API, metrics, traces and health checks that stay healthy during a rebuild. # Comparison with other libraries > How Deedbox differs from Marten, Polecat and Eventuous. This page compares Deedbox with other .NET event stores, to help you choose. Each library is good at what it sets out to do; the differences are in scope. | | Deedbox | Marten | Polecat | Eventuous | | ---------------- | ----------------------------------------------- | ------------------------------------------------------------- | --------------------------------- | ---------------------------------------------- | | Databases | Postgres, SQL Server | Postgres | SQL Server | Postgres, SQL Server, KurrentDB | | Scope | Event store only | Document database and event store | Document database and event store | Event store with its own command-service model | | Your data access | EF Core, Dapper or ADO.NET, in your transaction | Marten sessions | Polecat sessions | Eventuous services | | Async ordering | One serialized counter; readers never skip | A high-water mark; by design it can skip a gap it judges dead | The same daemon as Marten | See its docs | Deedbox also encrypts personal data per subject and erases it by deleting the key, and every Deedbox package is MIT-licensed. Check the other projects’ docs for their own approach to personal data and for their licence terms; some JasperFx monitoring add-ons need a commercial licence for production use. ## When not to use Deedbox [Section titled “When not to use Deedbox”](#when-not-to-use-deedbox) * You want a document database too. Use Marten or Polecat. * You need tens of thousands of appends per second to one store. Deedbox serializes appends; see [the benchmarks](/operations/benchmarks/). * You want KurrentDB (EventStoreDB). Use Eventuous or the KurrentDB client. Sources: the [Marten async daemon docs](https://martendb.io/events/projections/async-daemon.html), [Marten 9.25.0 release notes](https://github.com/JasperFx/marten/releases/tag/V9.25.0), [Polecat announcement](https://jeremydmiller.com/2026/03/22/announcing-polecat-event-sourcing-with-sql-server/), and [Eventuous](https://github.com/Eventuous/eventuous). # Decide and evolve > Why decisions are pure functions, and how Execute uses that. This page explains the two functions every stream needs, and how Deedbox runs them. * **Evolve** applies one event to a state. It is part of the state type, and it never fails. * **Decide** takes the current state and a command, and returns new events. It reads nothing and writes nothing. ```cs // Decisions: pure functions from state to new events. public static class CartDecider { public static IEnumerable Add(Cart cart, string sku, int qty) => cart.IsCheckedOut ? throw new InvalidOperationException("The cart is checked out.") : [new ItemAdded(sku, qty)]; public static IEnumerable CheckOut(Cart cart, DateTimeOffset now) => cart.IsCheckedOut || cart.Items.IsEmpty ? [] : [new CheckedOut(now)]; } ``` ## Execute locks, decides and appends [Section titled “Execute locks, decides and appends”](#execute-locks-decides-and-appends) ```cs // Load, decide, evolve and append in one transaction. var result = await store.Execute(cartId, cart => CartDecider.Add(cart, sku, qty)); // result.State is the new state; result.Version the new version; result.Events the appended envelopes. ``` `Execute` locks the stream, loads its state, runs your decision, and appends the events, in one transaction. Because the stream stays locked from load to commit, two writers of one stream take turns instead of conflicting. When two writers create the same new stream at once, the loser runs its decision again; `ExecuteRetries` sets how often (3 by default). ## The explicit form works with any error style [Section titled “The explicit form works with any error style”](#the-explicit-form-works-with-any-error-style) ```cs var (cart, version) = await store.Load(cartId); var events = CartDecider.CheckOut(cart, now).ToList(); if (events.Count > 0) await store.Append(cartId, ExpectedVersion.Exact(version), events); ``` `Append` checks the expected version and writes only if it matches. Deedbox has no `Result` type and no opinion on errors: your decision can return a result, throw, or return no events. ```cs try { await store.Append(cartId, ExpectedVersion.NoStream, [new ItemAdded("apple", 1)]); } catch (ConcurrencyException ex) { // ex.Expected is NoStream; ex.Actual is the version the stream is at. Console.WriteLine($"Cart {ex.StreamId} already exists at version {ex.Actual}."); } ``` `ExpectedVersion` is `NoStream`, `Any` or `Exact(n)`. `Exact(0)` is the same as `NoStream`. # Erasure and the key hierarchy > How crypto-shredding erases personal data from streams you keep. This page explains how Deedbox encrypts personal data, and what erasure does. The keys form a chain: 1. The **master key** lives in the database, in a key ring from configuration, or in Azure Key Vault. 2. It wraps one **tenant key** per tenant, which each process unwraps once. 3. Each tenant key wraps the **subject keys** in the `subject_keys` table. 4. Each subject key encrypts that subject’s **personal-data fields** in event payloads. * Each `[PersonalData]` field is encrypted with AES-256-GCM under its subject’s key. * Each subject key is wrapped by its tenant’s key. Each tenant key is wrapped by the master key. * Reads and rebuilds use only local AES. They never call a key service. * Erasing a subject deletes their key. Every copy of their data, in every stream, becomes unreadable at once. * Shredding a tenant destroys the tenant key and all its subject keys. ## What is safe to rely on [Section titled “What is safe to rely on”](#what-is-safe-to-rely-on) * A missing subject key is the only thing that reads as erased. A key that does not verify, or a master key that cannot unwrap a tenant key, stops Deedbox with an error. A misconfiguration never looks like an erasure. * Subject keys are cached for one operation only, so an erasure on one instance applies on every instance at once. * The stored state of a stream with personal data is encrypted with the tenant key, and cleared when a subject in it is erased. ## What it does not cover [Section titled “What it does not cover”](#what-it-does-not-cover) * Backups taken before an erasure keep the subject key until they age out. * Your projections and subscriptions receive decrypted data. Scrub it when they handle `SubjectErased`. * Metadata, stream IDs and subject IDs are plain text. # Known limits > What Deedbox does not do in 0.1, stated plainly. This page lists the limits of Deedbox 0.1, so you can plan around them. * **Appends take turns.** One counter orders all appends. See [the benchmarks](/operations/benchmarks/) for the ceiling. * **Two streams in one transaction can deadlock.** A transaction that appends to two streams can deadlock with a concurrent append; the database aborts one. Nothing is lost. Append to one stream per transaction, or retry. * **One database per store.** Database-per-tenant comes in a later release. * **Rebuilds are in place.** The read model is empty or partial during a rebuild. * **Projections run one event at a time, in order.** * **Only top-level properties hold personal data.** * **Metadata is set through `DeedboxContext`.** There is no ASP.NET Core helper; use the [middleware](/how-to/use-tenants/) shown in the tenants guide. # Ordering and global position > How Deedbox orders events across streams, and why no event is ever skipped. This page explains the global position, and the design choice that makes it safe to read. ## Every event has a global position [Section titled “Every event has a global position”](#every-event-has-a-global-position) Positions order every event of the store. They are gapless and follow commit order: an event at position 11 commits after the event at position 10. So when a reader sees position 11, position 10 is already committed. A reader that asks for “everything after my checkpoint” never misses an event that commits later. Compare positions; never do arithmetic on them. Deleting a stream removes its events and leaves gaps. ## One counter serializes appends [Section titled “One counter serializes appends”](#one-counter-serializes-appends) Deedbox takes positions from a single counter row. Each append updates the counter as its last statement and commits right after. The row stays locked in between, so the next append waits. A rolled-back append also rolls back the counter, so no position is ever lost. In one append, in this order: 1. Lock and update the stream row. 2. Run inline projections and appending hooks. 3. Update the position counter. 4. Insert the events at the new positions. 5. Commit, which releases the counter. The cost is that appends take turns for that short window. See [the benchmarks](/operations/benchmarks/) for the ceiling. A transaction you own keeps the counter locked until you commit, so commit soon after an append. ## Why not a sequence or a row version [Section titled “Why not a sequence or a row version”](#why-not-a-sequence-or-a-row-version) A sequence or row version hands out numbers before commit. Two transactions can then commit out of order, and a reader can pass a number that commits later. Other stores guess when such a gap is safe to skip, and several have shipped bugs that skipped events. Deedbox never skips; the counter makes that guess unnecessary. A torture suite checks it on both databases with rollbacks, long transactions and competing readers. # Projections and subscriptions > The difference between writing a read model and causing a side effect. This page explains the two kinds of handler and the guarantee each one gives. | | Projection | Subscription | | --------------- | ---------------------------------------- | --------------------------------------- | | Writes to | The same database | Anything: email, HTTP, other systems | | Checkpoint | In the same transaction as its writes | After the handler succeeds | | Guarantee | Each event’s effect happens exactly once | Each event arrives at least once | | Idempotency key | Not needed | `EventId`, or `StreamId` with `Version` | | Run modes | Inline or async | Async only | A projection can promise exactly once because its writes and its checkpoint commit together. A subscription cannot: an email can be sent and the process can die before the checkpoint moves. Pass the event ID on as an idempotency key. ```cs // A subscription does anything outside the database. Delivery is at least once, // so pass the event ID on as an idempotency key. public sealed class SendReceipt : Subscription { // Subscriptions are created once; a scoped service comes from ctx.Services instead. public SendReceipt(IEmailSender email) => On((_, ctx) => email.SendReceipt(ctx.Envelope.StreamId, ctx.Envelope.EventId, ctx.CancellationToken)); } ``` ## When a handler fails [Section titled “When a handler fails”](#when-a-handler-fails) 1. The runner retries the event with growing delays, up to `HandlerRetries` times. 2. Then it stops that consumer, sets it to `stalled`, and records the stream, version, event type and exception. 3. Metrics, logs, the health check and `deedbox status` report it. 4. You fix the code and restart, or [skip the one event](/operations/poison-event/) with an audited job. No timeout ever moves a consumer past an event it did not handle. ## Causation and tracing [Section titled “Causation and tracing”](#causation-and-tracing) A subscription’s `ctx.Services` is a scope made for the event. A store resolved there appends with the event’s tenant and correlation ID, and records the event as the cause. Each handler call runs in a trace span whose parent is the append that wrote the event. # Streams and state > What Deedbox stores for each stream, and how it names things. This page explains what a stream is, what Deedbox stores for it, and how names stay stable. ## A stream is one aggregate’s history [Section titled “A stream is one aggregate’s history”](#a-stream-is-one-aggregates-history) A stream holds the events of one thing, such as one cart. Its identity is (tenant, stream ID). Its events have versions 1, 2, 3 and so on, with no gaps. The stream’s state is what you get when you apply every event, in order, to the initial state. ```cs // State: Initial and Evolve. Nothing else. public record Cart(ImmutableDictionary Items, bool IsCheckedOut) : IState { public static Cart Initial { get; } = new(ImmutableDictionary.Empty, false); public static Cart Evolve(Cart s, object e) => e switch { ItemAdded x => s with { Items = s.Items.SetItem(x.Sku, s.Items.GetValueOrDefault(x.Sku) + x.Qty) }, CheckedOut => s with { IsCheckedOut = true }, _ => s, }; } ``` ## Names are stored, not derived at read time [Section titled “Names are stored, not derived at read time”](#names-are-stored-not-derived-at-read-time) | Thing | Convention | Override | | ------------- | --------------------- | ------------------------------------- | | Stream type | `Cart` becomes `cart` | `.Stream("shopping_cart", ...)` | | Event type | `cart.item_added` | `.Event(name: "...")` | | Old names | none | `.Alias("...")` | | Shape version | 1 | `.Event(version: 2, ...)` | ```cs builder.Services.AddDeedbox(es => es .UsePostgres(connStr) .Stream("shopping_cart", s => s // stored as "shopping_cart" .Event(name: "shopping_cart.line_added") // an explicit event name .Event())); // shopping_cart.checked_out ``` One CLR type maps to one stored name. Registering it twice fails at start-up. At start-up, Deedbox also checks that every stored event name maps to a registered event. ## Stream IDs are strings [Section titled “Stream IDs are strings”](#stream-ids-are-strings) ```cs var fromGuid = StreamId.From(Guid.NewGuid()); // "0f8fad5b-d9cb-469f-a165-70867728950e" var ns = Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); var forPair = StreamId.Deterministic(ns, userId.ToString(), titleId.ToString()); // the same pair gives the same ID ``` A stream ID has 1 to 200 characters and no leading or trailing white space. IDs compare case-sensitively on both databases. ## State is stored next to the events [Section titled “State is stored next to the events”](#state-is-stored-next-to-the-events) By default, every append also stores the new state, so a load reads one row. The stored state carries a state version. When you change the state record, raise the state version: each stream’s state is then rebuilt from its events on its next load. ```cs builder.Services.AddDeedbox(es => es .UsePostgres(connStr) .Stream(s => s .Events() .StateVersion(2) // raise it when you change the Cart record .Snapshots(SnapshotPolicy.Every(50)))); // or EveryAppend (default) or Never ``` For a stream type with personal data, the stored state is encrypted with the tenant’s key. # Add a projection > Build a read model from events, inline or in the background. This guide shows you how to build a read model from events with a projection. ## Write the projection [Section titled “Write the projection”](#write-the-projection) Register one handler per event type in the constructor with `On`. Deedbox skips a projection when an append holds none of its event types. * EF Core ```cs // EF Core flavour: your DbContext, enlisted in the append's transaction. Deedbox calls SaveChanges. public sealed class CartSummaryProjection : Projection { public CartSummaryProjection() { On(async (e, ctx) => { var row = await ctx.Db.CartSummaries.FindAsync([ctx.StreamId], ctx.CancellationToken) ?? ctx.Db.CartSummaries.Add(new CartSummaryRow(ctx.StreamId)).Entity; row.ItemCount += e.Qty; }); On(async (_, ctx) => { var row = await ctx.Db.CartSummaries.FindAsync([ctx.StreamId], ctx.CancellationToken); row!.CheckedOut = true; }); On(async (_, ctx) => await ctx.Db.CartSummaries.Where(r => r.Id == ctx.StreamId).ExecuteDeleteAsync(ctx.CancellationToken)); } // A rebuild calls ResetAsync, then replays every event. protected override Task ResetAsync(WriteContext context) => context.Db.CartSummaries.ExecuteDeleteAsync(context.CancellationToken); } ``` * Dapper or ADO.NET ```cs // ADO.NET or Dapper flavour: write through ctx.Connection and ctx.Transaction. public sealed class CartTotals : Projection { public CartTotals() { On((e, ctx) => Execute(ctx.Connection, ctx.Transaction, "UPDATE cart_totals SET items = items + @qty WHERE cart_id = @cart", ("qty", e.Qty), ("cart", ctx.StreamId))); } protected override Task ResetAsync(WriteContext context) => Execute(context.Connection, context.Transaction, "DELETE FROM cart_totals"); private static async Task Execute(DbConnection connection, DbTransaction transaction, string sql, params (string Name, object Value)[] values) { await using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = sql; foreach (var (name, value) in values) { var parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; command.Parameters.Add(parameter); } await command.ExecuteNonQueryAsync(); } } ``` Override `ResetAsync` to delete what the projection wrote. A [rebuild](/how-to/rebuild-a-projection/) needs it. ## Register it with a name and a run mode [Section titled “Register it with a name and a run mode”](#register-it-with-a-name-and-a-run-mode) ```cs builder.Services.AddDeedbox(es => es .UsePostgres(connStr) .Stream(s => s.Events()) .Projection("cart_summary", Run.Inline) .Projection("cart_totals", Run.Async) .Subscription("receipt_email")); ``` * The name, such as `cart_summary`, keys the projection’s checkpoint. Renaming the class keeps its progress. Renaming the projection starts it again from the first event. * `Run.Inline` applies the projection in the append’s transaction. The read model is never behind, and a failing handler fails the append. * `Run.Async` applies the projection in the background runner. The checkpoint commits in the same transaction as the projection’s writes, so each event applies exactly once. A projection has one run mode. Registering one class twice fails at start-up, because two registrations apply each event twice. ## Handle large async workloads in batches [Section titled “Handle large async workloads in batches”](#handle-large-async-workloads-in-batches) A batch projection receives each batch of its events in one call. It runs async only. ```cs // A batch projection receives each batch of its events in one call, for bulk writes. It runs async only. public sealed class CartArchive : BatchProjection { public CartArchive() => Handles(); protected override Task ApplyAsync(IReadOnlyList events, WriteContext context) { // One bulk insert for the whole batch, through context.Connection and context.Transaction. return Task.CompletedTask; } } ``` ## Know what a handler sees [Section titled “Know what a handler sees”](#know-what-a-handler-sees) | Property | Inline | Async | | -------------------------------------------- | ------------------ | ----------------- | | `EventId`, `StreamId`, `Version`, `Metadata` | yes | yes | | `GlobalPosition` | null | yes | | `Services` | the append’s scope | a scope per batch | Handle the built-in events `StreamDeleted` and `SubjectErased` to remove deleted and erased data from your read model. # Apply the schema > Create and upgrade the Deedbox tables at start-up, with the CLI, with EF Core migrations, or with DbUp. This guide shows you the ways to create and upgrade the Deedbox tables. Deedbox never changes the schema unless you ask it to. Without a current schema, start-up fails with the exact fix ([DBX001](/reference/errors/dbx001/)). ## At start-up [Section titled “At start-up”](#at-start-up) Call `ApplySchemaOnStartup()` in `AddDeedbox`. Pods take a database lock, so concurrent pods apply each migration once. ## With the CLI [Section titled “With the CLI”](#with-the-cli) ```sh dotnet tool install -g Deedbox.Cli --prerelease deedbox schema apply --provider postgres --connection "$DEEDBOX_CONNECTION" ``` Or print the SQL for a DBA: ```sh deedbox schema script --provider sqlserver --from 0 > deedbox.sql ``` `--from` is the version the database has now. The scripts are idempotent, and each records itself in `schema_version`. ## In an EF Core migration [Section titled “In an EF Core migration”](#in-an-ef-core-migration) ```cs // An EF Core migration that creates the Deedbox tables without adding them to your model. public partial class AddDeedbox : Migration { protected override void Up(MigrationBuilder migrationBuilder) => migrationBuilder.Sql(PostgresSchema.Script(fromVersion: 0)); // SqlServerSchema.Script on SQL Server } ``` The tables stay out of your EF Core model. For an upgrade, add a new migration with `Script(fromVersion: n)`. ## With DbUp or Flyway [Section titled “With DbUp or Flyway”](#with-dbup-or-flyway) Save the output of `deedbox schema script` as a numbered script in your migrations folder. For SQL Server, the script separates batches with `GO`. ## Use another schema name [Section titled “Use another schema name”](#use-another-schema-name) `.Schema("es")` puts the tables in `es` instead of `deedbox`. Names are lower-case letters, digits and underscores, at most 50 characters. # Change an event's shape > Add, remove or change fields of an event with upcasters. This guide shows you how to change an event’s fields and keep old events readable. An event’s shape version is stored with it, in `event_version`. The name does not change. When you change the shape, raise the version and add an upcaster for each step from the old versions. ## Edit the JSON for small changes [Section titled “Edit the JSON for small changes”](#edit-the-json-for-small-changes) ```cs services.AddDeedbox(es => es .UsePostgres(connStr) .Stream(s => s .Event(version: 2, up => up .From(1, json => json["qty"] ??= 1)) // version 1 had no quantity .Event())); ``` `From(1, ...)` receives the stored JSON of version 1 and changes it into version 2. It runs after decryption and before deserialization. ## Convert a kept record for a real reshape [Section titled “Convert a kept record for a real reshape”](#convert-a-kept-record-for-a-real-reshape) ```cs public record LineAdded(string Sku, int Qty); // was called ItemAdded public record ItemAddedV2(string Sku, int Qty); // the old shape, kept for the typed upcaster public record ItemPriced(string Sku, int Qty, decimal Price); ``` ```cs services.AddDeedbox(es => es .UsePostgres(connStr) .Stream(s => s .Event(version: 3, up => up .Name("cart.item_added") .From(1, json => json["qty"] ??= 1) .Upcast(old => new ItemPriced(old.Sku, old.Qty, 0m))) .Event())); ``` Steps run in order: version 1 goes through the JSON step to version 2, then the typed step converts version 2 to the current record. A typed step is always the last one. ## What needs a new version [Section titled “What needs a new version”](#what-needs-a-new-version) | Change | New version needed | | ---------------------------------- | ----------------------------------- | | Add a nullable property | No | | Add a property that cannot be null | Yes, with a default in the upcaster | | Remove or rename a property | Yes | | Change a property’s type | Yes | Start-up fails when a version has no upcaster for some step ([DBX018](/reference/errors/dbx018/)). The [lockfile](/how-to/test-deciders/#pin-event-contracts) fails a test when a shape changes without a new version. # Erase a person > Mark personal data, then erase a data subject everywhere. This guide shows you how to store personal data so that you can erase one person later, even when their data sits inside streams you must keep. ## Mark the data [Section titled “Mark the data”](#mark-the-data) ```cs public record ReviewerInvited( string ManuscriptId, [property: DataSubject] string ReviewerId, // whose data this is [property: PersonalData] string ReviewerName, // encrypted under ReviewerId's key [property: PersonalData] string? ReviewerEmail); ``` `[DataSubject]` marks whose data the event carries. Each `[PersonalData]` field is encrypted under that subject’s own key. A personal-data property must be a string or nullable, because it reads as null after erasure. Use person IDs, not role IDs, as subjects. One erasure then covers every role a person has. ```cs // Several subjects in one event: name each field's subject property. public record CoAuthorAdded( string ManuscriptId, string AuthorId, [property: PersonalData(Subject = "AuthorId")] string AuthorName, string EditorId, [property: PersonalData(Subject = "EditorId")] string? EditorNote); ``` ## Choose a key mode [Section titled “Choose a key mode”](#choose-a-key-mode) Start-up fails until you choose one ([DBX025](/reference/errors/dbx025/)). ```cs services.AddDeedbox(es => es .UsePostgres(connStr) .Keys(keys => keys.StoreInDatabase()) .Stream(s => s.Events())); ``` `StoreInDatabase` keeps the master key in the same database. Erasure works, but a copy of the database exposes personal data. Move to a key ring or Azure Key Vault before production: ```cs // DEEDBOX_MASTER_KEY holds a key ring: v2:,v1: services.AddDeedbox(es => es .UsePostgres(connStr) .Keys(keys => keys .FromEnvironment("DEEDBOX_MASTER_KEY") .RedactWith("[erased]")) .Stream(s => s.Events())); ``` See [rotate keys](/how-to/rotate-keys/) to move between modes. ## Erase the subject [Section titled “Erase the subject”](#erase-the-subject) ```cs // Their data reads as erased as soon as this returns; the job finishes the rest. var jobId = await erasure.EraseSubjectAsync("person:8421"); ``` 1. Deedbox deletes the subject’s key and clears the stored state of every stream with their data, in one transaction. From then on, every read returns their fields as null (or the `RedactWith` placeholder). 2. A job appends a `SubjectErased` event to each of those streams and stores rebuilt state. Handle `SubjectErased` in your projections to scrub what they stored. 3. The job resumes after a crash. From the CLI: `deedbox erase person:8421 --tenant acme`. ## Know the limits [Section titled “Know the limits”](#know-the-limits) * Backups taken before an erasure still hold the subject’s key until they age out. Set your backup retention with this in mind. * Metadata and subject IDs are not encrypted. Use pseudonymous IDs such as `person:8421`. * Only top-level properties are encrypted. * Data written about the subject after the erasure uses a new key and stays readable. # Rebuild a projection > Reset a projection and replay every event through it, without downtime. This guide shows you how to rebuild a projection after you change its logic. 1. Make sure the projection overrides `ResetAsync`. Without it, the rebuild job fails with [DBX023](/reference/errors/dbx023/). 2. Deploy the new projection code. 3. Queue the rebuild from the CLI: ```sh deedbox rebuild cart_summary --provider postgres --connection "$DEEDBOX_CONNECTION" --wait ``` Or from code: ```cs var status = await admin.GetStatusAsync(); foreach (var consumer in status.Consumers) Console.WriteLine($"{consumer.Name}: {consumer.Status}, {consumer.Lag} behind"); var rebuild = await admin.RebuildAsync("cart_summary"); var skip = await admin.SkipAsync("cart_totals", stalledEventId); var job = await admin.GetJobAsync(rebuild); ``` ## What happens [Section titled “What happens”](#what-happens) 1. A running app instance takes the job. It calls `ResetAsync` and sets the projection to `rebuilding`, in one transaction. 2. The background runner replays every event from position 0. 3. For an inline projection, appends skip the projection while it rebuilds. When the replay is within one batch of the head, the runner locks the position counter, applies the last events, and sets the projection back to `running`. Appends apply it inline again from then on. 4. For an async projection, the runner sets it back to `running` when it reads to the end. During the rebuild, the read model is empty or partial. The health check reports `rebuilding` as healthy, so Kubernetes does not restart the app. If appends arrive faster than the replay, the runner switches over anyway after 20 polls without progress. Appends then wait while it applies the rest. # Rename an event > Rename an event class without breaking stored events. This guide shows you how to rename an event class and keep its stored events readable. Every stored event keeps the name it was written under, such as `cart.item_added`. A rename changes the conventional name, so stored events have no class to read into. Deedbox fails at start-up in that case, with [DBX016](/reference/errors/dbx016/), and names the likely rename. ## Keep the old name as an alias [Section titled “Keep the old name as an alias”](#keep-the-old-name-as-an-alias) ```cs public record LineAdded(string Sku, int Qty); // was called ItemAdded public record ItemAddedV2(string Sku, int Qty); // the old shape, kept for the typed upcaster public record ItemPriced(string Sku, int Qty, decimal Price); ``` ```cs services.AddDeedbox(es => es .UsePostgres(connStr) .Stream(s => s .Event(e => e.Alias("cart.item_added")) // stored events keep their old name .Event())); ``` New events are stored under the new name, `cart.line_added`. Stored events under `cart.item_added` read as `LineAdded`. To keep the old name for new events too, set it explicitly instead: `.Event(name: "cart.item_added")`. ## Let CI catch it first [Section titled “Let CI catch it first”](#let-ci-catch-it-first) The [event contract lockfile](/how-to/test-deciders/#pin-event-contracts) fails a test when a name disappears without an alias. # Rotate keys > Rotate the master key, or move it out of the database, without touching events. This guide shows you how to rotate the master key or change where it lives. No event is re-encrypted: the master key only wraps one key per tenant. ## Rotate a key ring [Section titled “Rotate a key ring”](#rotate-a-key-ring) 1. Generate a new 32-byte key: `openssl rand -base64 32`. 2. Put it first in the ring, and keep the old key after it: `v2:,v1:`. 3. Deploy. New tenant keys use `v2`; existing ones still unwrap with `v1`. 4. Re-wrap the existing tenant keys: ```sh deedbox keys rewrap --from env:DEEDBOX_MASTER_KEY --to env:DEEDBOX_MASTER_KEY --provider postgres ``` 5. Remove `v1` from the ring and deploy again. ```cs // DEEDBOX_MASTER_KEY holds a key ring: v2:,v1: services.AddDeedbox(es => es .UsePostgres(connStr) .Keys(keys => keys .FromEnvironment("DEEDBOX_MASTER_KEY") .RedactWith("[erased]")) .Stream(s => s.Events())); ``` ## Move out of the database [Section titled “Move out of the database”](#move-out-of-the-database) 1. Set the new key ring in `DEEDBOX_NEW_MASTER_KEY`. 2. Run `deedbox keys rewrap --from database --to env:DEEDBOX_NEW_MASTER_KEY`. This also deletes the master key stored in the database. 3. Change the app’s key mode to `FromEnvironment` and deploy. ## Use Azure Key Vault [Section titled “Use Azure Key Vault”](#use-azure-key-vault) ```cs services.AddDeedbox(es => es .UsePostgres(connStr) .Keys(keys => keys.UseAzureKeyVault( new Uri("https://my-vault.vault.azure.net/keys/deedbox"), new DefaultAzureCredential())) .Stream(s => s.Events())); ``` `deedbox keys rewrap --to azure:https://my-vault.vault.azure.net/keys/deedbox` moves to Key Vault. The CLI signs in with `DefaultAzureCredential`. ## Shred a whole tenant [Section titled “Shred a whole tenant”](#shred-a-whole-tenant) To erase every personal field of a tenant at once, destroy its key: ```cs await admin.ShredTenantAsync("acme"); ``` From the CLI: `deedbox tenant shred acme --yes`. This cannot be undone. # Run on Kubernetes > Health probes, several replicas, schema changes and keys on Kubernetes. This guide shows you how to run Deedbox with several replicas on Kubernetes. ## Use the health check for liveness [Section titled “Use the health check for liveness”](#use-the-health-check-for-liveness) ```cs builder.Services.AddHealthChecks().AddDeedboxHealthChecks(); ``` The check is unhealthy only when a projection or subscription is stalled, or when events wait and its checkpoint has not moved for `StallAfter` (10 minutes by default). A projection that is behind but moving, or rebuilding, is healthy. Kubernetes therefore does not restart a pod in the middle of a rebuild. ```yaml livenessProbe: httpGet: { path: /health, port: 8080 } periodSeconds: 30 failureThreshold: 4 ``` ## Run several replicas [Section titled “Run several replicas”](#run-several-replicas) Replicas need no extra setup. Each batch of a projection runs while its replica holds the projection’s checkpoint row, so the projections spread across replicas and each runs on one replica at a time. To keep background work off your web pods, turn the runner off there and run a worker deployment with it on: ```cs builder.Services.AddDeedbox(es => es .UsePostgres(connStr) .Stream(s => s.Events()) .Runner(r => { r.BatchSize = 500; r.MaxPollDelay = TimeSpan.FromSeconds(5); r.HandlerRetries = 5; r.StallAfter = TimeSpan.FromMinutes(10); })); ``` Set `r.Enabled = false` in the web pods. ## Change the schema [Section titled “Change the schema”](#change-the-schema) Choose one: * `ApplySchemaOnStartup()`: every pod applies pending migrations under a database lock, so concurrent pods apply them once. * A Kubernetes Job before the rollout: `deedbox schema apply --provider postgres --connection "$DEEDBOX_CONNECTION"`. Without either, a pod fails at start-up with the exact fix ([DBX001](/reference/errors/dbx001/)). A newer schema than the build is fine, so old pods keep running during a rolling deploy. ## Keep the master key in a Secret [Section titled “Keep the master key in a Secret”](#keep-the-master-key-in-a-secret) ```yaml env: - name: DEEDBOX_MASTER_KEY valueFrom: secretKeyRef: { name: deedbox, key: master-key } ``` Back the Secret up outside the cluster. Losing every copy of the master key loses all personal data. # Test deciders and event contracts > Test decisions with Given/When/Then, and pin event contracts with a lockfile. This guide shows you how to test your decisions without a database, and how to stop a change to an event from breaking stored events. ```sh dotnet add package Deedbox.Testing --prerelease ``` ## Test decisions [Section titled “Test decisions”](#test-decisions) ```cs public class CartDeciderTests { private static readonly DateTimeOffset Now = new(2026, 9, 24, 12, 0, 0, TimeSpan.Zero); [Fact] public void A_cart_with_items_checks_out() => Decider.Given(new ItemAdded("apple", 2)) .When(cart => CartDecider.CheckOut(cart, Now)) .Then(new CheckedOut(Now)); [Fact] public void An_empty_cart_does_not_check_out() => Decider.Given() .When(cart => CartDecider.CheckOut(cart, Now)) .ThenNothing(); [Fact] public void A_checked_out_cart_takes_no_items() => Decider.Given(new ItemAdded("apple", 2), new CheckedOut(Now)) .When(cart => CartDecider.Add(cart, "pear", 1)) .ThenThrows(); } ``` `Given` folds past events through your state’s `Evolve`. `When` runs the decision. `Then` compares the events by type and by their JSON, so records that hold collections compare by content. The helpers throw `DeciderAssertionException`, so they work with any test framework. ## Pin event contracts [Section titled “Pin event contracts”](#pin-event-contracts) ```cs public class EventContractTests { [Fact] public void Event_contracts_are_stable() => EventContracts.Verify(es => es.Stream(s => s.Events()), "events.lock"); } ``` The first run writes `events.lock` next to the test file and fails once; commit the file. From then on: * A new event, alias, version or nullable property updates the file, and the test passes. Commit the change. * A removed name, a changed type, a removed property, a removed `[PersonalData]` marker, or a new non-nullable property without a new version fails the test with the fix. * Under CI (`CI=true`), any difference fails, so a stale lockfile cannot pass. Compare two lockfiles in a pull request with `deedbox lockfile diff main.lock events.lock`. # Use Deedbox with coding agents > Give Claude Code, Cursor and other agents the Deedbox docs and rules. This guide shows you how to give a coding agent what it needs to write correct Deedbox code. ## Point the agent at llms.txt [Section titled “Point the agent at llms.txt”](#point-the-agent-at-llmstxt) The site publishes the docs as plain Markdown for language models: * [`/llms.txt`](/llms.txt): an index of every page. * [`/llms-full.txt`](/llms-full.txt): every page in one file. * [`/llms-small.txt`](/llms-small.txt): a shorter version for small context windows. ## Install the agent skill [Section titled “Install the agent skill”](#install-the-agent-skill) The repository ships a skill with the rules an agent must follow, such as “decisions are pure” and “never append built-in events”. * Claude Code: copy [`skills/deedbox`](https://github.com/alternayte/deedbox/tree/main/skills/deedbox) to `.claude/skills/deedbox` in your repository. * Cursor: copy `skills/deedbox/SKILL.md` to `.cursor/rules/deedbox.mdc`. # Use Dapper or plain ADO.NET > Append events in your own connection and transaction. This guide shows you how to append events in a transaction you own, with Dapper or plain ADO.NET. ## Pass your transaction [Section titled “Pass your transaction”](#pass-your-transaction) ```cs await using var connection = await dataSource.OpenConnectionAsync(); await using var transaction = await connection.BeginTransactionAsync(); // Your own writes, with Dapper or plain ADO.NET, on the same connection and transaction... await store.UseTransaction(transaction).Append("cart-42", ExpectedVersion.Any, [new ItemAdded("apple", 1)]); await transaction.CommitAsync(); // Deedbox never commits your transaction. ``` `UseTransaction` returns a store that runs every operation in your transaction. Deedbox never commits or rolls it back. Inline projections and appending hooks run in it too. The position counter stays locked from your append until your commit. Commit soon after the append; other appends wait meanwhile. ## Write a projection with the same connection [Section titled “Write a projection with the same connection”](#write-a-projection-with-the-same-connection) ```cs // ADO.NET or Dapper flavour: write through ctx.Connection and ctx.Transaction. public sealed class CartTotals : Projection { public CartTotals() { On((e, ctx) => Execute(ctx.Connection, ctx.Transaction, "UPDATE cart_totals SET items = items + @qty WHERE cart_id = @cart", ("qty", e.Qty), ("cart", ctx.StreamId))); } protected override Task ResetAsync(WriteContext context) => Execute(context.Connection, context.Transaction, "DELETE FROM cart_totals"); private static async Task Execute(DbConnection connection, DbTransaction transaction, string sql, params (string Name, object Value)[] values) { await using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = sql; foreach (var (name, value) in values) { var parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; command.Parameters.Add(parameter); } await command.ExecuteNonQueryAsync(); } } ``` ## Append to one stream per transaction [Section titled “Append to one stream per transaction”](#append-to-one-stream-per-transaction) A transaction that appends to two streams holds the counter from its first append while it waits for the second stream. A concurrent append can hold that stream and wait for the counter. The database then aborts one transaction as a deadlock. Nothing is lost or reordered, but that transaction fails. Append to one stream per transaction, or retry the transaction when it fails. # Use tenants > Keep each tenant's streams apart in shared tables. This guide shows you how to scope streams to tenants. Every stream belongs to a tenant: its identity is the pair (tenant, stream ID). The default tenant is empty. Set the tenant once per request on the scoped `DeedboxContext`; every store in that scope loads and appends in it. ```cs // Set the tenant and metadata once per request; every store in the request scope uses them. app.Use(async (http, next) => { var deedbox = http.RequestServices.GetRequiredService(); deedbox.TenantId = http.Request.Headers["X-Tenant"].ToString(); deedbox.Metadata = new EventMetadata { CorrelationId = http.TraceIdentifier, Actor = http.User.Identity?.Name is { } user ? $"user:{user}" : null, }; await next(http); }); ``` * The same stream ID in two tenants is two streams. * Async projections see all tenants, in one global order. `ctx.TenantId` names each event’s tenant. * Each tenant has its own encryption key, so you can [shred a whole tenant](/how-to/rotate-keys/#shred-a-whole-tenant). * A background job sets the tenant in its own scope before it resolves a store. Tenant IDs are at most 100 characters with no leading or trailing white space. # Wire QueueBox > Publish events to brokers and webhooks through QueueBox. This guide shows you how to send events to other systems with [QueueBox](https://github.com/alternayte/queuebox). Deedbox writes a QueueBox outbox row in the append’s transaction, so a message exists exactly when its event commits. QueueBox delivers it, with retries, at least once. 1. Run QueueBox against the same database, and let it create its `outbox` table. 2. Add the package: ```sh dotnet add package Deedbox.QueueBox --prerelease ``` 3. Choose the events to publish: ```cs services.AddDeedbox(es => es .UsePostgres(connStr) .Keys(keys => keys.FromEnvironment("DEEDBOX_MASTER_KEY")) .Stream(s => s.Events()) .Stream(s => s.Events()) .UseQueueBox(q => q .Publish("cart.checked_out") // Events with [PersonalData] need a payload you shape, so no personal data leaks by default. .Publish("review.invited", (e, info) => new { e.ManuscriptId, e.ReviewerId }) // Tell downstream systems to erase too. .Publish("privacy.subject_erased"))); ``` ## What each row holds [Section titled “What each row holds”](#what-each-row-holds) | Column | Value | | ---------------- | ---------------------------------------------------------------------------- | | `id` | The event ID. A destination deduplicates on `X-Message-Id`. | | `topic` | The topic you chose. | | `key` | The stream ID, so one stream’s messages keep their order. | | `payload` | The event’s JSON, or the payload you shaped. | | `headers` | `X-Correlation-Id`, `traceparent`, and the event’s type, stream and version. | | `aggregate_type` | The stream type. | An event with `[PersonalData]` needs a payload you shape; start-up fails otherwise ([DBX032](/reference/errors/dbx032/)). If QueueBox runs with a custom table or column mapping, match it with `UseTable` and `UseColumns`. # Backup and restore > What to back up, and what erasure means for backups. This runbook explains what to back up and what a restore does to erasures. ## Back up [Section titled “Back up”](#back-up) * The database: the Deedbox schema and your read-model tables, in one backup, so projections and their checkpoints match. * The master key, outside the database, unless you use database mode. Without it, a restored database has unreadable personal data. ## Erasure and backups [Section titled “Erasure and backups”](#erasure-and-backups) An erasure deletes a subject key from the live database. A backup taken before the erasure still holds that key, so personal data in that backup is readable to anyone with the backup and the master key. * Keep backups only as long as your privacy duties allow. When the oldest backup from before an erasure ages out, the subject is gone everywhere. * After you restore an older backup, run every erasure that happened after the backup again. Keep a record of erasures outside the database, for example from the `deedbox.jobs` rows or your own audit log. ## Database mode [Section titled “Database mode”](#database-mode) In database mode, the master key sits in the same backup as the data. A copy of the backup exposes all personal data in it. Move to a key ring or Key Vault before production. # Benchmarks > Append throughput and latency on both databases. This page shows how fast appends are, and where the ceiling is. The nightly workflow runs the benchmarks, and fails when a cell’s throughput drops more than 30% below the baseline. Run them yourself with `just bench`. # Deedbox benchmarks Append throughput and latency across the matrix: writers × events per append × provider × mode. Each cell runs 2 s of warm-up, then 8 s of measurement. Each writer appends to its own stream, so the position counter is the only point of contention. Counter p50 and p99 are the time from the counter update, including any wait for its lock, to the end of the append. * Run: nightly workflow, GitHub-hosted `ubuntu-latest` runner, Postgres 17 and SQL Server 2022 in Docker on the same machine. * Commit: `0d48a6d`, 2026-09-24. * Modes: `neither` means Deedbox opens and commits the transaction; `efcore` means `UseDbContext` with one inline EF Core projection. | Provider | Mode | Writers | Events per append | Appends/s | Events/s | p50 ms | p99 ms | Counter p50 ms | Counter p99 ms | | --------- | ------- | ------- | ----------------- | --------- | -------- | ------ | ------- | -------------- | -------------- | | postgres | neither | 1 | 1 | 567 | 567 | 1.71 | 2.44 | 0.89 | 1.34 | | postgres | neither | 8 | 1 | 1167 | 1167 | 6.16 | 17.75 | 4.71 | 16.29 | | postgres | neither | 32 | 1 | 914 | 914 | 21.98 | 170.95 | 20.43 | 169.64 | | postgres | neither | 128 | 1 | 674 | 674 | 132.09 | 887.02 | 129.90 | 885.97 | | postgres | neither | 1 | 10 | 552 | 5520 | 1.81 | 1.98 | 0.98 | 1.11 | | postgres | neither | 8 | 10 | 978 | 9782 | 7.19 | 22.24 | 5.66 | 20.58 | | postgres | neither | 32 | 10 | 797 | 7974 | 25.80 | 197.80 | 24.31 | 196.22 | | postgres | neither | 128 | 10 | 610 | 6097 | 144.19 | 998.64 | 142.56 | 997.45 | | postgres | efcore | 1 | 1 | 287 | 287 | 3.42 | 4.39 | 0.91 | 1.18 | | postgres | efcore | 8 | 1 | 733 | 733 | 10.70 | 17.36 | 4.31 | 10.31 | | postgres | efcore | 32 | 1 | 608 | 608 | 33.84 | 251.41 | 27.18 | 245.26 | | postgres | efcore | 128 | 1 | 445 | 445 | 187.96 | 1397.83 | 180.37 | 1383.13 | | postgres | efcore | 1 | 10 | 267 | 2668 | 3.73 | 4.10 | 1.07 | 1.23 | | postgres | efcore | 8 | 10 | 652 | 6520 | 11.94 | 20.67 | 5.10 | 12.84 | | postgres | efcore | 32 | 10 | 557 | 5568 | 38.40 | 266.68 | 31.67 | 261.06 | | postgres | efcore | 128 | 10 | 437 | 4374 | 195.45 | 1460.19 | 189.39 | 1453.89 | | sqlserver | neither | 1 | 1 | 375 | 375 | 2.57 | 4.96 | 1.38 | 3.72 | | sqlserver | neither | 8 | 1 | 697 | 697 | 11.02 | 19.27 | 9.59 | 17.72 | | sqlserver | neither | 32 | 1 | 699 | 699 | 45.27 | 55.17 | 43.80 | 53.79 | | sqlserver | neither | 128 | 1 | 635 | 635 | 184.10 | 408.39 | 182.01 | 405.10 | | sqlserver | neither | 1 | 10 | 353 | 3527 | 2.74 | 5.15 | 1.53 | 3.91 | | sqlserver | neither | 8 | 10 | 560 | 5601 | 13.03 | 52.78 | 11.51 | 51.29 | | sqlserver | neither | 32 | 10 | 561 | 5610 | 54.62 | 101.20 | 53.16 | 99.56 | | sqlserver | neither | 128 | 10 | 505 | 5054 | 120.73 | 359.90 | 118.82 | 340.92 | | sqlserver | efcore | 1 | 1 | 208 | 208 | 4.63 | 7.76 | 1.40 | 3.97 | | sqlserver | efcore | 8 | 1 | 517 | 517 | 15.28 | 19.80 | 10.15 | 14.91 | | sqlserver | efcore | 32 | 1 | 471 | 471 | 64.09 | 233.16 | 58.89 | 228.84 | | sqlserver | efcore | 128 | 1 | 483 | 483 | 205.54 | 1389.85 | 199.45 | 226.35 | | sqlserver | efcore | 1 | 10 | 203 | 2032 | 4.81 | 6.92 | 1.56 | 3.48 | | sqlserver | efcore | 8 | 10 | 447 | 4468 | 17.16 | 28.01 | 12.03 | 22.48 | | sqlserver | efcore | 32 | 10 | 436 | 4359 | 71.38 | 160.45 | 66.12 | 156.92 | | sqlserver | efcore | 128 | 10 | 405 | 4053 | 148.37 | 2498.75 | 141.60 | 351.52 | ## Reading the numbers * Appends serialize on the position counter from its update to commit, so throughput peaks at a few writers and falls slowly as more writers queue. The peak is about 1,170 appends/s on Postgres and 700 on SQL Server on this runner. * More events per append cost little: 10 events per append move about 10 times the events at a similar append rate. * With many writers, latency is almost all counter wait: counter p50 is close to append p50. * A shared CI runner varies from run to run. These numbers compare releases on the same runner type; they are not a production sizing guide. The nightly job fails when a cell’s appends/s falls more than 30% below `bench/baseline.json`. # Long transaction holding the counter lock > Find and end a transaction that makes every append wait. This runbook helps you when every append waits. Appends take turns on one counter row, from the counter update to the commit. A transaction you own, through `UseTransaction` or `UseDbContext`, holds the counter from its append until you commit. If it stays open, every other append waits. ## Symptoms [Section titled “Symptoms”](#symptoms) * `deedbox.counter.duration` and `deedbox.append.duration` rise together. * Appends time out, while the database is otherwise idle. ## Steps [Section titled “Steps”](#steps) 1. Find the transaction that holds the lock. Postgres: ```sql SELECT pid, now() - xact_start AS open_for, state, query FROM pg_stat_activity WHERE pid IN (SELECT pid FROM pg_locks l JOIN pg_class c ON c.oid = l.relation WHERE c.relname = 'position' AND l.granted); ``` SQL Server: ```sql SELECT s.session_id, t.transaction_begin_time, s.program_name FROM sys.dm_tran_locks l JOIN sys.dm_exec_sessions s ON s.session_id = l.request_session_id JOIN sys.dm_tran_active_transactions t ON t.transaction_id = s.transaction_id WHERE l.resource_associated_entity_id = OBJECT_ID('deedbox.position') AND l.request_status = 'GRANT'; ``` 2. End it from its app if you can. Otherwise, end the session: `pg_terminate_backend(pid)` or `KILL `. Its append rolls back; no position is lost. 3. Fix the code: commit soon after an append, and keep slow work, such as HTTP calls, outside the transaction. # Lost or rotated master key > Recover when Deedbox cannot unwrap its keys. This runbook helps you when start-up fails with [DBX029](/reference/errors/dbx029/): the master key cannot unwrap a tenant key. Deedbox stops instead of reading personal data as erased. Nothing is lost yet. ## Steps [Section titled “Steps”](#steps) 1. Read the message. It names the tenant, the key version, and the master key version that wrapped it (for example `env:v1`). 2. If you rotated the key ring and removed the old version too early, add the old version back to the ring after the new one: `v2:,v1:`. Deploy. Then run `deedbox keys rewrap` and remove the old version again. See [rotate keys](/how-to/rotate-keys/). 3. If the app runs with the wrong key mode, for example `FromEnvironment` against a database still in database mode, configure the mode that wrapped the keys. 4. If the key is truly lost, the personal data under it is lost. The events and all other data remain. Recover the key from your secret store’s backup if you have one. ## Prevent it [Section titled “Prevent it”](#prevent-it) * Keep the master key in a secret store with its own backup, outside the cluster. * During rotation, keep the old version in the ring until `deedbox keys rewrap` reports that every tenant key is re-wrapped. # Poison event > Handle an event that a handler cannot process. This runbook helps you handle a poison event: an event that makes a handler throw every time. ## What Deedbox has done [Section titled “What Deedbox has done”](#what-deedbox-has-done) 1. It retried the event, with growing delays, `HandlerRetries` times. 2. It stopped the consumer, set it to `stalled`, and recorded the event. Other consumers keep running. 3. No event after it was applied, and no event was skipped. ## Steps [Section titled “Steps”](#steps) 1. Run `deedbox status`. It shows the event ID, event type, stream, version and exception. 2. Read the stream and the exception. Decide whether the handler or the event is wrong. 3. If the handler is wrong, fix it and deploy. At start-up, a poison stall gets one more round of retries, and the consumer moves on once the event succeeds. 4. If the event can never be handled, skip it: ```sh deedbox skip cart_totals --event 01a0d1cd-e1f2-73f6-9d0b-bd9eec9e61c9 --wait ``` The job checks that this is the event the consumer stalled on, moves the checkpoint one event past it, and records the event and the stall in the jobs table. The same operations are on the admin API: ```cs var status = await admin.GetStatusAsync(); foreach (var consumer in status.Consumers) Console.WriteLine($"{consumer.Name}: {consumer.Status}, {consumer.Lag} behind"); var rebuild = await admin.RebuildAsync("cart_summary"); var skip = await admin.SkipAsync("cart_totals", stalledEventId); var job = await admin.GetJobAsync(rebuild); ``` # Stalled projection > Find out why a projection or subscription stopped, and start it again. This runbook helps you get a stalled projection or subscription moving again. ## Symptoms [Section titled “Symptoms”](#symptoms) * The health check is unhealthy and names the consumer. * `deedbox.consumer.status` is 2, or `deedbox.consumer.lag` grows. * Log event 23 says “stalled on stream …”. ## Steps [Section titled “Steps”](#steps) 1. Run `deedbox status`. Find the consumer and its reason. 2. If the reason is `poison`, follow the [poison event runbook](/operations/poison-event/). 3. If the reason is `mode_changed`, the projection’s run mode changed between deploys. Its old checkpoint belongs to the other mode. Rebuild it: `deedbox rebuild --wait`. 4. If the consumer is `running` but the health check says it “has not moved”, no instance is running its batches. Check that at least one instance has the runner on and can reach the database. Look for log event 21. # API reference > Every public type, by package. This page lists every public type. Each member has XML docs, so your editor shows the details. ## Deedbox [Section titled “Deedbox”](#deedbox) | Type | What it does | | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AddDeedbox(Action)` | Registers Deedbox. Configuration errors throw here. | | `DeedboxBuilder` | `Schema`, `ApplySchemaOnStartup`, `Stream`, `Projection`, `Subscription`, `OnAppending`, `Keys`, `Runner`, `ConfigureJson`, `UseJsonContext`, `ExecuteRetries`. | | `StreamBuilder` | `Event`, `Events`, `EventsNestedIn`, `StateVersion`, `Snapshots`. | | `EventBuilder` | `Name`, `Alias`, `From` (JSON upcaster), `Upcast`. | | `IState` | `Initial` and `Evolve`: what a state type implements. | | `IEventStore` | `Load`, `Append`, `Execute`, `DeleteStream`, `UseTransaction`, `WithMetadata`. Scoped. | | `ExpectedVersion` | `Any`, `NoStream`, `Exact(n)`. | | `LoadResult`, `AppendResult`, `ExecuteResult` | What the store returns. | | `EventEnvelope` | A stored event with its IDs, positions, metadata and erased subjects. | | `EventMetadata` | Correlation, causation, actor, trace context and string headers. | | `DeedboxContext` | The scope’s tenant and metadata. Scoped. | | `StreamId` | `From(Guid)` and `Deterministic(namespace, parts)`. | | `SnapshotPolicy` | `EveryAppend`, `Every(n)`, `Never`. | | `Projection`, `ProjectionContext` | An ADO.NET or Dapper projection and what its handlers see. | | `BatchProjection` | An async projection that receives whole batches. | | `WriteContext` | Where a reset or a batch writes. | | `Run` | `Inline` or `Async`. | | `Subscription`, `SubscriptionContext` | A side-effect handler, delivered at least once. | | `IAppendingHook`, `AppendingContext`, `PendingEvent` | Code that runs inside every append’s transaction. | | `RunnerOptions` | Background runner settings. | | `DataSubjectAttribute`, `PersonalDataAttribute` | Mark personal data. | | `KeysBuilder`, `IMasterKeyProvider` | Choose where the master key lives. | | `ISubjectErasure` | Erase a data subject in the scope’s tenant. | | `SubjectErased`, `StreamDeleted` | Built-in events every handler can handle. | | `IEventStoreAdmin` | Status, jobs, rebuild, skip, erase, snapshots, key re-wrap, tenant shred. | | `StoreStatus`, `ConsumerStatus`, `JobInfo` | What the admin API returns. | | `AddDeedboxHealthChecks()` | The health check. | | `DeedboxException`, `ConcurrencyException` | Errors, each with a DBX code. | ## Providers [Section titled “Providers”](#providers) | Package | Types | | ------------------- | ------------------------------------------------------------------------------------------ | | `Deedbox.Postgres` | `UsePostgres(connectionString)`, `UsePostgres(NpgsqlDataSource)`, `PostgresSchema.Script`. | | `Deedbox.SqlServer` | `UseSqlServer(connectionString)`, `SqlServerSchema.Script`. | ## Other packages [Section titled “Other packages”](#other-packages) | Package | Types | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `Deedbox.EntityFrameworkCore` | `UseDbContext(context, others)`, `Projection`, `ProjectionContext`, `WriteContext`. | | `Deedbox.Testing` | `Decider.Given`, `DeciderScenario`, `DeciderOutcome`, `EventContracts.Verify`, `KeyProviderCompliance.VerifyAsync`. | | `Deedbox.Keys.AzureKeyVault` | `UseAzureKeyVault(keyId, credential)`, `UseAzureKeyVault(CryptographyClient)`. | | `Deedbox.QueueBox` | `UseQueueBox`, `QueueBoxBuilder`, `QueueBoxColumns`. | | `Deedbox.Cli` | The `deedbox` tool. See [CLI commands](/reference/cli/). | | `Deedbox.Templates` | `dotnet new deedbox`. | # CLI commands > The deedbox tool. This page lists every `deedbox` command. ```sh dotnet tool install -g Deedbox.Cli --prerelease ``` Every command that reaches the database takes `--provider postgres|sqlserver`, `--connection` (or the `DEEDBOX_CONNECTION` variable) and `--schema` (default `deedbox`). | Command | Does | Runs where | | ------------------------------------------------- | --------------------------------------------------------------------------------- | ------------- | | `deedbox schema script --from ` | Prints the migration SQL after version n. | CLI | | `deedbox schema apply` | Applies pending migrations under a lock. | CLI | | `deedbox status [--json]` | Checkpoints, lag, stalled consumers with their poison event, recent jobs. | CLI | | `deedbox keys rewrap --from --to ` | Re-wraps tenant keys. A key is `database`, `env:` or `azure:`. | CLI | | `deedbox tenant shred --yes` | Crypto-shreds a tenant. | CLI | | `deedbox lockfile diff ` | Shows event-contract changes; exits 1 on a break. | CLI | | `deedbox rebuild [--wait]` | Queues a rebuild. | App | | `deedbox skip --event [--wait]` | Queues an audited skip of a poison event. | App | | `deedbox erase [--tenant t] [--wait]` | Deletes the subject’s key now; queues the rest. | CLI, then app | | `deedbox snapshots rebuild [--wait]` | Queues a rebuild of stored state. | App | “App” means a running app instance with the runner on takes the queued job. `--wait` follows the job and exits 1 if it fails. # Configuration options > Every setting, with its default. This page lists every setting of `AddDeedbox`. | Setting | Default | What it does | | ----------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | `UsePostgres(...)` or `UseSqlServer(...)` | none; required | The database. | | `Schema(name)` | `deedbox` | The schema for Deedbox’s tables. | | `ApplySchemaOnStartup()` | off | Applies pending migrations at start-up, under a lock. | | `ExecuteRetries(n)` | 3 | How often `Execute` reruns a decision after a creation race. | | `ConfigureJson(o => ...)` | web defaults (camelCase); enums as numbers | Deedbox’s own JSON options. The app’s global options never apply. | | `UseJsonContext(context)` | reflection | Source-generated contracts, for trimmed and native AOT apps. | | `Keys(k => ...)` | none; required with `[PersonalData]` | `StoreInDatabase()`, `FromEnvironment(var)`, `FromKeyRing(ring)`, `Use(provider)`, `RedactWith(text)`. | | `Runner(r => r.Enabled)` | true | Runs async projections, subscriptions and jobs in this process. | | `Runner(r => r.BatchSize)` | 500 | Most events per batch. | | `Runner(r => r.MinPollDelay)` | 50 ms | First wait when idle. | | `Runner(r => r.MaxPollDelay)` | 5 s | Longest wait when idle. Postgres wakes sooner on LISTEN/NOTIFY. | | `Runner(r => r.HandlerRetries)` | 5 | Retries before a consumer stalls. | | `Runner(r => r.RetryDelay)` | 1 s | First retry delay; it doubles, up to 5 minutes. | | `Runner(r => r.StallAfter)` | 10 min | The health check’s “not moving” limit. | Per stream type: | Setting | Default | What it does | | ------------------- | ------------- | -------------------------------------------------------- | | `StateVersion(n)` | 1 | Raise it when the state record changes. | | `Snapshots(policy)` | `EveryAppend` | When to store state: `EveryAppend`, `Every(n)`, `Never`. | # Error catalogue > Every DBX error code. This page lists every Deedbox error. Each error message starts with its code and ends with a link to its page. | Code | Error | | ----------------------------------- | ------------------------------------------------------ | | [DBX001](/reference/errors/dbx001/) | The Deedbox schema is missing or older than this build | | [DBX002](/reference/errors/dbx002/) | No database provider is configured | | [DBX003](/reference/errors/dbx003/) | The schema name is not valid | | [DBX004](/reference/errors/dbx004/) | A stream type or state type is registered twice | | [DBX005](/reference/errors/dbx005/) | An event type or stored name is registered twice | | [DBX006](/reference/errors/dbx006/) | An event type is not registered | | [DBX007](/reference/errors/dbx007/) | A stored event type has no registered CLR type | | [DBX008](/reference/errors/dbx008/) | A stream belongs to another stream type | | [DBX009](/reference/errors/dbx009/) | A state type or stream type is not registered | | [DBX010](/reference/errors/dbx010/) | One append holds events of several stream types | | [DBX011](/reference/errors/dbx011/) | No JSON contract for a type | | [DBX012](/reference/errors/dbx012/) | DbContexts do not share one connection | | [DBX013](/reference/errors/dbx013/) | A database provider is configured twice | | [DBX014](/reference/errors/dbx014/) | The stream is at another version | | [DBX015](/reference/errors/dbx015/) | A stored name is not valid | | [DBX016](/reference/errors/dbx016/) | Stored events have no mapping | | [DBX017](/reference/errors/dbx017/) | Stored events are newer than this build | | [DBX018](/reference/errors/dbx018/) | An event version has no upcaster | | [DBX019](/reference/errors/dbx019/) | Stored events belong to another stream type | | [DBX020](/reference/errors/dbx020/) | A projection or subscription is registered twice | | [DBX021](/reference/errors/dbx021/) | A handler handles an unregistered event | | [DBX022](/reference/errors/dbx022/) | The tenant ID is not valid | | [DBX023](/reference/errors/dbx023/) | A projection cannot be rebuilt without ResetAsync | | [DBX024](/reference/errors/dbx024/) | A batch projection is registered inline | | [DBX025](/reference/errors/dbx025/) | Personal data needs a key mode | | [DBX026](/reference/errors/dbx026/) | A personal-data property cannot hold null | | [DBX027](/reference/errors/dbx027/) | A personal-data property has no subject | | [DBX028](/reference/errors/dbx028/) | The stream was deleted | | [DBX029](/reference/errors/dbx029/) | The master key cannot unwrap a key | | [DBX030](/reference/errors/dbx030/) | Encrypted data or a key does not verify | | [DBX031](/reference/errors/dbx031/) | A built-in event was appended or registered | | [DBX032](/reference/errors/dbx032/) | A QueueBox publication is not valid | # DBX001: The Deedbox schema is missing or older than this build > The Deedbox schema is missing or older than this build. This page explains error DBX001 and how to fix it. ## Cause [Section titled “Cause”](#cause) Start-up checks the Deedbox schema version. The schema does not exist, or it is older than this build needs. ## Fix [Section titled “Fix”](#fix) Apply the schema: call `ApplySchemaOnStartup()`, run `deedbox schema apply`, or run the script from `deedbox schema script --from `. See [apply the schema](/how-to/apply-the-schema/). # DBX002: No database provider is configured > No database provider is configured. This page explains error DBX002 and how to fix it. ## Cause [Section titled “Cause”](#cause) `AddDeedbox` has no database provider. ## Fix [Section titled “Fix”](#fix) Call `UsePostgres(...)` or `UseSqlServer(...)` in `AddDeedbox`. # DBX003: The schema name is not valid > The schema name is not valid. This page explains error DBX003 and how to fix it. ## Cause [Section titled “Cause”](#cause) The schema name has characters other than lower-case letters, digits and underscores, or is longer than 50 characters. ## Fix [Section titled “Fix”](#fix) Use a name such as `deedbox` or `event_store`. # DBX004: A stream type or state type is registered twice > A stream type or state type is registered twice. This page explains error DBX004 and how to fix it. ## Cause [Section titled “Cause”](#cause) Two `Stream` registrations use one state type, or two state types use one stream type name. ## Fix [Section titled “Fix”](#fix) Register each state type once. Give one of two stream types another name with `Stream("name", ...)`. # DBX005: An event type or stored name is registered twice > An event type or stored name is registered twice. This page explains error DBX005 and how to fix it. ## Cause [Section titled “Cause”](#cause) One event type is registered twice, or two event types use one stored name or alias. ## Fix [Section titled “Fix”](#fix) Register each event type once, on its own stream. Keep old names readable with `.Alias(...)` on the event that replaces them. # DBX006: An event type is not registered > An event type is not registered. This page explains error DBX006 and how to fix it. ## Cause [Section titled “Cause”](#cause) An append holds an event type that no stream registers. ## Fix [Section titled “Fix”](#fix) Add the event to its stream: `.Events()` or `.Event()`. # DBX007: A stored event type has no registered CLR type > A stored event type has no registered CLR type. This page explains error DBX007 and how to fix it. ## Cause [Section titled “Cause”](#cause) A stored event’s name maps to no registered event. ## Fix [Section titled “Fix”](#fix) Register the event, or add its old name as an alias. # DBX008: A stream belongs to another stream type > A stream belongs to another stream type. This page explains error DBX008 and how to fix it. ## Cause [Section titled “Cause”](#cause) The stream ID belongs to a stream of another type. ## Fix [Section titled “Fix”](#fix) Load and append with the state type registered for that stream type, or use another stream ID. # DBX009: A state type or stream type is not registered > A state type or stream type is not registered. This page explains error DBX009 and how to fix it. ## Cause [Section titled “Cause”](#cause) `Load` or `Execute` names a state type, or a job names a stream type, that is not registered. ## Fix [Section titled “Fix”](#fix) Register it with `Stream(...)`. # DBX010: One append holds events of several stream types > One append holds events of several stream types. This page explains error DBX010 and how to fix it. ## Cause [Section titled “Cause”](#cause) One `Append` or `Execute` returns events of several stream types. ## Fix [Section titled “Fix”](#fix) Append each stream’s events separately. One append writes one stream. # DBX011: No JSON contract for a type > No JSON contract for a type. This page explains error DBX011 and how to fix it. ## Cause [Section titled “Cause”](#cause) Deedbox has no JSON contract for an event or state type. The app is trimmed or native AOT, or the JSON context lacks the type. ## Fix [Section titled “Fix”](#fix) Pass a `JsonSerializerContext` with `[JsonSerializable(typeof(T))]` for every event and state type to `UseJsonContext(...)`. # DBX012: DbContexts do not share one connection > DbContexts do not share one connection. This page explains error DBX012 and how to fix it. ## Cause [Section titled “Cause”](#cause) `UseDbContext` got contexts on different `DbConnection` instances, so they cannot share a transaction. ## Fix [Section titled “Fix”](#fix) Create every context with the same `DbConnection`. # DBX013: A database provider is configured twice > A database provider is configured twice. This page explains error DBX013 and how to fix it. ## Cause [Section titled “Cause”](#cause) `UsePostgres` or `UseSqlServer` is called twice. ## Fix [Section titled “Fix”](#fix) Call one of them once. # DBX014: The stream is at another version > The stream is at another version. This page explains error DBX014 and how to fix it. ## Cause [Section titled “Cause”](#cause) An append expected the stream at one version, and it is at another. Another writer appended first. ## Fix [Section titled “Fix”](#fix) Load the stream again and decide again. `Execute` does this for you. # DBX015: A stored name is not valid > A stored name is not valid. This page explains error DBX015 and how to fix it. ## Cause [Section titled “Cause”](#cause) A stream, event, projection or subscription name has characters other than letters, digits, `_`, `.`, `:` or `-`, or has more than 200 characters. ## Fix [Section titled “Fix”](#fix) Choose another name. # DBX016: Stored events have no mapping > Stored events have no mapping. This page explains error DBX016 and how to fix it. ## Cause [Section titled “Cause”](#cause) Start-up found stored events whose name maps to no registered event. Usually an event class was renamed. ## Fix [Section titled “Fix”](#fix) Add `.Alias("old.name")` to the event that replaces it. See [rename an event](/how-to/rename-an-event/). # DBX017: Stored events are newer than this build > Stored events are newer than this build. This page explains error DBX017 and how to fix it. ## Cause [Section titled “Cause”](#cause) Stored events have a newer shape version than this build registers. A newer build wrote them. ## Fix [Section titled “Fix”](#fix) Deploy that build or a later one. # DBX018: An event version has no upcaster > An event version has no upcaster. This page explains error DBX018 and how to fix it. ## Cause [Section titled “Cause”](#cause) An event’s version has no upcaster for one of its older versions, or a typed upcaster is not the last step, or a step is past the current version. ## Fix [Section titled “Fix”](#fix) Add `up.From(n, json => ...)` for every version from 1 to the current one minus 1. See [change an event’s shape](/how-to/change-an-event-shape/). # DBX019: Stored events belong to another stream type > Stored events belong to another stream type. This page explains error DBX019 and how to fix it. ## Cause [Section titled “Cause”](#cause) Stored events of one stream type now map to an event registered under another stream type. ## Fix [Section titled “Fix”](#fix) Register the event on the stream type its stored events belong to. # DBX020: A projection or subscription is registered twice > A projection or subscription is registered twice. This page explains error DBX020 and how to fix it. ## Cause [Section titled “Cause”](#cause) A projection or subscription class, or its name, is registered twice. Running one projection inline and async applies events twice. ## Fix [Section titled “Fix”](#fix) Register each class once, with one name and one run mode. # DBX021: A handler handles an unregistered event > A handler handles an unregistered event. This page explains error DBX021 and how to fix it. ## Cause [Section titled “Cause”](#cause) A projection or subscription handles an event type that no stream registers. ## Fix [Section titled “Fix”](#fix) Register the event, or remove the handler. # DBX022: The tenant ID is not valid > The tenant ID is not valid. This page explains error DBX022 and how to fix it. ## Cause [Section titled “Cause”](#cause) The tenant ID is longer than 100 characters, or starts or ends with white space. ## Fix [Section titled “Fix”](#fix) Use a trimmed ID of at most 100 characters. # DBX023: A projection cannot be rebuilt without ResetAsync > A projection cannot be rebuilt without ResetAsync. This page explains error DBX023 and how to fix it. ## Cause [Section titled “Cause”](#cause) A rebuild job runs for a projection that does not override `ResetAsync`. ## Fix [Section titled “Fix”](#fix) Override `ResetAsync` to delete what the projection wrote, then queue the rebuild again. # DBX024: A batch projection is registered inline > A batch projection is registered inline. This page explains error DBX024 and how to fix it. ## Cause [Section titled “Cause”](#cause) A `BatchProjection` is registered with `Run.Inline`. ## Fix [Section titled “Fix”](#fix) Register it with `Run.Async`. # DBX025: Personal data needs a key mode > Personal data needs a key mode. This page explains error DBX025 and how to fix it. ## Cause [Section titled “Cause”](#cause) An event has `[PersonalData]`, and no key mode is chosen. ## Fix [Section titled “Fix”](#fix) Add `.Keys(keys => keys.StoreInDatabase())` to start, or `.Keys(keys => keys.FromEnvironment("DEEDBOX_MASTER_KEY"))`. See [erase a person](/how-to/erase-a-person/). # DBX026: A personal-data property cannot hold null > A personal-data property cannot hold null. This page explains error DBX026 and how to fix it. ## Cause [Section titled “Cause”](#cause) A `[PersonalData]` property cannot hold null, which it reads as after erasure. ## Fix [Section titled “Fix”](#fix) Make it a `string` or a nullable type. # DBX027: A personal-data property has no subject > A personal-data property has no subject. This page explains error DBX027 and how to fix it. ## Cause [Section titled “Cause”](#cause) A `[PersonalData]` property has no subject: the event has no single `[DataSubject]` property, the named subject property is missing or not a string, or the subject ID is empty at append time. ## Fix [Section titled “Fix”](#fix) Mark exactly one `[DataSubject]` string property, or name the subject with `[PersonalData(Subject = "PropertyName")]`, and give it a value. # DBX028: The stream was deleted > The stream was deleted. This page explains error DBX028 and how to fix it. ## Cause [Section titled “Cause”](#cause) The stream was deleted. Its ID is never reused. ## Fix [Section titled “Fix”](#fix) Use another stream ID. # DBX029: The master key cannot unwrap a key > The master key cannot unwrap a key. This page explains error DBX029 and how to fix it. ## Cause [Section titled “Cause”](#cause) The configured master key cannot unwrap a tenant key, the key ring is missing or not valid, or a key version is unknown. Deedbox stops rather than read personal data as erased. ## Fix [Section titled “Fix”](#fix) Configure the master key that wrapped the keys, or add its version to the key ring. See [master key runbook](/operations/master-key/). # DBX030: Encrypted data or a key does not verify > Encrypted data or a key does not verify. This page explains error DBX030 and how to fix it. ## Cause [Section titled “Cause”](#cause) An encrypted field, a subject key or a stored state does not verify. It was altered, or copied from another row. ## Fix [Section titled “Fix”](#fix) Restore the row from a backup. Deedbox does not read it as erased. # DBX031: A built-in event was appended or registered > A built-in event was appended or registered. This page explains error DBX031 and how to fix it. ## Cause [Section titled “Cause”](#cause) The app appended `SubjectErased` or `StreamDeleted`, or registered one of them on a stream. Deedbox appends these itself. ## Fix [Section titled “Fix”](#fix) Use `DeleteStream` or `ISubjectErasure`. Handle the built-in events without registering them. # DBX032: A QueueBox publication is not valid > A QueueBox publication is not valid. This page explains error DBX032 and how to fix it. ## Cause [Section titled “Cause”](#cause) A QueueBox publication names an unregistered event, publishes an event twice, uses a name that is not a plain identifier, or publishes personal data without a payload mapping. ## Fix [Section titled “Fix”](#fix) Fix the publication. For an event with `[PersonalData]`, pass a payload that holds only what the receiver needs. # Schema tables > The tables Deedbox owns, and what each column holds. This page describes the tables in the Deedbox schema. Deedbox owns only these tables; your read models stay in your own tables. | Table | Key | Holds | | ----------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `streams` | `(tenant_id, stream_id)` | One row per stream: `stream_type`, `version`, stored `state`, `state_version`, `state_at` (the version the state reflects), `deleted_at`. | | `events` | `global_position` | Every event: `event_id`, `tenant_id`, `stream_id`, `version`, `stream_type`, `event_type`, `event_version`, `payload`, `metadata`, `occurred_at`. Unique on `event_id` and on `(tenant_id, stream_id, version)`. | | `position` | one row | The global position counter. | | `event_types` | `(stream_type, event_type, event_version)` | Every event type ever stored; the start-up check reads it. | | `checkpoints` | `name` | One row per projection and subscription: `position`, `mode`, `status`, `error`. | | `jobs` | `id` | Rebuilds, skips, erasures and snapshot rebuilds; the audit trail. | | `master_keys` | `(tenant_id, key_version)` | Wrapped tenant keys; shredded tenants leave tombstone rows. | | `subject_keys` | `(tenant_id, subject_id)` | Wrapped subject keys. Erasure deletes a row. | | `subject_streams` | `(tenant_id, subject_id, stream_id)` | Which streams hold which subject’s data. | | `schema_version` | `version` | Applied migrations. | On Postgres, JSON columns are `jsonb`. On SQL Server, they are `nvarchar(max)`, and key columns use the `Latin1_General_100_BIN2` collation, so IDs compare case-sensitively on both databases. Migrations are numbered, idempotent and forward-only. See [apply the schema](/how-to/apply-the-schema/). # Metric and trace names > What Deedbox emits for OpenTelemetry. This page lists every span and instrument. Both the ActivitySource and the Meter are named `Deedbox`. ```cs // With OpenTelemetry: subscribe to the "Deedbox" ActivitySource and Meter. // .WithTracing(t => t.AddSource("Deedbox")) // .WithMetrics(m => m.AddMeter("Deedbox")) const string SourceAndMeter = "Deedbox"; ``` ## Spans [Section titled “Spans”](#spans) | Span | Tags | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `deedbox.append`, `deedbox.execute`, `deedbox.load` | `deedbox.stream_type`, `deedbox.stream_id`, `deedbox.events` | | `deedbox.delete_stream`, `deedbox.erase_stream` | `deedbox.stream_id` | | `deedbox.batch` | `deedbox.consumer`, `deedbox.from_position`, `deedbox.to_position` | | `deedbox.handle ` | `deedbox.consumer`, `deedbox.event_type`, `deedbox.global_position`. Its parent is the append that wrote the event. | | `deedbox.job` | `deedbox.job.kind`, `deedbox.job.id` | ## Instruments [Section titled “Instruments”](#instruments) | Instrument | Unit | Tags | | -------------------------------------------------------------------- | ---------------------------------- | ---------------------------------------- | | `deedbox.append.duration` | ms | `deedbox.stream_type` | | `deedbox.events.appended` | events | `deedbox.stream_type` | | `deedbox.append.conflicts` | conflicts | `deedbox.stream_type` | | `deedbox.execute.retries` | retries | `deedbox.stream_type` | | `deedbox.counter.duration` | ms | none. Counter wait plus hold time. | | `deedbox.consumer.lag` | positions | `deedbox.consumer`, `deedbox.schema` | | `deedbox.consumer.lag.seconds` | s | `deedbox.consumer`, `deedbox.schema` | | `deedbox.consumer.status` | 0 running, 1 rebuilding, 2 stalled | `deedbox.consumer`, `deedbox.schema` | | `deedbox.consumer.batch.duration` | ms | `deedbox.consumer` | | `deedbox.consumer.failures`, `deedbox.consumer.stalls` | count | `deedbox.consumer` | | `deedbox.jobs` | jobs | `deedbox.job.kind`, `deedbox.job.status` | | `deedbox.erasure.streams` | streams | none | | `deedbox.personal_data.decrypts`, `deedbox.personal_data.redactions` | fields | none | ## Logs [Section titled “Logs”](#logs) Log events have stable IDs: 1 to 3 at start-up, 20 to 25 in the runner, 30 to 33 for jobs. Every start-up error is a `DeedboxException` with a [DBX code](/reference/errors/). # Add Deedbox to an existing EF Core app > Event-source one aggregate of an EF Core app, with its read model in the same transaction. In this tutorial, you move one aggregate of an existing EF Core app to events. Your other tables stay in EF Core. A read model table, updated by an inline projection, commits in the same transaction as the events. It takes under an hour. The example app has a `ShopDb` DbContext. You event-source its cart and keep a `CartSummaries` table for queries. 1. Add the packages. ```sh dotnet add package Deedbox.Postgres --prerelease dotnet add package Deedbox.EntityFrameworkCore --prerelease ``` Use `Deedbox.SqlServer` for SQL Server. The rest of this tutorial is the same. 2. Write the events and the state, as in [your first stream](/tutorials/first-stream/). ```cs // Events: plain records. No marker interface, no base class. public record ItemAdded(string Sku, int Qty); public record CheckedOut(DateTimeOffset At); ``` ```cs // State: Initial and Evolve. Nothing else. public record Cart(ImmutableDictionary Items, bool IsCheckedOut) : IState { public static Cart Initial { get; } = new(ImmutableDictionary.Empty, false); public static Cart Evolve(Cart s, object e) => e switch { ItemAdded x => s with { Items = s.Items.SetItem(x.Sku, s.Items.GetValueOrDefault(x.Sku) + x.Qty) }, CheckedOut => s with { IsCheckedOut = true }, _ => s, }; } ``` 3. Write an inline projection. It updates your EF Core entity in the append’s transaction. You do not call `SaveChanges`; Deedbox does, just before it commits the events. ```cs // EF Core flavour: your DbContext, enlisted in the append's transaction. Deedbox calls SaveChanges. public sealed class CartSummaryProjection : Projection { public CartSummaryProjection() { On(async (e, ctx) => { var row = await ctx.Db.CartSummaries.FindAsync([ctx.StreamId], ctx.CancellationToken) ?? ctx.Db.CartSummaries.Add(new CartSummaryRow(ctx.StreamId)).Entity; row.ItemCount += e.Qty; }); On(async (_, ctx) => { var row = await ctx.Db.CartSummaries.FindAsync([ctx.StreamId], ctx.CancellationToken); row!.CheckedOut = true; }); On(async (_, ctx) => await ctx.Db.CartSummaries.Where(r => r.Id == ctx.StreamId).ExecuteDeleteAsync(ctx.CancellationToken)); } // A rebuild calls ResetAsync, then replays every event. protected override Task ResetAsync(WriteContext context) => context.Db.CartSummaries.ExecuteDeleteAsync(context.CancellationToken); } ``` 4. Register Deedbox. `Run.Inline` means the projection runs in the append’s transaction. ```cs builder.Services.AddDeedbox(es => es .UsePostgres(connStr) .Stream(s => s.Events()) .Projection("cart_summary", Run.Inline) .Projection("cart_totals", Run.Async) .Subscription("receipt_email")); ``` 5. Create the Deedbox tables with an EF Core migration. The tables stay out of your EF model. ```sh dotnet ef migrations add AddDeedbox ``` Replace the generated `Up` method with the Deedbox script: ```cs // An EF Core migration that creates the Deedbox tables without adding them to your model. public partial class AddDeedbox : Migration { protected override void Up(MigrationBuilder migrationBuilder) => migrationBuilder.Sql(PostgresSchema.Script(fromVersion: 0)); // SqlServerSchema.Script on SQL Server } ``` 6. Append through your DbContext. `UseDbContext` joins the context’s transaction, or opens one and commits it. Pass every context on the same connection that must commit with the events. ```cs // Both contexts share one DbConnection. Deedbox enlists them, calls SaveChanges on each, // and commits everything at once. With no transaction open, it opens and commits one. shop.CartSummaries.Add(new CartSummaryRow("cart-42")); await store.UseDbContext(shop, billing).Execute("cart-42", cart => CartDecider.Add(cart, "apple", 1)); ``` ## What changed [Section titled “What changed”](#what-changed) * The cart’s history is in `deedbox.events`, and its state in `deedbox.streams`. * `CartSummaries` is still an EF Core table. It changes only through the projection, in the same transaction as the events. * Every other table and handler in your app is unchanged. ## Next [Section titled “Next”](#next) * [Rebuild a projection](/how-to/rebuild-a-projection/) when you change its logic. * [Use Dapper or plain ADO.NET](/how-to/use-dapper/) instead of EF Core. # Your first stream > Store a shopping cart as events in about 10 minutes. In this tutorial, you store a shopping cart as a stream of events. You add items, check the cart out, and load its state back. It takes about 10 minutes. You need the .NET 10 SDK (or .NET 8) and a Postgres or SQL Server database. Docker is the fastest way to get one. 1. Create a web app and add the packages for your database. * Postgres ```sh dotnet new web -o Shop cd Shop dotnet add package Deedbox.Postgres --prerelease docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:17-alpine ``` * SQL Server ```sh dotnet new web -o Shop cd Shop dotnet add package Deedbox.SqlServer --prerelease docker run -d -p 1433:1433 -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD='yourStrong(!)Password' mcr.microsoft.com/mssql/server:2022-latest ``` 2. Write the events. An event is a plain record that states what happened. ```cs // Events: plain records. No marker interface, no base class. public record ItemAdded(string Sku, int Qty); public record CheckedOut(DateTimeOffset At); ``` 3. Write the state. `Initial` is the state of a new cart. `Evolve` applies one event. ```cs // State: Initial and Evolve. Nothing else. public record Cart(ImmutableDictionary Items, bool IsCheckedOut) : IState { public static Cart Initial { get; } = new(ImmutableDictionary.Empty, false); public static Cart Evolve(Cart s, object e) => e switch { ItemAdded x => s with { Items = s.Items.SetItem(x.Sku, s.Items.GetValueOrDefault(x.Sku) + x.Qty) }, CheckedOut => s with { IsCheckedOut = true }, _ => s, }; } ``` 4. Write the decisions. A decision is a pure function: it takes the current state and returns new events. It never writes anything. ```cs // Decisions: pure functions from state to new events. public static class CartDecider { public static IEnumerable Add(Cart cart, string sku, int qty) => cart.IsCheckedOut ? throw new InvalidOperationException("The cart is checked out.") : [new ItemAdded(sku, qty)]; public static IEnumerable CheckOut(Cart cart, DateTimeOffset now) => cart.IsCheckedOut || cart.Items.IsEmpty ? [] : [new CheckedOut(now)]; } ``` 5. Register the stream in `Program.cs`. `ApplySchemaOnStartup` creates the Deedbox tables when the app starts. * Postgres ```cs builder.Services.AddDeedbox(es => es .UsePostgres(connStr) .ApplySchemaOnStartup() .Stream(s => s // stream type "cart" .Events())); // cart.item_added, cart.checked_out ``` * SQL Server ```cs builder.Services.AddDeedbox(es => es .UseSqlServer(connStr) .ApplySchemaOnStartup() .Stream(s => s .Events())); ``` Deedbox names the stream type `cart` and the events `cart.item_added` and `cart.checked_out`. These names are stored with every event, so a class rename later does not break stored events. 6. Append events. Get `IEventStore` from dependency injection. `Execute` loads the cart, runs your decision, and appends the new events in one transaction. ```cs // Load, decide, evolve and append in one transaction. var result = await store.Execute(cartId, cart => CartDecider.Add(cart, sku, qty)); // result.State is the new state; result.Version the new version; result.Events the appended envelopes. ``` 7. Or do each part yourself. `Load` returns the state and the version. `Append` writes only if the stream is still at that version. ```cs var (cart, version) = await store.Load(cartId); var events = CartDecider.CheckOut(cart, now).ToList(); if (events.Count > 0) await store.Append(cartId, ExpectedVersion.Exact(version), events); ``` ## What you have now [Section titled “What you have now”](#what-you-have-now) * A `deedbox` schema with a `streams` table and an `events` table. * One stream, `cart-1`, with its events in order, and its current state stored next to it. * No mediator, no base class and no changes to the rest of your app. ## Next [Section titled “Next”](#next) * [Add a projection](/how-to/add-a-projection/) to build a read model. * [Add Deedbox to an existing EF Core app](/tutorials/existing-ef-core-app/). * Read [streams and state](/concepts/streams-and-state/) to see what Deedbox stores.