npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aventara/next

v1.0.0-rc.12

Published

Next.js App Router host integration for Aventara.

Readme

@aventara/next

Hosts an Aventara Framework in a Next.js App Router application (Next 16.4 or later 16.x, Node runtime). It serves the framework's HTTP protocol (AvProtocol.bind(framework) from @aventara/core/protocol) from one catch-all route file. Every rule of the protocol (routes, decoding, statuses, envelopes, headers, absence) is core's; this package only turns a Web Request into the protocol's request and writes the protocol's answer back as a Web Response.

@aventara/core is its dependency; next (>=16.4 <17) and react (^19) are peers, and @aventara/mcp an optional one (see MCP). The entry is server-only: a Client Component importing it fails the Next.js build.

Setup

// src/aventara/server.ts
import "server-only";
import { Aventara } from "@aventara/next";
import { createPrismaAdapter } from "@aventara/prisma7-adapter";
import { prisma } from "../prisma";
import adapter from "./adapter";
import { aventaraConfig } from "./config";

export const { handlers, avClient, avApp, ready } = Aventara({
  ...aventaraConfig, // api: "/api", resources, policy, ...
  adapter: () => createPrismaAdapter({ ...adapter, client: prisma }),
  state: async ({ headers }) => ({ user: await userFrom(headers) }), // your auth library
});
// src/app/api/[...aventara]/route.ts (the folder matches api: "/api")
import { handlers } from "@/aventara/server";

export const { GET, POST, PUT, PATCH, DELETE } = handlers;

| Option | What it is | |---|---| | everything createFramework takes | api, resources, policy, limits, prebuilt, ... checked exactly as createFramework checks them | | adapter | the adapter, or a function that builds it (it may return a promise), called on the first request | | state | ({ headers, request }) => state: seeds each request's ctx.$state (where policy.principal reads the user) | | access | an access provider (see @aventara/core): its policy checks each request and its state seeds ctx.$state, handed the same { headers, request }. Not together with state or policy |

Or hand it a Framework you built, as createFramework resolved it (as AventaraModule.forRoot({ framework }) takes one in NestJS), with state beside it (or none, when it was built with access):

const framework = await createFramework({ ...aventaraConfig, adapter: createPrismaAdapter({ ...adapter, client }) });
export const { handlers, avClient, avApp, ready } = Aventara({ framework, state });

Every member works on that Framework, which is never built again: avClient.tx and avApp.appTx build handles at once, and ready() checks it (a Resource named like a member, mcp without authorization) and registers the universal path. A Framework and a configuration are two sources of one Framework, so the options carry one or the other: a configuration key beside framework is a type error, and Aventara() refuses it when called, naming the keys. So is a framework that is not one.

Aventara() returns at once and builds nothing: next build imports the route module, so the Framework (and the adapter, when it is a function) is created on first use, or by ready(), once. A configuration createFramework refuses rejects that use with its AvConstructionError.

| Member | What it is | |---|---| | handlers | { GET, POST, PUT, PATCH, DELETE } for the catch-all route file | | avClient | the client layer for Server Components and Server Actions: framework.serverClient, with this request's state; avClient.tx and avClient.transaction([...]) | | avApp | the application layer for trusted server code: framework.application; avApp.appTx and avApp.transaction([...]) | | ready | () => Promise<void>: builds the Framework once; rejects with the construction error; registers the in-process path @/aventara/universal calls on the server |

handlers is typed AvNextHandlers, and avClient.transaction and avApp.transaction AvNextClientTransaction and AvNextAppTransaction, all three exported, so a project that emits declarations (declaration: true) writes export const { handlers, avClient, avApp, ready } = Aventara(...) with no annotation. AvNextStateContext is what state is handed ({ headers, request? }), for a state function written in a file of its own: export const state = async ({ headers }: AvNextStateContext) => ({ user: await userOf(headers) });.

Server Components and Server Actions

// src/app/campaigns/page.tsx
import { Suspense } from "react";
import { avClient } from "@/aventara/server";

async function Campaigns() {
  const campaigns = await avClient.Campaign.find.many({
    where: { startsAt: { gt: new Date() } },
    select: ["id", "name", "startsAt"],
  });
  return <ul>{campaigns.map((c) => <li key={c.id}>{c.name}</li>)}</ul>;
}

export default function Page() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <Campaigns />
    </Suspense>
  );
}

avClient.<Resource>.<family>.<variant>(args, options?) is framework.serverClient: typed from the Client Contract's view (what the client layer hides is absent), taking and answering application values (Date, bigint, AvDecimal, Uint8Array). A call runs in process, with no HTTP: the same client pipelines, validation, exposure and policy as a request to handlers, with the principal policy.principal reads from state. A refusal is thrown as its AvError class (AvAuthError for the policy).

avClient.all({ key: call, … }, { settle?, signal? }) awaits an object of calls together, in process, as the generated client's avClient.all does in the browser, so a component reads alike on both sides:

import { avLazy } from "@aventara/core";

