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.
| App | CF Pages Project | Subdomains |
|---|---|---|
| Marketing Website | semalink-website | semalink.africa |
| Customer Web App | semalink-app | app / staging-app / test-app .semalink.africa |
| Internal Admin App | semalink-admin | admin.semalink.africa |
| Agent Portal | semalink-agents | agents.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)
| Branch | Environment | Subdomain |
|---|---|---|
main → prod job | production | app.semalink.africa |
main → staging job | staging | staging-app.semalink.africa |
main → test job | test | test-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.
| Environment | Droplet | Domain |
|---|---|---|
| Staging | 209.38.197.79 (Frankfurt, FRA1) | staging-arc.semalink.africa |
| Test | TBD | test-arc.semalink.africa |
| Production | TBD | arc.semalink.africa |
Services per Droplet
Each droplet runs three Docker containers:
| Container | Image | Role |
|---|---|---|
caddy | caddy:2-alpine | Reverse proxy — TLS termination, forwards to API on port 3000 |
rabbitmq | rabbitmq:3.13-management-alpine | Message queue — AMQP broker for async jobs |
api | Built from Dockerfile | Fastify API — Node.js 22, CommonJS, port 3000 (internal only) |
Data Services (External)
Database and cache are fully managed external services — no containers for them:
| Service | Provider | Region | Notes |
|---|---|---|---|
| PostgreSQL | Neon (serverless) | Frankfurt | staging branch; connection pooling via PgBouncer (-pooler hostname) |
| Redis | Upstash | Frankfurt | TLS 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