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