const { campaigns, count } = await avClient.all({
  campaigns: avClient.Campaign.find.many({ select: ["id", "name"] }),
  count: avLazy(() => avClient.Campaign.find.count({})),
});

A call runs when it is made, so all cannot pace it. Wrap a call in avLazy(() => …) to have all start it: at most limits.maxAllConcurrency at once, and never once all has failed or been aborted. @aventara/core's avLazy keeps no answer: each await makes the call again, so one made at module scope never hands one request's answer to another. It takes any number of entries: limits.maxAll bounds POST /_all only. Strict by default: the first failure rejects it (fail-fast, not atomic). With settle: true each key is { status: "fulfilled", value } or { status: "rejected", reason }. Its signal aborts each entry; a call's own signal ({ signal } in its options) aborts only that call. Nothing is aborted for you. A Resource named all is refused when the Framework is built.

state is called with { headers } from next/headers (there is no request outside a route call), once per server render however many calls the render makes. Outside a render, in a Server Action's body for example, it runs once per call. A call that names its own state in its options uses it as given. Without a state option, calls start from {} and read no headers.

Request-time data

A call reads the database, so Next.js renders it at request time, never into the page next build prerenders. Every avClient, avApp and @/aventara/universal call first awaits Next's io() (next/cache). Under Cache Components (cacheComponents: true, which create-next-app sets), a page that calls one needs what any uncached database read needs, one of:

  • <Suspense> around the component that calls it (or a loading.tsx beside the page): the fallback is prerendered and the data streams in at request time, as above;
  • "use cache" on a function that makes the call: the result is cached, and prerendered with the page;
  • export const instant = false in the page: it renders whole, at request time.

With none of them, next build fails with Next's own message, which names the three. In a request, inside "use cache", in a Server Action, in an after() callback and without Cache Components the call runs at once.

With access, avClient.$access is framework.serverClient.$access with this request filled in: can([...]) is answered under the request's state, and me(), signIn(...) and the other verbs the provider offers run Aventara Access in process with the headers of next/headers. In a Server Action, Access sets a sign-in's cookie itself (Better Auth's nextCookies). @/aventara/universal's $access does the same on the server, through what ready() registers, and calls Access's own routes in the browser. $access holds the verbs the provider offers, read from the options before the Framework is built, as serverClient.$access holds them. Calling another one rejects with AvProtocolError A2002, naming why: no access, or a provider whose client does not offer that verb.

On Access's kiosk profile, call switchUser, lock and enrollDevice from a Server Action (or from the browser), never while rendering a Server Component: each rotates the device's credential cookie, which Next lets Access set only in a Server Action or a Route Handler, so a rotation during a render is lost and the device's next switch is refused. Never call a verb that changes anything (signIn, signUp, signOut, resetPassword, and the kiosk's five) inside "use cache": a cached result would replay one caller's answer to another and skip the call. Each verb's answer is projected onto its result schema on the server too, so no session token or device secret reaches your code.

avApp is framework.application: trusted server code. It reads no request, and the policy checks a call only when it names a principal ({ as: principal }).

Transactions keep their layer: avClient.tx builds client-layer handles (framework.serverClientTx) and avClient.transaction([...]) runs them with this request's state; avApp.appTx builds application handles (and avApp.appTx.$sql), and avApp.transaction([...]) runs them. A list that mixes the two is a type error, and refused at runtime (A2004). Handles are built on the built Framework: await ready() before building them.

Startup

// src/instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { ready } = await import("@/aventara/server");
    await ready();
  }
}

ready() builds the Framework and its routes, once: a second call returns the same promise. It rejects with the construction error, so a bad configuration fails next start. It reads no request, so it runs where there is none (instrumentation.ts, a script).

serverExternalPackages (required)

// next.config.ts
const nextConfig: NextConfig = {
  serverExternalPackages: ["@aventara/core", "@aventara/prisma7-adapter"],
};

Next.js bundles a package into each layer of the server (instrumentation, route handlers, Server Components) as a copy of its own. Listed here, @aventara/core and the adapter are loaded by Node.js once per server process instead: one core, so the Framework every layer shares (below) answers each layer with values and errors of the same classes (AvDecimal, AvValidationError, ...), and one Prisma client. aventara new --host next and aventara init add the line (an existing config they cannot edit safely gets it printed instead). @aventara/next itself stays bundled: it relies on Next.js's server aliases (next/navigation, next/headers, server-only).

Next.js evaluates src/aventara/server.ts more than once in one server process (once for instrumentation.ts, again for the routes and pages). Every copy that calls Aventara() with the same options shares one Framework, held per api path for the process, so ready() in instrumentation.ts builds the one the routes use, with one adapter, and avClient.tx works from the first request. Two instances on two api paths hold two. In production, a different configuration on a path already held is refused when Aventara() is called; in development it replaces the held one, as next dev evaluates the file again after an edit. Copies are told apart by the options' data (Next.js rewrites a function's source in each layer). An edit that changes only a function, a guard's or a pipe's body or the adapter factory, is told by each layer from the text that layer saw last: in development the first layer to see it builds the Framework again, and the other layers join that build. A Framework given as { framework } is that call's own.

