Railway is the backend hosting platform you're using right now for the RDR2 Companion's Claude proxy. It's also the backend hosting platform a large fraction of solo iOS developers and small teams have settled on between 2023 and 2026, for good reasons. This post is a full tour.
What Railway is
Railway is a Platform-as-a-Service. You give it your code; it runs the code on its servers. You don't think about virtual machines, operating systems, networking, or load balancers — the platform handles all of that. You write a Node / Python / Go / Ruby / Rust application, push it to GitHub, connect the repo to Railway, and within a couple of minutes you have a running backend with a public URL.
Founded in 2020, Railway has grown into a serious alternative to Heroku, Render, and Fly.io. Pricing is usage-based but predictable. The developer experience is genuinely good — one of the friendliest dashboards in the space.
The mental model: projects, services, environments
Three concepts:
Project
The top-level container. Your RDR2 Companion has one project. Your GTA V Companion has another. A project groups everything related — the backend service, the database, environment configuration.
Service
A running piece of software. A typical project has 1-3 services: the main backend, maybe a worker for background jobs, maybe a cron service. Each service has its own deployment configuration, its own environment variables, its own logs.
Environment
A copy of your services with potentially different config. Most teams run production and staging environments. Same code, different databases, different API keys, different domain names. You can promote a deployment from staging to production once you've verified it.
Concretely: your RDR2 project might have a production environment running the main backend that the live App Store app talks to, and a staging environment running the same code but pointing at a test database. You test in staging before flipping a new deploy to production.
How deployment works
The standard workflow:
- Your code lives in a GitHub repo.
- You connect the repo to a Railway service in the Railway dashboard.
- Every push to your chosen branch (usually
main) triggers a build on Railway. - Railway detects your stack automatically (Node.js, Python, etc.) via its Nixpacks builder.
- Build succeeds: a container image is created.
- Railway deploys the new container, runs your start command, performs health checks.
- Once healthy, traffic switches to the new deployment.
- The old deployment is retired.
You can also use a Dockerfile if you want full control over the build, or railway.toml for configuration. Most apps don't need either — Nixpacks auto-detection works.
Zero-downtime deploys are the default. If a new deployment fails its health checks, Railway keeps the old one running and surfaces the error.
Managed databases
One of Railway's strongest features. From the dashboard, click "Add Service" and choose:
- PostgreSQL — the relational default. Use this unless you have a specific reason otherwise.
- MySQL — for legacy compatibility.
- Redis — key-value cache, session store, rate-limiting state.
- MongoDB — document database.
- Other — various community templates for CockroachDB, Meilisearch, etc.
Railway provisions the database in seconds, generates a connection string, and exposes it to your services as an environment variable (e.g., DATABASE_URL). Your backend code reads the connection string and connects — you never manage the database server itself.
Backups are automatic at the daily granularity on paid plans. Storage grows automatically up to your plan's limit.
Environment variables — where your secrets live
This is where your Anthropic API key lives. Your App Store Connect API key. Your Stripe webhook secret. Anything that's not safe to commit to GitHub.
In the Railway dashboard, each service has a Variables tab. Add a key-value pair (e.g., ANTHROPIC_API_KEY = sk-ant-...). Save. The variable is now available to your code as process.env.ANTHROPIC_API_KEY (Node) or os.environ['ANTHROPIC_API_KEY'] (Python) or equivalent.
Variables can also be referenced across services: ${{Postgres.DATABASE_URL}} in your backend service automatically resolves to the Postgres service's connection string. When you swap databases, you don't update your backend code — the reference resolves to the new database.
Per-environment variables let you have different keys in production vs staging. The same code with different secrets — this is how you avoid making API calls against your real Stripe / Apple / Anthropic accounts during testing.
Custom domains and HTTPS
Every Railway service gets a free railway.app subdomain (e.g., rdr2-backend-production.up.railway.app). You can also attach your own domain:
- In the service settings, click "Custom Domain" and enter
api.djenterprises.ai. - Railway gives you a CNAME record to add at your domain registrar.
- Add the CNAME, wait a few minutes for DNS propagation.
- Railway provisions a free Let's Encrypt TLS certificate automatically.
- HTTPS works end-to-end.
For your iOS app, this means you can call https://api.djenterprises.ai/ask instead of an opaque railway.app URL — cleaner branding, and you can swap the backend later without an app update.
Logs, metrics, observability
The Logs tab in each service shows real-time stdout/stderr from your application. Filter by deployment, time, search term. Logs are retained based on your plan.
The Metrics tab shows CPU, memory, network, and request count over time. Useful for spotting "my backend is suddenly using 4x more memory" before it crashes.
For deeper observability, integrate a third-party service via environment variables — Sentry for error tracking, Datadog or Axiom for log aggregation, Better Stack for uptime monitoring. Railway plays nicely with all of them.
The Railway CLI
Install: npm install -g @railway/cli or brew install railway. Login: railway login.
Useful commands once you're logged in:
railway link— link your local repo to a Railway project.railway run npm start— run a local command with Railway's environment variables loaded. Lets you test locally with the real production credentials (use carefully).railway up— deploy the current directory directly without going through GitHub. Useful for quick iteration.railway logs— tail logs from the terminal.railway open— open the Railway dashboard for the linked project.railway variables— list / get / set environment variables.railway status— current deployment status.
From Claude Code, you can ask Claude to run Railway CLI commands directly. "Deploy the current branch to staging" or "tail the logs for the last 50 lines" both work as natural-language commands when the Railway CLI is installed.
Pricing in plain English
Railway's pricing is usage-based: you pay for the CPU, memory, and network your services actually consume, plus database storage. There's no fixed-per-month cost beyond the plan minimum.
Roughly, the tiers in 2026 look like:
- Hobby — $5/mo. Includes ~$5 of usage credits. Suitable for a single small backend + small database. You'll exceed this only if you have real traffic.
- Pro — $20/mo and up. Higher per-service limits, longer log retention, priority support, more concurrent deploys.
- Enterprise — custom pricing for serious production workloads.
For a typical solo iOS dev with a Claude proxy backend + Postgres + a few thousand monthly active users, expect to spend in the $10-$30/mo range. AI API costs (Claude / OpenAI) will dwarf the backend hosting bill.
Watch the dashboard's "Usage" page weekly. If your usage curve is climbing, that's the signal to optimize: smaller container, fewer cron runs, sleeping idle workers.
The Claude API proxy pattern on Railway
This is what you're running for RDR2 and GTA V Companion. The canonical shape:
// Node.js + Express example
import express from 'express';
import Anthropic from '@anthropic-ai/sdk';
const app = express();
app.use(express.json());
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY // Set in Railway Variables
});
app.post('/ask', async (req, res) => {
// 1. Verify the request came from your iOS app (signed JWT, App Attest, etc.)
if (!verifyClient(req)) return res.status(401).end();
// 2. Rate-limit by user (Redis counter)
if (await isRateLimited(req.userId)) return res.status(429).end();
// 3. Call Claude with your system prompt + the user's question
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
system: 'You are an RDR2 expert...',
messages: [{ role: 'user', content: req.body.question }]
});
// 4. Log usage for billing / abuse tracking
await logUsage(req.userId, response.usage);
// 5. Return the response to the iOS app
res.json({ text: response.content[0].text });
});
app.listen(process.env.PORT || 3000);
That's the skeleton. The pattern is the same regardless of language. The benefits:
- Your API key never leaves Railway. Users can't extract it from the iOS binary.
- You can rate-limit centrally. Abusive users hit your rate limit, not your bill.
- You can change models without an app update. Decide tomorrow to switch from Sonnet to Haiku for cost — one line of backend code, no App Store review.
- You own the conversation history if you want to (for premium features, analytics, training data).
- You can A/B test prompts, swap providers, layer in caching — all without touching the iOS app.
This is why every serious AI-iOS app routes through a backend instead of calling the model API directly. It's the architectural pattern, not just a security best practice.
Common patterns that work well on Railway
- Web backend + Postgres. The bread-and-butter.
- Web backend + Postgres + Redis cache. When you start needing rate limiting or session storage.
- Web backend + worker service + Postgres. The worker handles background jobs (sending push notifications, processing uploads, scheduled cleanup) while the main backend handles user-facing requests.
- Cron service — a Railway service that runs on a schedule (daily reports, weekly cleanup).
- Multiple services per project — if your app has a public API and a private admin dashboard, run them as two services in the same project.
When to grow out of Railway
Honest signals you've outgrown the platform:
- Your monthly Railway bill is consistently above $500-$1000, and you've optimized as much as you can.
- You need a service Railway doesn't offer (very large databases, niche message queues, specific networking).
- You need compliance certifications (HIPAA, SOC 2 in specific shapes) that Railway can't provide at your scale.
- You need region-specific deployment Railway doesn't offer.
- You need >100 services or extreme scale.
Most apps never hit these. For the ones that do, the migration path is usually AWS / GCP — covered in their respective deep dives.
Tips you only learn after a few months
- Pin Node.js version in your
package.jsonengines field. Railway will use your specified version instead of guessing. - Use service references for internal calls. Service-to-service calls within a project go over a fast private network. Use
${{api.RAILWAY_PRIVATE_DOMAIN}}instead of the public URL. - Health checks matter. Implement
GET /healthreturning 200 and configure Railway to use it. Prevents broken deploys from getting traffic. - Set
NODE_ENV=productionmanually. Many libraries change behavior based on it. - Watch deploy logs the first time. Build errors are easier to diagnose live than after-the-fact.
- Use the GraphQL API for automation. Railway's full functionality is accessible via API; useful for CI/CD or custom dashboards.
- Replicate envs precisely when creating staging. Mismatched env vars between staging and production is the most common "works on staging, broken in production" cause.
- Set CPU and memory limits on services you suspect are leaky. Prevents one bad deploy from eating your whole project's resources.
For the overview, see Backend Servers Explained. For comparison, see Railway vs AWS, AWS Deep Dive, and Firebase Deep Dive.
- Railway — Official documentation
- Railway — Pricing
- Nixpacks — Railway's auto-detect builder