Skip to content

Deployment Guide

This guide covers how to deploy Playupi from scratch at each growth phase, using the recommended services from the Architecture and Scaling docs. Each phase includes both manual step-by-step instructions and scripted/AI-assisted alternatives where applicable.


Prerequisites (All Phases)

Before any deployment, ensure you have:

ToolInstall
Node.js 20+nvm install 20 or nodejs.org
npmComes with Node.js
Gitgit-scm.com
Vercel CLInpm i -g vercel
Prisma CLIIncluded in project deps (npx prisma)

Repository Setup

Terminal window
git clone https://github.com/lait-kelomins/playupi.git
cd playupi
npm install

Environment Variables

All phases use a .env.local file for local development. Production secrets are set through each platform’s dashboard or CLI.

Terminal window
# .env.local template
DATABASE_URL="postgresql://..."
DATABASE_URL_POOLED="postgresql://..."
UPSTASH_REDIS_REST_URL="https://..."
UPSTASH_REDIS_REST_TOKEN="..."
JWT_SECRET="..."
NEXT_PUBLIC_SITE_URL="https://playupi.com"

Never commit .env.local or any file containing secrets to git.


Phase 1: MVP — $0/mo (Free Tiers)

Target: 0–5K DAU, < 10M events/mo, < 500 games, 1–2 person team.

Architecture Recap

