Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions RAILWAY.md
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 46 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
1 change: 1 addition & 0 deletions frontend/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
47 changes: 47 additions & 0 deletions railway.toml
Original file line number Diff line number Diff line change
@@ -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 }}"
Loading