Operate / Next.js integration

Next.js integration

Keep your agents in the same repository as the Next.js app that uses them. The agents live under foundry/ and run in their own Foundry process; your route handlers call that process through a typed HTTP client, so nothing from the agent graph is bundled into the app.

Add Foundry to an app

terminalbash
cd my-next-app
npx glove-foundry init . --target nextjs

--target nextjs is optional: a directory with a next.config.(js|mjs|ts|cjs) file or a next dependency is detected. --template picks travel-concierge (default; agent concierge) or minimal (agent assistant). Existing files are never overwritten. The command writes:

PathContents
foundry/agents/<agent>/**The template’s agent and its convention files
foundry/foundry.application.tsData adapter, accounts and routes (in-memory to start)
foundry/lib/**Template helpers (the travel concierge’s demo model)
foundry/package.json{ "type": "module", "engines": { "node": ">=22" } }, scoping ESM to the agents
foundry/README.mdHow to run both processes
foundry.config.mtsagentsDir: "foundry/agents", applicationFile: "foundry/foundry.application.ts", port 4141
foundry.env.exampleProvider variables (OPENROUTER_API_KEY, OPENROUTER_MODEL)
lib/foundry.tsThe typed client your app imports
app/api/<agent>/route.tsAn example route handler
.foundry/routes.d.tsGenerated route types; rewritten on every foundry:dev/foundry:start
package.json (merged)Adds foundry:dev (glove foundry dev) and foundry:start (glove foundry start) unless scripts with those names exist, plus the dependencies below
!

Dependencies are set, not merged: glove-foundry, glove-core, effect and zod (plus glove-js, glove-memory and glove-working-environment for the travel concierge) get the exact ranges this Foundry was built against, replacing any range your app already had. An app on zod@3 is moved to zod@4. Review the diff before installing. No tsconfig, ESLint config or .gitignore changes are made; ignore .foundry/ and any data directory yourself.

Why the runtime is a separate process

  • It owns durable state and timers: instances, conversations, schedules and sleeping runs should not restart when a React component hot-reloads.
  • It starts its own execution daemon and one Node process per agent run, which does not fit a route handler’s lifecycle.
  • The app only talks HTTP. glove-foundry/client has no runtime imports (it needs only fetch), and FoundryRoutes is a type-only import erased at build time.
  • Next.js apps are usually CommonJS packages. The nested foundry/package.json makes the agent tree ESM, and the .mts config is ESM whatever the root says, so Foundry’s ESM-only exports load correctly.

Run both in development

terminalbash
cp foundry.env.example .env.local   # or merge into your existing .env.local
pnpm install

pnpm foundry:dev   # terminal 1: Foundry runtime + inspector on http://127.0.0.1:4141
pnpm dev           # terminal 2: Next.js on http://localhost:3000

Both processes read the app root’s .env.local. Foundry also reads .env first and never overwrites a variable that is already set. foundry:dev restarts when a file under foundry/agents/, foundry.config.mts, .env or .env.local changes. It watches the project root non-recursively, so restart it yourself after editing foundry/foundry.application.ts or foundry/lib/. Open the inspector at http://127.0.0.1:4141 to watch runs your routes start (see Inspector & WebChat).

The generated client and route

lib/foundry.tstypescript
/**
 * The Next.js app talks to the Foundry runtime over HTTP, so nothing from
 * the agent graph is bundled into your app. `FoundryRoutes` is a type-only
 * import, erased at build time, and it is regenerated on every dev run — so
 * agent("concierge") is checked against the files that actually exist.
 */
import { createFoundryClient } from "glove-foundry/client";
import type { FoundryRoutes } from "../.foundry/routes.js";

export const foundry = createFoundryClient<FoundryRoutes>({
  baseUrl: process.env.FOUNDRY_URL ?? "http://127.0.0.1:4141",
});


// The agents live in foundry/agents. Start the runtime with
// `foundry:dev`; it serves the inspector at http://127.0.0.1:4141.
app/api/concierge/route.tstypescript
/**
 * POST /api/concierge — send a message to the agent and wait for its result.
 *
 * This runs on the server, so the Foundry runtime never has to be public.
 * For a streaming UI, subscribe to the runtime's /api/events instead of
 * awaiting run.wait().
 */
import { foundry } from "../../../lib/foundry.js";

export async function POST(request: Request) {
  const { message, workspaceId = "default" } = (await request.json()) as {
    message: string;
    workspaceId?: string;
  };

  // A real app reuses one instance per user rather than creating one per call.
  const agent = await foundry.agent("concierge").create({ workspaceId });
  const conversation = await foundry.createConversation(agent.id);
  const run = await foundry.send(agent.id, conversation.id, message);
  const completed = await run.wait();

  return Response.json({ runId: run.id, output: completed.output });
}

foundry.agent(route) only accepts routes that exist in .foundry/routes.d.ts. run.wait() polls every 150 ms, returns once the run is completed, failed or cancelled, and throws after 60 s by default. The agent’s answer is completed.output.value.

One instance and conversation per user

The example creates a new instance and conversation on every request, so nothing is remembered. Give each user a stable instance and reuse its conversation:

lib/support.tstypescript
import { foundry } from "./foundry.js";

/** One durable instance and conversation per user. Cache the ids in your user table. */
export async function supportSessionFor(userId: string) {
  const id = `support-${userId}`;
  const agent =
    (await foundry.agentInstances("support")).find((item) => item.id === id) ??
    (await foundry.agent("support").create({ id, workspaceId: "support", context: { userId } }));
  const [first] = await foundry.conversations(agent.id);
  const conversation = first ?? (await foundry.createConversation(agent.id, { title: "Support" }));
  return { agent, conversation };
}