Vercel (Free Tier)
├── Next.js SSR (frontend user + admin)
└── Serverless API Routes (/api/v1/*, /api/admin/*)
│ │
Neon PostgreSQL Upstash Redis
(free tier) (free tier)

Supporting services: Cloudflare (DNS), Sentry (errors), Betterstack (uptime), PostHog (analytics).


1.1 Database — Neon PostgreSQL

Manual

  1. Go to neon.tech and create a free account
  2. Create a new project named playupi
  3. Select the region closest to your users (e.g., eu-central-1 for Europe)
  4. Copy both connection strings from the dashboard:
    • Direct (for migrations): postgresql://user:pass@ep-xxx.neon.tech/playupi?sslmode=require
    • Pooled (for application): postgresql://user:pass@ep-xxx-pooler.neon.tech/playupi?sslmode=require
  5. Add them to your .env.local:
    DATABASE_URL="postgresql://...@ep-xxx.neon.tech/playupi?sslmode=require"
    DATABASE_URL_POOLED="postgresql://...@ep-xxx-pooler.neon.tech/playupi?sslmode=require"
  6. Push the schema:
    Terminal window
    npx prisma db push
  7. Seed development data:
    Terminal window
    npx tsx scripts/seed.ts

Scripted

#!/bin/bash
# setup-db.sh — Requires NEON_API_KEY environment variable
# Install Neon CLI: npm i -g neonctl
neonctl projects create --name playupi --region-id aws-eu-central-1 --output json > /tmp/neon-project.json
PROJECT_ID=$(jq -r '.project.id' /tmp/neon-project.json)
CONN_URI=$(neonctl connection-string --project-id "$PROJECT_ID")
POOLED_URI=$(neonctl connection-string --project-id "$PROJECT_ID" --pooled)
echo "DATABASE_URL=\"$CONN_URI\"" >> .env.local
echo "DATABASE_URL_POOLED=\"$POOLED_URI\"" >> .env.local
npx prisma db push
npx tsx scripts/seed.ts
echo "Database ready."

AI-Assisted

Prompt for Claude Code: “Set up a Neon PostgreSQL database for the playupi project. Create the project via the Neon CLI, save the connection strings to .env.local, run prisma db push, and seed the database.”


1.2 Cache — Upstash Redis

Manual

  1. Go to console.upstash.com and create an account
  2. Create a new Redis database:
    • Name: playupi-cache
    • Region: same as Neon (e.g., eu-central-1)
    • Type: Regional (free tier)
  3. Copy the REST credentials from the dashboard
  4. Add to .env.local:
    UPSTASH_REDIS_REST_URL="https://xxx.upstash.io"
    UPSTASH_REDIS_REST_TOKEN="AXxx..."

Scripted

#!/bin/bash
# setup-redis.sh — Requires UPSTASH_EMAIL and UPSTASH_API_KEY
curl -s -X POST "https://api.upstash.com/v2/redis/database" \
-u "$UPSTASH_EMAIL:$UPSTASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"playupi-cache","region":"eu-central-1","tls":true}' \
> /tmp/upstash-db.json
REST_URL=$(jq -r '.endpoint' /tmp/upstash-db.json)
REST_TOKEN=$(jq -r '.rest_token' /tmp/upstash-db.json)
echo "UPSTASH_REDIS_REST_URL=\"https://$REST_URL\"" >> .env.local
echo "UPSTASH_REDIS_REST_TOKEN=\"$REST_TOKEN\"" >> .env.local
echo "Redis ready."

1.3 Hosting — Vercel

Manual

  1. Go to vercel.com and sign up with your GitHub account
  2. Click “Import Project” and select the playupi repository
  3. Set the framework preset to Next.js
  4. Set the root directory to apps/web (or wherever the main Next.js app lives)
  5. Add environment variables in the Vercel dashboard:
    • DATABASE_URL_POOLED (from Neon — use the pooled URL)
    • DATABASE_URL (from Neon — direct URL, for build-time migrations)
    • UPSTASH_REDIS_REST_URL
    • UPSTASH_REDIS_REST_TOKEN
    • JWT_SECRET (generate with openssl rand -hex 32)
    • NEXT_PUBLIC_SITE_URL = https://playupi.com
  6. Deploy:
    • Push to main branch triggers automatic deployment
    • Or deploy manually from the Vercel dashboard

Scripted (Vercel CLI)

deploy-vercel.sh
#!/bin/bash
# Link the project (first time only)
vercel link
# Set environment variables
vercel env add DATABASE_URL_POOLED production
vercel env add DATABASE_URL production
vercel env add UPSTASH_REDIS_REST_URL production
vercel env add UPSTASH_REDIS_REST_TOKEN production
vercel env add JWT_SECRET production
# Deploy to production
vercel --prod
echo "Deployed to Vercel."

CI/CD (GitHub Actions)

.github/workflows/deploy.yml
name: Deploy to Vercel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx prisma generate
- run: npm run build
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: --prod

1.4 DNS — Cloudflare

Manual

  1. Go to dash.cloudflare.com and create a free account
  2. Add your domain playupi.com
  3. Update nameservers at your registrar to Cloudflare’s NS records
  4. Add DNS records:
    TypeNameValueProxy
    CNAME@cname.vercel-dns.comDNS only (gray cloud)
    CNAMEwwwcname.vercel-dns.comDNS only (gray cloud)
  5. In Vercel, add the domain playupi.com to your project and verify

Important: Set Cloudflare proxy to DNS only (gray cloud) for Vercel. Vercel handles its own edge CDN and SSL. Orange-cloud proxying can cause certificate conflicts.


1.5 Cron Jobs — Vercel Cron + Upstash QStash

Vercel Cron (free tier) supports 2 cron jobs with a minimum daily interval. For the hourly aggregation job, use Upstash QStash (free: 500 messages/day).

Manual

  1. Add to vercel.json:
    {
    "crons": [
    {
    "path": "/api/cron/rank",
    "schedule": "0 */6 * * *"
    }
    ]
    }
  2. Create the route handler app/api/cron/rank/route.ts with a CRON_SECRET check
  3. For the hourly aggregation job, set up Upstash QStash:
    • Go to console.upstash.com/qstash
    • Create a schedule that calls https://playupi.com/api/cron/aggregate every hour
    • Add QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY to Vercel env vars

1.6 Monitoring & Analytics

Sentry (Error Tracking)

  1. Go to sentry.io and create a free account
  2. Create a project for Next.js
  3. Install: npm install @sentry/nextjs
  4. Run npx @sentry/wizard@latest -i nextjs
  5. Add SENTRY_DSN and SENTRY_AUTH_TOKEN to Vercel env vars

Betterstack (Uptime Monitoring)

  1. Go to betterstack.com and create a free account
  2. Add monitors:
    • https://playupi.com (homepage)
    • https://playupi.com/api/v1/games (API health)
  3. Configure email alerts for downtime

