Skip to content

Tenancy

Serving several customers from one Chronicle installation takes three separate decisions. Chronicle and Cratis Arc each own some of them, and keeping them apart is what keeps tenants isolated.

Request

1. Selection: which tenant is this?

2. Namespace: where do its events live?

3. Placement: which server, database and identity?

DecisionWho decidesDefault
Selection: which tenant a request belongs to, and whether the caller may use itYour application. With Arc, a tenant resolver plus your authorizationNo tenant, so the Default namespace
Namespace: the logical partition of events, observers and read modelsChronicle, from the tenant. With Arc the tenant id is the namespace nameDefault
Placement: the physical database, server, schema and credentialsChronicle derives names. With storage connection resolution your platform decidesOne server, one credential, names derived from the event store and namespace

Chronicle isolates by the namespace it is given. It does not check that a caller may use a tenant, so authorize before you select.

You configure nothing beyond the connection to your database server. Every namespace gets its own database, and Chronicle creates what is missing the first time a namespace is used.

For an event store named Orders and a tenant acme:

HoldsDefault namespaceNamespace acme
Event sequences, observer stateOrders+es+DefaultOrders+es+acme
Read modelsOrdersOrders+acme

The names are the same on MongoDB, PostgreSQL and SQL Server, with the cluster database name as the first part on SQL (<cluster>+es+Orders+acme). Two details are worth knowing:

  • Default-namespace read models live in the bare event store name. Event sequences suffix every namespace, including Default. Composing <eventStore>+<namespace> for read models yields Orders+Default, which does not exist, and reads come back empty instead of failing. See namespaces.
  • Namespaces are created on first use. A client that still has a namespace open brings it back the moment it appends.

With Arc, one fragment in the application is enough to map the tenant to the namespace. The tenant resolver decides where the tenant comes from; the Chronicle integration registers the namespace mapping. See Arc’s Chronicle tenancy. Without Arc, resolve the namespace yourself as described in ASP.NET Core namespace resolution.

The model is the same. The details underneath are not.

MongoDBPostgreSQL and SQL Server
Isolation unitA database per event store, namespace and read model setThe same, plus an optional schema inside each database
Database name limits63 bytes; rejects / \ . " $ * < > : | ? and the space. A bad namespace throws InvalidDatabaseName63 bytes (PostgreSQL) or 128 UTF-16 code units (SQL Server). Longer names become a readable prefix, ~ and a hash
Where the name is rememberedDerived each timeRecorded permanently in the cluster’s DatabaseNames table, so a namespace never moves
Shared serverstorage.databaseNamePrefix keeps several installations apartThe cluster database name is the prefix
SchemasNot applicablestorage.schema keeps Chronicle’s tables in a schema you choose
Resolved access tokenMONGODB-OIDC credentialAn Entra ID token on SQL Server, the password on PostgreSQL
Resolved passwordKept by the database client until the kernel restarts, so prefer tokensApplied every time a connection opens, so rotation needs no restart

See storage for the full naming and binding rules.

Defaults suit a single team that owns its database server. A platform that places each tenant’s database itself needs Chronicle to ask instead of derive. Storage connection resolution does that for every event store, namespace and read model database, on MongoDB and SQL alike. Nothing changes until you configure storage.connectionResolution.

Chronicle describes what it connects for, and a resolver answers with only the parts it decides:

Chronicle asksA resolver answers (every part optional)
storage, purpose (Runtime or Schema)connectionString, database, schema
scope.kind: cluster, eventStore, namespace or readModelsidentity: userName and password, or accessToken
scope.eventStore and scope.namespaceexpiresAt

The resolvers (Default, Template, Http and Custom) and the REST contract are Arc’s, so they behave the same for an Arc application and for the kernel: connection resolution in Arc and the connection resolution contract.

A rule is enough when every tenant follows the same pattern. This puts each namespace’s events and read models in separate databases on PostgreSQL:

{
"storage": {
"type": "PostgreSql",
"connectionDetails": "Host=pg.example;Database=chronicle;Username=chronicle;Password=secret",
"connectionResolution": {
"connection": {
"resolver": "Template",
"template": { "database": "{eventStore}_{namespace}_{kind}" }
}
}
}
}

A resolved database is used as given. Chronicle does not derive a name for it and does not record a DatabaseNames binding. The cluster database is always connectionDetails.

When placement should stay Chronicle’s but credentials come from your platform, resolve just the identity. The connection slot is left alone:

{
"storage": {
"type": "MongoDB",
"connectionDetails": "mongodb://cluster.example:27017",
"connectionResolution": {
"identity": {
"resolver": "Http",
"http": {
"endpoint": "https://platform.example/internal/connections",
"headers": { "X-Api-Key": "from-your-secret-store" }
}
}
}
}
}

A database per tenant, with managed identities

Section titled “A database per tenant, with managed identities”

For a platform that provisions tenant databases, with Entra managed identities and no database created by Chronicle, see a database per tenant on Azure SQL. It combines createIfMissing: false, schema and an Http resolver that answers Runtime and Schema with different identities.

Keep Chronicle and your application aligned

Section titled “Keep Chronicle and your application aligned”

Chronicle writes read models into its read model database. Your application, often through Arc, reads them from its own connection. They must end up in the same place, or reads come back empty:

DefaultWith resolution
Arc’s MongoDB naming is <database>+<tenant>, with the bare name for the default tenant. That matches Chronicle’s read model naming when the Arc database name is the event store nameBoth sides must resolve the same database. Serve one endpoint that answers Chronicle’s readModels scope and Arc’s tenant scope from the same placement record
Arc’s EF Core uses one connection string for every tenantConfigure Arc’s EF Core resolution to point at the database Chronicle’s SQL sink writes to

Chronicle’s kernel and your Arc application are configured separately. Setting Cratis:Arc:ConnectionResolution does not change where the kernel stores anything, and storage.connectionResolution does not change what Arc connects to. The same resolver service can back both.

See Arc: tenancy end to end for the application side.

Removing a tenant removes its namespace. Chronicle drops the events and observer state it owns, and stops the observers. It does not drop read model data, because your application owns it. With a resolved connection that data may be on a server Chronicle has never seen, so your platform is also the one to deprovision it. See removing a namespace.

  • Authorize the caller for the tenant before selecting it. Namespaces isolate; they do not authenticate.
  • Decide how the tenant id becomes a valid database name. Display-style ids such as Hive Consulting are rejected by MongoDB.
  • Decide placement before storing data. Changing a resolution moves Chronicle to the new location and does not move existing data.
  • Test an unset tenant, the default tenant and two named tenants, including concurrent requests.
  • Verify the actual database each tenant’s events and read models reach, not only the namespace name.