Aviary

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 ability

This 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.

On this page