PostHog (Analytics)

  1. Go to posthog.com and create a free account
  2. Create a project and copy the API key
  3. Add NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST to Vercel env vars

1.7 Database Migrations (Production)

When schema changes are needed in production:

Terminal window
# Generate a migration file
npx prisma migrate dev --name add-new-field
# Deploy migration to production (run in CI or manually)
npx prisma migrate deploy

Vercel build command should include prisma generate (not prisma migrate deploy). Run migrations separately before deploying, either manually or in a CI step.


1.8 MVP Deployment Checklist

StepCommand / ActionDone
Neon database createdDashboard or neonctl
Schema pushednpx prisma db push
Upstash Redis createdDashboard or API
Vercel project linkedvercel link
Env vars set in VercelDashboard or vercel env add
Domain configuredCloudflare DNS + Vercel domain
Sentry integratednpx @sentry/wizard
Betterstack monitors addedDashboard
PostHog integratedDashboard + env vars
SSL/HTTPS verifiedVisit https://playupi.com
Security headers verifiedCheck with securityheaders.com
Cron jobs configuredvercel.json + QStash
First deploy successfulvercel --prod or push to main

Phase 2: Growth — ~$200–500/mo

Target: 5K–50K DAU, 10M–100M events/mo, 500–5K games, 2–4 person team.

Migration triggers:

  • API p95 response time > 500ms
  • Database CPU consistently > 60%
  • Event write throughput > 1K/sec sustained
  • Hitting Vercel free tier limits (~100K invocations/mo)

Architecture Recap

Cloudflare CDN
Load Balancer (Railway / Fly.io)
┌───┴───┐
Backend x2 Backend x2
(user API) (admin API)
└───┬───┘
PostgreSQL (Primary + Read Replica)
Redis (dedicated instance)

2.1 Upgrade Vercel (or Migrate Backend)

You have two options at this phase:

Option A: Vercel Pro ($20/mo)

If API routes are still sufficient:

  1. Upgrade to Vercel Pro in the dashboard
  2. This gives you 1M serverless invocations/mo, 1TB bandwidth, and more cron jobs
  3. No code changes needed

Option B: Separate Backend on Railway / Fly.io

When you need persistent processes (background workers, WebSockets):

Manual (Railway)
  1. Go to railway.app and create an account
  2. Create a new project
  3. Add a service from your GitHub repo:
    • Set the root directory to apps/api
    • Set start command: npm start
    • Set build command: npm run build
  4. Add environment variables (same as Vercel + backend-specific ones)
  5. Add a PostgreSQL service from Railway’s template library (or keep Neon)
  6. Add a Redis service from Railway’s template library (or keep Upstash)
  7. Configure a custom domain: api.playupi.com
  8. Scale to 2 replicas from the Railway dashboard
Manual (Fly.io)
  1. Install Fly CLI: curl -L https://fly.io/install.sh | sh
  2. Sign up: fly auth signup
  3. Create a fly.toml in your API directory:
    app = "playupi-api"
    primary_region = "cdg" # Paris
    [build]
    dockerfile = "Dockerfile"
    [http_service]
    internal_port = 3000
    force_https = true
    auto_stop_machines = true
    auto_start_machines = true
    min_machines_running = 2
    [env]
    NODE_ENV = "production"
  4. Set secrets:
    Terminal window
    fly secrets set DATABASE_URL="..." UPSTASH_REDIS_REST_URL="..." JWT_SECRET="..."
  5. Deploy:
    Terminal window
    fly deploy
  6. Add a custom domain:
    Terminal window
    fly certs add api.playupi.com
Scripted (Railway CLI)
deploy-railway.sh
#!/bin/bash
# Install Railway CLI
npm i -g @railway/cli
# Login
railway login
# Create project and service
railway init --name playupi-api
# Link to repo
railway link
# Set environment variables
railway variables set DATABASE_URL="$DATABASE_URL"
railway variables set DATABASE_URL_POOLED="$DATABASE_URL_POOLED"
railway variables set UPSTASH_REDIS_REST_URL="$UPSTASH_REDIS_REST_URL"
railway variables set UPSTASH_REDIS_REST_TOKEN="$UPSTASH_REDIS_REST_TOKEN"
railway variables set JWT_SECRET="$JWT_SECRET"
# Deploy
railway up
echo "Backend deployed on Railway."

