Savannah Cloud

Architecture

How Savannah Cloud components fit together.

System Overview

┌──────────────────────────────────────────────────────┐
│                  Client / Deployed App               │
│    (API Key · JWT · MCP tool call · Dashboard UI)    │
└────────────────────────┬─────────────────────────────┘

               ┌─────────▼──────────┐
               │   Go API Server    │  :8080
               │  auth · jobs       │
               │  deploy · domains  │
               │  email · sms · mcp │
               └──┬──────────────┬──┘
                  │              │
        ┌─────────▼──┐   ┌───────▼────────┐
        │   Redis    │   │   PostgreSQL   │
        │  Job Queue │   │  orgs · users  │
        │  Sessions  │   │  jobs · domains│
        └────────────┘   │  deployments   │
                         │  api_keys      │
                         └───────────────-┘

        ┌─────────▼──────────┐     ┌─────────────────┐
        │  Worker Pool (x4)  │     │  PowerDNS       │
        │  send_email        │     │  (custom domains │
        │  send_sms          │     │   ns1/ns2        │
        │  webhook · deploy  │     │   savannahcloud) │
        └────────────────────┘     └─────────────────┘
                  │  WebSocket
        ┌─────────▼──────────────────────┐
        │  React Dashboard  :80          │
        │  Deployments · Domains · Email │
        │  SMS · Pipelines · Team · Billing│
        └────────────────────────────────┘

Components

Go API Server (:8080)

The central hub. Handles:

  • Authentication — JWT issuance, bcrypt, API key validation, Redis session cookies
  • Job ingestion — validates, stores in Postgres, enqueues into Redis
  • Deployments — framework detection, Docker build/run, port assignment (9100–9999)
  • Custom domains — PowerDNS zone management, background DNS verifier goroutine
  • Emailsend_email job handler via Resend API
  • SMSsend_sms job handler + POST /sms/send via Africa's Talking
  • MCP server — JSON-RPC 2.0 over POST /mcp, SSE stream at GET /mcp/sse
  • Metrics — Prometheus counters at /metrics

Redis

Dual purpose:

  • Job queueLPUSH / BLPOP (FIFO), 4 concurrent worker goroutines
  • Sessions — Redis-backed HttpOnly cookie sessions with sliding 30-min TTL

PostgreSQL

TablePurpose
jobsJob records with status, payload, retry count
job_logsPer-attempt execution log lines
orgsOrganizations (tenants)
usersUser accounts with bcrypt-hashed passwords
api_keysHashed sc_... keys scoped to an org
invitesPending team invitation tokens (72h, single-use)
domainsCustom domain records with DNS verification state
deploymentsApp deployment records, status, env vars

Worker Pool (4 goroutines)

Each goroutine runs a BLPOP loop with 5s timeout:

  1. BLPOP from Redis — blocks until job arrives
  2. Marks job processing in Postgres
  3. Executes handler for job.Type
  4. On success → marks completed, writes job_logs
  5. On failure → increments retry counter; re-enqueues or marks failed

PowerDNS Auth 4.9

Manages DNS for custom domains delegated to Savannah's nameservers:

  • ns1.savannahcloud.com / ns2.savannahcloud.com
  • REST API at :8081 (internal)
  • Background goroutine resolves domains via Google DNS (8.8.8.8) every 5 min
  • Sets verified=true when A record matches VPS IP 178.105.152.34

Cloudflare R2

Stores deployment build artefacts. Configured via R2_ENDPOINT, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET.

React Dashboard (:80 production)

RouteFeature
/dashboardLive job feed, WebSocket updates, quick stats
/deploymentsApp deployments — create, logs, env vars, redeploy
/domainsCustom domain management + DNS verification status
/email3-column inbox UI — compose, sent folder
/smsSMS compose + delivery history
/teamMembers, invite by email, role management
/billingPlan + usage + Paystack integration
/api-keysCreate and revoke API keys

Data Flow: Job Lifecycle

POST /jobs

   ├─ Auth middleware (JWT / API Key / Session) ──→ 401/403 if invalid

   ├─ Insert job row (status=pending) into Postgres

   ├─ LPUSH job ID onto Redis queue

   └─ Return 202 Accepted


   Worker BLPOP loop picks up job

          ├─ UPDATE status=processing

          ├─ Execute handler (send_email, send_sms, webhook, deploy, ...)

          ├─ Success → UPDATE status=completed, INSERT job_logs

          └─ Failure → increment retries
                       if retries >= MaxRetries → UPDATE status=failed
                       else re-enqueue with backoff

Docker Compose Services

ContainerRolePort
riftcloud-app-1Go API + Worker pool8080
riftcloud-dashboard-1React UI (Nginx)80
riftcloud-postgres-1PostgreSQL 165432
riftcloud-redis-1Redis 76379
riftcloud-pdns-1PowerDNS Auth 4.953, 8081

Security

  • Passwords hashed with bcrypt (cost 12)
  • JWTs signed HS256, expire 30 days
  • API keys stored as SHA-256 hashes; raw key shown once at creation
  • Invite tokens single-use, expire 72 hours
  • Sessions: Redis-backed HttpOnly cookie, sliding 30-min TTL
  • Postgres: trust for Docker 172.x.x.x network, scram-sha-256 for external connections

On this page