Skip to content

Multi-Tenancy

CritterWatch provides first-class support for monitoring multi-tenant Wolverine applications that use Marten's multi-tenancy features. Tenant management, per-tenant DLQ operations, and per-tenant projection health are all supported.

How Multi-Tenancy Works in Wolverine

Wolverine supports multi-tenancy via Marten's tenancy capabilities. In a multi-tenant Wolverine application:

  • Each tenant has its own message store (inbox/outbox) database
  • Each tenant has its own Marten document/event store
  • Each tenant has its own projection shards
  • A message envelope carries a TenantId that determines which database is used

When AddCritterWatchMonitoring() is called, the observer reports the service's tenancy configuration to CritterWatch, including:

  • The tenancy cardinality (None, Single, ConjoindTenancy, DefaultTenant, ExtendedDynamic)
  • The current list of active tenants
  • Per-tenant message store URIs

CritterWatch uses this information to scope DLQ queries, projection monitoring, and scheduled message views to individual tenants.

Tenancy Cardinality

CardinalityDescription
NoneNo multi-tenancy (single database for everything)
SingleAll tenants in one database, partitioned by tenant ID
ConjoindTenancySeparate databases per tenant, connection strings defined at startup
DefaultTenantMixed: some operations use a default tenant, others are tenant-specific
ExtendedDynamicFully dynamic: tenants can be added/removed at runtime

CritterWatch's tenant management features (AddTenant, RemoveTenant, etc.) are primarily useful for ExtendedDynamic tenancy, where tenant databases are provisioned at runtime.

Setting Up a Multi-Tenant Service

cs
// In your multi-tenant service's Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseWolverine(opts =>
{
    opts.UseRabbitMq(new Uri("amqp://localhost")).AutoProvision();

    // CritterWatch automatically discovers and reports tenant information
    // from the Wolverine runtime — no additional configuration required
    opts.AddCritterWatchMonitoring(
        critterWatchUri: new Uri("rabbitmq://critterwatch"),
        systemControlUri: new Uri("rabbitmq://multi-tenant-service-control")
    );
});

builder.Build().Run();

snippet source | anchor

Adding Tenants at Runtime

For ExtendedDynamic tenancy, tenants can be added without restarting the service:

cs
// Add a new tenant database at runtime — no service restart required
await bus.SendAsync(new AddTenant(
    TenantId: "new-customer",
    ConnectionString: "Host=new-customer-db;Database=app;Username=app;Password=secret"
)
{
    ServiceName = "multi-tenant-service"
});

snippet source | anchor

The AddTenant command is handled by Wolverine.CritterWatch which:

  1. Calls Wolverine's tenant management API to add the database at runtime
  2. Applies the Marten schema to the new database (table creation, etc.)
  3. Notifies CritterWatch of the new tenant via the telemetry stream

Connection String Security

Connection strings for tenant databases are transmitted to the target service via RabbitMQ. Ensure your RabbitMQ deployment uses TLS for connections and that only authorized services can produce to the command exchange.

CritterWatch does not persist tenant connection strings — it only stores the database URI (host and database name) for identification purposes.

Per-Tenant DLQ Operations

In the CritterWatch UI, the Dead Letter Queue explorer has a Tenant selector for multi-tenant services. Selecting a tenant scopes all queries to that tenant's message store database.

cs
// CritterWatch UI supports filtering DLQ by tenant.
// The HTTP API also accepts a tenantId query parameter:
//
// GET /api/critterwatch/dead-letters?serviceName=multi-tenant-service&tenantId=acme-corp
//
// Replay operations target a specific tenant's message store:
// POST /api/critterwatch/dead-letters/replay
// {
//   "serviceName": "multi-tenant-service",
//   "tenantId": "acme-corp",
//   "envelopeIds": ["..."]
// }

snippet source | anchor

Per-Tenant Projections

Async projections in multi-tenant Marten applications run independently per tenant. Each tenant gets its own projection shards with independent sequence tracking. CritterWatch displays all tenant-specific shards in the Projections view with the tenant ID shown alongside the shard name.

Rebuild and rewind operations target a specific tenant's event store. A rebuild of trip-service / TripSummary:All / acme-corp only affects the acme-corp tenant's data.

Tenant List

CritterWatch tracks the known tenant list for each service. If you need to refresh this list (e.g., after adding tenants programmatically), use the RequestTenantList command:

cs
// Request the current tenant list from a service
await bus.SendAsync(new RequestTenantList
{
    ServiceName = "multi-tenant-service"
});
// The service responds with TenantListResponse, which CritterWatch relays to the browser

snippet source | anchor

Tenant Management in the UI

The Services > Service Detail page includes a Tenants section for multi-tenant services. From this section, operators can:

  • View the current tenant list with status (Active / Disabled)
  • Add new tenants (for ExtendedDynamic tenancy)
  • Disable tenants (stops processing, retains data)
  • Re-enable disabled tenants
  • Remove tenants (irreversible — use with caution)

All tenant management operations appear in the Activity Timeline with the operator's identity and a timestamp.

Released under the MIT License.