Skip to content

Commit f1b7954

Browse files
jerelvelardeclaude
andcommitted
docs(plan): KiteBot Railway one-click template (Phase 3) implementation plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7283610 commit f1b7954

1 file changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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+
`[![Deploy on Railway](https://railway.com/button.svg)](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

Comments
 (0)