Mountaingreen

Mountaingreen.at — Platform Migration & Data Recovery

Rebuilt an affiliate platform's backend off deprecated JavaScript frameworks onto a modern Node.js/Express stack with a proper routing layer, migrated it from a single hand-configured EC2 box onto ECS Fargate behind Terraform, and reconstructed 2,347 orders that a failed database migration had silently dropped two years earlier.

Role
Cloud Operations / Backend Engineer / Data Consultant
Period
Feb 2026–Aug 2026

01 Problem What was actually hard

The platform ran on one EC2 instance with host Nginx, manual SSL and no infrastructure-as-code — the architecture existed only in somebody's memory, and troubleshooting meant SSHing into production. Worse, a MySQL-to-PostgreSQL migration two years prior had failed quietly: 2,347 orders never arrived, commission chains were broken, and reconciling against Stripe and the bank was impossible. The platform had to keep serving traffic throughout. The application layer was its own liability: the backend ran on JavaScript frameworks that were years past end-of-life, with routing scattered through the codebase rather than declared in one place, so no request path could be reasoned about without reading everything.

02 Data Sourcing and preparation

Audited an 8-year, 2GB MySQL dump against the partial PostgreSQL migration and catalogued what was wrong: 2,347 orders present in MySQL and absent from PostgreSQL, 412 duplicate users differing only by whitespace in the email, 38 orphaned affiliates and 156 orders with no owning user. Wrote a CSV reconciliation tool that maps old IDs to new and emits SQL patches. The 2,347 orders were reconstructed field by field from email archives and the Stripe transaction history — order date from message timestamps, value from the Stripe charge, owner from the sender — and 18 months of commission payouts were then recomputed from the restored data and checked against bank deposits.

03 Models Evaluated, kept, cut

10 evaluated 6 kept 4 cut

Kept 6

  • PostgreSQL 16

    Enforceable constraints, native JSON, replication-ready, Terraform-managed

  • ECS Fargate

    Declarative deploys and auto-scaling with no instance to maintain

  • ElastiCache Redis

    Rate limiting, session cache and the distributed lock that fixed double-counted commissions

  • Express.js routing layer

    One declarative router with middleware composition, replacing routing logic scattered across the old framework's conventions

  • Node.js 20 LTS + modern Express stack

    Replaced end-of-life JavaScript frameworks; supported, patchable, and hireable-for

  • Terraform + GitHub Actions

    Whole environment reproducible from the repository; every change has an audit trail

Cut 4

  • The legacy JS framework stack

    Past end-of-life, unpatched CVEs, and no upgrade path that did not amount to a rewrite anyway

  • MySQL 8 on EC2

    A decade of accumulated drift, no replication, manual backups

  • EC2 + host Nginx

    Single point of failure, SSH deploys, no scaling path

  • CloudFormation

    Less expressive than Terraform for the module structure this needed

04 Architecture How it fits together

The backend was rewritten onto a current Node.js LTS with Express as the routing layer: every route declared in one place, cross-cutting concerns (auth, validation, rate limiting, error handling) expressed as composable middleware instead of being reimplemented per handler, and controllers kept thin over a service layer. That is what made the rest tractable — a request path you can read is a prerequisite for a migration you can verify. An ALB terminates TLS and fronts two Fargate services — a stateless Node.js Express API using JWT so nothing lives in server memory, and a React SPA served from nginx:alpine behind CloudFront. Data sits in RDS PostgreSQL 16 with read replicas carrying the reporting queries away from the primary, ElastiCache Redis holds rate-limit counters and distributed locks, and S3 takes user uploads. Everything below the application — VPC, subnets, security groups, ALB rules, scaling policies, secrets — is Terraform. GitHub Actions builds the image, runs integration tests against real PostgreSQL and Redis containers, and rolls the task definition forward with no downtime.

Architecture flow: Users then ALB then Fargate services then RDS PostgreSQL 16 then Terraform + GitHub Actions 01 Users 02 ALB TLS via ACM, WAF rules CloudFront cache 03 Fargate services Express API (2–20 tasks) · React SPA on nginx ElastiCache Redis S3 uploads 04 RDS PostgreSQL 16 read replicas for reporting 05 Terraform + GitHub Actions every change reproducible from the repo

05 Production Deployment and operation

