# Tenants

Resolve which tenant owns a request, from single-tenant to header-based SaaS.

`tenant` answers "which customer owns this request" without tenant parsing in every handler. `Resolve` maps request metadata to a tenant ID, and `Scoped` carries it on the context. It resolves ownership but is not an access gate: pair it with [Authorization](https://zenta-dev.github.io/zever/guides/security/authz-permissions/) for allow and deny.

## Quick start

`single` (the default) gives every request one hardcoded ID (`id`, default `"default"`). Stay there until a second customer exists.

```go title="main.go"
import (
    "context"
    "github.com/zenta-dev/zever/config"
    "github.com/zenta-dev/zever/container"
)

c := container.New(config.Default())
t, err := c.Tenant()
if err != nil {
    panic(err)
}
defer t.Close()

ctx := context.Background()
id, err := t.Resolve(ctx, map[string]string{"tenant": ""})
fmt.Println(id, err)
scoped, err := t.Scoped(ctx, id)
fmt.Println(scoped != nil, err)
```

Expected output:

```text
default <nil>
true <nil>
```

## Resolve tenants from a header or subdomain

Use `header` for multi-tenant SaaS APIs where a gateway or client sets a per-call header (default `X-Tenant-ID`, `DefaultHeader`). With `X-Tenant-ID: acme` metadata, `Resolve` returns `acme`. The header wins; otherwise `subdomain_regex` matches the first capture group against the lowercased `Host` metadata. With neither, `Resolve` returns `tenant.ErrNotFound`. Meta keys match case-insensitively, and patterns prone to catastrophic backtracking are rejected at `Open`. Strip or overwrite the tenant header at your edge, since the adapter trusts it.

```sh
TENANT_ADAPTER=header
TENANT_HEADER=X-Tenant-ID
TENANT_SUBDOMAIN_REGEX=^(?P<tenant>[a-z0-9-]+)\.example\.com$
```

```sh
TENANT_ADAPTER=header
TENANT_HEADER=X-Tenant-ID
TENANT_SUBDOMAINREGEX=^([a-z0-9-]+)\.example\.com$
TENANT_ID=default
```

`Validate` rejects an invalid `header` key (printable ASCII, no whitespace, colon, or DEL) and any `subdomain_regex` over 500 characters (`MaxRegexLength`, a ReDoS bound). An empty `header` means the default. Adapters are `single` (one fixed ID, constant-time lookup) and `header` (request header plus optional subdomain regex). See the [tenant config reference](https://zenta-dev.github.io/zever/reference/config/#tenant).

## Production checklist

- Resolve once at the middleware edge and propagate through the `tenant/context.go` helpers. Never re-derive deeper, where an unvalidated value could slip in.
- Fail closed on an unknown tenant. Mapping it to a default turns one typo into a cross-tenant data leak.
- Both adapters are stateless, so replicas agree when they share config. `Tenant.Close() error` takes no context.
- Tenant IDs are customer data. Document the header name and validation rule for API clients, since it is public contract. Never log raw option maps.

## Troubleshooting

<details>
<summary>Every request lands on `default` after switching to `header`</summary>

The gateway is not sending the configured header, or the app reads a different name. Confirm `TENANT_HEADER` matches what the client sends (`X-Tenant-ID` vs `X-Tenant`) and that proxies forward it.
</details>

<details>
<summary>`header must be a valid header key`</summary>

The name contains whitespace, a colon, or non-ASCII characters. Fix the name. Empty is allowed (it means the default), garbage is not.
</details>

<details>
<summary>`subdomain_regex exceeds max length`</summary>

The pattern is over 500 characters. Shorten it, or split matching into a regex plus code.
</details>

## Related

- [Feature flags](https://zenta-dev.github.io/zever/guides/operate/flags/): Per-tenant targeting signals.
- [Rate limiting](https://zenta-dev.github.io/zever/guides/security/ratelimit/): Per-tenant buckets.
- [Observability](https://zenta-dev.github.io/zever/guides/operate/observability/): Tenant as a span attribute.
