From ae44b80c862f0a216a6c3524d98beab002063a6b Mon Sep 17 00:00:00 2001 From: comwanga Date: Sat, 8 Aug 2026 23:20:39 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20add=20Railway=20deployment=20=E2=80=94?= =?UTF-8?q?=20blueprint,=20frontend=20Dockerfile,=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - railway.toml — defines squadsync-db (Postgres 16), squadsync-api (FastAPI), squadsync-web (Next.js) services with build configs and environment variables - frontend/Dockerfile — multi-stage standalone Next.js production build - frontend/next.config.ts — set output: 'standalone' for Docker deployment - RAILWAY.md — step-by-step deploy guide, env reference, architecture diagram, gotchas --- RAILWAY.md | 131 ++++++++++++++++++++++++++++++++++++++++ frontend/Dockerfile | 46 ++++++++++++++ frontend/next.config.ts | 1 + railway.toml | 47 ++++++++++++++ 4 files changed, 225 insertions(+) create mode 100644 RAILWAY.md create mode 100644 frontend/Dockerfile create mode 100644 railway.toml diff --git a/RAILWAY.md b/RAILWAY.md new file mode 100644 index 0000000..212fc8a --- /dev/null +++ b/RAILWAY.md @@ -0,0 +1,131 @@ +# Deploying SquadSync on Railway + +Single-platform deployment — both frontend and backend run on Railway with a +managed PostgreSQL database. No Vercel, no Render. + +--- + +## Quick deploy + +1. Push this branch to GitHub. + +2. In the Railway dashboard → **New Project** → **Deploy from GitHub repo** + → select this repository. + +3. Railway reads `railway.toml` and provisions: + - `squadsync-db` — managed PostgreSQL 16 + - `squadsync-api` — FastAPI backend (builds from `backend/Dockerfile`) + - `squadsync-web` — Next.js frontend (builds from `frontend/Dockerfile`) + +4. Set the required secrets. Go to each service → **Variables** and set: + + **squadsync-api** + | Key | How to get | + |---|---| + | `SECRET_KEY` | Generate: `openssl rand -base64 32` | + | `DATABASE_URL` | Railway auto-injects this from the Postgres link — no need to set it | + | `FRONTEND_URL` | Railway URL of `squadsync-web`, e.g. `https://squadsync-web.up.railway.app` | + | `PUBLIC_API_URL` | Railway URL of `squadsync-api`, e.g. `https://squadsync-api.up.railway.app` | + + **squadsync-web** + | Key | How to get | + |---|---| + | `NEXT_PUBLIC_API_URL` | Same as `PUBLIC_API_URL` above — **must match exactly** | + | `AUTH_SECRET` | Generate: `openssl rand -base64 32` — must be the same value in every environment | + + > **Do not set `AUTH_URL`.** The app uses `trustHost: true`, so Auth.js derives + > the URL from the request host. + +5. **Redeploy** after setting variables — Railway needs to rebuild `squadsync-web` + since `NEXT_PUBLIC_API_URL` is inlined at build time. + +6. Done. Open `squadsync-web`'s Railway URL. + +--- + +## Environment reference + +| Key | Service | Required | Notes | +|---|---|---|---| +| `DATABASE_URL` | api | ✅ | Auto-injected by Railway from the Postgres service link | +| `SECRET_KEY` | api | ✅ | JWT signing key. Generate a strong random string | +| `FRONTEND_URL` | api | ✅ | CORS origin. No trailing slash | +| `PUBLIC_API_URL` | api | ✅ | Used for NIP-98 auth URL binding. Must equal `NEXT_PUBLIC_API_URL` | +| `NEXT_PUBLIC_API_URL` | web | ✅ | Inlined at build time. Must equal `PUBLIC_API_URL` | +| `AUTH_SECRET` | web | ✅ | NextAuth session secret. Must be set or `/api/auth/session` returns 500 | +| `ANTHROPIC_API_KEY` | api | optional | Enables AI normalization of free-text "Other" strengths | +| `SQUADSYNC_NSEC` | api | optional | Dedicated bot Nostr nsec for DM signing. Unset → DMs no-op | +| `FEEDBACK_NPUB` | api | optional | Owner npub for feedback DMs | +| `NOSTR_RELAYS` | api | optional | Defaults to `relay.damus.io,nos.lol,relay.nostr.band` | + +--- + +## Architecture + +``` + ┌──────────────────────┐ + │ load balancer │ + │ (Railway) │ + └──────┬───────────────┘ + │ + ┌─────────────┴──────────────┐ + │ │ + ┌───────▼──────┐ ┌────────▼──────┐ + │ squadsync-web│ │ squadsync-api │ + │ Next.js 16 │────HTTP────▶ FastAPI │ + │ port 3000 │ │ port 8000 │ + └──────────────┘ └───────┬───────┘ + │ + │ TCP:5432 + │ + ┌───────▼──────┐ + │ squadsync-db │ + │ PostgreSQL 16│ + └──────────────┘ +``` + +All HTTPS terminates at Railway's load balancer. Internal traffic between +services uses Railway's private network (service DNS names). + +--- + +## Running migrations + +Migrations run automatically on every deploy — the backend's `CMD` runs +`alembic upgrade head` before starting the server. To run them manually: + +```bash +railway connect --service squadsync-api +railway run "alembic upgrade head" +``` + +--- + +## Local development + +For local dev, use Docker Compose: + +```bash +docker compose up --build +``` + +Frontend: `http://localhost:3000` +Backend health: `http://localhost:8000/health` +API docs: `http://localhost:8000/docs` + +No env changes needed for local — `docker-compose.yml` has defaults for all +required variables. + +--- + +## Gotchas + +- **`NEXT_PUBLIC_*` needs a rebuild.** Changing `NEXT_PUBLIC_API_URL` after deploy + won't take effect until the web service redeploys — these vars are inlined at + build time. +- **URLs must match exactly.** `NEXT_PUBLIC_API_URL` and `PUBLIC_API_URL` must be + identical, no trailing slash — NIP-98 auth binds to this URL. +- **Railway free tier** includes $5 credit/month. The Postgres + two services fit + within that for light use. Monitor usage in the dashboard. +- **Cold starts.** Railway's hobby plan has a sleep policy after inactivity. + Upgrade to a paid plan for production use. diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..716c8ec --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,46 @@ +FROM node:20-alpine AS base + +ENV NEXT_TELEMETRY_DISABLED=1 + +FROM base AS deps + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +FROM base AS builder + +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 + +# NEXT_PUBLIC_* vars are inlined at build time. Railway injects them +# as service variables — pass them through as build args. +ARG NEXT_PUBLIC_API_URL +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + +RUN npm run build + +FROM base AS runner + +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/public ./public + +USER nextjs + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/frontend/next.config.ts b/frontend/next.config.ts index c556fc1..d3c711d 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -19,6 +19,7 @@ const contentSecurityPolicy = [ ].join("; "); const nextConfig: NextConfig = { + output: "standalone", turbopack: { // Pin the workspace root to this app directory so Turbopack doesn't infer it // from parent lockfiles (the repo root and the home directory both have one), diff --git a/railway.toml b/railway.toml new file mode 100644 index 0000000..55d88b3 --- /dev/null +++ b/railway.toml @@ -0,0 +1,47 @@ +# SquadSync Railway blueprint +# Deploy: railway up --from-project-root or connect repo in Railway dashboard. +# +# This defines two services and a Postgres database: +# - squadsync-db — managed PostgreSQL +# - squadsync-api — FastAPI backend (Dockerfile in backend/) +# - squadsync-web — Next.js frontend (Dockerfile in frontend/) + +[build] + +[database] + [database.postgres] + name = "squadsync-db" + version = "16" + +[service.squadsync-api] + name = "squadsync-api" + [service.squadsync-api.build] + dockerfilePath = "./backend/Dockerfile" + [service.squadsync-api.deploy] + healthcheckPath = "/ready" + healthcheckTimeout = 15 + [service.squadsync-api.variables] + # Railway injects DATABASE_URL automatically from the Postgres service link. + # Set the rest in the Railway dashboard or via `railway variables set`. + SECRET_KEY = "{{ secrets.SECRET_KEY }}" + ALGORITHM = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES = "1440" + FRONTEND_URL = "{{ secrets.FRONTEND_URL }}" + PUBLIC_API_URL = "{{ secrets.PUBLIC_API_URL }}" + ANTHROPIC_API_KEY = "{{ secrets.ANTHROPIC_API_KEY_OPTIONAL }}" + SQUADSYNC_NSEC = "{{ secrets.SQUADSYNC_NSEC_OPTIONAL }}" + FEEDBACK_NPUB = "{{ secrets.FEEDBACK_NPUB_OPTIONAL }}" + NOSTR_RELAYS = "wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band" + +[service.squadsync-web] + name = "squadsync-web" + [service.squadsync-web.build] + dockerfilePath = "./frontend/Dockerfile" + [service.squadsync-web.deploy] + healthcheckPath = "/" + healthcheckTimeout = 15 + [service.squadsync-web.variables] + # Railway injects $PORT automatically. PUBLIC_API_URL must point to the + # squadsync-api service's public Railway URL. + NEXT_PUBLIC_API_URL = "{{ secrets.NEXT_PUBLIC_API_URL }}" + AUTH_SECRET = "{{ secrets.AUTH_SECRET }}"