# @suaveplan/api-tenant - Multi-tenant resolution + scoping layer for the Suaveplan API. Ships a canonical Tenant type with role/permission helpers, six resolver strategies plus ChainedTenantResolver, a pluggable TenantCache with TTL + negative-entry cache + request coalescing, fail-closed NullTenantContext, a single-context-key Hono middleware (Hono ContextVariableMap augmentation), requirePlugin/requireFeature fail-closed guards, pluggable tenantRateLimit with bounded MemoryRateLimitStore (LRU eviction), TenantAwarePluginDescriptor with tenantScopes opt-out, and opt-in AsyncLocalStorage threading. Server-tier — runs in Bun and Node. Multi-tenant resolution + scoping for the Suaveplan API. Server-tier — runs on Bun and Node, depends on `hono` and `node:async_hooks`. ## Documentation tree - [cache](./src/cache/cache.md) — The `cache` module wraps any `TenantResolver` with a bounded, TTL-aware in-process cache that prevents lookup stampedes… - [cache](./src/cache/cache.md) — The `cache` module wraps any `TenantResolver` with a bounded, TTL-aware in-process cache that prevents lookup stampedes… - [context](./src/context/context.md) — The `context` module ships `createTenantContext(tenant)` — a pure factory that wraps a resolved `Tenant` in an ergonomi… - [context](./src/context/context.md) — The `context` module ships `createTenantContext(tenant)` — a pure factory that wraps a resolved `Tenant` in an ergonomi… - [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module defines the seven stable `TENANT.*` failure codes shipped by `@suaveplan/api-tenant` and the c… - [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module defines the seven stable `TENANT.*` failure codes shipped by `@suaveplan/api-tenant` and the c… - [lifecycle](./src/lifecycle/lifecycle.md) — The `lifecycle` module ships `TenantLifecycle`, a thin dispatcher that wires the seven tenant lifecycle hooks (`onTenan… - [lifecycle](./src/lifecycle/lifecycle.md) — The `lifecycle` module ships `TenantLifecycle`, a thin dispatcher that wires the seven tenant lifecycle hooks (`onTenan… - [middleware](./src/middleware/middleware.md) — The `middleware` module ships `createTenantMiddleware(options)` — the Hono middleware that resolves the inbound tenant,… - [middleware](./src/middleware/middleware.md) — The `middleware` module ships `createTenantMiddleware(options)` — the Hono middleware that resolves the inbound tenant,… - [require-feature](./src/middleware/require-feature.md) — `requireFeature(name)` returns a Hono middleware that gates route access on the boolean at `tenant.settings.features?.[… - [require-plugin](./src/middleware/require-plugin.md) — `requirePlugin(name)` returns a Hono middleware that gates route access on `tenant.enabledPlugins.includes(name)`. The… - [rate-limit](./src/rate-limit/rate-limit.md) — The `rate-limit` module ships `tenantRateLimit(options)` — a Hono middleware that throttles requests per resolved tenan… - [rate-limit](./src/rate-limit/rate-limit.md) — The `rate-limit` module ships `tenantRateLimit(options)` — a Hono middleware that throttles requests per resolved tenan… - [resolver](./src/resolver/resolver.md) — The `resolver` module ships the six built-in `TenantResolver` implementations consumed by `createTenantMiddleware` plus… - [resolver](./src/resolver/resolver.md) — The `resolver` module ships the six built-in `TenantResolver` implementations consumed by `createTenantMiddleware` plus… - [storage](./src/storage/storage.md) — The `storage` module ships the opt-in `AsyncLocalStorage` thread for the resolved tenant. defaults the thread O… - [storage](./src/storage/storage.md) — The `storage` module ships the opt-in `AsyncLocalStorage` thread for the resolved tenant. defaults the thread O… - [types](./src/types/tenant.md) — The `tenant` module ships the single canonical `Tenant` interface consumed by every other module in `@suaveplan/api-ten… - [tenant](./src/types/tenant.md) — The `tenant` module ships the single canonical `Tenant` interface consumed by every other module in `@suaveplan/api-ten… ## What you get - `Tenant` — one canonical entity. `c.set("tenant", tenant)` is the only context key the middleware writes; the Hono `ContextVariableMap` augmentation gives `c.get("tenant")` a typed `Tenant` with no cast. - Six resolver strategies — `HeaderTenantResolver`, `SubdomainTenantResolver`, `DomainTenantResolver`, `PathTenantResolver`, `JWTTenantResolver`, `ChainedTenantResolver`. A `TimeoutTenantResolver` wraps any resolver with a millisecond timeout that surfaces as `TenantError(TENANT_LOOKUP_TIMEOUT)`. - `TenantCache.wrap(resolver, options)` — TTL + negative-entry cache - in-flight request coalescing + LRU eviction at `maxEntries`. - `createTenantMiddleware({ resolver, required?, excludePaths?, allowSuspended?, onError?, onResolved? })` — Hono middleware that applies the status gate (`deleted → TENANT_NOT_FOUND`, `suspended → TENANT_SUSPENDED`, `pending → TENANT_PENDING`). - `requirePlugin(name)` / `requireFeature(name)` — fail-closed guards. No tenant resolved → `TENANT_NOT_FOUND`. Missing entitlement → `TENANT_INVALID_INPUT`. - `tenantRateLimit({ maxRequests, windowMs, store? })` with the pluggable `RateLimitStore` interface; default `MemoryRateLimitStore` is bounded by `maxEntries` with LRU eviction. - `TenantAwarePluginDescriptor` + `TenantLifecycle` — seven tenant lifecycle hooks dispatched through the `HookExecutor` shared with `@suaveplan/api-plugin`. `tenantScopes: "all" | "explicit" | "none"` controls how the events fan out. - Opt-in `AsyncLocalStorage` thread via `withTenantStorage(true)` + `tenantStorageMiddleware()`; default OFF so `getCurrentTenant()` is `undefined` and cannot disagree with `c.get("tenant")`. - `NullTenantContext` — fail-closed sentinel for tenant-less code paths. Every predicate returns `false` / `undefined`. - 7 stable `TENANT.*` error codes via `defineCodes`. ## Quick-start ```ts import { Hono } from "hono"; import { chainResolvers, createTenantMiddleware, headerResolver, requirePlugin, subdomainResolver, tenantRateLimit, TenantCache, TenantError, } from "@suaveplan/api-tenant"; const app = new Hono(); const resolver = TenantCache.wrap( chainResolvers( headerResolver("x-tenant-id", (id) => db.tenants.byId(id)), subdomainResolver("suaveplan.io", (slug) => db.tenants.bySlug(slug) ) ), { ttlMs: 30_000, negativeTtlMs: 5_000 } ); app.use("*", createTenantMiddleware({ resolver })); app.use("*", tenantRateLimit({ maxRequests: 60, windowMs: 60_000 })); app.get("/payments", requirePlugin("payment"), (c) => c.json({ tenant: c.get("tenant").id }) ); app.onError((err, c) => { if (err instanceof TenantError) { return c.json( { code: err.code, message: err.message }, err.code === "TENANT.NOT_FOUND" ? 404 : 403 ); } return c.text("Server error", 500); }); ``` ## Cache configuration ```ts import { TenantCache, headerResolver } from "@suaveplan/api-tenant"; const cached = TenantCache.wrap( headerResolver("x-tenant-id", lookup), { ttlMs: 60_000, negativeTtlMs: 5_000, maxEntries: 10_000, keyFn: (c) => c.req.header("x-tenant-id") ?? "", clock: () => Date.now(), } ); ``` ## Pluggable rate-limit store ```ts import { type RateLimitStore, tenantRateLimit, } from "@suaveplan/api-tenant"; const redisStore: RateLimitStore = { async get(key) { const raw = await redis.get(key); return raw ? JSON.parse(raw) : undefined; }, async set(key, value) { await redis.set(key, JSON.stringify(value), "PX", value.resetAt); }, async delete(key) { await redis.del(key); }, }; app.use("*", tenantRateLimit({ maxRequests: 1000, windowMs: 60_000, store: redisStore, keyPrefix: "rl:v1", })); ``` ## Opt-in AsyncLocalStorage ```ts import { getCurrentTenant, tenantStorageMiddleware, withTenantStorage, } from "@suaveplan/api-tenant"; withTenantStorage(true); app.use("*", tenantStorageMiddleware()); async function deep() { const tenant = getCurrentTenant(); return tenant?.id ?? "anonymous"; } ``` When `withTenantStorage` is `false` (the default), `getCurrentTenant()` always returns `undefined`. This is deliberate — it eliminates the upstream dual-access-path footgun where ALS state could diverge from `c.get("tenant")`. ## Error codes `TENANT_NOT_FOUND`, `TENANT_SUSPENDED`, `TENANT_PENDING`, `TENANT_RESOLUTION_FAILED`, `TENANT_RATE_LIMITED`, `TENANT_INVALID_INPUT`, `TENANT_LOOKUP_TIMEOUT`. All thrown as `TenantError extends SuaveplanError`. ## License MIT