Skip to content
draft Visibility internal Owner erik@uvilo.com Approver _ Created 2026-05-14 Updated 2026-05-14

Migrate to uvilo-mono Research

1. Agentic Inference on Single-Server Runtime

1.1 Finding

ChatPostHandler.ts is the inference entry point used by both uvilo-ai and bot-craft. It uses Vercel AI SDK’s streamText()toUIMessageStreamResponse() for SSE streaming. Its dependencies:

DependencyLocationWorks on Railway?
getServerAuthSession()@dakoda/database/server✅ Uses Better Auth getSession — cookie-based for browsers, but needs API-key support for programmatic access
getEnhancedExtendedPrisma()@dakoda/database/server✅ Neon pooled connection — works anywhere
getAiTools()@dakoda/ai-tool/aiToolRegistry✅ In-process registry — works anywhere
getLanguageModel()@dakoda/bot/server✅ OpenAI provider — works anywhere
getModelParameters()@dakoda/bot/server✅ Reads convo fields — works anywhere
computeBotSystemPrompt()@dakoda/bot/server✅ DB reads + scripting — works anywhere
request.signalNext.js Request⚠️ Needs long-lived HTTP connection — NOT compatible with Vercel serverless 10–60s timeout
onFinish callbackVercel AI SDK⚠️ Same timeout constraint

Next.js can run in standalone mode (next start) on Railway, which gives long-lived HTTP connections with no serverless timeout. This is exactly how bot-craft is already configured (it has start script). The request.signal and onFinish work fine in standalone mode.

The real blocker is auth: ChatPostHandler uses getServerAuthSession() which requires a browser cookie session. For programmatic access (forge-spawn), we need API-key auth.

1.2 Options

#OptionProsCons
AConvert BotCraft → Forge runtime on Railway standaloneAlready has app/api/ai/chat/route.ts wired to ChatPostHandler; shares all @erikdakoda/* packages; same Neon DB + Better Auth; clean separation (uvilo-ai = SaaS on Vercel, bot-craft = Forge on Railway)Must strip/repurpose UI code; must add API-key auth path; must add MCP server lifecycle, per-Bot tool binding, Forge system prompts
BDeploy second uvilo-ai instance on Railway standaloneZero code changes to inference pathDuplicates entire SaaS app for one workload; two copies of same codebase to maintain; confusing architecture
CExtract inference into plain Express/Fastify on RailwayFull control; lightweight; no Next.js overheadMust replicate auth, DB access, tool registry, message persistence outside monorepo context; duplicates infrastructure
DVercel Sandbox / HybridSandbox is for code execution, not API serving; Vercel timeout still applies; defeats the purpose

1.3 Recommendation

Option A — Convert BotCraft → Forge runtime. It already has ChatPostHandler wired at app/api/ai/chat/route.ts, shares all packages, and provides clean architectural separation. The only additions needed are: (1) API-key auth path, (2) external MCP server lifecycle management, (3) per-Bot MCP tool binding, (4) Forge system prompts. All are additive — no inference code changes needed.

Option B is a viable fallback if BotCraft’s UI code creates excessive drag, but stripping pages and adding routes is simpler than maintaining two copies of uvilo-ai.

Options C and D are not viable.

1.4 Decision

Selected: Option A — Convert BotCraft → Forge runtime

Decided by: erik@uvilo.com — 2026-05-14

1.5 Deploying BotCraft Next.js on Railway

To run BotCraft as a long-lived Next.js server on Railway (Option A), the following deployment changes are required:

ItemCurrent StateRequired Change
next.config.ts output'export' for static builds onlyAdd output: 'standalone' as the default (non-static) output mode
DockerfileNone — currently deployed to VercelCreate a Dockerfile for Railway; Nixpacks does not handle pnpm monorepo standalone output well
Start commandnext startnode .next/standalone/apps/bot-craft/server.js (standalone output produces a self-contained server.js)
PORT handlingVercel manages thisRailway injects PORT env var; standalone server.js reads it automatically
Static assetsPart of Vercel buildCopy .next/static and public into the Docker image beside the standalone output

Dockerfile outline:

FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate

FROM base AS deps
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
COPY apps/bot-craft/package.json ./apps/bot-craft/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile

FROM base AS builder
WORKDIR /app
COPY --from=deps /app .
COPY . .
RUN pnpm build-bot-craft

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/apps/bot-craft/.next/standalone ./
COPY --from=builder /app/apps/bot-craft/.next/static ./apps/bot-craft/.next/static
COPY --from=builder /app/apps/bot-craft/public ./apps/bot-craft/public
CMD ["node", "apps/bot-craft/server.js"]

Key environment variables for Railway:

  • NEON_URL — Postgres connection string
  • AUTH_SECRET — Better Auth secret
  • NEXT_PUBLIC_BASE_URL — BotCraft’s own Railway URL
  • BETTER_AUTH_URL — Same as NEXT_PUBLIC_BASE_URL
  • Any API keys needed by AI tools (e.g., GOOGLE_API_KEY, TAVILY_API_KEY)

Monorepo standalone quirk: Next.js standalone mode traces monorepo workspace dependencies and copies them into .next/standalone/ with the full path structure (apps/bot-craft/, packages/*/). The Dockerfile must preserve this directory structure when copying the standalone output.

