Aimform

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"]

On this page