Files
suaveplan/packages/api-stack/api/api-tenant/src/storage/storage.md
2026-05-27 23:29:53 +01:00

3.2 KiB

storage

(no submodules)

Purpose

The storage module ships the opt-in AsyncLocalStorage thread for the resolved tenant. defaults the thread OFF — the canonical access path is c.get("tenant") — and exposes withTenantStorage(true) plus the matching tenantStorageMiddleware() for applications that legitimately need to read the tenant from deep async code without threading the Hono Context through every function signature.

This eliminates the upstream dual-access-path footgun where c.get("tenant") and getCurrentTenant() could disagree. With ALS off, getCurrentTenant() always returns undefined, so any code that "accidentally" relied on the thread surfaces immediately rather than silently picking up stale state from a previous request.

Features

  • Opt-in via withTenantStorage(true) — defaults to false.
  • tenantStorageMiddleware() — pass-through when ALS off, wraps next() in tenantStorage.run(tenant, ...) when ALS on.
  • getCurrentTenant() / getCurrentTenantId() — read the threaded value, undefined when off or outside a scope.
  • runWithTenant(tenant, fn) — explicit scope helper for background jobs spawned outside a request lifecycle.
  • isTenantStorageEnabled() — inspect the toggle (primarily for tests).
  • node:async_hooks import is bare so bundlers route it to the Node runtime; safe on Bun (which provides a compatible implementation).

Usage: Basic

import {
    createTenantMiddleware,
    headerResolver,
    tenantStorageMiddleware,
    withTenantStorage,
    getCurrentTenant,
} from "@suaveplan/api-tenant";

withTenantStorage(true);
app.use(
    "*",
    createTenantMiddleware({
        resolver: headerResolver("x-tenant-id", lookup),
    })
);
app.use("*", tenantStorageMiddleware());

async function deepCall(): Promise<string> {
    return getCurrentTenant()?.id ?? "anonymous";
}

Usage: Advanced

Background job that spawns from a queue worker and needs the tenant context inside an inner async boundary:

import { runWithTenant, withTenantStorage } from "@suaveplan/api-tenant";

withTenantStorage(true);

async function processJob(job: { tenant: Tenant; payload: unknown }) {
    return runWithTenant(job.tenant, async () => {
        await runUserDefinedHandler(job.payload);
    });
}

API Reference

  • withTenantStorage(enabled: boolean): void — toggle.
  • tenantStorageMiddleware(): MiddlewareHandler.
  • getCurrentTenant(): Tenant | undefined.
  • getCurrentTenantId(): string | undefined.
  • runWithTenant<T>(tenant: Tenant, fn: () => T): T.
  • isTenantStorageEnabled(): boolean.

Implementation Notes

The toggle is a module-level boolean rather than a per-instance flag because AsyncLocalStorage itself is a process-wide construct — two parallel enabled=true regions would conflict regardless of how the flag is stored. The middleware short-circuits in three places (ALS disabled, no tenant resolved, or non-Hono caller) so the cost when disabled is two cheap reads plus the call to next(). The runWithTenant helper is the one piece that runs fn synchronously when ALS is off, keeping the call signature stable across both modes.