VPS / Node.js Deployment
Run on any VPS (EC2, DigitalOcean Droplet, Hetzner, Linode) or bare metal with Node.js.
Architecture
Browser ──WebSocket──▶ Node.js Server (ws library)
│
▼
Express/Fastify/Hono HTTP server
│
├─ PostgreSQL / SQLite
├─ Redis (session cache)
└─ S3-compatible storage (MinIO, etc.)
Setup
pnpm add @aimform/ai @aimform/core ws pg
import { AI } from "@aimform/ai";
import { WebSocketServer } from "ws";
import { Pool } from "pg";
import express from "express";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const wss = new WebSocketServer({ noServer: true });
const dbAdapter = {
provider: "postgres",
async query<T>(sql: string, params?: unknown[]): Promise<T[]> {
const result = await pool.query(sql, params);
return result.rows as T[];
},
async execute<T>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]> {
const sql = strings.reduce((acc, s, i) => acc + s + (i < values.length ? `$${i + 1}` : ""), "");
return dbAdapter.query<T>(sql, values);
},
};
const transport = new WebSocketTransportAdapter(wss);
const ai = new AI({
model: "deepseek-v4-flash",
apiKeys: { deepseekApiKey: process.env.DEEPSEEK_API_KEY! },
db: dbAdapter as any,
transport,
enableMemory: true,
plugins: {
files: new LocalFilePlugin("./uploads"),
webSearch: new TavilySearchPlugin(process.env.TAVILY_API_KEY!),
},
});
WebSocket Transport
class WebSocketTransportAdapter implements ChatTransportAdapter {
readonly provider = "node-ws";
private sessions = new Map<string, Set<WebSocket>>();
constructor(private wss: WebSocketServer) {
wss.on("connection", (ws, req) => {
const url = new URL(req.url!, "http://localhost");
const runId = url.searchParams.get("runId");
if (runId) {
if (!this.sessions.has(runId)) this.sessions.set(runId, new Set());
this.sessions.get(runId)!.add(ws);
ws.on("close", () => this.sessions.get(runId)?.delete(ws));
}
});
}
async publishEvents(runId: string, events: AnyChatEvent[]) {
const clients = this.sessions.get(runId);
if (!clients) return;
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) {
for (const event of events) ws.send(JSON.stringify(event));
}
}
}
getConnectionUrl(runId: string): string {
return `ws://localhost:8080?runId=${runId}`;
}
async getReplayState(runId: string) { return null; }
async getEvents(runId: string, afterSeq?: number) { return { events: [], latestSeq: 0 }; }
async markComplete(runId: string, result: ProcessMessageResult) {}
async markError(runId: string, error: string) {}
}
Express Server
const app = express();
app.post("/api/messages", async (req, res) => {
const { message, conversationId } = req.body;
const runId = crypto.randomUUID();
// Start processing in background
ai.chat({
runId,
conversationId,
messages: [{ role: "user", content: message }],
toolContext: { userId: req.user.id, profileId: req.user.profileId, sessionId: runId, env: process.env },
}).catch(console.error);
res.json({ runId, assistantMessageId: runId });
});
const server = app.listen(8080);
server.on("upgrade", (request, socket, head) => {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
});
Docker
FROM node:22-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]