3.9 KiB
middleware
(no submodules)
Purpose
The middleware module ships createTenantMiddleware(options) — the
Hono middleware that resolves the inbound tenant, applies the status
gate, and writes the result under the single canonical
c.set("tenant", tenant) key. suaveplan deliberately collapses
upstream's three keys (tenantId, tenant, tenantContext) into
one; the helper accessors live in context.ts and read from the
same key. The module also augments Hono's ContextVariableMap so
c.get("tenant") returns a typed Tenant with no cast.
Status gating raises typed TenantError values: deleted → TENANT_NOT_FOUND, suspended → TENANT_SUSPENDED (unless
allowSuspended), pending → TENANT_PENDING. A throw from the
underlying resolver is wrapped as TENANT_RESOLUTION_FAILED with the
original error preserved on .cause. An optional onError(error, c)
hook lets callers map errors to bespoke responses without losing the
typed contract.
Features
- Single canonical context key —
c.set("tenant", tenant)— typed via theContextVariableMapaugmentation. - Status gate covers all four statuses;
allowSuspendedopt-in lets read-only paths serve suspended tenants. - Excluded paths (default
/health, /ready, /docs, /openapi) bypass the resolver entirely so liveness probes never block on database health. onResolved(tenant, c)callback fires after the gate passes — use it to attach OTel attributes or per-tenant logger context.onError(error, c)mapsTenantErrorto aResponse; non-tenant errors re-throw so the framework's global handler runs.getTenant(c)andgetTenantId(c)typed helpers — read the resolved tenant withoutas Tenantcasts.
Usage: Basic
import {
chainResolvers,
createTenantMiddleware,
headerResolver,
} from "@suaveplan/api-tenant";
import { Hono } from "hono";
const app = new Hono();
app.use(
"*",
createTenantMiddleware({
resolver: headerResolver("x-tenant-id", (id) =>
db.tenants.get(id)
),
})
);
app.get("/me", (c) => c.json({ tenant: c.get("tenant") }));
Usage: Advanced
Composing a cached chained resolver with an error mapper:
import {
chainResolvers,
createTenantMiddleware,
headerResolver,
subdomainResolver,
TenantCache,
TenantError,
} from "@suaveplan/api-tenant";
const resolver = TenantCache.wrap(
chainResolvers(
headerResolver("x-tenant-id", lookupById),
subdomainResolver("suaveplan.io", lookupBySlug)
),
{ ttlMs: 30_000 }
);
app.use(
"*",
createTenantMiddleware({
resolver,
allowSuspended: false,
excludePaths: ["/health", "/metrics"],
onError: (error: TenantError, c) =>
c.json({ code: error.code, message: error.message }, 404),
onResolved: (tenant, c) => {
c.header("x-tenant-id", tenant.id);
},
})
);
API Reference
createTenantMiddleware(options): MiddlewareHandler.interface TenantMiddlewareOptions—resolver,required?,excludePaths?,allowSuspended?,onError?,onResolved?.getTenant(c): Tenant | undefined.getTenantId(c): string | undefined.- Module augmentation —
declare module "hono" { interface ContextVariableMap { tenant: Tenant } }.
Implementation Notes
Status checks raise BEFORE c.set so a suspended tenant never lands
under c.get("tenant") — downstream handlers cannot accidentally
serve suspended data. Resolver throws are caught and re-wrapped as
TENANT_RESOLUTION_FAILED instead of bubbling up as a raw Error
because the framework's global error handler benefits from the
discriminated code field. The tenant: Tenant ambient declaration
is reachable from anywhere that imports any value from
@suaveplan/api-tenant, which is enforced by the side-effect import
in the package root barrel.