2.2 Database — Add Read Replica

Manual (Neon)

  1. Upgrade to Neon Launch plan ($19/mo — 300 compute hours, 10 GB)
  2. In the Neon dashboard, create a read replica endpoint
  3. Copy the read replica connection string
  4. Add to environment:
    DATABASE_URL_READ="postgresql://...@ep-xxx-read.neon.tech/playupi?sslmode=require"
  5. Update your backend to route read queries (game lists, search, metrics) to the read replica

Manual (Railway PostgreSQL)

  1. Create a PostgreSQL service in Railway (comes with the Starter plan at $5/mo)
  2. Enable read replicas from the Railway dashboard
  3. Railway automatically provides separate read/write connection strings

2.3 Cache — Upgrade Redis

Option A: Upstash Pay-as-you-go

Upgrade from free to pay-as-you-go in the Upstash dashboard. Cost: ~$0.2 per 100K commands. No action needed beyond enabling billing.

Option B: Dedicated Redis (Railway/Fly.io)

  1. Add a Redis service in Railway or deploy Redis on Fly.io
  2. Update REDIS_URL environment variable
  3. Restart API services

2.4 Background Workers

For hourly aggregation and ranking jobs that shouldn’t block API requests:

Manual

  1. Create a separate service in Railway/Fly.io for the worker
  2. Use a lightweight process runner (e.g., node-cron, BullMQ with Redis, or Inngest)
  3. Configure jobs:
    • Hourly: Event aggregation → DailyMetric table
    • Every 6 hours: Rank recalculation
    • Daily: Tag recomputation (New, Trendy)

Scripted (BullMQ + Railway)

Terminal window
# In apps/worker/package.json
# Start command: "node dist/worker.js"
railway service create --name playupi-worker
railway variables set REDIS_URL="$REDIS_URL" DATABASE_URL="$DATABASE_URL"
railway up

2.5 CI/CD for Growth Phase

.github/workflows/deploy-growth.yml
name: Deploy (Growth)
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm audit --audit-level=critical
migrate:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
deploy-frontend:
needs: migrate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: --prod
deploy-api:
needs: migrate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm i -g @railway/cli
- run: railway up --service playupi-api
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
deploy-worker:
needs: migrate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm i -g @railway/cli
- run: railway up --service playupi-worker
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

2.6 Growth Deployment Checklist

StepActionDone
Vercel upgraded to Pro OR backend moved to Railway/Fly.io
Backend running 2+ replicas
Database upgraded with read replica
Redis upgraded (pay-as-you-go or dedicated)
Background worker deployed
CI/CD pipeline updated for multi-service deploy
CDN caching enabled for game list endpoints
APM/monitoring upgraded (response time tracking)
Load testing performed (target: 1K concurrent users)

Phase 3: Scale — ~$1,000–3,000/mo

Target: 50K–500K DAU, 100M–1B events/mo, 5K–50K games, 4–10 person team.

Migration triggers:

  • Event volume > 10K/sec
  • PostgreSQL event table > 500GB
  • Metric query time > 2s
  • Need real-time or near-real-time ranking updates

Architecture Recap

Cloudflare CDN
Load Balancer
┌───┴────────┐
User API x4 Admin API x2
└───┬────────┘
PostgreSQL Cluster (Primary + 2 Read Replicas)
Kafka/Redpanda ──> ClickHouse (OLAP)
Redis Cluster
Meilisearch (dedicated search)

3.1 Event Pipeline — Kafka/Redpanda + ClickHouse

This is the biggest architectural change. Events stop going directly to PostgreSQL and instead flow through a streaming pipeline.

Manual (Managed Services)

