A minimal LLM chat app built on
@appx-org/agent-client and
agent-server, using Next.js 15 (App Router).
It exists for two reasons:
- A runnable example of consuming agent-client from Next.js — streaming chat, sessions, tool-call cards, and the provider-credential settings panel.
- A reference for the Next.js consumer requirements (see
agent-client#3): the
package ships TypeScript source, so a Next consumer needs
transpilePackages, the GitHub Packages scope registry, and React deduping.
- Talks to agent-server through a same-origin proxy route
(
src/app/api/agent/[...path]/route.ts). The browser only sees/api/agent/...; the agent-server origin and bearer token stay server-side. - Ensures a
defaultproject exists, then renders the batteries-includedAgentChat(session sidebar + streaming chat + model controls). - Includes the
AgentSettingspanel (the Settings button) so you can add an LLM provider API key, then chat.
You need agent-server running first.
# 1. start agent-server (in ../../agent-server)
cd ../../agent-server
npm install && npm run build
WORKSPACE_DIR=/tmp/agent-ws npm start # → http://127.0.0.1:4001
# 2. start this app
cd ../apps/next-app-example
cp .env.example .env # defaults point at http://127.0.0.1:4001
npm install
npm run dev # → http://localhost:3000Open http://localhost:3000, click Settings, add a provider API key (or set
ANTHROPIC_API_KEY when launching agent-server), then go Back to chat,
create a session, and start chatting.
| Env var | Default | Notes |
|---|---|---|
AGENT_SERVER_URL |
http://127.0.0.1:4001 |
Origin the proxy forwards to. |
AGENT_SERVER_TOKEN |
(empty) | Bearer token, if agent-server runs with AGENT_SERVER_TOKEN. Injected server-side; never exposed to the browser. |
These are the gotchas this example encodes (issue #3).
agent-client's exports map points at src/ (raw .ts/.tsx), so Next must
transpile it like first-party code:
// next.config.ts
const nextConfig: NextConfig = {
transpilePackages: ["@appx-org/agent-client"],
};Every agent-client component uses hooks, so mount them under a client component.
Here, app/page.tsx (server) renders app/Chat.tsx which starts with
'use client'. Global CSS (import '@appx-org/agent-client/styles.css') is
imported in app/layout.tsx, the only place the App Router allows it.
The published package lives in GitHub Packages under the @appx-org scope. Add
the scope registry to .npmrc:
@appx-org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
and swap the dependency from the local link to a version range:
${GITHUB_TOKEN} is expanded from the environment at install time, so the token
is never committed.
Do not bake the token into an image layer (it stays in history). Use a
BuildKit secret mount so it exists only for the npm ci step:
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json .npmrc ./
RUN --mount=type=secret,id=github_token \
GITHUB_TOKEN="$(cat /run/secrets/github_token)" npm cidocker build --secret id=github_token,env=GITHUB_TOKEN .In GitHub Actions, npm can read secrets.GITHUB_TOKEN (which has read access
to packages in the same org) via the same GITHUB_TOKEN env var.
This example links the package locally (file:../../agent-client) for live
editing. A local install materialises the package's own node_modules/react
(React is a peer dep, but file: installs still pull the devDependency), and
two React copies break hooks ("invalid hook call").
The package README's resolve.dedupe tip is Vite-specific and doesn't apply
to Next. Instead, alias React to this app's single copy — scoped to the client
bundle so Next's server/RSC React internals are left alone:
// next.config.ts
webpack(config, { isServer }) {
if (!isServer) {
config.resolve.alias = {
...config.resolve.alias,
react: path.resolve(process.cwd(), "node_modules/react"),
"react-dom": path.resolve(process.cwd(), "node_modules/react-dom"),
};
}
return config;
}When installing the published package (not a file: link), React resolves
once from the app as a normal peer dep, so this alias is harmless but
unnecessary.
src/app/
├── layout.tsx # imports agent-client styles.css + globals.css
├── globals.css # page shell layout
├── page.tsx # server component → <Chat/>
├── Chat.tsx # 'use client' — ensures project, renders AgentChat/AgentSettings
└── api/agent/[...path]/route.ts # same-origin proxy → agent-server /v1 (token stays server-side)