Convoy.Cli
1.1.0
dotnet tool install --global Convoy.Cli --version 1.1.0
dotnet new tool-manifest
dotnet tool install --local Convoy.Cli --version 1.1.0
#tool dotnet:?package=Convoy.Cli&version=1.1.0
nuke :add-package Convoy.Cli --version 1.1.0

Multi-tenancy for .NET. All of it.
You have 200 tenants.
You need to deploy a migration.
So you write a foreach loop, call MigrateAsync, and hope nothing fails at tenant 94.
It does. You don't know which tenants succeeded. You don't know which ones failed. You have no rollback path, no retry, no audit trail, and a production incident at 2am that nobody planned for.
Every multi-tenant team hits this wall. Most of them build around it. Convoy tears it down.
Resolve. One line.
Seven strategies built in. Use one. Chain several. Write your own. Done.
convoy.WithResolution(r => r.FromSubdomain());
// acme.yourapp.com → "acme". That's it.
Need more?
convoy.WithResolution(r =>
r.FromHeader("X-Tenant-Id")
.ThenFromSubdomain()
.ThenFromClaim("tid"));
// Try header first. Fall back to subdomain. Fall back to claim.
// Convoy figures it out. You move on.
Every strategy you'll ever need — subdomain, header, route, claim, query string, static mapping, custom — all included, all tested, all production-ready.
Isolate. Your way.
Three data isolation models. Pick the one that fits. Or mix them — per tenant, at runtime.
// Enterprise customers get their own database.
// Pro customers share one, separated by schema.
// Everyone else lives in the same table.
// Convoy routes each request to the right place automatically.
ef.UseIsolation(t => t.Plan switch
{
"enterprise" => TenantIsolation.DatabasePerTenant,
"pro" => TenantIsolation.SchemaPerTenant,
_ => TenantIsolation.RowLevel
});
Your DbContext is already pointing at the right database before your first line of business logic runs. No per-query filtering. No WHERE TenantId = ? everywhere. Just inject and use.
Migrate. Across your entire fleet.
Not one database. All of them. In parallel.
var result = await orchestrator.MigrateAllAsync(new MigrationOptions
{
MaxParallelism = 10, // 10 tenant databases, simultaneously
RetryPolicy = RetryPolicy.ExponentialBackoff(maxAttempts: 3),
Progress = new Progress<TenantMigrationProgress>(p =>
Console.WriteLine($"[{p.Completed}/{p.Total}] {p.TenantId}: {p.Status}"))
});
Not sure it's safe? Run a dry run first. Zero writes. Full simulation.
await orchestrator.MigrateAllAsync(new MigrationOptions { DryRun = true });
// See exactly what would happen. Commit nothing.
Not ready to run anything at all? Just validate.
await orchestrator.ValidateAsync();
// Connectivity, migration gaps, schema conflicts — all checked before a single query runs.
Know exactly where every tenant stands, at any moment.
var status = await orchestrator.GetFleetStatusAsync();
// TENANT MIGRATION PENDING
// acme 20250501_AddOrdersTable 0
// globex 20250401_InitialCreate 3 ← behind
// initech 20250501_AddOrdersTable 0
Roll back. Like it never happened.
Something went wrong at tenant 47. Roll it back.
await orchestrator.RollbackTenantAsync("globex", targetMigration: "20250401_InitialCreate");
Something went wrong across the whole fleet. Roll it all back.
await orchestrator.RollbackAllAsync("20250401_InitialCreate", new RollbackOptions
{
DryRun = true, // see what would happen first
MaxParallelism = 5
});
Every run is checkpointed. If something is interrupted, resume from exactly where you left off. Nothing is lost.
Auth. Per tenant. Not per app.
Your enterprise customers use Okta. Your startup customers use Auth0. Your on-prem customers run their own identity server. No problem.
convoy.WithAuth(auth =>
{
auth.UseJwtBearer(jwt =>
{
// Each tenant gets their own issuer, audience, and signing keys.
// Validated correctly on every single request. Automatically.
jwt.WithIssuerResolver(t => t.Settings["auth:issuer"]);
jwt.WithAudienceResolver(t => t.Settings["auth:audience"]);
jwt.WithSigningKeyResolver(async (tenant, token) =>
await jwks.GetSigningKeysAsync(tenant.Settings["auth:jwksUri"]));
});
// Different authorization rules per plan.
auth.WithPolicy("RequiresPro", (policy, tenant) =>
{
if (tenant.Plan == "free") policy.RequireClaim("plan", "pro", "enterprise");
else policy.RequireAuthenticatedUser();
});
});
Implemented cleanly, using IPostConfigureOptions — the right way, not the hack everyone else reaches for.
See everything. Miss nothing.
Every request. Every migration. Every tenant. Observable.
convoy.WithOpenTelemetry();
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddConvoyInstrumentation())
.WithMetrics(m => m.AddConvoyInstrumentation());
Convoy emits spans for every tenant request (convoy.request) and every migration (convoy.migrate), tagged with tenant context. Metrics for request count, resolution latency, migration duration, and active tenant count — all ready for Grafana, Datadog, or whatever you run.
A CLI that feels like a superpower.
dotnet tool install -g Convoy.Cli
# What's the state of your fleet right now?
convoy status
# Don't touch anything yet. Just show me what would happen.
convoy migrate --dry-run
# Alright. Do it. Ten at a time.
convoy migrate --parallel 10
# Just these two. Nothing else.
convoy migrate --tenants acme,globex
# That migration broke something. Put it back.
convoy rollback --tenant acme --target 20250401_InitialCreate
# New customer signed up. Provision them.
convoy tenant add --id newcorp --name "New Corp" --connection-string "..."
# Plug it into CI. Get JSON back. Gate your deploy on it.
convoy status --format json
Configure it once. Use it everywhere.
{ "Url": "https://yourapp.com", "ApiKey": "your-key" }
A REST API for everything else.
Admin dashboards. Ops tooling. Internal scripts. CI/CD pipelines. Whatever you're building, the management API has an endpoint for it.
app.MapConvoyManagementApi<AppTenantInfo>();
GET /convoy/api/tenants → list every tenant
POST /convoy/api/tenants → create one
POST /convoy/api/tenants/{id}/activate → go live
POST /convoy/api/tenants/{id}/deactivate → suspend
GET /convoy/api/migrations/status → fleet snapshot
POST /convoy/api/migrations/validate → pre-flight check
POST /convoy/api/migrations/migrate → trigger a run
POST /convoy/api/migrations/rollback → roll it back
GET /convoy/api/migrations/runs → history
Install what you need. Leave the rest.
| Package | What it does |
|---|---|
Convoy |
Core. Tenant model, DI wiring, in-memory store. |
Convoy.AspNetCore |
Middleware + 7 resolution strategies. |
Convoy.EFCore |
Per-tenant DbContext, 3 isolation models, connection pooling. |
Convoy.Migrations |
The fleet migration engine. Parallel, dry-run, rollback, retry, audit. |
Convoy.Auth |
Per-tenant JWT validation, policies, claims transformation. |
Convoy.HealthChecks |
Health probes for your tenant store. |
Convoy.OpenTelemetry |
Spans and metrics for every tenant operation. |
Convoy.Management.Api |
Self-hosted REST API. |
Convoy.Stores.SqlServer |
SQL Server tenant store. |
Convoy.Stores.Postgres |
PostgreSQL tenant store. |
Convoy.Stores.Sqlite |
SQLite tenant store for dev and tests. |
Convoy.Cli |
The convoy command. |
# The essentials
dotnet add package Convoy
dotnet add package Convoy.AspNetCore
dotnet add package Convoy.EFCore
dotnet add package Convoy.Migrations
dotnet add package Convoy.Stores.SqlServer
# The full suite
dotnet add package Convoy.Auth
dotnet add package Convoy.HealthChecks
dotnet add package Convoy.OpenTelemetry
dotnet add package Convoy.Management.Api
# The CLI
dotnet tool install -g Convoy.Cli
Get started in five minutes.
// Program.cs — the whole thing
builder.Services.AddConvoy<AppTenantInfo>(convoy =>
{
convoy
.WithResolution(r => r.FromSubdomain())
.WithStore(s => s.UseSqlServer(connectionString))
.WithEFCore<AppDbContext>(ef =>
{
ef.UseIsolation(TenantIsolation.DatabasePerTenant);
ef.UseProvider((opt, cs) => opt.UseSqlServer(cs));
})
.WithMigrations()
.WithAuth(auth => auth.UseJwtBearer(jwt =>
{
jwt.WithIssuerResolver(t => t.Settings["auth:issuer"]);
jwt.WithAudienceResolver(t => t.Settings["auth:audience"]);
}))
.WithHealthChecks()
.WithOpenTelemetry();
});
app.UseConvoyTelemetry<AppTenantInfo>();
app.UseConvoy<AppTenantInfo>();
app.UseAuthentication();
app.UseAuthorization();
// Anywhere in your app
public class OrderService(IConvoyContext<AppTenantInfo> convoy, AppDbContext db)
{
public async Task<Order> CreateAsync(CreateOrderRequest req)
{
var tenant = convoy.CurrentTenant;
// tenant.Id, tenant.Plan, tenant.Settings — right here, right now
// db is already pointed at this tenant's database
// No extra work. Just write your feature.
}
}
Built for .NET 8 and .NET 10.
Targets both. Tests pass on both. Ships on both.
Open source. MIT. Always.
git clone https://github.com/priceds/convoy
cd convoy
dotnet build
dotnet test
All tests run without any external dependencies. SQL Server and PostgreSQL integration tests are off by default — set CONVOY_SQLSERVER_TEST_CS or CONVOY_POSTGRES_TEST_CS to turn them on.
Built by Sarvesh Patil · Pune, India
Stop writing foreach loops over your tenants at 2am. Use Convoy.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0 | 97 | 5/9/2026 |
Convoy 1.1.0 adds .NET 10 multi-targeting, tenant resolution, EF Core isolation, fleet migrations, per-tenant auth, health checks, OpenTelemetry, management API, SQL stores, and CLI tooling.