Health check: Railway can use the default Next.js response on GET / or a dedicated /api/health endpoint (to be added).

1.6 Decision

Selected: Option A — Convert BotCraft → Forge runtime (deployment via standalone Dockerfile on Railway)


2. API-Key Auth for Programmatic Access (R5)

Already implemented. No further work needed.

getServerAuthSession() (called by ChatPostHandler) already resolves dual auth:

  • Authorization: Bearer <key> → validated against ApiKey table via resolveBearerApiKeyAuth()
  • No Authorization header → falls back to Better Auth cookie session

Supporting infrastructure already exists:

  • ApiKey model (Better Auth plugin, in @erikdakoda/auth/models/ApiKey.zmodel)
  • McpToolsAuthenticateHandler (in @erikdakoda/mcp)
  • GenerateUserApiTokenHandler (admin-only key minting from user grid)
  • apiKeyAuthSessionToExecutionUser() (maps API-key session to execution context)

3. External MCP Server Lifecycle Management (R2)

3.1 Finding

LibreChat configures 16 MCP servers in librechat.yaml with per-agent tool selection via UI checkboxes. uvilo-mono has:

  • Native AI Tools (@erikdakoda/ai-tool): In-process tools registered at startup, available to all Bots. Works perfectly.
  • External MCP proxy (apps/uvilo-mcp): A stdio MCP server that proxies to uvilo-ai’s HTTP MCP API. Used for external consumers — does NOT attach external MCP servers to Bots.
  • MCP HTTP API (@erikdakoda/mcp): Handlers for listing/inoking tools via HTTP. Used by uvilo-mcp.

What’s missing: the ability to attach arbitrary external MCP servers (stdio or SSE) to a Bot and have the system manage their lifecycle (start, health-check, stop) and discover their tools at inference time.

Vercel AI SDK natively supports MCP tool discovery via @ai-sdk/mcp-filesystem and related packages. The streamText() call in ChatPostHandler can accept MCP tools alongside native AI tools.

3.2 Options

#OptionProsCons
ADB-stored MCP server configs + process managerConfig lives in DB (no YAML); per-Bot binding; can be managed via API/UI; process lifecycle managed by a singleton service on the Railway serverRequires building process manager (spawn/health/restart); stdio MCP servers are long-lived processes
BYAML/file-based MCP config (like LibreChat)Familiar pattern; simple to implementHarder to manage programmatically; doesn’t fit uvilo-mono’s DB-first architecture
CUse Vercel AI SDK’s MCP integration directlySDK handles client-side MCP connectionStill need server-side process lifecycle; SDK’s MCP support is designed for connecting to already-running servers

3.3 Recommendation

Option A — DB-stored MCP server configs + in-process lifecycle manager. Store server configs (command, args, env, timeout, instructions) in a new McpServer model. Add a per-Bot join table BotMcpServer for binding. Build an McpServerManager singleton that spawns stdio processes on demand, tracks health, and discovers tools. At inference time, ChatPostHandler collects both native AI tools and MCP tools from the Bot’s bound servers.

