Skip to content

Hosting & Infrastructure

Frontend — Cloudflare Pages

All four frontend apps are deployed to Cloudflare Pages. Deployments are triggered automatically by GitHub Actions on push to main.

AppCF Pages ProjectSubdomains
Marketing Websitesemalink-websitesemalink.africa
Customer Web Appsemalink-appapp / staging-app / test-app .semalink.africa
Internal Admin Appsemalink-adminadmin.semalink.africa
Agent Portalsemalink-agentsagents.semalink.africa

Why Cloudflare Pages:

  • Zero marginal cost for static apps
  • Global CDN — fast for East African users
  • Instant rollbacks via the Cloudflare dashboard
  • Preview deployments for PRs at no extra cost
  • Integrates with Cloudflare Access for Zero Trust access control

Environment Strategy (Customer Web App)

BranchEnvironmentSubdomain
main → prod jobproductionapp.semalink.africa
main → staging jobstagingstaging-app.semalink.africa
main → test jobtesttest-app.semalink.africa

Three separate GitHub Actions workflows run on each push to main, each targeting a different Cloudflare Pages environment with different API base URLs injected at build time.


Backend — DigitalOcean Droplet (Docker)

The main API runs on a DigitalOcean droplet as a Dockerised Node.js application. All services for a given environment are defined in a single Docker Compose file and run on one VM.

EnvironmentDropletDomain
Staging209.38.197.79 (Frankfurt, FRA1)staging-arc.semalink.africa
TestTBDtest-arc.semalink.africa
ProductionTBDarc.semalink.africa

Services per Droplet

Each droplet runs three Docker containers:

ContainerImageRole
caddycaddy:2-alpineReverse proxy — TLS termination, forwards to API on port 3000
rabbitmqrabbitmq:3.13-management-alpineMessage queue — AMQP broker for async jobs
apiBuilt from DockerfileFastify API — Node.js 22, CommonJS, port 3000 (internal only)

Data Services (External)

Database and cache are fully managed external services — no containers for them:

ServiceProviderRegionNotes
PostgreSQLNeon (serverless)Frankfurtstaging branch; connection pooling via PgBouncer (-pooler hostname)
RedisUpstashFrankfurtTLS required (rediss://); free tier for staging

Why external data services:

  • Neon's branching model allows staging to have a live fork of the production schema with isolated data, without managing migrations across a separate Postgres cluster
  • Upstash provides managed Redis with TLS and a generous free tier, removing the need to run a Redis container on the droplet

Why a Droplet Instead of App Platform

The API uses RabbitMQ as a local message broker (no managed RabbitMQ in the same region on DigitalOcean's managed offerings). A droplet running Docker Compose gives full control over the service topology without additional cost. The tradeoff is manual OS/Docker maintenance compared to App Platform's managed runtime.


Cloudflare Zero Trust — API Access

Backend APIs (arc.semalink.africa, dlr.semalink.africa) and the internal docs site are protected by Cloudflare Access.

The Customer Web App frontend includes the CF-Access-Client-Id and CF-Access-Client-Secret headers on every API request (injected at build time as environment variables). This prevents direct access to the API without a valid service token.

See Zero Trust & API Access for full configuration details.


DNS

All subdomains are CNAME'd to their Cloudflare Pages or DigitalOcean target. DNS is managed through Cloudflare.

See DNS Records for the full record list.


Future Considerations

  • Direct MNO connections via SMPP — evaluate once volume makes Celcom Africa's wholesale rate the bottleneck
  • Dedicated DigitalOcean droplets — if App Platform limits become a constraint for RabbitMQ or PostgreSQL at high volume
  • Multi-region — DigitalOcean regions in Frankfurt or Singapore for latency-sensitive enterprise customers outside East Africa
  • Kafka — introduce only when the modular monolith is split into separate services that need event streaming between them

Internal use only — Sema Link Engineering