Resolving tenants
Turn an incoming request into a tenant - from subdomains, headers, paths, or your store.
Every integration takes a resolver: a function that turns a request into a tenant. This guide covers the common patterns and how to fail closed correctly.
The shape
A resolver returns the tenant (or enough to identify it). The simplest form reads an identifier straight off the request:
resolver: (req) => ({ id: req.subdomains.at(-1) ?? "" });If it can't produce a valid tenant, the integration responds with the right status and your handler never runs.
Common sources
// acme.example.com → "acme"
resolver: (req) => ({ id: req.subdomains.at(-1) ?? "" });resolver: (req) => ({ id: req.header("x-tenant") ?? "" });// /t/acme/orders → "acme"
resolver: (req) => ({ id: req.path.split("/")[2] ?? "" });// look the domain up in your store
resolver: async (req) => {
const tenant = await store.findByDomain(req.hostname);
return tenant ?? null; // null → fails closed (404)
};Looking up and validating
For anything beyond reading a string, resolve against your store - and let unknown or suspended tenants fail closed:
resolver: async (req) => {
const slug = req.subdomains.at(-1);
if (!slug) return null; // 400 - no identifier
const tenant = await store.find(slug);
if (!tenant || tenant.status === "suspended") return null; // 404
return tenant;
};Consistent failure semantics
tenancyjs-identifiers gives you composable resolvers and a single, consistent mapping from resolution
outcome to HTTP status - 400 for a missing/invalid identifier, 404 for not-found/suspended - so
every framework behaves the same way. Reach for it instead of hand-rolling status codes.
A resolver should never "guess" a tenant or fall back to a default on failure. Returning null (or
throwing) is the safe outcome - it keeps the request from ever running unscoped.