Pacerelle Docs

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

  1. Use the hosted app unless you are testing the gateway itself.
  2. Create an agent in Agents and copy the .env configuration.
  3. Run a small encrypted echo agent with the JavaScript SDK.
  4. Persist the agent Signal state on disk.
  5. 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

NeedRecommended path
Integrate an existing agent quicklyJavaScript SDK with @pacerelle/sdk
Integrate a Python agentPython SDK with pacerelle
Connect Claude Desktop, Cursor, or ZedMCP server @pacerelle/mcp-server
Test the complete product locallydocker compose from the repo
Debug the gatewayLocal 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:

  1. Receive the message in onMessage.
  2. Read message.text, message.attachments, or message.widgetResponse.
  3. Run your local workflow.
  4. Reply with the same conversationId.
  5. Use message.from as 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.
  • onOpen appears in the logs.
  • onMessage receives the message sent from the browser.
  • The reply uses the inbound conversationId and message.from.
  • ~/.pacerelle/agents/<agent-id>.signal.b64 exists 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, and e2ee: true.
  • Localhost unavailable: check http://localhost:8080/healthz, then the web app on http://localhost:5173.

See also SDK integration, MCP server, and Security.