Introduction
A shared AsyncLocalStorage context for NestJS that carries user, tenant and traceId across the whole request — and across the ecosystem.
@dudousxd/nestjs-context is a small piece of plumbing with an outsized payoff: a single, process-wide AsyncLocalStorage (ALS) store that carries the current user, the current tenant, and a trace id through every layer of a NestJS request — controllers, services, guards, interceptors, ORM subscribers, and plain functions — without you threading those values through method signatures or reaching for request-scoped providers.
It is deliberately unglamorous. Most of the time you will not call it directly at all. You wire it once, your auth guard drops a userRef into it, and from then on the rest of the ecosystem reads from it for free.
Alpha
This package is published as 0.1.0-alpha.0. The API documented here reflects the current alpha. Pin a version and expect refinements before 1.0.
Quickstart
Install, register the module, set the user once, read it anywhere. For the full setup (tenant, custom fields, non-HTTP entrypoints), see Getting Started.
Install:
pnpm add @dudousxd/nestjs-contextRegister the module — it's global and installs the request middleware (which seeds a traceId) automatically:
import { Module } from '@nestjs/common';
import { ContextModule } from '@dudousxd/nestjs-context';
@Module({ imports: [ContextModule.forRoot()] })
export class AppModule {}Drop the current user in from your auth guard, once you've authenticated the request:
import { Context } from '@dudousxd/nestjs-context';
Context.set('userRef', { type: 'user', id: user.id });
// Context.set('tenantId', user.tenantId); // if you're multi-tenantRead it anywhere downstream — no @Inject(REQUEST), no request-scoped providers, even outside the DI container:
import { Context } from '@dudousxd/nestjs-context';
Context.userRef(); // { type: 'user', id: 42 }
Context.tenantId(); // the active tenant, if set
Context.traceId(); // correlates logs, spans and workflows for this requestWhat problem it solves
In a typical NestJS app, "who is the current user?" and "which tenant are we serving?" are answered by injecting @Inject(REQUEST) and switching the relevant providers to request scope. That works, but it is contagious: once a provider is request-scoped, everything that depends on it becomes request-scoped too, the DI container re-instantiates that subtree on every request, and code that lives outside the container — an ORM subscriber, a queue worker, a logging transport — simply cannot reach the request at all.
nestjs-context sidesteps all of that. The store lives in a module-level singleton backed by ALS, so:
- Any code can read it — injectable or not. ORM subscribers, plain helper functions, and queue handlers all call the same
Context.traceId()/Context.tenantId()/Context.userRef(). - Nothing becomes request-scoped. Your providers stay singletons. The per-request data rides in ALS, not in the container.
- No prop-drilling. You stop passing
currentUserdown through five service calls just so the bottom one can stamp an audit row.
Think of it as the reflect-metadata of the ecosystem: nearly everything depends on it, almost nobody calls it by hand. If you have used Laravel 11's Context facade or Adonis's HttpContext, this is the same idea, expressed for NestJS.
Its role: plumbing the ecosystem consumes
The store carries three things that the rest of the libraries care about, and exposes them through tiny accessors:
| What | Accessor | Who reads it |
|---|---|---|
The acting principal ({ type, id }) | Context.userRef() | audit — stamps the causer of every change |
| The active tenant | Context.tenantId() | filter — scopes queries to the tenant automatically |
| A correlation id for the request | Context.traceId() | telescope / durable — correlate logs, spans and workflows |
Notice the pattern: the context library never decides anything. It does not authenticate, it does not pick a tenant, it does not log. Some other layer resolves those values and sets them; this library is the shared place they live so everyone else can read them. Consumer libraries inject the context through an optional token and degrade cleanly when it is absent — so adding nestjs-context lights them up, and removing it does not break them.
It is worth installing on its own
Even with no other ecosystem library in your project, nestjs-context earns its keep. The standalone pitch is one sentence:
The current user and tenant, anywhere, without
@Inject(REQUEST)and without a single request-scoped provider.
A service buried four calls deep can ask Context.userRef() for the acting principal. A logging interceptor can stamp Context.traceId() on every line. A MikroORM or TypeORM subscriber — which the DI container cannot inject the request into — can still read the current tenant. That alone is reason enough to wire it in.
Where to go next
Getting Started
Install, register ContextModule.forRoot(), read the traceId, and populate the user/tenant from your auth guard.
The Store
The ContextStore shape, why it carries a UserRef and not the full user, and how to add your own typed fields.
Customization
The five levels of customization — from a custom field to swapping the accessor your libraries consume.
Cross-Process
Carry the context across queue and durable boundaries with serialize() / deserialize().
Testing
Run unit tests inside a fake store with runWithContext and enterContext.