diff --git a/.claude/CUSTOM-AGENTS-GUIDE.md b/.claude/CUSTOM-AGENTS-GUIDE.md index 0e3dbd3..a8da4e7 100644 --- a/.claude/CUSTOM-AGENTS-GUIDE.md +++ b/.claude/CUSTOM-AGENTS-GUIDE.md @@ -89,7 +89,7 @@ Integration testing, contract testing, load testing for APIs. ### devops-automator (blue) Docker, CI/CD pipeline generation, deployment automation. -- **When to use:** Setting up Docker, creating CI/CD, deploying to Cloudflare Workers, Node.js, or AWS Lambda +- **When to use:** Setting up Docker, creating CI/CD, deploying to Cloudflare Workers, Node.js, AWS Lambda, or Railway - **Key capabilities:** Multi-stage Docker, GitHub Actions, wrangler deploy, PM2, health endpoints --- diff --git a/.claude/agents/devops-automator.md b/.claude/agents/devops-automator.md index e227dd3..6be2c67 100644 --- a/.claude/agents/devops-automator.md +++ b/.claude/agents/devops-automator.md @@ -1,6 +1,6 @@ --- name: devops-automator -description: API deployment and CI/CD specialist for Docker, GitHub Actions, Cloudflare Workers, Node.js, and AWS Lambda deployments. Use when setting up deployment pipelines, configuring Docker, managing Workers deployments, deploying with SAM, or automating build processes. +description: API deployment and CI/CD specialist for Docker, GitHub Actions, Cloudflare Workers, Node.js, AWS Lambda, and Railway deployments. Use when setting up deployment pipelines, configuring Docker, managing Workers deployments, deploying with SAM, configuring Railway services, or automating build processes. color: blue tools: Bash, Read, Write, Grep, MultiEdit --- diff --git a/.claude/pipeline.config.json b/.claude/pipeline.config.json index d200a11..60a9df7 100644 --- a/.claude/pipeline.config.json +++ b/.claude/pipeline.config.json @@ -68,7 +68,7 @@ "noAnyTypes": true }, "deployment": { - "targets": ["cloudflare-workers", "node-docker", "aws-lambda"], + "targets": ["cloudflare-workers", "node-docker", "aws-lambda", "railway"], "defaultTarget": "cloudflare-workers", "cloudflareWorkers": { "compatibilityDate": "2024-12-01", @@ -96,6 +96,15 @@ "framework": "sam", "provisionedConcurrency": 0 }, + "railway": { + "builder": "dockerfile", + "healthcheckPath": "/health", + "healthcheckTimeoutSeconds": 300, + "restartPolicy": "on-failure", + "restartPolicyMaxRetries": 10, + "managedPostgres": true, + "autoDeployFromGitHub": true + }, "cicd": { "provider": "github-actions", "runTests": true, diff --git a/.claude/skills/deployment-config-generator/SKILL.md b/.claude/skills/deployment-config-generator/SKILL.md index 70e8bfd..26f1140 100644 --- a/.claude/skills/deployment-config-generator/SKILL.md +++ b/.claude/skills/deployment-config-generator/SKILL.md @@ -5,11 +5,13 @@ description: > build-spec.json. For Cloudflare Workers, generates wrangler.toml with bindings. For Node.js, generates Dockerfile and docker-compose.yml with PostgreSQL. For AWS Lambda, generates a SAM template (API Gateway HTTP API), esbuild bundling - config, and an OIDC deploy workflow. Always generates GitHub Actions CI/CD - workflow and .env.example. Keywords: deploy, deployment, wrangler, cloudflare, - workers, docker, dockerfile, docker-compose, aws, lambda, sam, api-gateway, - serverless, esbuild, oidc, cold-start, github-actions, ci-cd, pipeline, env, - configuration, infrastructure + config, and an OIDC deploy workflow. For Railway, generates railway.toml + (Dockerfile build, health check, start command) and nixpacks.toml. Always + generates GitHub Actions CI/CD workflow and .env.example. Keywords: deploy, + deployment, wrangler, cloudflare, workers, docker, dockerfile, docker-compose, + aws, lambda, sam, api-gateway, serverless, esbuild, oidc, cold-start, railway, + nixpacks, paas, github-actions, ci-cd, pipeline, env, configuration, + infrastructure --- # Deployment Config Generator (Phase 5) @@ -45,7 +47,7 @@ const spec = JSON.parse( await readFile('.claude/plans/build-spec.json', 'utf-8'), ); -const target = spec.deployment.target; // 'cloudflare-workers' | 'node' | 'aws-lambda' +const target = spec.deployment.target; // 'cloudflare-workers' | 'node' | 'aws-lambda' | 'railway' const authStrategy = spec.auth.strategy; ``` @@ -347,6 +349,67 @@ permissions: --parameter-overrides "DatabaseUrl=${{ secrets.DATABASE_URL }} JwtSecret=${{ secrets.JWT_SECRET }}" ``` +### Step 2d -- Railway Configuration + +If target is `railway`, reuse the Node.js Dockerfile from Step 2b and generate +Railway config-as-code. Canonical templates live in `templates/railway/` +(`railway.toml`, `nixpacks.toml`). Railway builds the service from the +Dockerfile, so the deployed image is identical to the local +`docker compose up` image. + +```toml +# api/railway.toml +[build] +builder = "DOCKERFILE" +dockerfilePath = "Dockerfile" +watchPatterns = ["src/**", "package.json", "pnpm-lock.yaml", "Dockerfile"] + +[deploy] +startCommand = "node dist/index.js" +healthcheckPath = "/health" +healthcheckTimeout = 300 +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 10 +numReplicas = 1 +``` + +No deploy workflow is generated: Railway auto-deploys the connected GitHub +repo on every push, gated by the `/health` check. (CLI deploys from CI are +possible with `railway up` and a `RAILWAY_TOKEN` secret, but not required.) + +Environment variables live on the Railway service (Variables tab), not in +config files. Generate this reference table into the project README: + +| Variable | Value | +|---|---| +| `DATABASE_URL` | `${{Postgres.DATABASE_URL}}` (reference to managed PostgreSQL) | +| `JWT_SECRET` | generated secret, 32+ chars | +| `NODE_ENV` | `production` | +| `LOG_LEVEL` | `info` | + +`PORT` is injected by Railway; the Node.js entry point already binds it. +Remind the user of the one dashboard setting config-as-code cannot express: +service **Root Directory** must be `api` so Railway finds `railway.toml` and +the Dockerfile. + +Nixpacks alternative (`builder = "NIXPACKS"`) for building without the +Dockerfile -- `nixpacks.toml` pins the toolchain: + +```toml +# api/nixpacks.toml +[phases.setup] +nixPkgs = ["nodejs_22", "pnpm"] + +[phases.install] +cmds = ["pnpm install --frozen-lockfile"] + +[phases.build] +cmds = ["pnpm build"] + +[start] +cmd = "node dist/index.js" +``` + ### Step 3 -- Generate GitHub Actions CI/CD ```yaml @@ -449,6 +512,8 @@ jobs: # role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} # aws-region: ${{ vars.AWS_REGION }} # - run: cd api && pnpm run build:lambda && sam deploy --no-confirm-changeset + # For Railway: no deploy step -- Railway auto-deploys the connected + # GitHub repo on push, gated by the /health check (see Step 2d). deploy-production: runs-on: ubuntu-latest @@ -525,6 +590,8 @@ Ensure the API package.json has all necessary scripts: | SAM template | `api/template.yaml` | AWS Lambda + API Gateway HTTP API stack | | SAM config | `api/samconfig.toml` | Stack name, region, deploy defaults | | esbuild config | `api/esbuild.config.mjs` | Single-file Lambda bundle build | +| Railway config | `api/railway.toml` | Build, health check, start command, restart policy | +| Nixpacks config | `api/nixpacks.toml` | Toolchain pins for the non-Docker Railway build | | CI/CD workflow | `.github/workflows/ci.yml` | GitHub Actions pipeline | | Deploy workflow | `.github/workflows/deploy.yml` | OIDC-based AWS Lambda deploy (Lambda target) | | Env template | `.env.example` | Environment variable template | diff --git a/.claude/skills/schema-intake/SKILL.md b/.claude/skills/schema-intake/SKILL.md index 7089e47..444b7b8 100644 --- a/.claude/skills/schema-intake/SKILL.md +++ b/.claude/skills/schema-intake/SKILL.md @@ -84,7 +84,7 @@ continuing. Adapt follow-up questions based on responses. **Question 6 -- Deployment Target** > Where will this API be deployed? -> Options: Cloudflare Workers (default), Node.js / Docker, AWS Lambda +> Options: Cloudflare Workers (default), Node.js / Docker, AWS Lambda, Railway **Question 7 -- Additional Requirements** (optional) > Any special requirements? (pagination style, rate limiting, soft deletes, etc.) @@ -294,7 +294,7 @@ interface BuildSpec { sourceType: 'file' | 'interview'; }; deployment: { - target: 'cloudflare-workers' | 'node' | 'aws-lambda'; + target: 'cloudflare-workers' | 'node' | 'aws-lambda' | 'railway'; runtime: string; }; auth: { @@ -553,7 +553,7 @@ function validateBuildSpec(spec: BuildSpec): string[] { | `tdd-from-schema` (Phase 2) | Reads `endpoints` and `auth` to generate test cases | | `route-generation` (Phase 3) | Reads `endpoints` to scaffold Hono route handlers | | `api-authentication` | Reads `auth` to configure JWT/API key middleware | -| `deployment-config-generator` | Reads `deployment` to generate wrangler.toml, Dockerfile, or SAM template | +| `deployment-config-generator` | Reads `deployment` to generate wrangler.toml, Dockerfile, SAM template, or railway.toml | | `api-documentation` | Reads the generated OpenAPI spec or regenerates from build-spec | ### Triggering the Next Phase diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ab8f56..f719989 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,7 +166,7 @@ jobs: fi done - for dir in templates/shared templates/cloudflare-workers templates/node-server templates/docker templates/aws-lambda; do + for dir in templates/shared templates/cloudflare-workers templates/node-server templates/docker templates/aws-lambda templates/railway; do if [ -d "$dir" ]; then echo " $dir: OK" else diff --git a/CLAUDE.md b/CLAUDE.md index 510e273..3d90eb0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a **Claude Code-integrated API & backend development framework** providing specialized agents, skills, scripts, and schema-to-API conversion pipelines. Built with TypeScript, Hono (HTTP framework), Drizzle ORM, and PostgreSQL. Deployed to Cloudflare Workers, Node.js, or AWS Lambda. +This is a **Claude Code-integrated API & backend development framework** providing specialized agents, skills, scripts, and schema-to-API conversion pipelines. Built with TypeScript, Hono (HTTP framework), Drizzle ORM, and PostgreSQL. Deployed to Cloudflare Workers, Node.js, AWS Lambda, or Railway. Nerva is the backend counterpart to **Aurelius** (frontend framework). @@ -12,7 +12,7 @@ The framework is designed for: - Schema-first API development (OpenAPI spec to working API) - OpenAPI-to-working-API conversion with TDD-mandatory development - Comprehensive testing (integration tests, contract tests, load tests with k6) -- Deployment to Cloudflare Workers, Node.js (Docker), or AWS Lambda (SAM) +- Deployment to Cloudflare Workers, Node.js (Docker), AWS Lambda (SAM), or Railway - Full product lifecycle support (engineering, database, testing, DevOps, operations) ## Project Structure @@ -39,6 +39,7 @@ project-root/ │ ├── cloudflare-workers/ # Wrangler config │ ├── node-server/ # Node.js server config │ ├── aws-lambda/ # SAM template, esbuild config, OIDC deploy workflow +│ ├── railway/ # Railway config-as-code (railway.toml, nixpacks.toml) │ └── docker/ # Dockerfile, docker-compose ├── docs/ │ ├── schema-to-api/ # Pipeline guide @@ -58,7 +59,7 @@ project-root/ ```bash # Setup a new API project -./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda +./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda / --railway # Run tests with coverage ./scripts/run-tests.sh @@ -233,7 +234,7 @@ Autonomous 10-phase pipeline that converts an OpenAPI specification into a worki - **Schema-first** — OpenAPI spec drives database schema, types, handlers, and tests - **Contract testing** — generated tests validate API responses against OpenAPI spec - **Load testing** — k6 scripts generated for performance-critical endpoints -- **Multi-target deployment** — Cloudflare Workers, Node.js, Docker, AWS Lambda with single config change +- **Multi-target deployment** — Cloudflare Workers, Node.js, Docker, AWS Lambda, Railway with single config change - **Type safety end-to-end** — Drizzle schema to Zod validators to TypeScript types - Quality gate: 80%+ coverage, TypeScript strict, security audit, bundle analysis - Resumable: TodoWrite tracks progress across interrupted sessions @@ -489,6 +490,7 @@ pnpm drizzle-kit studio # Open database GUI ./scripts/setup-project.sh my-api --cloudflare # New Cloudflare Workers project ./scripts/setup-project.sh my-api --node # New Node.js project ./scripts/setup-project.sh my-api --lambda # New AWS Lambda (SAM) project +./scripts/setup-project.sh my-api --railway # New Railway project ``` --- diff --git a/README.md b/README.md index 6eea2d3..ff69a53 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A Claude Code-integrated API & backend development framework with TypeScript, Ho - **12 Development Skills** — Automated workflows for schema parsing, TDD, route generation, authentication, validation, documentation - **10-Phase Schema-to-API Pipeline** — Convert OpenAPI specs into fully working, tested API servers with a single command - **TDD-Mandatory Development** — Integration tests written before route handlers, hard gate enforced -- **Three Deployment Targets** — Cloudflare Workers (edge), Node.js (Docker), or AWS Lambda (SAM) with one config change +- **Four Deployment Targets** — Cloudflare Workers (edge), Node.js (Docker), AWS Lambda (SAM), or Railway (PaaS) with one config change - **Testing Stack** — Vitest, supertest, contract tests against OpenAPI, k6 load tests - **Schema-First Design** — Database schemas, types, and validators all derived from your API spec @@ -37,7 +37,7 @@ cd Nerva pnpm install # Initialize a new API project -./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda +./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda / --railway # Start development cd api && pnpm dev @@ -176,6 +176,7 @@ Full catalog: [`.claude/CUSTOM-AGENTS-GUIDE.md`](.claude/CUSTOM-AGENTS-GUIDE.md) | `templates/cloudflare-workers/` | Wrangler config with Hyperdrive | | `templates/node-server/` | Dockerfile + docker-compose with PostgreSQL | | `templates/aws-lambda/` | SAM template, esbuild config, OIDC deploy workflow | +| `templates/railway/` | Railway config-as-code + Nixpacks build config | | `templates/docker/` | Extended Docker with Redis + pgAdmin | ## Part of the PMDS Framework Series diff --git a/docs/onboarding/architecture.md b/docs/onboarding/architecture.md index 325df88..9f37d59 100644 --- a/docs/onboarding/architecture.md +++ b/docs/onboarding/architecture.md @@ -61,7 +61,7 @@ Shell scripts for common development tasks: | Script | Purpose | |--------|---------| -| `setup-project.sh` | Initialize a new API project (--cloudflare, --node, or --lambda) | +| `setup-project.sh` | Initialize a new API project (--cloudflare, --node, --lambda, or --railway) | | `run-tests.sh` | Vitest test suite with coverage | | `check-types.sh` | TypeScript strict mode checking | | `security-scan.sh` | Dependency and code security audit | @@ -71,7 +71,7 @@ Shell scripts for common development tasks: | `generate-openapi-docs.sh` | Build API documentation | | `generate-client.sh` | Generate typed API client for frontends | -### Templates (5 directories) +### Templates (6 directories) Starter configurations for different deployment targets: @@ -81,6 +81,7 @@ Starter configurations for different deployment targets: | `templates/cloudflare-workers/` | Wrangler config with D1/KV/Hyperdrive bindings | | `templates/node-server/` | Dockerfile + docker-compose with PostgreSQL | | `templates/aws-lambda/` | SAM template (API Gateway HTTP API), esbuild config, OIDC deploy workflow | +| `templates/railway/` | Railway config-as-code (Docker build, health check) + Nixpacks config | | `templates/docker/` | Extended Docker setup with Redis + pgAdmin | ## Pipelines diff --git a/docs/onboarding/quickstart.md b/docs/onboarding/quickstart.md index 75e04e7..c95fc2b 100644 --- a/docs/onboarding/quickstart.md +++ b/docs/onboarding/quickstart.md @@ -32,7 +32,7 @@ If you have an OpenAPI specification file: ```bash # Initialize the project structure -./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda +./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda / --railway # Open Claude Code and run the pipeline /build-from-schema path/to/openapi-spec.yaml diff --git a/docs/schema-to-api/README.md b/docs/schema-to-api/README.md index 96a0f2a..c2756bb 100644 --- a/docs/schema-to-api/README.md +++ b/docs/schema-to-api/README.md @@ -15,7 +15,7 @@ Convert an OpenAPI specification into a fully working, tested API server through | 7 | Validation Layer | Generate Zod schemas for request/response validation | | 8 | Auth & Security | Wire authentication middleware, CORS, rate limiting | | 9 | Documentation | Generate API docs, Swagger UI config, example responses | -| 10 | Deployment Config | Produce Wrangler TOML, Dockerfile, or SAM template | +| 10 | Deployment Config | Produce Wrangler TOML, Dockerfile, SAM template, or Railway config | ## Phase-by-Phase Deep Dive @@ -102,12 +102,12 @@ Convert an OpenAPI specification into a fully working, tested API server through ### Phase 10: Deployment Config -**What it does:** Produces deployment configuration files matching the target platform. For Cloudflare Workers: `wrangler.toml` with D1 bindings. For Node.js: `Dockerfile` with multi-stage build and `docker-compose.yml`. For AWS Lambda: `template.yaml` (SAM with an API Gateway HTTP API), `samconfig.toml`, esbuild bundle config, and an OIDC-based deploy workflow. +**What it does:** Produces deployment configuration files matching the target platform. For Cloudflare Workers: `wrangler.toml` with D1 bindings. For Node.js: `Dockerfile` with multi-stage build and `docker-compose.yml`. For AWS Lambda: `template.yaml` (SAM with an API Gateway HTTP API), `samconfig.toml`, esbuild bundle config, and an OIDC-based deploy workflow. For Railway: `railway.toml` (Dockerfile build, health check, start command) and `nixpacks.toml`. - **Inputs:** Project structure, database config, environment variables -- **Outputs:** `wrangler.toml`, `Dockerfile` + `docker-compose.yml`, or `template.yaml` + `samconfig.toml` + `esbuild.config.mjs` +- **Outputs:** `wrangler.toml`, `Dockerfile` + `docker-compose.yml`, `template.yaml` + `samconfig.toml` + `esbuild.config.mjs`, or `railway.toml` + `nixpacks.toml` - **Tools:** `deployment-config-generator` skill -- **Config:** `pipeline.config.json` -- `deployment.target` (cloudflare, node, aws-lambda, both) +- **Config:** `pipeline.config.json` -- `deployment.target` (cloudflare, node, aws-lambda, railway, both) ## Configuration diff --git a/package.json b/package.json index 5777482..f7d6cec 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "schema-first", "tdd", "cloudflare-workers", - "aws-lambda" + "aws-lambda", + "railway" ], "author": "PAMulligan", "license": "MIT", diff --git a/scripts/setup-project.sh b/scripts/setup-project.sh index f24f34a..ae9c15c 100644 --- a/scripts/setup-project.sh +++ b/scripts/setup-project.sh @@ -3,7 +3,7 @@ set -euo pipefail # ============================================================================ # setup-project.sh - Initialize a new Nerva API project -# Usage: ./scripts/setup-project.sh [--cloudflare|--node|--lambda] [--dry-run] +# Usage: ./scripts/setup-project.sh [--cloudflare|--node|--lambda|--railway] [--dry-run] # ============================================================================ RED='\033[0;31m' @@ -26,10 +26,11 @@ TEMPLATES_DIR="$PROJECT_ROOT/templates" if [[ $# -lt 1 ]]; then error "Missing project name." - echo "Usage: $0 [--cloudflare|--node|--lambda] [--dry-run]" + echo "Usage: $0 [--cloudflare|--node|--lambda|--railway] [--dry-run]" echo " --cloudflare Set up for Cloudflare Workers deployment" echo " --node Set up for Node.js / Docker deployment (default)" echo " --lambda Set up for AWS Lambda deployment (SAM + API Gateway HTTP API)" + echo " --railway Set up for Railway deployment (Docker build + managed PostgreSQL)" echo " --dry-run Preview what would be created without making changes" exit 1 fi @@ -44,6 +45,7 @@ while [[ $# -gt 0 ]]; do --cloudflare) PLATFORM="cloudflare"; shift ;; --node) PLATFORM="node"; shift ;; --lambda) PLATFORM="lambda"; shift ;; + --railway) PLATFORM="railway"; shift ;; --dry-run) DRY_RUN=true; shift ;; *) error "Unknown option: $1"; exit 1 ;; esac @@ -293,6 +295,36 @@ LENVEOF fi success "AWS Lambda configured. Edit samconfig.toml with your stack name and region." +elif [[ "$PLATFORM" == "railway" ]]; then + step "Setting up Railway..." + # Railway builds the same multi-stage image as the Node.js target + copy_file "$TEMPLATES_DIR/node-server/Dockerfile" "$API_DIR/Dockerfile" + copy_file "$TEMPLATES_DIR/node-server/docker-compose.yml" "$API_DIR/docker-compose.yml" + copy_file "$TEMPLATES_DIR/railway/railway.toml" "$API_DIR/railway.toml" + copy_file "$TEMPLATES_DIR/railway/nixpacks.toml" "$API_DIR/nixpacks.toml" + + write_file "$API_DIR/.env.example" << 'RENVEOF' +# Local development values. On Railway, set variables on the service +# (Variables tab) instead. Wire the managed PostgreSQL with a reference +# variable: +# DATABASE_URL=${{Postgres.DATABASE_URL}} +# Railway injects PORT automatically; the server binds it at startup. +NODE_ENV=development +PORT=3000 +DATABASE_URL=postgresql://nerva:nerva_secret@localhost:5432/nerva_db +LOG_LEVEL=debug +APP_VERSION=0.0.1 +HEALTH_DB_TIMEOUT_MS=2000 +# Required in production (min 32 chars). Generate with: openssl rand -hex 32 +JWT_SECRET=dev-secret-change-me-in-production-min-32-chars +RENVEOF + + if ! command -v railway &>/dev/null; then + warn "Railway CLI not found. Optional -- GitHub auto-deploy works without it:" + warn " https://docs.railway.com/guides/cli" + fi + + success "Railway configured. Connect the repo in Railway and set Root Directory to 'api'." else step "Setting up Node.js / Docker..." copy_file "$TEMPLATES_DIR/node-server/Dockerfile" "$API_DIR/Dockerfile" @@ -323,6 +355,10 @@ elif [[ "$PLATFORM" == "lambda" ]]; then DEV_BASE_URL="http://localhost:3000" PLATFORM_LABEL="AWS Lambda" DEV_STEPS=$'docker compose up -d # start PostgreSQL\npnpm dev # start the dev server' +elif [[ "$PLATFORM" == "railway" ]]; then + DEV_BASE_URL="http://localhost:3000" + PLATFORM_LABEL="Railway" + DEV_STEPS=$'docker compose up -d # start PostgreSQL\npnpm dev # start the dev server' else DEV_BASE_URL="http://localhost:3000" PLATFORM_LABEL="Node.js / Docker" @@ -551,6 +587,57 @@ which proxies Lambda events to the HTTP port the server listens on. Useful for container-only dependencies or bundles beyond the 250 MB zip limit; the trade-off is slower cold starts than the zip deployment this project uses. READMELAMBDA + elif [[ "$PLATFORM" == "railway" ]]; then + cat << 'READMERAILWAY' + +## Deploying to Railway + +The API deploys to [Railway](https://railway.com) as a Docker service: Railway +builds `api/Dockerfile` (the same hardened multi-stage image as the Node.js +target), and `api/railway.toml` configures the build, health check, start +command, and restart policy -- so what deploys is exactly what +`docker compose up` runs locally. + +### One-time setup + +1. Create a project with a database: in the [dashboard](https://railway.com/new) + choose **Deploy PostgreSQL**, then **New -> GitHub Repo** to add the API + service from this repository. (CLI: `railway init`, then + `railway add --database postgres`.) +2. Point the service at the API: **Settings -> Source -> Root Directory** = + `api`, so Railway picks up `railway.toml` and the `Dockerfile`. +3. Set service variables (**Variables** tab). `${{Postgres.DATABASE_URL}}` is + a Railway reference variable that resolves to the managed database: + + | Variable | Value | + |----------|-------| + | `DATABASE_URL` | `${{Postgres.DATABASE_URL}}` | + | `JWT_SECRET` | output of `openssl rand -hex 32` | + | `NODE_ENV` | `production` | + | `LOG_LEVEL` | `info` | + + Railway injects `PORT` automatically; the server binds it at startup. + +### Deploys + +Railway auto-deploys every push to the connected branch. Each deploy must +answer on `/health` (`healthcheckPath` in `railway.toml`) before traffic +switches over; a failing check keeps the previous deployment live. To deploy +from the CLI instead, run `railway up` from `api/`. + +Run database migrations against the Railway database from your machine: + +```bash +railway run pnpm db:migrate +``` + +### Nixpacks alternative + +To build without the Dockerfile, set `builder = "NIXPACKS"` in +`api/railway.toml`; `api/nixpacks.toml` pins pnpm + Node 22 and the +build/start commands. The Dockerfile build stays the default because it ships +the exact image you can test locally. +READMERAILWAY fi cat << 'READMESTATIC' @@ -621,6 +708,11 @@ if ! $DRY_RUN; then echo " pnpm dev # Start dev server" echo " pnpm build:lambda # Bundle for Lambda" echo " sam deploy --guided # First AWS deploy" + elif [[ "$PLATFORM" == "railway" ]]; then + echo " docker compose up -d # Start PostgreSQL" + echo " pnpm dev # Start dev server" + echo " railway init # Create Railway project (or connect GitHub)" + echo " railway up # Deploy from the CLI" else echo " docker compose up -d # Start PostgreSQL" echo " pnpm dev # Start dev server" diff --git a/templates/railway/nixpacks.toml b/templates/railway/nixpacks.toml new file mode 100644 index 0000000..d45a3c6 --- /dev/null +++ b/templates/railway/nixpacks.toml @@ -0,0 +1,18 @@ +# Nerva API - Nixpacks build configuration (alternative to the Dockerfile) +# https://nixpacks.com/docs/configuration/file +# +# Only used when railway.toml sets builder = "NIXPACKS". Nixpacks auto-detects +# Node.js + pnpm from pnpm-lock.yaml; this file pins the toolchain and the +# build/start commands explicitly so builds stay reproducible. + +[phases.setup] +nixPkgs = ["nodejs_22", "pnpm"] + +[phases.install] +cmds = ["pnpm install --frozen-lockfile"] + +[phases.build] +cmds = ["pnpm build"] + +[start] +cmd = "node dist/index.js" diff --git a/templates/railway/railway.toml b/templates/railway/railway.toml new file mode 100644 index 0000000..f62658d --- /dev/null +++ b/templates/railway/railway.toml @@ -0,0 +1,36 @@ +# Nerva API - Railway configuration (config as code) +# https://docs.railway.com/reference/config-as-code +# +# Railway builds this service from the Dockerfile -- the same hardened +# multi-stage image used by the Node.js target -- so what deploys is exactly +# what `docker compose up` runs locally. To build with Nixpacks instead, set +# builder = "NIXPACKS"; nixpacks.toml then controls the toolchain. +# +# One dashboard setting is required: Settings -> Source -> Root Directory = "api" +# so Railway picks up this file and the Dockerfile. + +[build] +builder = "DOCKERFILE" +dockerfilePath = "Dockerfile" +# Rebuild only when service files change (skip docs-only commits) +watchPatterns = ["src/**", "package.json", "pnpm-lock.yaml", "Dockerfile"] + +[deploy] +startCommand = "node dist/index.js" +# Each deploy must answer on /health before traffic switches over; a failing +# check keeps the previous deployment live. +healthcheckPath = "/health" +healthcheckTimeout = 300 +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 10 +numReplicas = 1 +# Run migrations before each deploy goes live. Requires drizzle-kit inside the +# image -- the production Dockerfile prunes dev dependencies, so either run +# migrations from your machine (railway run pnpm db:migrate) or switch to the +# Nixpacks build before uncommenting: +# preDeployCommand = "pnpm db:migrate" + +# Environment variables are set on the Railway service (Variables tab), not in +# this file. Wire the managed PostgreSQL with a reference variable: +# DATABASE_URL = ${{Postgres.DATABASE_URL}} +# Railway injects PORT automatically; the server binds it at startup.