Skip to content

@stratum-hq/hono

@stratum-hq/hono provides Hono middleware that extracts tenant identity from requests and sets up AsyncLocalStorage context for downstream handlers.

npm install @stratum-hq/hono @stratum-hq/sdk @stratum-hq/core
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.

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 }));

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 }));
  • 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 runWithTenantContext for 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.