---
title: Resolving Tenant IDs
---

Arc resolves tenant IDs through pluggable strategies. Each request is evaluated by the configured resolver, and the resulting tenant ID becomes the active tenant context for the request lifecycle.

## Built-in resolvers

Code below is configuration fragments for an existing `ArcApplicationBuilder` or `WebApplicationBuilder`; import `Cratis.Arc` and `Cratis.Arc.Tenancy`. Register before `Build()` and activate the resulting app with `UseCratisArc()`. The separate `IHostBuilder.AddCratisArcCore(...)` overload is for generic-host wiring, not these builders.

All resolvers **select**, rather than authorize, a tenant. Validate membership against the authenticated principal and authoritative data before using tenant-scoped storage. Caller-controlled headers, query strings, and hosts are not membership evidence; claim-based selection is only as trustworthy as the authentication that supplied the claim.

### Header Resolver (Default)

Resolves the tenant ID from an HTTP header.

```csharp
builder.AddCratisArc(options =>
{
    options.UseHeaderTenancy("X-Custom-Tenant");
});
```

Default header name: `x-cratis-tenant-id`

### Query Parameter Resolver

Resolves the tenant ID from a query string parameter.

```csharp
builder.AddCratisArc(options =>
{
    options.UseQueryTenancy("tenant");
});
```

Default parameter name: `tenantId`

### Claim Resolver

Resolves the tenant ID from a claim on the authenticated user.

```csharp
builder.AddCratisArc(options =>
{
    options.UseClaimTenancy("tenant_id");
});
```

Default claim type: `tenant_id`

### Subdomain Resolver

Resolves the tenant ID from the subdomain of a configured base domain, and falls back to the configured HTTP header for every host that does not carry one.

```csharp
builder.AddCratisArc(options =>
{
    options.UseSubdomainTenancy("myapp.com", "X-Custom-Tenant");
});
```

A host carries a tenant only when it is **exactly one label in front of the base domain**. A request to `acme.myapp.com` resolves the tenant as `acme`. A request to `myapp.com` falls back to the `X-Custom-Tenant` header. This pattern is useful for SaaS applications where each tenant is routed through its own subdomain.

The number of labels in a host is never used to decide whether it carries a tenant, so the following all fall back to the header:

| Host | Resolved tenant |
| --- | --- |
| `acme.myapp.com` | `acme` |
| `myapp.com` (the base domain itself) | the header |
| `acme.staging.myapp.com` (more than one label) | the header |
| `10.0.0.5`, `[::1]` (IP literals) | the header |
| `otherapp.com` (an unrelated domain) | the header |
| `evil-myapp.com` (ends with the same text, no label boundary) | the header |
| `acme.myapp.com.evil.com` (base domain in the middle) | the header |
| `*.myapp.com`, `user@acme.myapp.com`, `a_b.myapp.com` (not a DNS label) | the header |

Hosts are normalized before matching, so a trailing dot, a port, mixed casing and an internationalized name all resolve the same tenant: `ACME.MyApp.com.`, `acme.myapp.com:5000` and `acme.myapp.com` all resolve `acme`, and `münchen.myapp.com` resolves the punycode label `xn--mnchen-3ya` — the same tenant as `xn--mnchen-3ya.myapp.com`.

Use the base domain to decide what your application's own host is. If the application is served from `www.myapp.com`, configure that as the base domain and `www.myapp.com` falls back to the header while `acme.www.myapp.com` resolves `acme`. With `myapp.com` as the base domain, `www` is an ordinary tenant label like any other — no host name is treated as special.

Default fallback header: `x-cratis-tenant-id`

#### The base domain is required

The base domain is what separates a tenant host from any other host on the internet, so your application refuses to start without a usable one. `UseSubdomainTenancy` throws `BaseDomainIsNotADomainName` when the value is empty, is a single label, is an address literal, or is not made of letter-digit-hyphen labels:

```csharp
options.UseSubdomainTenancy("myapp.com");       // fine
options.UseSubdomainTenancy("X-Tenant-Id");     // throws — a header name is not a domain
options.UseSubdomainTenancy("localhost");       // throws — a single label is not a registrable domain
options.UseSubdomainTenancy("192.168.1.10");    // throws — an address identifies no domain
```

Selecting the subdomain resolver in `appsettings.json` gets the same treatment: Arc validates `Tenancy` while the host is starting, so `"ResolverType": "Subdomain"` with a missing or unusable `BaseDomain` throws the same exception out of host startup, before the application accepts its first request.

Failing to start is deliberate. Without a base domain no host would ever resolve a tenant, and every request would silently take its tenant from the fallback header instead — which any client can set. Failing on the first request instead would mean a real user finds the mistake, on a process that already reported itself healthy.

**Configure the registrable domain your application is served from, and nothing broader.** A bare top-level domain such as `com` is refused because it is a single label, but the check cannot know that `co.uk` is a public suffix: with `co.uk` as the base domain, anyone who registers `evil.co.uk` becomes the tenant `evil`. Pick the domain you actually own.

#### The resolved tenant must be a DNS label

The resolved label can become part of a MongoDB database name or, with optional Chronicle integration, an event-store namespace, so it is required to be a valid letter-digit-hyphen label — up to 63 characters, starting and ending with a letter or digit. Anything else falls back to the header rather than travelling on as a tenant ID.

#### One domain, many spellings

Hosts are matched after IDNA compatibility mapping, which is what lets `münchen.myapp.com` and `xn--mnchen-3ya.myapp.com` be the same tenant. The same mapping means several byte sequences are **the same host**, and all resolve the tenant `admin`:

| Written as | Why it is the same |
| --- | --- |
| `admin.myapp.com` | the canonical spelling |
| `ａdmin.myapp.com` | fullwidth Latin letters map to ASCII |
| `admin.myapp.com.` | a root-anchored name |
| `admin。myapp.com`, `admin．myapp.com`, `admin｡myapp.com` | U+3002, U+FF0E and U+FF61 are label separators |
| `ad<ZWSP>min.myapp.com` | zero width space, soft hyphen, byte order mark and word joiner are ignorable |

This is IDNA working as specified — browsers resolve these the same way — so Arc does not reject them. Be aware of the consequence: **a WAF, ingress or router that matches the literal `Host` string sees a different value than Arc does.** If something upstream makes decisions per tenant host, normalize the host there too, or make the decision from Arc's resolved tenant rather than from the raw header.

#### The fallback header is client-supplied

Every host that does not carry a tenant falls back to `HttpHeader`, and that header arrives on the request unauthenticated — any caller can set it. **Strip the fallback header at your ingress** so only your own infrastructure can set it, exactly as you would for any other trusted request header.

### Fixed Resolver

Resolves every request to one configured tenant ID.

```csharp
builder.AddCratisArc(options =>
{
    options.UseFixedTenancy("acme");
});
```

Default tenant ID: `development`

The tenant ID is returned regardless of the request and regardless of the hosting environment, which makes this the
resolver for a single-tenant deployment: one deployment, one tenant, decided at configuration time rather than per
request. When the relevant integrations are enabled, the resolved tenant drives the Chronicle namespace or Arc MongoDB database. A fixed tenant is a
deployment-wide data-isolation decision — review it before promoting a configuration.

### Development Resolver

The same behavior as the Fixed resolver, under its original name.

```csharp
builder.AddCratisArc(options =>
{
    options.UseDevelopmentTenancy("my-test-tenant");
});
```

Default tenant ID: `development`

Despite the name, this resolver has never consulted `IHostEnvironment` - it returns the configured tenant ID in every
environment, production included. Prefer `UseFixedTenancy` when the fixed tenant is a deployment constant rather than a
local-development convenience; `UseDevelopmentTenancy` remains supported and configures the same tenant ID.
