Context
A module-level AsyncLocalStorage for AdonisJS that carries user, tenant and traceId across every boundary — HTTP, queue, durable and ace.
@adonis-agora/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 your app — controllers, services, middleware, Lucid model hooks, queue workers, durable steps, and plain functions — without you threading those values through method signatures.
It is deliberately unglamorous. Most of the time you will not call it directly at all. You wire it once with node ace configure, your auth layer drops a userRef into it, and from then on the rest of the Agora ecosystem reads from it for free.
Alpha
The API is still settling: pin the version you install and expect refinements before the package reaches 1.0.
Why not just HttpContext?
AdonisJS already ships an ALS-backed HttpContext — enable useAsyncLocalStorage and HttpContext.getOrFail() reaches the current request from anywhere. So why a second store?
Because HttpContext only lives inside an HTTP request. HttpContext.getOrFail() throws the moment you step outside one — and the whole point of this library is the work that happens outside the request:
- a
@adonisjs/queueworker processing a job someone enqueued an hour ago, - a durable workflow step running in another process (or another language),
- an ace command run from cron,
- a Lucid model hook firing during a seeder.
@adonis-agora/context is the thin, serializable layer that crosses the boundaries HttpContext cannot. It runs identically inside and outside HTTP, and — crucially — it is read from a module-level singleton, so code that the IoC container never touches can still reach it.
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's Context facade, this is the same idea, expressed for AdonisJS — but built to travel across processes.
Quickstart
Install, configure, set the user once, read it anywhere. For the full setup (tenant, custom fields, non-HTTP entrypoints), see Getting Started.
Install and configure — the codemod registers the provider, plugs the middleware onto the server stack, and publishes config/context.ts:
node ace add @adonis-agora/contextThe middleware now seeds a fresh context (with a traceId) on every request automatically. Drop the current user in from your auth layer once you've authenticated the request:
import { Context } from '@adonis-agora/context'
Context.set('userRef', { type: 'user', id: user.id })
// Context.set('tenantId', user.tenantId) // if you're multi-tenantRead it anywhere downstream — in a service, a Lucid hook, an event listener, even outside the container:
import { Context } from '@adonis-agora/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 AdonisJS app, "who is the current user?" and "which tenant are we serving?" are answered either by passing ctx.auth.user down through every call, or by reaching for HttpContext.getOrFail(). The first is prop-drilling; the second only works inside the request and throws everywhere else.
@adonis-agora/context sidesteps both:
- Any code can read it — injectable or not. Lucid model hooks, plain helper functions, and queue handlers all call the same
Context.traceId()/Context.tenantId()/Context.userRef(). - No prop-drilling. You stop passing
currentUserdown through five service calls just so the bottom one can stamp an audit row. - It crosses processes. Serialize the context onto a queue job; re-hydrate it in the worker. The worker knows who enqueued the work and which tenant it belongs to.
Its role: plumbing the ecosystem consumes
The store carries a handful of values the rest of the libraries care about, exposed through tiny accessors:
| What | Accessor | Who reads it |
|---|---|---|
The acting principal ({ type, id }) | Context.userRef() | authz — 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() | diagnostics / 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.
With diagnostics that reading is automatic: install both packages and every agora:* event any ecosystem library emits arrives stamped with the current request's trace id, without a line of code in your app — see Getting Started.
Consumer libraries don't even import @adonis-agora/context — they read a read-only accessor that this package publishes on a globalThis slot at import time, and degrade cleanly when it is absent. Adding @adonis-agora/context lights them up; removing it does not break them. See Customization → swapping the accessor.
It is worth installing on its own
Even with no other ecosystem library in your project, @adonis-agora/context earns its keep. The standalone pitch is one sentence:
The current user, tenant and trace id, anywhere — including queue workers and ace commands — without prop-drilling and without
HttpContext.getOrFail()throwing on you.
A service buried four calls deep can ask Context.userRef() for the acting principal. A logging hook can stamp Context.traceId() on every line. A Lucid @beforeSave hook — which has no access to the request — can still read the current tenant. A queue worker can read the user who enqueued the job. That alone is reason enough to wire it in.
Where to go next
Getting Started
Configure the package, read the traceId, and populate the user/tenant from your auth layer.
The Store
The ContextStore shape, why it carries a UserRef and not the full user, and how to add your own typed fields.
Cross-Process
Carry the context across queue and durable boundaries with serialize() / deserialize() and W3C baggage.
Customization
The five levels of customization — from a custom field to swapping the accessor your libraries consume.
Testing
Run unit tests inside a fake store with runWithContext and enterContext.