TenancyJS
Guides

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.

On this page