From 294124154c1564e09c64ba4f6cf905578c7d6299 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Dec 2025 20:25:34 +0000 Subject: [PATCH 1/7] Add Railway deployment configuration - Backend: Use PORT env var, add CORS for Railway domain - Backend: Serve frontend static files from /frontend/dist - Frontend: Use relative API URLs in production - Add railway.json, nixpacks.toml, Procfile for deployment - Add requirements.txt for compatibility --- Procfile | 1 + backend/main.py | 46 ++++++++++++++++++++++++++++++++++++--- frontend/.env.development | 2 ++ frontend/src/api.js | 4 +++- nixpacks.toml | 16 ++++++++++++++ railway.json | 12 ++++++++++ requirements.txt | 5 +++++ 7 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 Procfile create mode 100644 frontend/.env.development create mode 100644 nixpacks.toml create mode 100644 railway.json create mode 100644 requirements.txt diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..65427fbc0 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python -m backend.main diff --git a/backend/main.py b/backend/main.py index e33ce59a6..614611e4b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,23 +1,39 @@ """FastAPI backend for LLM Council.""" +import os from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse +from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from typing import List, Dict, Any import uuid import json import asyncio +from pathlib import Path from . import storage from .council import run_full_council, generate_conversation_title, stage1_collect_responses, stage2_collect_rankings, stage3_synthesize_final, calculate_aggregate_rankings app = FastAPI(title="LLM Council API") -# Enable CORS for local development +# CORS configuration - allow localhost for dev and any origin for production +# In production, the frontend is served from the same origin so CORS isn't needed +# but we keep permissive settings for flexibility +cors_origins = [ + "http://localhost:5173", + "http://localhost:3000", + "http://localhost:8001", +] + +# Add Railway domain if RAILWAY_PUBLIC_DOMAIN is set +railway_domain = os.getenv("RAILWAY_PUBLIC_DOMAIN") +if railway_domain: + cors_origins.append(f"https://{railway_domain}") + app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:5173", "http://localhost:3000"], + allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -194,6 +210,30 @@ async def event_generator(): ) +# Mount static files for frontend (must be after all API routes) +# This serves the built frontend from /frontend/dist +static_dir = Path(__file__).parent.parent / "frontend" / "dist" +if static_dir.exists(): + from fastapi.responses import FileResponse + + # Serve static assets + app.mount("/assets", StaticFiles(directory=static_dir / "assets"), name="assets") + + # Catch-all route for SPA - serve index.html for any non-API route + @app.get("/{full_path:path}") + async def serve_spa(full_path: str): + # Don't intercept API routes + if full_path.startswith("api/"): + raise HTTPException(status_code=404, detail="Not found") + + # Serve index.html for all other routes (SPA routing) + index_file = static_dir / "index.html" + if index_file.exists(): + return FileResponse(index_file) + raise HTTPException(status_code=404, detail="Frontend not built") + + if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8001) + port = int(os.getenv("PORT", 8001)) + uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 000000000..68792dd5b --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,2 @@ +# Development environment - API runs on separate port +VITE_API_BASE=http://localhost:8001 diff --git a/frontend/src/api.js b/frontend/src/api.js index 87ec685ba..d16b8286a 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -2,7 +2,9 @@ * API client for the LLM Council backend. */ -const API_BASE = 'http://localhost:8001'; +// In production (same origin), use empty string for relative URLs +// In development, use localhost:8001 +const API_BASE = import.meta.env.VITE_API_BASE ?? ''; export const api = { /** diff --git a/nixpacks.toml b/nixpacks.toml new file mode 100644 index 000000000..f03fbe5e5 --- /dev/null +++ b/nixpacks.toml @@ -0,0 +1,16 @@ +# Nixpacks configuration for Railway deployment + +[phases.setup] +nixPkgs = ["python310", "nodejs_20"] + +[phases.install] +cmds = [ + "cd frontend && npm ci", + "pip install -e ." +] + +[phases.build] +cmds = ["cd frontend && npm run build"] + +[start] +cmd = "python -m backend.main" diff --git a/railway.json b/railway.json new file mode 100644 index 000000000..d3d1ed997 --- /dev/null +++ b/railway.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "NIXPACKS", + "buildCommand": "cd frontend && npm ci && npm run build && cd .. && pip install -e ." + }, + "deploy": { + "startCommand": "python -m backend.main", + "healthcheckPath": "/", + "healthcheckTimeout": 300 + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..0e47c3319 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.32.0 +python-dotenv>=1.0.0 +httpx>=0.27.0 +pydantic>=2.9.0 From 95d9c786f7e6c20e2c75e38331cff7cdb6534f76 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Dec 2025 20:39:31 +0000 Subject: [PATCH 2/7] Fix pip not found in Railway build --- nixpacks.toml | 5 +++-- railway.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/nixpacks.toml b/nixpacks.toml index f03fbe5e5..729b92185 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -1,12 +1,13 @@ # Nixpacks configuration for Railway deployment [phases.setup] -nixPkgs = ["python310", "nodejs_20"] +nixPkgs = ["python310", "nodejs_20", "python310Packages.pip"] [phases.install] cmds = [ "cd frontend && npm ci", - "pip install -e ." + "python -m pip install --upgrade pip", + "python -m pip install -r requirements.txt" ] [phases.build] diff --git a/railway.json b/railway.json index d3d1ed997..3ddf4754a 100644 --- a/railway.json +++ b/railway.json @@ -2,7 +2,7 @@ "$schema": "https://railway.app/railway.schema.json", "build": { "builder": "NIXPACKS", - "buildCommand": "cd frontend && npm ci && npm run build && cd .. && pip install -e ." + "buildCommand": "cd frontend && npm ci && npm run build && cd .. && python -m pip install -r requirements.txt" }, "deploy": { "startCommand": "python -m backend.main", From c534f47885de8ce8f07533fc0edcfbf447b06a36 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Dec 2025 20:45:30 +0000 Subject: [PATCH 3/7] Use explicit providers for Node + Python in Nixpacks --- nixpacks.toml | 8 +++++--- railway.json | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/nixpacks.toml b/nixpacks.toml index 729b92185..e65f7178c 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -1,13 +1,15 @@ # Nixpacks configuration for Railway deployment +# Use both Node and Python providers +providers = ["node", "python"] + [phases.setup] -nixPkgs = ["python310", "nodejs_20", "python310Packages.pip"] +nixPkgs = ["nodejs_20"] [phases.install] cmds = [ "cd frontend && npm ci", - "python -m pip install --upgrade pip", - "python -m pip install -r requirements.txt" + "pip install -r requirements.txt" ] [phases.build] diff --git a/railway.json b/railway.json index 3ddf4754a..8fa94f4cc 100644 --- a/railway.json +++ b/railway.json @@ -1,8 +1,7 @@ { "$schema": "https://railway.app/railway.schema.json", "build": { - "builder": "NIXPACKS", - "buildCommand": "cd frontend && npm ci && npm run build && cd .. && python -m pip install -r requirements.txt" + "builder": "NIXPACKS" }, "deploy": { "startCommand": "python -m backend.main", From d91b2733a3ec8a3866dcc0af63fd0afddc74899f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Dec 2025 20:49:45 +0000 Subject: [PATCH 4/7] Add root package.json and simplify nixpacks config --- nixpacks.toml | 12 ------------ package.json | 8 ++++++++ 2 files changed, 8 insertions(+), 12 deletions(-) create mode 100644 package.json diff --git a/nixpacks.toml b/nixpacks.toml index e65f7178c..20406e912 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -3,17 +3,5 @@ # Use both Node and Python providers providers = ["node", "python"] -[phases.setup] -nixPkgs = ["nodejs_20"] - -[phases.install] -cmds = [ - "cd frontend && npm ci", - "pip install -r requirements.txt" -] - -[phases.build] -cmds = ["cd frontend && npm run build"] - [start] cmd = "python -m backend.main" diff --git a/package.json b/package.json new file mode 100644 index 000000000..b8a028470 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "llm-council", + "private": true, + "scripts": { + "build": "cd frontend && npm ci && npm run build", + "dev": "cd frontend && npm run dev" + } +} From d4811d1c260dfc3100a65eb7d26a32303e76e197 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Dec 2025 20:57:54 +0000 Subject: [PATCH 5/7] Add explicit install and build phases for frontend --- nixpacks.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nixpacks.toml b/nixpacks.toml index 20406e912..aaaf092d3 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -3,5 +3,14 @@ # Use both Node and Python providers providers = ["node", "python"] +[phases.install] +cmds = [ + "cd frontend && npm ci", + "pip install -r requirements.txt" +] + +[phases.build] +cmds = ["cd frontend && npm run build"] + [start] cmd = "python -m backend.main" From 4ff3e07f61cadc1dca9fa44d3a92f3d8312f470f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Dec 2025 21:00:44 +0000 Subject: [PATCH 6/7] Switch to Dockerfile for reliable Node+Python build --- Dockerfile | 32 ++++++++++++++++++++++++++++++++ railway.json | 3 +-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..e8c4c6d32 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Multi-stage build for LLM Council + +# Stage 1: Build frontend +FROM node:20-slim AS frontend-builder +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +# Stage 2: Python runtime +FROM python:3.10-slim +WORKDIR /app + +# Install Python dependencies +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy backend code +COPY backend/ ./backend/ + +# Copy built frontend from stage 1 +COPY --from=frontend-builder /app/frontend/dist ./frontend/dist + +# Copy other necessary files +COPY pyproject.toml ./ + +# Set environment variables +ENV PORT=8080 + +# Run the application +CMD ["python", "-m", "backend.main"] diff --git a/railway.json b/railway.json index 8fa94f4cc..3c2069125 100644 --- a/railway.json +++ b/railway.json @@ -1,10 +1,9 @@ { "$schema": "https://railway.app/railway.schema.json", "build": { - "builder": "NIXPACKS" + "builder": "DOCKERFILE" }, "deploy": { - "startCommand": "python -m backend.main", "healthcheckPath": "/", "healthcheckTimeout": 300 } From a0c9a3bee62661d071d004a9281e60a3053d13fa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Dec 2025 21:06:39 +0000 Subject: [PATCH 7/7] Move health check to /api/health so SPA can serve root --- backend/main.py | 4 ++-- railway.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index 614611e4b..05694f7f2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -66,8 +66,8 @@ class Conversation(BaseModel): messages: List[Dict[str, Any]] -@app.get("/") -async def root(): +@app.get("/api/health") +async def health_check(): """Health check endpoint.""" return {"status": "ok", "service": "LLM Council API"} diff --git a/railway.json b/railway.json index 3c2069125..d1ba9cfb1 100644 --- a/railway.json +++ b/railway.json @@ -4,7 +4,7 @@ "builder": "DOCKERFILE" }, "deploy": { - "healthcheckPath": "/", + "healthcheckPath": "/api/health", "healthcheckTimeout": 300 } }