Redpanda (recommended over Kafka — simpler, lower resource usage):

  1. Go to cloud.redpanda.com and create an account
  2. Create a Serverless cluster in your region
  3. Create a topic: playupi-events (partitions: 6, retention: 7 days)
  4. Copy the bootstrap servers and credentials

ClickHouse:

  1. Go to clickhouse.cloud and create an account
  2. Create a service (Development tier, ~$190/mo)
  3. Create the events table:
    CREATE TABLE events (
    id UUID,
    type LowCardinality(String),
    game_id UUID,
    session_id UUID,
    device_id String,
    platform LowCardinality(String),
    timestamp DateTime64(3),
    received_at DateTime64(3),
    context String -- JSON stored as String, extracted via JSONExtract
    )
    ENGINE = MergeTree()
    PARTITION BY toYYYYMM(timestamp)
    ORDER BY (game_id, type, timestamp)
    TTL timestamp + INTERVAL 90 DAY;
  4. Set up a Kafka/Redpanda consumer (ClickHouse has a native Kafka engine):
    CREATE TABLE events_kafka (
    id UUID,
    type String,
    game_id UUID,
    session_id UUID,
    device_id String,
    platform String,
    timestamp DateTime64(3),
    received_at DateTime64(3),
    context String
    )
    ENGINE = Kafka
    SETTINGS
    kafka_broker_list = 'xxx.redpanda.com:9092',
    kafka_topic_list = 'playupi-events',
    kafka_group_name = 'clickhouse-consumer',
    kafka_format = 'JSONEachRow';
    CREATE MATERIALIZED VIEW events_mv TO events AS
    SELECT * FROM events_kafka;

Update API event ingestion:

Change the event API route to publish to Redpanda instead of inserting into PostgreSQL:

// Before: await prisma.event.createMany({ data: events })
// After:
import { Kafka } from 'kafkajs'
const kafka = new Kafka({ brokers: [process.env.REDPANDA_BROKER!] })
const producer = kafka.producer()
await producer.send({
topic: 'playupi-events',
messages: validatedEvents.map(e => ({
key: e.gameId,
value: JSON.stringify(e),
})),
})

AI-Assisted

Prompt for Claude Code: “Refactor the event ingestion API route to publish events to Redpanda/Kafka instead of inserting directly into PostgreSQL. Set up the KafkaJS producer with the connection from REDPANDA_BROKER env var. Keep the Zod validation layer, just change the storage backend.”


3.2 PostgreSQL — Cluster with Connection Pooling

Manual

  1. Migrate to a managed PostgreSQL provider with clustering support:
    • Neon Scale ($69/mo — 750 compute hours, 50 GB, read replicas)
    • Railway with dedicated PostgreSQL
    • Supabase Pro ($25/mo + compute add-ons)
  2. Set up 2 read replicas
  3. Add PgBouncer in front of PostgreSQL (if not using Neon’s built-in pooler):
    Terminal window
    # On Railway, add PgBouncer as a separate service
    # Or use Supavisor (Supabase's built-in pooler)
  4. Update application to use read replicas for:
    • Game list queries
    • Search queries
    • Metric dashboard queries
    • Exploration queue reads

3.3 Search — Meilisearch

Manual (Meilisearch Cloud)

  1. Go to meilisearch.com/cloud and create an account
  2. Create an index named games
  3. Configure searchable attributes: title, description, author.name
  4. Configure filterable attributes: categories, platform, visibility, state
  5. Set up a sync job to push game data from PostgreSQL to Meilisearch on game create/update

Manual (Self-hosted on Fly.io)

Terminal window
fly launch --image getmeili/meilisearch:latest --name playupi-search --region cdg
fly secrets set MEILI_MASTER_KEY="$(openssl rand -hex 16)"
fly scale memory 1024 # 1GB RAM

3.4 Redis Cluster

Upgrade from single Redis to a cluster for distributed caching:

  1. Upstash Pro: Upgrade to a Pro plan with higher throughput limits
  2. Self-managed: Deploy Redis Cluster on Fly.io or Railway with 3 nodes
  3. Update the Redis client configuration to use cluster mode

3.5 Scale Deployment Checklist

