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
TenantIdthat 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
| Cardinality | Description |
|---|---|
None | No multi-tenancy (single database for everything) |
Single | All tenants in one database, partitioned by tenant ID |
ConjoindTenancy | Separate databases per tenant, connection strings defined at startup |
DefaultTenant | Mixed: some operations use a default tenant, others are tenant-specific |
ExtendedDynamic | Fully 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
// 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();Adding Tenants at Runtime
For ExtendedDynamic tenancy, tenants can be added without restarting the service:
// 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"
});The AddTenant command is handled by Wolverine.CritterWatch which:
- Calls Wolverine's tenant management API to add the database at runtime
- Applies the Marten schema to the new database (table creation, etc.)
- 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.
// 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": ["..."]
// }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:
// 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 browserTenant 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.