Cut over gradually: Terraform stack in staging, then a canary running 5% of traffic on Fargate alongside EC2, then full cutover and decommission. Backend tasks auto-scale on CPU and request rate; Fargate spot capacity carries the baseline. CloudWatch dashboards and Datadog APM cover latency, error rate and cross-service tracing, with PagerDuty on call. Three problems shaped the final design: a race that double-counted commissions (fixed with a Redis lock around the critical section), connection-pool exhaustion under spikes (fixed with pgBouncer in transaction mode) and slow Fargate cold starts (fixed by keeping a warm minimum and widening the health-check grace period).

06 Deep dive The long version, in full

Problem

Mountaingreen operated a growing affiliate/MLM platform for cannabis plant leasing, serving hundreds of users managing orders, commissions, and KYC compliance. The infrastructure was ancient:

  • MySQL 8 monolith on a single EC2 instance with host Nginx (manual SSL, snowflake configs)
  • No IaC — infrastructure in someone’s head; SSH into production to troubleshoot
  • Data integrity crisis — A failed MySQL->PostgreSQL migration 2 years prior left 2,347 orders orphaned in the old database; commission calculations broken; Stripe/bank reconciliation impossible
  • No auto-scaling — Single EC2 host; any spike meant downtime or manual intervention

The business was growing (500+ active users), but the infrastructure couldn’t scale. The team needed:

  1. Recover lost data (or face audit liability)
  2. Migrate to PostgreSQL + ECS Fargate (serverless, auto-scaling)
  3. Implement GitOps (automated deployments, zero manual SSH)
  4. Keep the platform live during cutover

Data Preparation

The data recovery was a forensic challenge. We exported MySQL (2GB, 8 years of transactions) and compared against the partial PostgreSQL migration:

Identified inconsistencies:

  • 2,347 orders in MySQL but missing from PostgreSQL (old migration script failed silently)
  • 412 duplicate user records (emails differing by whitespace)
  • Commission payouts calculated as of Sep 2023 but never exported; no record after cutover
  • Foreign key mismatches (38 orphaned affiliates, 156 orders without users)

Recovery steps:

  1. Wrote CSV reconciliation tool: export MySQL -> map to PostgreSQL IDs -> generate SQL patches
  2. Manual reconstruction: For 2,347 lost orders, cross-referenced email archives + Stripe API to recover:
    • Order date (from email timestamps)
    • Order value (from Stripe transaction history)
    • User ID (from email sender)
    • Commission metadata (calculated retroactively)
  3. Re-ran commission calculations for 18 months (Mar 2024–Aug 2025) using recovered data; verified against bank deposits

Result: All data recovered and reconciled. PostgreSQL now source-of-truth.

Models Evaluated

Layer Option Result Note
Database MySQL 8 Cut 10-year tech debt; no replication; manual backups
PostgreSQL 16 Kept JSON support, constraints, RDS-native, Terraform-friendly
Compute EC2 + host Nginx Cut Single host; manual deployments; no scaling
ECS Fargate Kept Serverless, auto-scaling, stateless tasks, GitOps-ready
Cache ElastiCache Redis Kept Rate-limiting, session store, distributed locks
IaC CloudFormation Cut YAML-heavy, limited expressiveness
Terraform Kept Declarative, modular, strong community for AWS
CI/CD Manual (SSH) Cut Error-prone, audit trail missing
GitHub Actions Kept GitOps, declarative workflows, tight GitHub integration

Architecture

High-level flow:

Internet (users)
  v
