@stratum-hq/hono
@stratum-hq/hono provides Hono middleware that extracts tenant identity from requests and sets up AsyncLocalStorage context for downstream handlers.
Installation
Section titled “Installation”npm install @stratum-hq/hono @stratum-hq/sdk @stratum-hq/coreQuick start
Section titled “Quick start”import { Hono } from "hono";import { jwt } from "hono/jwt";import { stratumMiddleware } from "@stratum-hq/hono";import { StratumClient, getTenantContext } from "@stratum-hq/sdk";
const app = new Hono();
// A client for the control plane, used by `resolve` below. A `read` key is// enough to resolve tenant contexts.const sdkClient = new StratumClient({ controlPlaneUrl: process.env.STRATUM_URL ?? "http://localhost:3001", apiKey: process.env.STRATUM_API_KEY!,});
// The tenant binding comes from a signed token, so the server does not start// without the key that verifies it.const jwtSecret = process.env.JWT_SECRET;if (!jwtSecret) { throw new Error("JWT_SECRET is required to verify bearer tokens.");}
// hono/jwt rejects a missing or invalid bearer token with 401.// It stores the verified claims as jwtPayload.app.use("*", jwt({ secret: jwtSecret, alg: "HS256" }));
app.use("*", stratumMiddleware({ // Read the tenant from the verified `tenant_id` claim jwtClaim: "tenant_id", // Optional: resolve full tenant context resolve: async (tenantId) => sdkClient.resolveTenant(tenantId),}));
app.get("/users", async (c) => { const tenantId = c.get("tenantId"); // set by the middleware const ctx = getTenantContext(); // full context via AsyncLocalStorage // ...});stratumMiddleware does not verify a token. The jwtClaim option reads the jwtPayload that Hono’s JWT middleware sets. Therefore, register jwt() before stratumMiddleware.
Read the tenant from a path parameter
Section titled “Read the tenant from a path parameter”pathParam reads a URL path parameter, for example pathParam: "tenantId" for a route /tenants/:tenantId/.... The path is client-controlled like a header: any client can put another tenant’s ID in it. Use pathParam only when your application separately authorizes the caller for that tenant, for example with a later middleware that compares it with a claim of the verified token. For this reason, path parameter mode requires trustPathParam: true; without it, stratumMiddleware throws at construction. When jwtClaim is also set, the JWT claim is the tenant source and trustPathParam is not needed.
// Your application authorizes the caller for the tenant in the path.app.use("/tenants/:tenantId/*", stratumMiddleware({ pathParam: "tenantId", trustPathParam: true }));Read the tenant from a header
Section titled “Read the tenant from a header”Without jwtClaim or pathParam, the middleware reads the x-tenant-id header (or the one named by header). Any client can send that header and select another tenant, so this mode requires trustTenantHeader: true; without it, stratumMiddleware throws at construction.
Set trustTenantHeader: true only when all of these are true:
- A gateway that you control sets the header.
- That gateway removes any copy of the header that the client sent.
- Clients cannot reach this server except through that gateway.
// Only the gateway you control sets x-tenant-id. It removes any client copy.app.use("*", stratumMiddleware({ header: "x-tenant-id", trustTenantHeader: true }));Features
Section titled “Features”- Tenant extraction: from a verified JWT claim, a URL path parameter, or a header that a trusted gateway sets
- ALS context: sets tenant context via
runWithTenantContextfor downstream handlers - Optional resolve callback: fetch full tenant context from the control plane
- Lightweight: no heavy dependencies, structural types only
If resolve rejects with a tenant error from @stratum-hq/core, the middleware answers the request itself: TenantNotFoundError gets 404 TENANT_NOT_FOUND, TenantSuspendedError gets 403 TENANT_SUSPENDED, TenantArchivedError gets 410 TENANT_ARCHIVED, and ForbiddenError gets 403 FORBIDDEN. StratumClient.resolveTenant throws these errors.
The middleware also answers two control plane failures. A control plane timeout gets 504 CONTROL_PLANE_TIMEOUT. An UnauthorizedError for the SDK’s own API key gets 500 CONTROL_PLANE_AUTH_FAILED and a console.error line, because the fault is in the server configuration and not in the request. Other errors go to the Hono error handler.