Integration path
The smoothest path to connect a real agent with the SDK, MCP, or a local stack.
This page captures the path validated in real testing: create an agent in the app, run a local encrypted runtime, send a message from the browser, then verify that the reply comes back without exposing message contents to the relay.
The simplest path
- Use the hosted app unless you are testing the gateway itself.
- Create an agent in Agents and copy the
.envconfiguration. - Run a small encrypted echo agent with the JavaScript SDK.
- Persist the agent Signal state on disk.
- Send a message from Conversations and confirm that the reply appears.
After this works, replace the echo with your real workflow: Ollama, an MCP server, an internal script, a build worker, an automation, or a custom agent.
Choose the right mode
| Need | Recommended path |
|---|---|
| Integrate an existing agent quickly | JavaScript SDK with @pacerelle/sdk |
| Integrate a Python agent | Python SDK with pacerelle |
| Connect Claude Desktop, Cursor, or Zed | MCP server @pacerelle/mcp-server |
| Test the complete product locally | docker compose from the repo |
| Debug the gateway | Local stack plus PACERELLE_BASE_URL and PACERELLE_WS_URL |
Copy these variables
From the New agent dialog, copy at least:
PACERELLE_AGENT_ID=agent_...
PACERELLE_AGENT_TOKEN=...
For the hosted app, do not set anything else. For a local gateway:
PACERELLE_BASE_URL=http://localhost:8080
PACERELLE_WS_URL=ws://localhost:8080
The agent token is shown once. If it is lost, open the agent in the app, rotate
the token, update .env, and restart the runtime.
Option A: hosted integration
Install the SDK:
npm i @pacerelle/sdk
Create agent.mjs:
import {
createNodeAgentGatewayClient,
createNodeSignalSnapshotStore,
} from "@pacerelle/sdk/node";
const agentId = process.env.PACERELLE_AGENT_ID;
const token = process.env.PACERELLE_AGENT_TOKEN;
if (!agentId || !token) {
throw new Error("PACERELLE_AGENT_ID and PACERELLE_AGENT_TOKEN are required");
}
const signalStore = createNodeSignalSnapshotStore(agentId);
const client = createNodeAgentGatewayClient({
agentId,
token,
baseUrl: process.env.PACERELLE_BASE_URL,
wsUrl: process.env.PACERELLE_WS_URL,
e2ee: true,
signalSnapshot: signalStore.signalSnapshot,
onSignalSnapshot: signalStore.onSignalSnapshot,
onOpen: () => console.log("Pacerelle agent connected"),
onClose: () => console.log("Pacerelle agent disconnected"),
onError: console.error,
});
client.onMessage(async (message, agent) => {
await agent.sendMessage({
conversationId: message.conversationId,
to: message.from,
replyToMessageId: message.id,
text: `Received: ${message.text}`,
});
});
await client.connect();
Run it:
node --env-file=.env agent.mjs
The Node helper keeps the Signal identity under
~/.pacerelle/agents/<agent-id>.signal.b64. Keep that file with the same
PACERELLE_AGENT_ID; changing snapshots for the same agent breaks older
encrypted conversations.
Option B: local stack
In this repo, the complete path starts Postgres, Redis, MinIO, migrations, the gateway, and the web app:
wsl -e sh -lc "cd /mnt/c/Projects/MyAgents && docker compose --env-file .env.dev up -d"
If Postgres or Redis are already used on the machine, keep the app ports on their defaults and move only the database host ports:
wsl -e sh -lc "cd /mnt/c/Projects/MyAgents && POSTGRES_PORT=55432 REDIS_PORT=6380 docker compose --env-file .env.dev up -d"
Then verify:
Invoke-WebRequest http://localhost:8080/healthz -UseBasicParsing
Invoke-WebRequest http://localhost:5173 -UseBasicParsing
The gateway should return 200 on /healthz, and the web app should load at
http://localhost:5173.
To run the gateway directly with Go under WSL, without its development container, start only the dependencies and then use the local script:
wsl -e bash -lc "cd /mnt/c/Projects/MyAgents && POSTGRES_PORT=55432 REDIS_PORT=6380 docker compose --env-file .env.dev up -d db redis db-migrate"
wsl -e bash -lc "cd /mnt/c/Projects/MyAgents && POSTGRES_PORT=55432 REDIS_PORT=6380 bash scripts/start_gateway_for_smoke.sh"
The script loads .env.dev and forces Postgres, Redis, and the test mailer to
stay local. It does not reuse remote connections that may exist in .env.
Move from echo to a real agent
Keep this structure:
- Receive the message in
onMessage. - Read
message.text,message.attachments, ormessage.widgetResponse. - Run your local workflow.
- Reply with the same
conversationId. - Use
message.fromas the recipient.
client.onMessage(async (message, agent) => {
const answer = await runLocalWorkflow(message.text);
await agent.sendMessage({
conversationId: message.conversationId,
to: message.from,
replyToMessageId: message.id,
text: answer,
});
});
Permissions and widgets
For sensitive actions, ask for permission before acting. The flow validated in
real testing is: send the widget, wait for the response, check
granted: true, then run the action with the chosen scope.
client.onMessage(async (message, agent) => {
if (message.widgetResponse?.ref === "permission-web-search") {
const permission = message.widgetResponse.value as { granted?: boolean; scope?: string };
if (!permission.granted) return;
const answer = await runWebSearch({ scope: permission.scope });
await agent.sendMessage({
conversationId: message.conversationId,
to: message.from,
text: answer,
});
return;
}
await agent.sendPermissionWidget({
conversationId: message.conversationId,
to: message.from,
id: "permission-web-search",
title: "Allow web search?",
body: "The agent wants to call an external service before replying.",
scopes: ["once", "session"],
});
});
Validation checklist
- The agent appears online in the app.
- The local process stays open without an authentication error.
onOpenappears in the logs.onMessagereceives the message sent from the browser.- The reply uses the inbound
conversationIdandmessage.from. ~/.pacerelle/agents/<agent-id>.signal.b64exists after the first encrypted message.- Widgets wait for the user response before running the action.
If it gets stuck
- Agent offline: check
PACERELLE_AGENT_ID,PACERELLE_AGENT_TOKEN, and outbound WebSocket access. unauthorized: the token is missing, old, mistyped, or belongs to another agent.- Messages do not decrypt: do not regenerate the Signal identity; reuse the agent's stable snapshot.
- Replies do not appear: check
conversationId,to: message.from, ande2ee: true. - Localhost unavailable: check
http://localhost:8080/healthz, then the web app onhttp://localhost:5173.
See also SDK integration, MCP server, and Security.