The McpServerManager should:

  1. Lazy-start servers on first tool invocation
  2. Keep servers alive for the duration of the inference session
  3. Support explicit start/stop via API
  4. Health-check idle servers and shut them down after a configurable timeout

3.4 Decision

Selected: Option A — DB-stored configs + process manager

Decided by: erik@uvilo.com — 2026-05-14


4. Sub-Agent Spawning API (R3)

4.1 Finding

The current forge-spawn MCP server works by:

  1. Authenticating to LibreChat via email/password → session cookie
  2. Creating a conversation via LibreChat’s REST API
  3. Sending a message via LibreChat’s UI Chat API
  4. Polling for completion (disconnect mode) or monitoring for stalls (Ralph Wiggum mode)

After migration, we need to replace the LibreChat API calls with calls to uvilo-mono’s API. The key endpoints needed:

OperationCurrent (LibreChat)Target (uvilo-mono)
Create convoPOST /api/convosPOST /api/convos (exists in @erikdakoda/convo)
Send messagePOST /api/ask (SSE stream)POST /api/ai/chat (SSE stream)
Poll statusParse SSE eventsParse SSE events (same protocol)
AbortPOST /api/edit/abortNeed new abort endpoint

4.2 Options

#OptionProsCons
ARewrite forge-spawn to call uvilo-mono APIDirect; uses existing convo/chat endpoints; API-key auth already existsMust implement abort endpoint; must adapt SSE parsing to Vercel AI SDK’s stream format
BBuild spawn into uvilo-mono as a native featureTighter integration; could expose as AI ToolScope creep; conflates runtime with orchestration
CKeep forge-spawn but target uvilo-monoMinimal change to orchestration layerSame as A but acknowledges forge-spawn remains an MCP server

4.3 Recommendation

Option C — Keep forge-spawn as MCP server, rewrite internals to target uvilo-mono API. The forge-spawn MCP server already has the right architecture (spawn, check_job, abort_job). We just replace the LibreChat HTTP client with a uvilo-mono HTTP client. Key changes:

  1. Replace email/password auth with API-key auth (Authorization: Bearer <key>)
  2. Replace LibreChat convo creation with uvilo-mono convo API
  3. Replace LibreChat chat API with POST /api/ai/chat (same SSE protocol via Vercel AI SDK)
  4. Add abort endpoint to uvilo-mono (POST /api/ai/chat/abort)
  5. Adapt SSE event parsing to Vercel AI SDK’s UIMessageStream format

4.4 Decision

Selected: Option C — Rewrite forge-spawn internals

Decided by: erik@uvilo.com — 2026-05-14


5. Runtime Model Override (R4)

5.1 Finding

Bots currently bind to a single integration + modelName at creation time. ChatPostHandler reads these from the convo’s associated Bot. LibreChat works around this by creating duplicate modelSpecs (e.g., “Forge Sonnet 4.6” and “Forge GLM 5.1 [Master]” are the same agent with different models).

5.2 Options

#OptionProsCons
AAdd modelOverride param on chat APIClean; no Bot duplication; user picks model at conversation startRequires updating ChatPostHandler to accept override; requires UI support
BAccept Bot duplication (same as LibreChat workaround)No code changes; already provenCreates many duplicate Bots; harder to manage; not scalable as models increase
CAdd model variants to Bot definitionSingle Bot with multiple model options; structuredMore complex Bot model; UI must support variant selection

5.3 Recommendation

Option A — Add modelOverride param on chat/convo creation API. This is the simplest approach and matches how users expect to interact: pick an agent, then pick a model. The override is passed as an optional parameter to POST /api/ai/chat (or POST /api/convos), and ChatPostHandler uses it instead of the Bot’s default model when present.

5.4 Decision

Selected: Option A — Model override param

Decided by: erik@uvilo.com — 2026-05-14


6. Bot-to-Agent Parity Mapping (R1)

6.1 Finding

Forge’s 19 LibreChat agents need to map to 19 uvilo-mono Bots. The mapping is straightforward:

LibreChat AgentBot NameModelRole
Forge Chat (default)Forge ChatGPT 5.5Primary chat
Forge GLM 5.1 [Master]Forge GLM 5.1GLM 5.1Master model
Forge GLM 5 TurboForge GLM 5 TurboGLM 5Fast model
Forge Opus 4.6Forge OpusOpus 4.6High-reasoning
Forge Sonnet 4.6Forge SonnetSonnet 4.6Balanced
Forge GPT 5.5Forge GPT 5.5GPT 5.5Latest GPT
Forge GPT 5.4Forge GPT 5.4GPT 5.4GPT 5.4
Forge GPT 5.1Forge GPT 5.1GPT 5.1GPT 5.1
Forge GPT 5.4 NanoForge GPT 5.4 NanoGPT 5.4 NanoLightweight
Forge Gemini 3.1 ProForge Gemini ProGemini 3.1 ProGoogle Pro
Forge Gemini 3.2 FlashForge Gemini FlashGemini 3.2 FlashGoogle Flash
Forge Page WorkerForge Page WorkerSonnet 4.6Page handling
Forge Project WorkerForge Project WorkerSonnet 4.6Project work
Forge Project EvaluatorForge Project EvaluatorGLM 5.1Evaluation
Forge Project ThinkerForge Project ThinkerGLM 5.1Planning
Forge Project Runner GLM 5.1 [Master]Forge Project RunnerGLM 5.1Project execution
Forge Project Runner Sonnet 4.6Forge Project RunnerSonnet 4.6Project execution
Forge Task Runner GLM 5.1 [Master]Forge Task RunnerGLM 5.1Task execution
Forge Task Runner Sonnet 4.6Forge Task RunnerSonnet 4.6Task execution

Note: 4 agents are duplicates differing only in model (Project Runner x2, Task Runner x2). With R4 (model override), these collapse to 2 unique Bots with model variants.

6.2 Options

Only one viable approach: create Bots via seed script or API, matching each agent’s system prompt, model, and tool bindings.

6.3 Recommendation

Create a seed script that:

  1. Creates 15 unique Bots (after collapsing model-duplicates via R5)
  2. Sets each Bot’s system prompt (composed from child prompts matching current Forge_Chat_Prompt.md structure)
  3. Binds the correct MCP servers per Bot (matching current LibreChat per-agent tool config)
  4. Sets default model parameters (temperature, reasoning effort, verbosity)

6.4 Decision

Selected: Seed script approach

Decided by: erik@uvilo.com — 2026-05-14


7. Deployment Architecture (R0)

7.1 Finding

Forge currently runs on Railway as a single service containing:

  • LibreChat (Node.js server)
  • 5 custom MCP servers (forge-bash, forge-discovery, forge-spawn, uvilo-typesense, uvilo-shell)
  • 11 third-party MCP servers (npx-based: filesystem, vercel, github, neon, notion, linear, railway, context7, playwright; python-based: suprsend)

After migration, the architecture becomes:

  • BotCraft app (Next.js standalone) — the Forge runtime, deployed as a Railway service
  • Custom MCP servers — still running within the same container or as separate Railway services
  • Third-party MCP servers — managed by the new McpServerManager inside BotCraft

7.2 Options

#OptionProsCons
AAll MCP servers as BotCraft-managed processesSingle container; simple networking; matches LibreChat’s modelHigher memory usage; longer startup; all processes share one container
BCustom MCP servers as Railway services; third-party managed by BotCraftCustom servers are long-lived and benefit from independent scaling; third-party are ephemeralMore complex deployment; networking between services
CAll MCP servers as separate Railway servicesFull isolation; independent scalingMost complex; 16+ services; high cold-start overhead

7.3 Recommendation

Option A — All MCP servers managed by BotCraft. This matches LibreChat’s proven model. The McpServerManager spawns stdio processes on demand and manages their lifecycle. Custom MCP servers (forge-bash, forge-discovery, etc.) are always-on; third-party servers (npx-based) start on first use and idle-timeout. This keeps the deployment simple — one Railway service, one container, same pattern as today.

The forge-spawn MCP server is a special case: it makes HTTP calls to the BotCraft API (itself). It doesn’t need process management — it’s just an MCP server that calls back to the app. It can remain a standalone process managed by McpServerManager.

7.4 Decision

Selected: Option A — All MCP servers managed by BotCraft

Decided by: erik@uvilo.com — 2026-05-14