// your code: resolve the signed-in user from your session or auth library.
export async function currentUserId(request: Request): Promise<string | null> {
  return request.headers.get("x-demo-user");
}
app/api/support/route.tstypescript
import { foundry } from "../../../lib/foundry.js";
import { currentUserId, supportSessionFor } from "../../../lib/support.js";

export async function POST(request: Request) {
  const userId = await currentUserId(request);
  if (!userId) return Response.json({ error: "Sign in first." }, { status: 401 });
  const { message } = (await request.json()) as { message: string };

  const { agent, conversation } = await supportSessionFor(userId);
  const run = await foundry.send(agent.id, conversation.id, message);
  // Stop polling if the browser goes away; the run itself keeps going.
  const settled = await run.wait({ timeoutMs: 120_000, signal: request.signal });

  if (settled.status !== "completed") {
    return Response.json({ runId: run.id, error: settled.error ?? settled.status }, { status: 502 });
  }
  return Response.json({ runId: run.id, reply: settled.output?.value });
}

Multi-turn memory also needs a durable conversation store: the templates’ MemoryStore gives every run an empty history. See Persistence.

Streaming replies

Foundry’s /v1/chat/completions endpoint streams OpenAI-style chunks when stream: true. A route handler can forward that stream as-is:

app/api/support/stream/route.tstypescript
import { currentUserId, supportSessionFor } from "../../../../lib/support.js";

const FOUNDRY_URL = process.env.FOUNDRY_URL ?? "http://127.0.0.1:4141";

export async function POST(request: Request) {
  const userId = await currentUserId(request);
  if (!userId) return Response.json({ error: "Sign in first." }, { status: 401 });
  const { message } = (await request.json()) as { message: string };
  const { agent } = await supportSessionFor(userId);

  const upstream = await fetch(`${FOUNDRY_URL}/v1/chat/completions`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      ...(process.env.FOUNDRY_API_TOKEN ? { authorization: `Bearer ${process.env.FOUNDRY_API_TOKEN}` } : {}),
    },
    body: JSON.stringify({
      model: agent.id, // an instance id
      stream: true,
      user: userId, // same user → same durable conversation on that instance
      messages: [{ role: "user", content: message }],
    }),
    signal: request.signal,
  });
  if (!upstream.ok || !upstream.body) {
    return new Response(await upstream.text(), { status: upstream.status });
  }
  return new Response(upstream.body, {
    headers: {
      "content-type": "text/event-stream; charset=utf-8",
      "cache-control": "no-cache, no-transform",
      "x-foundry-conversation-id": upstream.headers.get("x-foundry-conversation-id") ?? "",
    },
  });
}
lib/stream-reply.tstypescript
/** Browser helper: read the OpenAI-style chunks your stream route forwards. */
export async function streamReply(message: string, onText: (text: string) => void) {
  const response = await fetch("/api/support/stream", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ message }),
  });
  if (!response.ok || !response.body) throw new Error(await response.text());
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) return;
    buffer += value;
    const events = buffer.split("\n\n");
    buffer = events.pop() ?? "";
    for (const event of events) {
      const data = event.replace(/^data: /, "");
      if (data === "[DONE]") return;
      const chunk = JSON.parse(data) as { choices: Array<{ delta: { content?: string } }> };
      const text = chunk.choices[0]?.delta.content;
      if (text) onText(text);
    }
  }
}
  • Text arrives as it is generated when the agent’s model adapter streams (for example createAdapter({ …, stream: true })); otherwise the final answer arrives as one chunk. A failed run ends the stream with a Foundry error: … chunk.
  • model is an instance id, or a definition id when exactly one instance of it exists. Chat Completions conversations are keyed by user (or conversation_id / the x-foundry-conversation-id header) into an openai-… conversation on that instance, separate from conversations you create with createConversation. See OpenAI-compatible API.
  • To stream a native run instead, call foundry.send() and forward GET /v1/runs/:id/events with Accept: text/event-stream; that stream ends when the run does and carries agent.text_delta events (see Client & HTTP API).

Production: FOUNDRY_URL and credentials

Deploy the Foundry runtime as its own service (see the container recipe), then set FOUNDRY_URL in the Next.js server environment. Keep it server-only: never prefix it with NEXT_PUBLIC_, and never call Foundry from the browser. When the runtime binds a non-loopback address it requires requestAuthorization, so send credentials from the client:

lib/foundry.tstypescript
import { createFoundryClient } from "glove-foundry/client";
import type { FoundryRoutes } from "../.foundry/routes.js";

export const foundry = createFoundryClient<FoundryRoutes>({
  baseUrl: process.env.FOUNDRY_URL ?? "http://127.0.0.1:4141",
  // Only needed when the runtime enforces requestAuthorization.
  ...(process.env.FOUNDRY_API_TOKEN
    ? {
        authorization: {
          identifier: "next-server",
          headers: () => ({ authorization: `Bearer ${process.env.FOUNDRY_API_TOKEN}` }),
        },
      }
    : {}),
});

The headers function runs on every request, so a rotated token is picked up without a restart. If your host caps route-handler duration, return the runId right away and poll foundry.getRun(runId) or stream instead of waiting on run.wait().

Foundry behind Next.js vs glove-next alone

glove-next’s createChatHandler is a streaming model proxy for glove-react: the agent loop, its tools and its display stack run in the browser, and the route only forwards model calls. With Foundry, the agent runs on your server as a durable instance and the route is a thin client.

ChooseWhen
glove-react + createChatHandlerA copilot that lives in one page, with tools that render React UI or act on client state, and no server-side history or background work.
Foundry behind Next.jsAgents that must remember users across sessions, run on schedules or sleep, receive webhooks or messages through transmissions, ask for approvals, use server-only credentials, or be inspected and steered by operators.

One app can use both for different features. New to Foundry? Start with the tutorial.