Ecosystem & integrations
The glue packages that make authz disappear into your stack — context, Inertia, codegen, React, Telescope, and the filter integration.
The core of authz is a small, focused authorization engine. Its real leverage comes from the glue — a set of companion packages that wire authorization into the rest of your stack so you write a rule once and it shows up everywhere: in your guards, in your typed client, in your React components, and in your observability tools.
The guiding principle across the ecosystem: the integration lives on the side that already has the import. You never install a bridge package just to connect two libraries you already use; the consumer reads optionally, the producer carries the data when present, and in most apps you wire nothing at all.
Current user — @dudousxd/nestjs-context
The most important integration ships in the box. When @dudousxd/nestjs-context is present, the gate reads the current user (and tenant) for free — gate.authorize('update', post) needs no explicit user, and permission checks become tenant-scoped via Context.tenantId(). authz consumes the context structurally through the CONTEXT_ACCESSOR token, so it never imports nestjs-context directly.
See Current user for the full story, including the UserRef hydration caveat.
This pairing is what lets your authentication layer and authorization layer stay completely decoupled: your auth guard populates Context.userRef(), and authz reads it. Neither knows about the other.
Inertia — @dudousxd/nestjs-authz-inertia
If you render with Inertia, the authz Inertia integration injects an auth.can map into your shared props — in the spirit of Laravel's HandleInertiaRequests. Your frontend receives precomputed permissions and can branch on them without a round-trip:
// In a React/Vue page, props.auth.can was populated server-side
{auth.can('update', post) && <EditButton post={post} />}The server evaluates the abilities for the current user during the Inertia response, so the client never has to ask "am I allowed?" again for the data it already has.
Codegen — @dudousxd/nestjs-authz-codegen
The codegen integration emits a typed can() into your generated client (api.ts). Because the ability names come from your policies, asking for an ability that doesn't exist becomes a compile-time error rather than a silent false:
api.can('update', post); // ✅ ok
api.can('upadte', post); // ✗ TypeScript error — 'upadte' is not a known abilityThis closes the usual gap where frontend authorization strings drift out of sync with the backend rules they mirror.
React — @dudousxd/nestjs-authz-react
For React apps that aren't using Inertia, @dudousxd/nestjs-authz-react provides a hook and a gating component:
import { useCan, Can } from '@dudousxd/nestjs-authz-react';
function PostActions({ post }) {
const canEdit = useCan('update', post);
return (
<>
{canEdit && <EditButton post={post} />}
<Can ability="delete" of={post}>
<DeleteButton post={post} />
</Can>
</>
);
}<Can ability="..." of={...}> renders its children only when the ability is granted, keeping authorization declarative in your component tree.
Diagnostics & Telescope — @dudousxd/nestjs-diagnostics
Authz emits every authorization decision — the ability, the allow/deny verdict, the reason, and the user — as a standard diagnostics event on the aviary:authz:decision channel, via @dudousxd/nestjs-diagnostics (Node's built-in diagnostics_channel under the hood). It costs essentially nothing when nothing is listening, and the payload rides the standard envelope with traceId auto-filled from nestjs-context.
To see those decisions in Telescope, add the one generic diagnostics watcher — @dudousxd/nestjs-diagnostics-telescope, which records every aviary:<lib>:<event> from any library, with no per-library watcher to wire:
import { TelescopeModule } from '@dudousxd/nestjs-telescope';
import { nestjsDiagnosticsTelescope } from '@dudousxd/nestjs-diagnostics-telescope';
TelescopeModule.forRoot({
extensions: [nestjsDiagnosticsTelescope()],
});Now every unexpected 403 lands in Telescope tagged lib:authz / event:decision, showing exactly which policy method ran and why it denied — authorization failures stop being opaque ("forbidden — but why?").
Typed client — @dudousxd/nestjs-authz-client
The client package provides the runtime that the codegen output builds on — the small surface your frontend calls to evaluate abilities against the backend (for cases where the answer can't be precomputed and shipped with the page).
Filter — query scoping
The filter integration (on the roadmap) lets a policy's viewAny-style ability apply a query scope, so a listing endpoint returns only the records the current user is allowed to see — authorization pushed all the way down into the database query rather than filtered in memory after the fact.
Putting it together
A single ability you write in a PostPolicy.update method can, with the glue in place:
- guard the route via
@Can, - decide whether the Edit button renders (Inertia props or the React
<Can>), - be type-checked in your generated client (codegen),
- and leave an auditable trail when it denies (Telescope).
One rule, enforced consistently from the database to the button. That consistency — not the engine itself — is the point of authz.