Host it yourself
Self-hostingRun MCP servers and messaging agents on your own Node infrastructure.
Flow runs in your Node process. Host an MCP server behind HTTPS, or run a messaging worker with a durable database. You choose the model, messaging provider and observability system.
Before you start
Section titled “Before you start”Use Node 22.18 or later and a project from the quickstart. Install its locked
dependencies with npm ci. Keep agent/, src/ and the package files in the deployed application;
TypeScript source runs directly on Node.
Start a production process
Section titled “Start a production process”The starter’s npm run mcp binds to loopback for local development. Add this entry point to
listen on the container or server’s network interface:
import { createServer } from "node:http";import { mcp } from "./mcp.ts";
const port = Number(process.env.PORT ?? 8080);createServer((req, res) => { if (req.url === "/health") { res.writeHead(200).end("ok"); return; } void mcp.node(req, res);}).listen(port, "0.0.0.0");Start it with:
node --env-file-if-exists=.env src/serve-mcp.tsPut a TLS reverse proxy or load balancer in front of the process. Preserve the MCP paths and
OAuth discovery routes, and allow streaming responses. Connect remote hosts to
https://your-domain.example/mcp; plugins use /mcp/plugin.
Add the MCP authentication configuration before exposing account data. A public listener is not an authentication boundary.
This entry point reuses your buildAgent and receives messages through Flow Messaging’s
outbound stream. It needs FLOW_MESSAGING_KEY, configured senders and a model key. It does
not need a public webhook URL.
import { mkdirSync } from "node:fs";import { createServer } from "node:http";import { sqlite } from "@flow-engineer/flow";import { flowMessaging } from "@flow-engineer/flow/flow-messaging";import { buildAgent } from "./agent.ts";
mkdirSync("data", { recursive: true });const fm = flowMessaging();const agent = buildAgent({ host: { database: sqlite("data/agent.db"), messaging: fm, },});await agent.resume();
let streamHealthy = true;const live = fm.stream(agent, { cursorFile: "data/flow-messaging-cursor",});live.stopped.then((why) => { console.error("Messaging stream stopped:", why.code, why.type); streamHealthy = false;});
createServer((req, res) => { if (req.url !== "/health") { res.writeHead(404).end(); return; } res.writeHead(streamHealthy ? 200 : 503).end(streamHealthy ? "ok" : "stream stopped");}).listen(Number(process.env.PORT ?? 8080), "0.0.0.0");Start it with:
node --env-file-if-exists=.env src/serve-agent.tsMount data/ on persistent storage, including the database’s WAL files and the stream cursor.
Keep one process using this SQLite database. The starter’s host.messaging adds Telegram,
plus WhatsApp and iMessage when their sender variables are set. Add an explicit fm.sms()
channel if you need SMS.
To use your own messaging provider instead, supply its channel definitions to buildAgent.
Verify its webhooks before passing incoming messages to the agent.
Check it worked: curl http://127.0.0.1:8080/health returns ok. Then make a real tool request
or send a test message; a healthy listener alone does not prove the integration works.
Persist conversations
Section titled “Persist conversations”Messaging state includes history, pending messages, background tools, scheduled messages and the agent’s own records. The default database is in memory and is lost on restart.
| Storage | Configuration |
|---|---|
| SQLite | database: sqlite("data/agent.db") on a persistent volume |
| Postgres | database: postgres(pool) with your node-postgres pool |
| Your backend | Implement Database or provide a store |
With a custom buildAgent, pass the database through host: options.host to createAgent,
as the starter does. Here is a Postgres host configuration fragment; pool is your application’s
node-postgres pool:
import { postgres } from "@flow-engineer/flow";
const agent = buildAgent({ host: { database: postgres(pool) }, channels,});await agent.resume();Call await agent.resume() before accepting messages with a durable database. Cut-off turns run
again; a tool that was running is reported as interrupted rather than silently executed again.
Handle retries and restarts
Section titled “Handle retries and restarts”Pass the provider’s message id as messageId to agent.message(). A retried webhook with that id
is handled once. Ids are kept for seven days by default (keepMessageIdsDays). Flow Messaging’s
adapter supplies these ids for you.
Run one steady-state messaging instance per database, kept warm with CPU available for timers. Conversation leases protect deployment overlap; a dead copy’s lease expires after 15 seconds. They are not a substitute for verifying multi-instance steady-state operation.
Keep the process under a supervisor, retain logs and alert when its messaging stream stops.
On release, stop routing new work to the old process and allow active work to finish before
termination. agent.status() reports activity and idle time.
Production checklist
Section titled “Production checklist”- Store secrets in your hosting platform’s secret store; never bake
.envinto an image. - Use live messaging senders for real customers, not shared trial numbers.
- Keep the local playground private; the entry points above do not serve it.
- Back up the database and test restoring it. Never share a SQLite file across machines.
- Exercise restart recovery, a retried webhook and a slow tool before sending customer traffic.
The MCP server and tool CLI need no Flow account. Using Flow’s models, Messaging or scenario-test cloud remains optional and uses the credentials for those services.
Thanks, we read every comment.