Agora

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/queue worker 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/context

The 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-tenant

Read 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 request

What 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 currentUser down 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:

WhatAccessorWho reads it
The acting principal ({ type, id })Context.userRef()authz — the causer of every change
The active tenantContext.tenantId()filter — scopes queries to the tenant automatically
A correlation id for the requestContext.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

On this page