StepActionDone
Redpanda cluster created and topic configured
ClickHouse service created with events table
Kafka consumer (materialized view) active
API event ingestion refactored to use Redpanda
PostgreSQL upgraded with 2 read replicas
Connection pooling configured (PgBouncer)
Meilisearch deployed and synced
Redis upgraded to cluster/pro
Aggregation jobs read from ClickHouse
Ranking recalculation runs every 15–60 minutes
Load testing performed (target: 10K concurrent users)
Monitoring dashboards for Kafka lag, ClickHouse query time

Phase 4: Platform — ~$5,000–15,000/mo

Target: 500K+ DAU, 1B+ events/mo, 50K+ games, 10+ person team.

Migration triggers:

  • Multiple geographic regions needed
  • Dev portal with third-party traffic
  • Multiple teams with independent release cycles

Architecture Recap

Cloudflare (CDN + WAF + DDoS)
API Gateway (rate limiting, auth, routing)
┌──────┼──────────┐
User API Admin API Dev Portal API
(multi-region)
PostgreSQL (Primary + Global Read Replicas)
ClickHouse Cluster (sharded)
Redis (multi-region)
Meilisearch Cluster
S3/R2 (object storage for assets)
Kafka Cluster (event bus)
Grafana + Prometheus (full observability)

4.1 Multi-Region Deployment

Manual

  1. Identify target regions based on user distribution (e.g., EU, US, Asia)
  2. Deploy API services in 2–3 regions:
    Terminal window
    # Fly.io multi-region
    fly regions add cdg iad nrt # Paris, Virginia, Tokyo
    fly scale count 3 --region cdg
    fly scale count 2 --region iad
    fly scale count 1 --region nrt
  3. Configure Cloudflare load balancing to route users to the nearest region
  4. Set up PostgreSQL global read replicas (one per region):
    • Primary in main region (writes)
    • Read replicas in secondary regions (reads)
  5. Deploy Redis in each region for local caching

4.2 Multi-Repo Migration

When team size and release cycles demand it:

  1. Split the monorepo into:
    RepoContents
    playupi-webPlayer-facing frontend
    playupi-apiPublic + Admin API
    playupi-adminAdmin dashboard frontend
    playupi-devportalDeveloper portal (frontend + API)
    playupi-sharedShared types, constants, utilities (published as npm package)
    playupi-infraTerraform/Pulumi IaC, Docker configs, CI/CD templates
  2. Publish playupi-shared as a private npm package
  3. Each repo gets its own CI/CD pipeline

4.3 Infrastructure as Code (Terraform/Pulumi)

At this phase, all infrastructure should be defined as code:

# infrastructure/main.tf (Terraform example)
module "database" {
source = "./modules/postgres"
instance = "db.m5.xlarge"
replicas = 3
regions = ["eu-central-1", "us-east-1"]
}
module "redis" {
source = "./modules/redis"
node_type = "cache.m5.large"
clusters = 2
regions = ["eu-central-1", "us-east-1"]
}
module "clickhouse" {
source = "./modules/clickhouse"
tier = "production"
shards = 3
replicas = 2
}
module "search" {
source = "./modules/meilisearch"
instances = 3
memory = "4GB"
}

AI-Assisted

Prompt for Claude Code: “Generate Terraform modules for the Playupi platform infrastructure: PostgreSQL with read replicas, Redis cluster, ClickHouse, and Meilisearch. Target AWS eu-central-1 as primary region with us-east-1 as secondary.”


4.4 API Gateway

Deploy an API gateway for centralized auth, rate limiting, and routing:

Options:

  • Kong (open-source, self-hosted on Kubernetes)
  • AWS API Gateway (managed, if on AWS)
  • Cloudflare Workers (edge-based, integrates with existing Cloudflare)
Request → Cloudflare → API Gateway → Service
├── Auth validation
├── Rate limiting
├── Request logging
└── Route to correct service

4.5 Full Observability Stack

Manual

  1. Metrics: Deploy Prometheus + Grafana (or use Grafana Cloud free tier)
    Terminal window
    # Self-hosted on Fly.io or dedicated VPS
    fly launch --image grafana/grafana --name playupi-grafana
    fly launch --image prom/prometheus --name playupi-prometheus
  2. Logging: Deploy Loki or use Betterstack Logs
  3. Tracing: Add OpenTelemetry instrumentation to all services
  4. Alerting: Configure PagerDuty or Opsgenie for on-call rotation

