BUILD AN AGENT / WEB APPS

Put an agent behind your web app.

nah-js connects the NAH runtime to browser clients. It provides Web Request/Response handlers, an SSE event stream, resumable approvals, and an optional React hook—without requiring a particular web framework.

Install the web runtime

Install nah-js for the shorter import path, the core runtime peer, and the AI SDK provider you use. The browser hook is a separate nah-js/client entry point; React is an optional peer for applications that use it.

bashnah
npm install nah-js not-another-harness ai zod @ai-sdk/anthropic

Next.js App Router

Create the handler once in a route module and export its GET and POST methods. Keep provider credentials and tool implementations on the server.

tsnah
// app/api/nah/route.ts
import { createNahHandler } from "nah-js";
import { anthropic } from "@ai-sdk/anthropic";
import { after } from "next/server";

export const runtime = "nodejs";
export const maxDuration = 300;

const handlers = createNahHandler({
  agent: async ({ sessionId }) => ({
    model: anthropic("claude-sonnet-4-5"),
    system: "You are a helpful assistant. Use tools when needed.",
    tools: toolsForSession(sessionId),
    maxSteps: 24,
  }),
  session: databaseSessionStore,
  events: databaseEventLog,
  after,
});

async function guard(request: Request) {
  // App code authenticates the user and checks session ownership.
  return authorizeNahRequest(request);
}

export async function GET(request: Request) {
  if (!(await guard(request))) return new Response("Forbidden", { status: 403 });
  return handlers.GET(request);
}
export async function POST(request: Request) {
  if (!(await guard(request))) return new Response("Forbidden", { status: 403 });
  return handlers.POST(request);
}

The handler’s agent factory runs once per turn after the transcript is loaded. It receives sessionId, messages, and the request signal. To pass authenticated identity into the factory, resolve it in requestContext after your route authenticates the request. If you want a registry of named agents, tools, and workflows mounted as one API, use the NAH server adapter.

The route exports runtime and maxDuration because deployment settings belong to the application. Pass Next.js after when post-turn bookkeeping should use the framework’s after-response hook.

Expose a capability API

createNahHandler gives you one chat endpoint to place in a route. createNahServer is for apps that want to register multiple named agents, directly callable tools, and workflows behind a shared API prefix. It returns a single Web request handler for your framework to mount; it does not start a listener or own deployment.

tsnah
import { createNahServer } from "nah-js";

const nah = createNahServer({
  agents: { support: supportAgentOptions },
  tools: { lookupOrder: lookupOrderEndpoint },
  workflows: { refundReview },
  authenticate: async (request) => {
    const user = await getUserFromRequest(request);
    if (!user) return new Response("Unauthorized", { status: 401 });
    return { userId: user.id, tenantId: user.tenantId };
  },
});

// Mount in Hono. Next.js can export the same handler from a catch-all route.
app.all("/api/*", (context) => nah.handle(context.req.raw));

The default routes are /api/agents/:id, /api/tools/:id/execute, and /api/workflows/:id/runs. Agent endpoints stream NAH events; tool and workflow endpoints return JSON. See the server adapter guide for route payloads, context handling, OpenAPI, and storage limits.

Connect a React client

In a client component, call useNahChat from the client entry point. The hook tracks messages, stream status, approval requests, and suspended tools; your app controls the UI.

tsxnah
"use client";
import { useNahChat } from "nah-js/client";

export function Chat({ sessionId }: { sessionId: string }) {
  const chat = useNahChat({ api: "/api/nah", sessionId });

  return (
    <section>
      {chat.messages.map((message) => (
        <p key={message.id}><b>{message.role}:</b> {message.text}</p>
      ))}
      {chat.pendingApprovals.map((item) => (
        <button key={item.approvalId}
          onClick={() => chat.approve(item.approvalId, true)}>
          Approve {item.toolName}
        </button>
      ))}
      {chat.pendingSuspensions.map((item) => (
        <form key={item.toolCallId}
          onSubmit={(event) => {
            event.preventDefault();
            chat.answer(item.toolCallId, new FormData(event.currentTarget).get("answer"));
          }}>
          <input name="answer" aria-label="Your answer" />
          <button>Continue</button>
        </form>
      ))}
      <button onClick={() => chat.send("Summarize my workspace")}>Send</button>
      <button onClick={chat.stop}>Stop</button>
    </section>
  );
}

Pass a stable, server-issued sessionId to continue the same conversation across mounts. Without one, the hook creates an ID for that mount, which is useful for prototypes. The hook sends approvals and answers as separate requests from new user messages because a paused call must be resumed before another turn begins.

Use other Web frameworks

createNahHandler returns standard request handlers. Mount them wherever your framework accepts a Web Request and returns a Web Response.

tsnah
// Hono
import { Hono } from "hono";
import { createNahHandler } from "nah-js";

const nah = createNahHandler({ agent });
const app = new Hono();
app.all("/api/nah", (context) => {
  const handler = context.req.method === "GET" ? nah.GET : nah.POST;
  return handler(context.req.raw);
});
tsnah
// Remix resource route (loader/action)
import { createNahHandler } from "nah-js";

const nah = createNahHandler({ agent });
export const loader = ({ request }: { request: Request }) => nah.GET(request);
export const action = ({ request }: { request: Request }) => nah.POST(request);
tsnah
// SvelteKit or Astro endpoint
import { createNahHandler } from "nah-js";

const nah = createNahHandler({ agent });
export const GET = ({ request }: { request: Request }) => nah.GET(request);
export const POST = ({ request }: { request: Request }) => nah.POST(request);

The same handlers also fit a Cloudflare Worker, Deno, Bun, or Node server that speaks the Web API. Add the framework’s authentication, runtime, and deployment configuration in its route module.

Use durable session and event stores

By default, sessions and reconnect events live in process memory. That works for local development or one long-lived server. In serverless and multi-instance deployments, inject shared stores so another instance can load the conversation and replay missed events.

tsnah
import type { NahSessionStore, NahEventLog } from "nah-js";

const session: NahSessionStore = {
  load: (id) => db.loadMessages(id),
  append: (id, messages) => db.appendMessages(id, messages),
  replace: (id, messages) => db.replaceMessages(id, messages),
};

const events: NahEventLog = {
  append: (frame) => db.appendFrame(frame),
  read: (id, afterSeq) => db.readFrames(id, afterSeq),
};

const handlers = createNahHandler({ agent, session, events });

replace matters when context compaction rewrites the transcript. The runtime persists the update before finishing the response. Session records and event frames are separate stores because their retention needs can differ.

Routes and reconnects

MethodPathPurpose
POST/api/nahStart a turn, answer an approval or suspended tool, steer, or abort.
GET/api/nah?sessionId=…&after=…Replay missed frames, then follow the live event stream.

Every frame has a sequence number. A client reconnects with the last sequence it received; the GET handler sends missed frames before following new ones. The React hook consumes the POST stream directly, while custom clients can use parseEventStream or the exported SSE helpers.