|
| 1 | +# KiteBot Railway One-Click Template (Phase 3) — Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Make the KiteBot stack one-click deployable to Railway via a `.railway/railway.ts` Infrastructure-as-Code file defining 3 wired services (Python deep-research `agent`, `notion-mcp` sidecar, TS `channel` host), plus per-service build config and README deploy docs. |
| 6 | + |
| 7 | +**Architecture:** Railway IaC (`railway/iac` SDK) declares all 3 services from this one repo (isolated monorepo via per-service `rootDirectory`), their build/start/health, non-secret env + inter-service reference-variable wiring, and secrets via `preserve()` (deployer sets values in Railway). Applied with `railway config apply`. Secrets + the "Deploy on Railway" button + the live deploy are the deployer's steps (need `railway login`). |
| 8 | + |
| 9 | +**Tech Stack:** Railway IaC (`railway` npm SDK v3.5.7, `railway/iac`), TypeScript, nixpacks (agent + node services), the existing `pnpm channel` / `pnpm notion-mcp` scripts and the `agent/` uv project. |
| 10 | + |
| 11 | +## Global Constraints |
| 12 | +- Secrets are NEVER written into `.railway/railway.ts` or any committed file — declared via `preserve()` and set by the deployer in Railway. Only non-secret wiring/defaults are literal. |
| 13 | +- Reference-variable wiring uses literal Railway `${{service.VAR}}` strings in env values (resolved by Railway at deploy) — service names: `agent`, `notion-mcp`, `channel`. |
| 14 | +- Model default stays `gpt-5.5`; channel name default `kitebot` (consistent with Phases 1–2). |
| 15 | +- Do not disturb the Phase-1/2 gates: `pnpm check-types`, `pnpm test`, and `agent` pytest must stay green. `.railway/` is outside the root tsconfig `include`, so it won't affect `pnpm check-types`. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## Task 1: Railway IaC definition + SDK dependency |
| 20 | + |
| 21 | +**Files:** |
| 22 | +- Create: `.railway/railway.ts` |
| 23 | +- Modify: `package.json` (add `railway` devDependency) |
| 24 | + |
| 25 | +**Interfaces:** |
| 26 | +- Produces: a `.railway/railway.ts` that `railway config plan/apply` consumes; 3 services named `agent`, `notion-mcp`, `channel`. |
| 27 | + |
| 28 | +- [ ] **Step 1: Add the `railway` SDK devDependency** |
| 29 | + |
| 30 | +Run: `pnpm add -D railway@^3.5.7` |
| 31 | +Expected: adds `railway` to `devDependencies`; `pnpm-lock.yaml` updates; resolves cleanly. |
| 32 | + |
| 33 | +- [ ] **Step 2: Create `.railway/railway.ts`** |
| 34 | + |
| 35 | +```ts |
| 36 | +import { defineRailway, github, preserve, project, service } from "railway/iac"; |
| 37 | + |
| 38 | +// KiteBot on CopilotKit Intelligence — one-click Railway topology. |
| 39 | +// Three services build from this repo; the Python agent uses rootDirectory "agent". |
| 40 | +// Inter-service URLs use Railway reference variables (${{svc.VAR}}), resolved at |
| 41 | +// deploy over private networking. SECRETS are declared with preserve() so applying |
| 42 | +// never clobbers deployer-set values — set their actual values in the Railway UI |
| 43 | +// (see README "Deploy to Railway"). |
| 44 | +const REPO = "CopilotKit/OpenTag"; |
| 45 | + |
| 46 | +export default defineRailway(() => { |
| 47 | + // Notion MCP sidecar — streamable-HTTP MCP server on $PORT. |
| 48 | + const notionMcp = service("notion-mcp", { |
| 49 | + source: github(REPO), |
| 50 | + start: "pnpm notion-mcp", |
| 51 | + env: { |
| 52 | + // secrets (set in Railway UI): |
| 53 | + NOTION_TOKEN: preserve(), |
| 54 | + NOTION_MCP_AUTH_TOKEN: preserve(), |
| 55 | + }, |
| 56 | + }); |
| 57 | + |
| 58 | + // Python deep-research agent — deepagents over AG-UI (uvicorn, /health). |
| 59 | + const agent = service("agent", { |
| 60 | + source: github(REPO, { rootDirectory: "agent" }), |
| 61 | + start: "uvicorn main:app --host 0.0.0.0 --port $PORT", |
| 62 | + healthcheck: "/health", |
| 63 | + env: { |
| 64 | + OPENAI_MODEL: "gpt-5.5", |
| 65 | + // internal research source (optional Notion), wired to the sidecar: |
| 66 | + NOTION_MCP_URL: |
| 67 | + "http://${{notion-mcp.RAILWAY_PRIVATE_DOMAIN}}:${{notion-mcp.PORT}}/mcp", |
| 68 | + // secrets (set in Railway UI): OPENAI_API_KEY required; others optional: |
| 69 | + OPENAI_API_KEY: preserve(), |
| 70 | + TAVILY_API_KEY: preserve(), |
| 71 | + NOTION_MCP_AUTH_TOKEN: preserve(), |
| 72 | + LINEAR_API_KEY: preserve(), |
| 73 | + }, |
| 74 | + }); |
| 75 | + |
| 76 | + // KiteBot channel host — runs the bot over the Intelligence Realtime Gateway. |
| 77 | + const channel = service("channel", { |
| 78 | + source: github(REPO), |
| 79 | + start: "pnpm channel", |
| 80 | + env: { |
| 81 | + // brain: points at the agent service over private networking: |
| 82 | + AGENT_URL: "http://${{agent.RAILWAY_PRIVATE_DOMAIN}}:${{agent.PORT}}/", |
| 83 | + INTELLIGENCE_CHANNEL_NAME: "kitebot", |
| 84 | + // secrets (set in Railway UI): |
| 85 | + INTELLIGENCE_GATEWAY_WS_URL: preserve(), |
| 86 | + INTELLIGENCE_API_KEY: preserve(), |
| 87 | + INTELLIGENCE_ORG_ID: preserve(), |
| 88 | + INTELLIGENCE_PROJECT_ID: preserve(), |
| 89 | + INTELLIGENCE_CHANNEL_ID: preserve(), |
| 90 | + }, |
| 91 | + }); |
| 92 | + |
| 93 | + return project("kitebot", { resources: [notionMcp, agent, channel] }); |
| 94 | +}); |
| 95 | +``` |
| 96 | + |
| 97 | +- [ ] **Step 3: Type-check the IaC file against the real `railway/iac` types** |
| 98 | + |
| 99 | +Run: `pnpm exec tsc --noEmit --strict --module nodenext --moduleResolution nodenext --skipLibCheck .railway/railway.ts` |
| 100 | +Expected: no errors (the `railway/iac` exports `defineRailway`, `github`, `preserve`, `project`, `service` per the SDK's `dist/iac/index.d.ts`). If `preserve` or `github` is not exported under this SDK version, check `node_modules/railway/dist/iac/index.d.ts` and adjust the import/usage to the actual export names; if composite `${{...}}` literal env strings are rejected by the env value type, keep them as plain strings (they are strings) — the type is `string`, so literals pass. |
| 101 | + |
| 102 | +- [ ] **Step 4: Confirm the Phase-1/2 gate is unaffected** |
| 103 | + |
| 104 | +Run: `pnpm check-types` |
| 105 | +Expected: PASS (`.railway/` is not in the root tsconfig `include`, so this is unchanged from main). |
| 106 | + |
| 107 | +- [ ] **Step 5: Commit** |
| 108 | + |
| 109 | +```bash |
| 110 | +git add .railway/railway.ts package.json pnpm-lock.yaml |
| 111 | +git commit -m "feat(deploy): Railway IaC — agent + notion-mcp + channel services, wired" |
| 112 | +``` |
| 113 | + |
| 114 | +--- |
| 115 | + |
| 116 | +## Task 2: Per-service build config for the agent |
| 117 | + |
| 118 | +**Files:** |
| 119 | +- Create: `agent/railway.toml` |
| 120 | + |
| 121 | +**Interfaces:** |
| 122 | +- Consumes: nothing. Produces: a per-service config Railway reads if a deployer configures the `agent` service with config path `/agent/railway.toml` instead of (or alongside) IaC. |
| 123 | + |
| 124 | +- [ ] **Step 1: Create `agent/railway.toml`** (patterned on the deep-agents showcase; nixpacks builds the `uv` project) |
| 125 | + |
| 126 | +```toml |
| 127 | +[build] |
| 128 | +builder = "nixpacks" |
| 129 | + |
| 130 | +[deploy] |
| 131 | +startCommand = "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8123}" |
| 132 | +healthcheckPath = "/health" |
| 133 | +healthcheckTimeout = 300 |
| 134 | +restartPolicyType = "ON_FAILURE" |
| 135 | +restartPolicyMaxRetries = 5 |
| 136 | +``` |
| 137 | + |
| 138 | +- [ ] **Step 2: Sanity-check TOML validity** |
| 139 | + |
| 140 | +Run: `pnpm exec node -e "const fs=require('fs');const s=fs.readFileSync('agent/railway.toml','utf8');if(!/builder\s*=\s*\"nixpacks\"/.test(s)||!/healthcheckPath\s*=\s*\"\/health\"/.test(s))throw new Error('bad toml');console.log('railway.toml ok')"` |
| 141 | +Expected: `railway.toml ok`. |
| 142 | + |
| 143 | +- [ ] **Step 3: Commit** |
| 144 | + |
| 145 | +```bash |
| 146 | +git add agent/railway.toml |
| 147 | +git commit -m "feat(deploy): agent/railway.toml (nixpacks + uvicorn + /health)" |
| 148 | +``` |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Task 3: README "Deploy to Railway" section + secrets checklist |
| 153 | + |
| 154 | +**Files:** |
| 155 | +- Modify: `README.md` |
| 156 | + |
| 157 | +**Interfaces:** |
| 158 | +- Consumes: the service names + secrets from Task 1. Produces: deployer-facing docs. |
| 159 | + |
| 160 | +- [ ] **Step 1: Add a "Deploy to Railway (one-click)" section to `README.md`** |
| 161 | + |
| 162 | +Insert a section covering, accurately to the 3 services: |
| 163 | +- **What deploys:** 3 services from this repo — `agent` (Python deep-research, `rootDirectory: agent`), `notion-mcp` (`pnpm notion-mcp`), `channel` (`pnpm channel`); the `channel` reaches `agent` and `agent` reaches `notion-mcp` over Railway private networking (auto-wired by the IaC). |
| 164 | +- **Deploy via IaC (recommended):** |
| 165 | + ```bash |
| 166 | + npm i -g @railway/cli # or: brew install railway |
| 167 | + railway login |
| 168 | + railway link # create/select a Railway project |
| 169 | + railway config apply # provisions agent + notion-mcp + channel from .railway/railway.ts |
| 170 | + ``` |
| 171 | +- **Set the secrets** (Railway → each service → Variables) — grouped checklist: |
| 172 | + - `agent`: `OPENAI_API_KEY` (required), `TAVILY_API_KEY` (optional — enables web research), `NOTION_MCP_AUTH_TOKEN` (must match `notion-mcp`), `LINEAR_API_KEY` (optional). |
| 173 | + - `notion-mcp`: `NOTION_TOKEN`, `NOTION_MCP_AUTH_TOKEN` (same value as `agent`). |
| 174 | + - `channel`: `INTELLIGENCE_GATEWAY_WS_URL`, `INTELLIGENCE_API_KEY`, `INTELLIGENCE_ORG_ID`, `INTELLIGENCE_PROJECT_ID`, `INTELLIGENCE_CHANNEL_ID` (from your CopilotKit Intelligence project/channel). `INTELLIGENCE_CHANNEL_NAME` defaults to `kitebot`. |
| 175 | +- **"Deploy on Railway" button (optional):** to get a literal one-click button, publish this repo as a Railway template (Railway dashboard → Templates → New, or `railway` template flow) — that requires your Railway account — then add the generated button: |
| 176 | + `[](https://railway.com/new/template/<your-template-id>)` |
| 177 | +- **Note:** applying the IaC creates the services + wiring; KiteBot goes live only once the secrets are set and the channel connects (dashboard flips *Waiting for runtime → live*). |
| 178 | + |
| 179 | +- [ ] **Step 2: Verify the section references only real service names + env vars** |
| 180 | + |
| 181 | +Run: `grep -nE "notion-mcp|INTELLIGENCE_(GATEWAY_WS_URL|API_KEY|ORG_ID|PROJECT_ID|CHANNEL_ID|CHANNEL_NAME)|OPENAI_API_KEY|NOTION_MCP_AUTH_TOKEN|railway config apply" README.md` |
| 182 | +Expected: all present; cross-check names byte-match `.railway/railway.ts`. |
| 183 | + |
| 184 | +- [ ] **Step 3: Commit** |
| 185 | + |
| 186 | +```bash |
| 187 | +git add README.md |
| 188 | +git commit -m "docs(deploy): README Deploy-to-Railway section + secrets checklist" |
| 189 | +``` |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## Task 4: Full verification |
| 194 | + |
| 195 | +**Files:** none (verification only) |
| 196 | + |
| 197 | +- [ ] **Step 1: Phase-1/2 gates unaffected** |
| 198 | + |
| 199 | +Run: `pnpm check-types && pnpm test` and `cd agent && OPENAI_API_KEY=sk-dummy uv run pytest tests/ -q` |
| 200 | +Expected: TS check-types PASS + all TS tests pass; agent tests pass. (This phase adds only deploy config + docs + a devDep; no source behavior changes.) |
| 201 | + |
| 202 | +- [ ] **Step 2: IaC file resolves against the SDK** |
| 203 | + |
| 204 | +Run: `pnpm exec tsc --noEmit --strict --module nodenext --moduleResolution nodenext --skipLibCheck .railway/railway.ts` |
| 205 | +Expected: no errors. |
| 206 | + |
| 207 | +- [ ] **Step 3 (MANUAL — needs the deployer's Railway auth):** `railway login && railway link && railway config plan` validates the topology against a real Railway project, and `railway config apply` + setting the secrets deploys it live. Cannot run in this environment (unauthenticated; secrets are the deployer's). Flag for the maintainer. |
| 208 | + |
| 209 | +--- |
| 210 | + |
| 211 | +## Notes for the executor |
| 212 | +- Secrets never get literal values in any committed file — `preserve()` only. If a reviewer sees a real key, that's a defect. |
| 213 | +- If `railway/iac` under v3.5.7 differs from the documented API (export names, `github` options, env value types), adapt to the installed `node_modules/railway/dist/iac/index.d.ts` and note the deviation — the DSL shape here matches the Railway IaC reference but the SDK is the source of truth. |
| 214 | +- The literal `${{svc.VAR}}` env strings are intentional (Railway reference variables); do not "resolve" them to typed refs unless the SDK requires it. |
| 215 | +- Live deploy + the published "Deploy on Railway" button are out of scope (need the deployer's Railway login + secrets) — same live-creds gate as the Phase-2 smoke test. |
0 commit comments