ALB (AWS, HTTPS via ACM, rate-limiting via WAF)
  ├─ /api/* -> ECS Fargate service: backend
  │           (Node.js Express, stateless, JWT auth, scales 2–20 tasks)
  └─ /*      -> ECS Fargate service: frontend
             (React SPA on nginx:alpine, CDN-cached via CloudFront)

             ├─ RDS PostgreSQL 16 (+ read replicas for reports)
             ├─ ElastiCache Redis (rate-limit store, session locks)
             └─ S3 (user uploads: KYC documents, etc.)

Backend (Node.js Express):

  • Modular OOP design: src/modules/<domain> (e.g., orders, users, commissions, support)
  • Stateless: JWT tokens (no sessions on server), all state in PostgreSQL/Redis
  • Health check: /healthz (ALB uses this for auto-scaling decisions)
  • Graceful shutdown on SIGTERM (100ms timeout for in-flight requests, then close)

Frontend (React + Vite):

  • SPA: served from nginx:alpine container
  • nginx config: SPA routing (all 404s -> index.html), cache headers (immutable for /dist/, no-cache for HTML)
  • Vite build: code-splitting, tree-shaking, <200KB JS bundle

Database (PostgreSQL 16):

  • Schema: users, orders, affiliates, commissions, support_tickets, transactions, audit_log
  • Foreign keys enforced; trigger-based commission recalc (on order status change)
  • Read replicas: run heavy analytical queries (reports, reconciliation) without hitting primary
  • Automated backups: daily snapshots to S3 (30-day retention)

Cache (Redis):

  • Rate-limiting: key = rate:${userID}:${endpoint}, TTL = 60s
  • Session locks: distributed lock on order updates (prevent race conditions)
  • Ephemeral: if Redis unavailable, fallback to in-memory map (loses cross-process state, but service stays up)

Infrastructure (Terraform):

  • VPC with public/private subnets, NAT gateway, security groups (ALB allows 80/443, tasks allow 3001/5173)
  • ALB: target groups for backend (port 3001) and frontend (port 80); health checks every 30s
  • Auto-scaling policy: backend scales when CPU > 70% or requests/task > 1000; frontend fixed at 2 tasks
  • RDS: Multi-AZ, automated failover, encryption at rest
  • Secrets Manager: DB credentials, API keys (rotated quarterly)

CI/CD (GitHub Actions):

On: push to main
  1. Build backend Docker image
  2. Run integration tests (PostgreSQL + Redis in containers)
  3. Push image to ECR
  4. Update ECS task definition
  5. Deploy to Fargate (rolling update, 0 downtime)
  6. Smoke tests against production (login, place order, check commission)
  7. Post Slack notification

Production & Scale

Deployment timeline:

  • Feb 2026: Data recovery completed; Terraform infrastructure stood up in staging
  • Mar 2026: Canary deployment (5% of traffic to Fargate) — ran parallel with EC2 for 2 weeks
  • Apr 2026: Full cutover to Fargate; EC2 decommissioned
  • May–Aug 2026: Stability & optimization (Redis tuning, CloudFront caching, auto-scaling tweaks)

Results:

  • Deployment time: 1.5h manual (SSH, docker pull, restart) -> 8min automated (GitHub Actions)
  • Data integrity: 100% (all 2,347 lost orders recovered & reconciled)
  • Uptime: 99.8% (6h downtime = intentional DB failover testing; no unplanned incidents)
  • Scaling: Auto-scales from 2 to 20 backend tasks within 3 minutes during traffic spikes
  • Cost: 42% reduction (Fargate spot instances vs EC2 always-on)

Monitoring:

  • CloudWatch dashboards: latency (P50/P95/P99), error rates, DB connections, Redis hit ratio
  • Datadog APM: distributed tracing (request spans across backend/DB/Redis)
  • PagerDuty: on-call rotation; automated alerts for error rate >1%, latency P95 >2s
  • Weekly review: analyze slow queries (RDS Performance Insights), optimize indexes

Challenges & resolutions:

  1. Race condition on order updates: Multiple API calls could increment commission twice
    • Solution: Distributed lock (Redis with key expiry); serialized critical sections
  2. PostgreSQL connection pool exhaustion: During traffic spikes, connection pool drained
    • Solution: Tuned pgBouncer (transaction pooling mode); added connection monitoring
  3. Cold start on Fargate: New tasks took 8s to be healthy (Node.js startup)
    • Solution: Pre-warming tasks (maintain min 2 always running); health check grace period = 30s

Future work:

  • Migrate frontend to Next.js SSR (for SEO + performance)
  • Add Redis Cluster (from standalone instance) for higher throughput
  • Implement CQRS pattern (read replicas for real-time analytics)

Result: Mountaingreen now runs on modern, scalable infrastructure. Data is recovered and auditable. Deployments are automated (0 downtime). The team can focus on features instead of ops toil.

orders recovered and reconciled
2,347
deploy time
1.5h by hand to 8min automated

Stack

  1. Runtime & services What holds the connection open
    • Node.js (Express, modular OOP)
    • Docker (nginx:alpine, Node.js)
  2. Data & state What is remembered
    • PostgreSQL 16 (RDS, read replicas)
    • Redis (ElastiCache)
  3. Cloud & delivery What it runs on
    • Terraform
    • GitHub Actions
    • AWS (ECS Fargate, ALB, RDS, CloudFront, S3, WAF, Secrets Manager)
  4. Interfaces & integrations What people and other systems touch
    • React 18 (TypeScript, Vite)