Historical decision text is preserved. Accepted describes the decision, not deployment of every capability discussed. Consult the Architecture and Infrastructure tabs for the verified running system.
Date: 2026-05-10
Status: Accepted (amended 2026-05-20 — see "Amendment" below)
Deciders: Daniel Meza
Amendment (2026-05-20)
The original problem statement called for "separate staging and production environments." Per ADR-011 (Trunk-Based Development with Single Environment), the current release uses only a production environment tied to the single main branch. The Railway platform choice is unchanged; only the environment count has shrunk. The architecture remains compatible with re-introducing staging and develop environments if integration risk later justifies the cost.
Context and Problem Statement
Urza's Wallet needs a cloud platform to host its web app, API service, ingestion cron job, PostgreSQL database, and (eventually) Redis cache. The platform must:
- Be deployable by a small team without dedicated DevOps or SRE (Site Reliability Engineering) expertise.
- Support managed PostgreSQL and (future) managed Redis.
- Integrate with GitHub for CI/CD.
- Provide separate staging and production environments.
- Support preview environments for pull request review.
- Be reproducible via Infrastructure as Code (IaC) for onboarding and disaster recovery.
- Be cost-effective at launch-scale traffic (100–2,000 DAU (Daily Active Users)).
The NerdWallet reference architecture recommends AWS EKS (Kubernetes) for production-grade deployments, with Terraform for IaC. The question is whether this recommendation applies to a Phase 1 product on a small team, or whether a platform-as-a-service (PaaS) provides a better tradeoff at this stage.
Decision Drivers
- Low-ops requirement: The team's engineering time must go toward product development, not Kubernetes cluster management, VPC networking, IAM policies, container registries, or ECS task definitions.
- Managed services: Managed PostgreSQL (with automatic backups and patching) is a hard requirement. Railway, Render, Fly.io, and Heroku all offer managed PostgreSQL. AWS RDS is also an option but requires more configuration.
- GitHub integration: CI/CD (Continuous Integration/Continuous Deployment) via GitHub Actions is the required pipeline. The platform must support deployment via webhook or CLI from GitHub Actions without complex credential management.
- Preview environments: Railway's GitHub integration automatically creates preview environments for open PRs and tears them down on close/merge. This is a first-class Railway feature.
- IaC support: Railway provides a Terraform provider (
terraform-provider-railway), enabling reproducible provisioning of projects, environments, services, and variables. - Portability: The application code (Next.js, Node.js) must be portable. If Railway's pricing changes unfavorably or a capability is needed that Railway cannot provide, migrating to another platform should require only CI/CD and environment variable changes, not application code changes.
- Cost at Phase 1: Railway's Pro plan ($20/month base + usage) covers all Phase 1 services. AWS with EKS, RDS, and an Application Load Balancer would cost $150–300/month at equivalent resources, plus requires a part-time DevOps resource to manage.
Considered Options
- Railway (PaaS) — Deploy all services as Railway projects. Use Railway managed PostgreSQL. Use Terraform Railway provider for IaC.
- AWS (ECS + RDS + CloudFront) — Deploy containers to ECS Fargate, use RDS for PostgreSQL, CloudFront for CDN. Terraform with AWS provider.
- Fly.io — Container-focused PaaS with built-in PostgreSQL (Fly Postgres), global edge deployment, Terraform support.
- Render — PaaS with managed PostgreSQL, preview environments, Terraform support.
- Vercel + Supabase — Vercel for Next.js hosting (first-class), Supabase for PostgreSQL + Auth.
- Google Cloud Run + Cloud SQL — Serverless containers + managed PostgreSQL. Terraform with Google provider.
Decision Outcome
Option 1: Railway as the primary platform is adopted, managed via Terraform using the terraform-provider-railway.
Why Railway wins on the low-ops + managed services combination:
- Railway's managed PostgreSQL requires zero configuration: provision a database service and Railway injects
DATABASE_URLautomatically into all services in the same environment. - Railway's GitHub integration deploys on push/merge without managing deploy keys, service accounts, or GitHub Actions OIDC federation — a deploy token scoped to a Railway environment is sufficient.
- Preview environments are created automatically by Railway's GitHub App for every PR. No manual workflow configuration required.
- Railway's internal networking: services within the same Railway project communicate over Railway's private network by service name (e.g.,
http://api.railway.internal). No VPC, security group, or service mesh configuration is required. - Railway supports persistent volumes for document storage (mounted at a path, survives service restarts).
Terraform module strategy:
The terraform-provider-railway is used to manage:
- Railway project and environment definitions (
production,staging). - Service definitions:
web,api,ingestper environment. - Database provisioning:
postgres,redisper environment. - Environment variable declarations (references to Railway secrets, not secret values — those are set via Railway dashboard or CI).
- Custom domain assignment.
The terraform-provider-github manages:
- GitHub Actions secrets (
RAILWAY_DEPLOY_TOKEN_PROD,RAILWAY_DEPLOY_TOKEN_STAGING). - GitHub environment definitions and protection rules (production requires 1 reviewer approval).
- Branch protection rules (
main,develop).
The infra/terraform/shared/ module manages:
- Cloudflare R2 bucket for document storage (Phase 2).
- DNS records (if the domain registrar supports Terraform, otherwise managed manually).
Terraform state backend: Terraform Cloud free tier (up to 500 resources, 1 concurrent run). Alternatively, a Cloudflare R2 bucket used as an S3-compatible backend with DynamoDB-compatible state locking via a third-party solution. Terraform Cloud is simpler for a small team.
Portability safeguards:
- All infrastructure configuration is in environment variables, not application code.
- The
INTERNAL_API_URLenvironment variable abstracts the Backend API Service URL. Changing from Railway internal networking to a public URL or a load balancer requires only an environment variable change. - Docker is not required locally (Railway builds from Nixpacks by default), but a
Dockerfileis maintained in each service directory to ensure portability to any container-based platform.
Positive Consequences
- Zero Kubernetes ops: No cluster management, node pools, ingress controllers, or horizontal pod autoscalers to configure. Railway handles resource scheduling.
- Managed PostgreSQL: Automatic backups, automatic failover (Railway Pro), connection pooling (PgBouncer via Railway plugin), no manual pg_hba.conf or WAL configuration.
- Preview environments for free: Every PR gets an isolated Railway preview environment. QA and design review happen against real deployments, not staging snapshots.
- GitHub Actions simplicity:
railway up --service web --environment productionor a Railway deploy hook triggered by a webhook is the entire deployment step. No ECS task definition updates, CloudFormation stack changes, or Kubernetes manifest patches. - Cost efficiency: Railway Pro at ~$20/month base + metered usage (CPU, memory, egress) is significantly cheaper than an equivalent AWS setup with EKS, RDS, and a load balancer.
- IaC reproducibility: A new team member or a disaster recovery scenario can recreate the entire Railway infrastructure with
terraform applyin under 5 minutes.
Negative Consequences / Trade-offs
- Railway as a single point of failure: If Railway has an outage, both staging and production are affected. Mitigation: Railway's SLA (Service Level Agreement) is 99.9% uptime for Pro projects. For a Phase 1 product, this is acceptable. A multi-cloud DR strategy is a Phase 2+ concern.
- Less fine-grained control than AWS: Railway abstracts away networking, load balancing, and compute scheduling. If Urza's Wallet needs custom network routing, WAF (Web Application Firewall) rules, or multi-region active-active deployment, Railway may become a constraint. Mitigation: Railway's constraints are unlikely to bind until the platform reaches 1M+ monthly visitors. The portability safeguards (Dockerfile, env var abstraction) make migration feasible.
- Terraform Railway provider maturity: The
terraform-provider-railwayis community-maintained and less mature than the AWS or Google Terraform providers. Some Railway features (e.g., volume configuration, specific service settings) may not be exposed in the Terraform provider and may require Railway dashboard configuration. Mitigation: document any dashboard-only configuration ininfra/README.mdso it is not lost on team changes. - Railway volume backup limitations: Railway volumes are backed up daily (Pro plan) but cannot be restored to a point-in-time within a day. For document storage, this is acceptable because documents can be re-uploaded from source. For PostgreSQL, use Railway's database snapshot feature before any schema migration.
Pros and Cons of the Options
| Option | Ops Overhead | Managed PostgreSQL | Preview Envs | IaC Support | Cost at Phase 1 |
|---|---|---|---|---|---|
| Railway (chosen) | Very Low | Yes (auto-inject) | Yes (native) | Yes (community Terraform) | ~$20–40/month |
| AWS (ECS + RDS) | High | Yes (RDS) | Manual setup | Yes (mature Terraform) | ~$150–300/month + DevOps time |
| Fly.io | Low | Yes (Fly Postgres) | Yes | Yes (Terraform) | ~$20–50/month |
| Render | Low | Yes | Yes | Yes (Terraform) | ~$20–50/month |
| Vercel + Supabase | Very low | Yes (Supabase) | Yes (Vercel) | Partial | ~$20–40/month |
| GCP Cloud Run + Cloud SQL | Medium | Yes | Manual setup | Yes (Terraform) | ~$30–80/month |
Fly.io and Render were close alternatives. Fly.io has better geographic distribution (Railway is primarily US-East and EU-West), which could be beneficial if Costa Rican users experience latency. However, Railway's GitHub integration and Terraform provider are more mature for this use case, and latency from Costa Rica to US-East is acceptable (~70–100 ms round-trip) for an SSG/ISR-heavy site.
Vercel + Supabase was considered for its zero-configuration Next.js hosting. However, using Supabase only for PostgreSQL while Clerk handles auth would be a mixed provider setup that provides no benefit. Additionally, Vercel has per-seat pricing for team features and does not offer a managed cron job service, which would require an additional provider for the Data Ingestion Service.
References
- Richards & Ford, Fundamentals of Software Architecture, Ch. 4 (Architecture Characteristics — operability, deployability), Ch. 18 (Choosing the Appropriate Style)
- Kleppmann, Designing Data-Intensive Applications, Ch. 1 (Reliability — operational requirements)
- Railway documentation: Terraform provider, GitHub integration, preview environments
- Architecture reference §10 (Infrastructure Architecture)