Tenants
Provision and manage tenants — the top-level isolation boundary — with quotas, metadata, feature flags, and soft-delete lifecycle.
A tenant is Ductor's top-level isolation boundary. Every pool, recipient, rule, connection, workflow definition, and run belongs to exactly one tenant, and that identity flows through storage queries, cache keys, and capacity counters so isolation is a structural property, not a filter you might forget (see Tenancy). Managing tenants is therefore the first thing you do on a fresh deployment and the boundary within which every other resource in this section is created.
What a tenant carries
A tenant is more than an ID. Each one bundles three things you manage over its lifetime:
- Quotas — hard ceilings on how much routing topology and traffic the tenant may consume.
- Metadata — arbitrary string key/value pairs for your own bookkeeping (billing account, region, owner).
- Feature flags — a list of capability names that gate optional behavior for
this tenant (
advanced_routing,webhooks, …).
Where it lives
Tenants are served by TenantService and persisted to the tenants table in
Postgres. The application aggregate is at
application/tenant/service.go; quota and config defaults are applied there
when you omit them at creation.
Why and when you manage tenants
You create a tenant whenever you onboard a new customer, environment, or
isolated workload. You update one to raise a quota as a customer grows, to flip
on a feature they've purchased, or to suspend an account. Because a tenant is
the isolation boundary, these operations are admin-gated: every mutating
tenant RPC requires the admin role.
The tenant object
Prop
Type
Quotas
| Quota field | Default | Governs |
|---|---|---|
max_pools | 10 | Number of pools the tenant may create. |
max_recipients_per_pool | 100 | Recipients in any one pool. |
max_rules_per_pool | 50 | Rules attached to any one pool. |
daily_routes_limit | 10000 | Routing decisions per rolling 24h window. |
The daily route counter resets automatically 24 hours after it was last reset.
When a tenant hits a quota, the offending operation fails with
429/ErrQuotaExceeded rather than silently dropping work — consistent with
Ductor's fail-visibly principle.
Provision a tenant
CreateTenant — POST /api/tenants — requires tenant:write and the admin
role. Omit quotas to inherit system defaults.
curl -s -X POST https://api.ductor.io/api/tenants \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "acme-production",
"quotas": {
"max_pools": 50,
"max_recipients_per_pool": 500,
"max_rules_per_pool": 100,
"daily_routes_limit": 5000000
},
"metadata": { "billing_account": "acct_123", "region": "us-east" },
"features": ["advanced_routing", "webhooks"]
}'{
"data": {
"id": "9c8b7a6d-1234-4e5f-8a9b-0c1d2e3f4a5b",
"name": "acme-production",
"status": "active",
"quotas": { "max_pools": 50, "max_recipients_per_pool": 500, "max_rules_per_pool": 100, "daily_routes_limit": 5000000 },
"metadata": { "billing_account": "acct_123", "region": "us-east" },
"features": ["advanced_routing", "webhooks"],
"created_at": "2026-07-11T10:30:00Z",
"updated_at": "2026-07-11T10:30:00Z"
},
"metadata": { "trace_id": "trace-...", "request_id": "req-...", "fetched_at": "2026-07-11T10:30:00Z" }
}New tenants default to status: active. The returned id is what you put in
the X-Tenant-ID header for every subsequent call under this tenant.
Read tenants
GetTenant — GET /api/tenants/{tenant_id} (tenant:read):
curl -s https://api.ductor.io/api/tenants/$DUCTOR_TENANT \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN"ListTenants — GET /api/tenants (tenant:read, admin role) — paginates with
plain limit/offset (default limit 50, max 1000):
curl -s "https://api.ductor.io/api/tenants?limit=100&offset=0" \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN"The response carries data (the tenants), count, and metadata.
Update a tenant
UpdateTenant — PATCH /api/tenants/{tenant_id} (tenant:write, admin). It's
a partial update, but note two replace-not-merge behaviors:
quotas— only the sub-fields you supply are modified.metadata— replaces the entire map. To add one key, send the full map.features— replaces the entire list. To add one feature, send them all.
# Raise the daily route ceiling and suspend the tenant, leaving other quotas intact.
curl -s -X PATCH https://api.ductor.io/api/tenants/$DUCTOR_TENANT \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "suspended", "quotas": { "daily_routes_limit": 10000000 } }'Metadata and features are whole-value writes
Because metadata and features overwrite entirely, always read-modify-write:
GET the tenant, mutate the map/list client-side, then PATCH the complete
value. Sending a partial map silently drops the keys you left out.
Check a quota before you act
CheckQuota — GET /api/tenants/{tenant_id}/quota/{operation} (tenant:read) —
tells you whether an operation is currently allowed and how much headroom
remains, without performing it. Operations include pools, recipients, and
daily_routes.
curl -s https://api.ductor.io/api/tenants/$DUCTOR_TENANT/quota/daily_routes \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN"{ "allowed": true, "current_usage": 41234, "limit": 5000000,
"metadata": { "trace_id": "trace-...", "request_id": "req-...", "fetched_at": "2026-07-11T10:31:00Z" } }Retire a tenant
DeleteTenant — DELETE /api/tenants/{tenant_id} (tenant:delete, admin) — is
a soft delete. The tenant's status moves to deleted and its rows are
retained for audit rather than dropped; the row stops serving as an active
boundary.
curl -s -X DELETE https://api.ductor.io/api/tenants/$DUCTOR_TENANT \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN"Defaults applied on create
When you omit quotas (or the deeper tenant config), Ductor fills in system
defaults at creation time. These are the effective starting ceilings and
capabilities for a bare CreateTenant:
| Setting | Default |
|---|---|
| Status | active |
| Default selection strategy | smooth_weighted_round_robin |
| Max pools | 100 |
| Max recipients per pool | 1000 |
| Max rules per pool | 100 |
| Requests per second | 1000 |
| Requests per day | 1,000,000 |
| Enabled features | basic_routing, rules, queuing |
The proto-level TenantQuotas you set on CreateTenant (the four fields in
the table above) and the richer internal tenant config (which also carries the
default strategy and rate limits) are related but not identical shapes. Set
what you need explicitly at create time; anything omitted takes the system
default.
Per-tenant configuration and routing weights
Two adjacent surfaces refine a tenant without going through TenantService:
- Per-tenant config overrides —
TenantConfigServiceat/api/tenants/{tenant_id}/config, including a resolved view that merges system defaults + tenant + pool overrides. See Configuration. - Routing weights —
TenantWeightsServicetunes how a tenant's traffic is weighted in selection. See Strategies.
Where to go next
Managing Resources
The Ductor management API surface — how to provision and operate tenants, keys, pools, rules, workflows, experiments, connections, and configuration over REST and Connect-RPC.
API Keys
Mint, list, and revoke tenant-scoped API keys — roles, per-key scopes, expiry, and environment constraints — the operational workflow.