Sinks
A sink is the storage backend where Chronicle writes read model instances produced by projections and reducers. Chronicle ships with two built-in sink types.
MongoDB Sink
Section titled “MongoDB Sink”The default sink. Read model instances are stored as MongoDB documents, one document per instance. The container name defaults to the read model type name (camelCase), controlled by the INamingPolicy in use.
This requires no additional setup — MongoDB is the default when you configure Chronicle.
What a MongoDB convention pack reaches
Section titled “What a MongoDB convention pack reaches”Because read model instances land in MongoDB as documents, it’s natural to assume that configuring the MongoDB driver configures how Chronicle hands read models back. It doesn’t, and the gap is worth knowing before you spend an afternoon on it.
A convention pack — whether you provide one through Arc’s ICanProvideMongoDBConventionPacks or call ConventionRegistry.Register yourself — is registered in the MongoDB driver’s global convention registry. That registry configures the driver’s class maps. So a convention pack reaches exactly as far as the driver does, and no further:
| Path a read model takes | Does the convention pack apply |
|---|---|
Your own IMongoCollection<T> query | Yes — the driver builds the instance from a class map |
| Chronicle’s read model APIs — by key, materialized, snapshots, watching | No |
A read model injected into a command’s validator, Provide(), or Handle() | No |
| The document the sink writes | No |
Chronicle returns a read model as JSON and materializes it with System.Text.Json, so no BsonClassMap is consulted anywhere on that path. Command-side read model injection is the one that surprises people: it resolves through the same read model API, so a rename, a custom serializer, or an IgnoreExtraElements convention that is visibly working in your query code has no effect at all on the instance your command receives. And the sink writes the document from the projected state rather than from a class map of your type, so a convention pack doesn’t change what gets stored either.
That is what the convention registration in Get started is for. It isn’t configuring Chronicle — it’s telling your driver how to map your type onto element names Chronicle has already chosen.
SQL Sink
Section titled “SQL Sink”The SQL sink treats the database as a document store. Each read model type gets its own table with two columns:
| Column | Type | Description |
|---|---|---|
Id | nvarchar / varchar | The read model key |
Document | nvarchar(max) / text | The serialized JSON of the instance |
Tables are created automatically on first use via IReadModelMigrator. You do not need to write migrations by hand.
This allows teams using SQL databases (SQL Server, PostgreSQL, SQLite) to take advantage of Chronicle projections without maintaining a MongoDB instance.
Enabling the SQL Sink
Section titled “Enabling the SQL Sink”Set DefaultSinkTypeId in ChronicleOptions:
using Microsoft.Extensions.Hosting;using Cratis.Chronicle.Sinks;
public static class SinksSqlRegistration{ public static void Configure(string[] args) { var builder = Host.CreateApplicationBuilder(args);
builder.AddCratisChronicle(configureOptions: options => { options.DefaultSinkTypeId = WellKnownSinkTypes.SQL; }); }}Or in appsettings.json:
{ "Cratis": { "Chronicle": { "DefaultSinkTypeId": "MongoDB" } }}Prerequisites
Section titled “Prerequisites”The SQL sink is provided by the Cratis.Chronicle.Storage.Sql NuGet package. Add it to your Kernel host project and register it in the IChronicleBuilder:
siloBuilder.AddChronicleToSilo(chronicle =>{ chronicle.WithSql(options);});Refer to the hosting documentation for full server setup details.
Choosing a Sink
Section titled “Choosing a Sink”| MongoDB | SQL | |
|---|---|---|
| Setup complexity | Requires MongoDB | Requires SQL database |
| Schema management | Schemaless | Tables auto-created on first use |
| Query flexibility | Rich aggregation pipeline | Standard SQL queries |
| Best for | Event-driven microservices, flexible schemas | Existing SQL infrastructure, relational tooling |
The sink type applies globally to all projections and reducers registered by the client. It cannot be set per-read-model from client configuration today — all read models share the same sink backend.