Key Dashboards

DashboardPanels
API HealthRequest rate, p50/p95/p99 latency, error rate, status codes
DatabaseQuery time, connections, replication lag, disk usage
Events PipelineKafka lag, ClickHouse insert rate, consumer throughput
BusinessDAU, plays, new games, ranking changes
InfrastructureCPU, memory, network across all services

4.6 Object Storage (S3/R2)

Migrate game assets off broker CDNs to your own storage:

  1. Create a Cloudflare R2 bucket: playupi-assets
  2. Set up an image processing pipeline:
    Original upload → Resize (32, 64, 128, 256, 512px) → Store in R2 → Serve via Cloudflare CDN
  3. Use Cloudflare Image Resizing or a Workers script for on-the-fly transforms
  4. Migrate existing thumbnailUrl references to R2 URLs

4.7 Platform Deployment Checklist

StepActionDone
Multi-region API deployment (2–3 regions)
Cloudflare load balancing configured
PostgreSQL global read replicas
Multi-region Redis
Monorepo split into multi-repo
Shared package published
Infrastructure as Code (Terraform/Pulumi)
API gateway deployed
Full observability (Grafana + Prometheus + Loki)
On-call rotation configured
Object storage (R2) for game assets
Dev portal deployed
Load testing (target: 100K concurrent users)
Disaster recovery plan documented and tested

Quick Reference: Services by Phase

ServicePhase 1 (MVP)Phase 2 (Growth)Phase 3 (Scale)Phase 4 (Platform)
FrontendVercel FreeVercel ProVercel ProVercel Enterprise / CDN
BackendVercel ServerlessRailway/Fly.io x2Railway/Fly.io x4Kubernetes / Fly.io multi-region
DatabaseNeon FreeNeon Launch + replicaNeon Scale + 2 replicasManaged PostgreSQL cluster
CacheUpstash FreeUpstash ProRedis ClusterMulti-region Redis
EventsPostgreSQLPostgreSQLRedpanda + ClickHouseKafka Cluster + ClickHouse Cluster
SearchPostgreSQL ILIKEPostgreSQL tsvectorMeilisearchMeilisearch Cluster
CDNVercel EdgeCloudflare + VercelCloudflare ProCloudflare Enterprise
MonitoringSentry Free + Betterstack FreeSentry + APMGrafana + PrometheusFull observability stack
AnalyticsPostHog FreePostHogPostHogPostHog + custom dashboards
AssetsBroker CDNBroker CDNR2 (optional)Cloudflare R2 + Image Resizing
Cost$0/mo~$200–500/mo~$1,000–3,000/mo~$5,000–15,000/mo

Rollback Strategy

For all phases, maintain the ability to roll back:

LayerRollback Method
FrontendVercel instant rollback (previous deployment)
BackendRailway/Fly.io previous release (fly releases, railway rollback)
DatabaseNever rollback schema — use forward migrations. Neon: branch from PITR for data
ClickHouseReplay events from Kafka retention window
RedisCache rebuild on restart (no persistent data)

Emergency Rollback

Terminal window
# Vercel — rollback to previous production deployment
vercel rollback
# Fly.io — rollback to previous release
fly releases
fly deploy --image registry.fly.io/playupi-api:v42 # specific version
# Railway — rollback via dashboard or CLI
railway rollback

Security Checklist (All Phases)

Before every production deployment, verify:

  • No secrets in git history (git log --all -p | grep -i "password\|secret\|token")
  • npm audit passes with 0 critical vulnerabilities
  • Environment variables set correctly (not hardcoded)
  • HTTPS enforced on all endpoints
  • Security headers present (check securityheaders.com)
  • Database not publicly accessible
  • Rate limiting active on public endpoints
  • Admin auth working (JWT + bcrypt)
  • Iframe sandbox attributes in place
  • CSP headers configured

See Security for the full security specification.