Skip to main content

Embedding Atomic in a web server

This guide puts an Atomic agent behind an HTTP endpoint, using a Next.js App Router project as the example. The same pattern works in any long-running Node.js server: keep one session per conversation, start a prompt per request, and stream the session’s events back as newline-delimited JSON (NDJSON). Read the SDK page first for createAgentSession() and session events. The examples use createTranscript(), createAgentSessionAdapter(), and RunOpts.onStageSessionEvent, which require an Atomic release newer than 0.9.28-alpha.1. Check the changelog for your installed version.

Install

Atomic needs Node.js 22.19 or newer and does not require dependency install scripts. Bun blocks the install scripts of some dependencies, such as @embedded-postgres/<platform>, protobufjs, and @google/genai, and reports them after bun add @bastani/atomic. You don’t need to add them to trustedDependencies in package.json: Atomic prepares the embedded Postgres runtime itself the first time it starts it, and it doesn’t depend on the other scripts.

Keep Atomic out of the server bundle

Atomic finds its bundled resources and native modules relative to its installed package, so it must load from node_modules rather than from Next.js’s server bundle. List it in serverExternalPackages (Next.js 15 and later):
Atomic’s own dependencies load from node_modules once @bastani/atomic is external. The other entries cover packages your app may import directly, such as typebox for workflow schemas, so your code and Atomic share one copy. Add any other Atomic dependency you import yourself. Every route that imports Atomic must use the Node.js runtime:
Import Atomic only from server code. Client components may use import type from @bastani/atomic, which adds nothing to the browser bundle.

Keep one session per conversation

An AgentSession holds the conversation’s history, so create one per conversation and reuse it across requests. Keep sessions on globalThis: in next dev, module-level variables reset on every hot reload, but globalThis survives, so open conversations don’t vanish and old sessions aren’t leaked. Restart the dev server after changing session options; existing sessions keep the options they were created with.
SessionManager.inMemory() keeps history in process memory. To keep conversations across restarts, pass a file-backed SessionManager instead; see Sessions. Call closeConversation() when a conversation ends or has been idle for a while, because each open session holds resources until it is disposed. Sessions on globalThis live in one server process. Run this on a long-lived Node.js server, and route each conversation to the same instance if you run several. Short-lived serverless functions lose the session between requests.

Stream a prompt

The route below starts one prompt and streams the assistant’s output as NDJSON. createTranscript() turns session events into ordered, JSON-serializable parts (text, thinking, and tool calls joined to their results), so each line carries the complete output so far and the client simply replaces what it shows.
A few details matter here:
  • One prompt at a time. session.prompt() rejects while the session is already streaming. The route answers 409 instead. busy is set synchronously, so two requests that arrive together can’t both start a prompt. To queue a message instead of rejecting it, call session.prompt(message, { streamingBehavior: "followUp" }).
  • Stop on disconnect. When the browser disconnects or aborts its fetch, request.signal aborts and the stream is cancelled. Both call session.abort(), which stops the current turn and its tools but keeps the session usable.
  • Payload size. Each line repeats the whole output so far. For long answers, send lines on a timer instead of on every event, or send only the parts that changed.
createTranscript() reports a tool result’s text by default. To send a tool’s structured details instead, pass createTranscript({ toolResult: (toolName) => (toolName === "todo" ? "details" : "content") }).

Read the stream in the browser

Render each part by type: text and thinking are strings, and a toolCall part has name, arguments, and, once the tool reports progress or finishes, a result with content, isError, and isPartial. Pass an AbortController’s signal to stop the agent from the page.

Run a workflow from a route

run() from @bastani/atomic/workflows runs a workflow definition in the server process. Each stage runs as an in-process Atomic session, so run() needs no adapters. Use createAgentSessionAdapter() when every stage should share session options, such as the same tool restrictions as your chat sessions:
onStageSessionEvent receives every stage’s session events tagged with its run and stage ids, including stages that switch to a fallback model and stages of nested workflows. Aborting signal, here when the client disconnects, cancels the run.

Choose workflow durability

By default, run() makes workflow state durable by starting a managed local Postgres, so a run can resume after the process exits. Request-scoped servers rarely want that:
  • durability: { mode: "memory" } keeps state in memory for this call only. No Postgres starts, and the run can’t resume after the process exits. Use it when the request owns the run.
  • durability: { mode: "durable", systemDatabaseUrl } stores state in a Postgres database you run, such as a hosted one, and Atomic doesn’t start its own. Use it in deployments that need runs to survive restarts. DBOS_SYSTEM_DATABASE_URL, when set, overrides the URL.
One process uses one workflow database. See Choosing the durable backend for the full rules, and Workflows for writing workflow definitions.

Secure the endpoint

A route that drives an agent session lets whoever calls it run tools on your server with the server process’s permissions. Atomic has no built-in sandbox.
  • Require authentication on every route that creates sessions, sends prompts, or starts workflows, and scope conversation ids to the authenticated user, as getConversation() does above.
  • Allow only the tools you need. tools is an allowlist; the examples allow only the read-only read, find, and search. Leave out bash, edit, and write unless the agent must change files or run commands. excludedTools removes tools, and noTools: "all" exposes none. ask_user_question needs a person to answer, so leave it out of headless sessions. See Tools.
  • Disable builtins you don’t use. builtins turns off the shipped workflows, subagents, MCP, web access, and Intercom packages, along with their tools.
  • Choose the working directory deliberately. cwd sets the starting directory for tool paths and controls which project settings, context files, and extensions Atomic discovers. It does not restrict file access: tools can use absolute paths and paths outside cwd. Point it at a directory you control, never at an upload or a user-supplied path.
  • Isolate the process in a container, VM, or restricted OS account when callers must not access other server files, even with read-only tools, or when the agent can write files or run commands. See Containerization.
  • Keep credentials on the server. Provider keys come from the server’s ModelRuntime. Never send them to the browser or accept them from requests.

Next steps