Managing Resources

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 fieldDefaultGoverns
max_pools10Number of pools the tenant may create.
max_recipients_per_pool100Recipients in any one pool.
max_rules_per_pool50Rules attached to any one pool.
daily_routes_limit10000Routing 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

CreateTenantPOST /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

GetTenantGET /api/tenants/{tenant_id} (tenant:read):

curl -s https://api.ductor.io/api/tenants/$DUCTOR_TENANT \
  -H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN"

ListTenantsGET /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

UpdateTenantPATCH /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.
  • metadatareplaces the entire map. To add one key, send the full map.
  • featuresreplaces 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

CheckQuotaGET /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

DeleteTenantDELETE /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:

SettingDefault
Statusactive
Default selection strategysmooth_weighted_round_robin
Max pools100
Max recipients per pool1000
Max rules per pool100
Requests per second1000
Requests per day1,000,000
Enabled featuresbasic_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 overridesTenantConfigService at /api/tenants/{tenant_id}/config, including a resolved view that merges system defaults + tenant + pool overrides. See Configuration.
  • Routing weightsTenantWeightsService tunes how a tenant's traffic is weighted in selection. See Strategies.

Where to go next