Once the Framework is built, ready() also registers the path @/aventara/universal's avClient takes on the server (below). Without instrumentation.ts, that avClient refuses its calls on the server in one sentence.

Prefetch and hydrate: @/aventara/universal, avDehydrate, avHydrate

aventara-build --client writes src/aventara/universal.ts. Its avClient has the browser client's types and runs on both sides: in the browser it calls /api; on the server it answers the same request in process, through the bound protocol and with this request's state, so a query prefetched in a Server Component and refetched in the browser gets the same values (Date, bigint, the client's own AvDecimal).

Its calls are the generated client's own on both sides: made when written, aborted by their own signal, and reached by avClient.all's signal and strict abort and by avLazy. In the browser the calls made in one tick are grouped into one /_all. On the server none is grouped: each call is sent on its own, from the async context of the code that made it (its request, unstable_cache's scope, your own), so it runs with that scope's state; in process a group would save no round trip. It exports avLazy beside avClient, for components that run on both sides. In the browser an avLazy makes its call once and keeps the answer. On the server it keeps one answer per request inside a render (React's cache), and none where React keeps no request, such as a route handler or a Server Action, where each await makes the call again.

A decimal is a class instance, which React refuses to pass from a Server Component to a Client Component. avDehydrate (this package, server side) turns each Date, bigint, Uint8Array and decimal into a tagged plain object, and avHydrate (@/aventara/client, browser side) turns them back.

// src/app/notes/notes.queries.ts (server and browser)
import { queryOptions } from "@tanstack/react-query";
import { avClient } from "@/aventara/universal";

export const notesQuery = queryOptions({
  queryKey: ["notes"],
  queryFn: () => avClient.Note.find.many({ select: ["id", "title", "createdAt"] }),
});
// src/app/notes/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
import { Suspense } from "react";
import { avDehydrate } from "@aventara/next";
import { notesQuery } from "./notes.queries";
import { NoteList } from "./note-list";

async function Notes() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery(notesQuery);
  return (
    <HydrationBoundary state={dehydrate(queryClient, { serializeData: avDehydrate })}>
      <NoteList />
    </HydrationBoundary>
  );
}

export default function Page() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <Notes />
    </Suspense>
  );
}
// src/app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { avHydrate } from "@/aventara/client";

const queryClient = new QueryClient({ defaultOptions: { hydrate: { deserializeData: avHydrate } } });
export const Providers = ({ children }: { children: React.ReactNode }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

With SWR, pass avDehydrate({ [key]: data }) from the Server Component and avHydrate(fallback) to SWRConfig's fallback in the Client Component.

Importing @/aventara/client instead of @/aventara/universal in shared code opts out of running on the server: its calls are refused there.

Routes

handlers answers the protocol's routes under api: each operation route (POST /api/_resources/...), GET /api/_contract (with its ETag, and 304 on If-None-Match) and POST /api/_transactions when the ClientContract's transactions are interactive. Every method is exported so that a wrong method on an operation route gets the protocol's 405 (A2012) with Allow. A path the protocol does not own goes to Next's notFound().

A request body is read up to limits.maxRequestBytes decoded bytes; gzip, deflate and br bodies are inflated, and any other content coding is the protocol's 415 (A2011).

MCP

With mcp in the configuration, the same handlers also serve @aventara/mcp's endpoint and its OAuth protected-resource metadata. Add one route file for each path:

// src/aventara/server.ts
export const { handlers } = Aventara({
  ...aventaraConfig,
  mcp: {
    path: "/mcp",
    authorization: {
      servers: ["https://auth.example.com"],
      verify: async (token) => (await verifyAccessToken(token)) ?? null, // your token check
    },
  },
  adapter: () => createPrismaAdapter({ ...adapter, client: prisma }),
});
// src/app/mcp/route.ts
import { handlers } from "@/aventara/server";

export const { GET, POST, DELETE } = handlers;
// src/app/.well-known/oauth-protected-resource/mcp/route.ts
import { handlers } from "@/aventara/server";

export const { GET } = handlers;

Install @aventara/mcp at the same version as @aventara/core (npm install --save-exact @aventara/mcp@<version>). It is an optional peer, loaded only when mcp is configured; when it is missing, the first request fails with a sentence that names the install command.

On Next.js, mcp needs authorization. Without it, @aventara/mcp answers only callers on the same machine that send no x-forwarded-for, and Next.js adds x-forwarded-for to every request, so every caller would be refused. An mcp without authorization is a type error on the Aventara({...}) call, and, for a configuration typed wider than its literal, refused when the Framework is built (the first request), with a sentence that says so.

Everything the endpoint answers is @aventara/mcp's: the bearer check and its 401 with WWW-Authenticate, the tools, the tool calls (each one an agent-origin protocol request, with state and the token's claims in ctx.$state.mcp.claims), the 405 on GET and DELETE (the endpoint is stateless) and the metadata document. The request body is read up to the Agent Contract's maxRequestBytes and handed on as sent.

Status

A prerelease, installed with its tag:

npm install @aventara/next@rc