Skip to content

Router.Africa — Tech Stack

What’s being used, by component. For the reasoning behind each choice — alternatives considered, why they were rejected — see the companion Decisions Log; this document is a clean reference of what’s settled, not the audit trail.


Language/runtime: Node/TypeScript, deployed as Cloudflare Workers.

Routing: Hono, for this Worker’s own routes — ingest, hold/settle, the Bifrost call. Better Auth and the dashboard’s backend routes live in a separate Worker (apps/dashboard-api, see Domain & Service Split below).


Choice: Direct atomic writes from the Worker to PlanetScale Postgres (UPDATE ... WHERE balance - held >= :amount).


Choice: Bifrost, unchanged, on the existing K3s droplet. Called from the Worker via direct HTTP, synchronously, in the request path. Each Router.Africa API key maps 1:1 to a Bifrost virtual key (rate limits, budgets, model allowlists enforced there; configuration held in Postgres).


Choice: PlanetScale Postgres, via Cloudflare Hyperdrive.

Pricing model: flat cluster-tier pricing (billed by instance size, to the millisecond), not per-read/per-write billing. HA tier (~$148/month+) is the production floor; exact tier depends on load-test results under sustained write concurrency (~1,000 writes/sec at stated peak volume).


Choice: Four separate origins, each with its own Worker/deploy where applicable:

Origin Serves Worker Auth CORS
router.africa Marketing site (Astro, separate)
app.router.africa Dashboard SPA + its own backend apps/dashboard-api Better Auth (session/cookie) Scoped tightly to this one origin
api.router.africa External, OpenAI-compatible developer API apps/api API key (Bearer) None — server-to-server only
admin.router.africa Staff-only admin console (frontend: apps/admin, separate build and deploy) apps/dashboard-api (shared backend, admin routes in their own route group with their own middleware) Better Auth, staff flag, separate session cookie Scoped tightly to this one origin

Choice: Better Auth (open-source, self-hosted, MIT-licensed) with Hono as the required routing layer on Workers.

Stack implications:

  • Runs natively on Node — no compatibility concern there.
  • On Cloudflare Workers, Better Auth’s supported path requires Hono as the routing layer — deployed in the dedicated apps/dashboard-api Worker, not the external-facing apps/api Worker.
  • better-auth-cloudflare (community-maintained, active as of April 2026) provides a ready integration across Workers, Hyperdrive, D1, KV, and R2 — directly compatible with the existing PlanetScale-via-Hyperdrive setup.

Costs accepted: no managed infrastructure (Router.Africa owns uptime and security patching for this piece), API stability across minor versions is still described as evolving, and there’s no SOC 2 certification to point to if an enterprise customer’s procurement process asks for one later.


Choice: Cloudflare Queues, consumed via a queue() handler inside apps/dashboard-api — not a standalone Worker.

Trade-off accepted: couples the consumer’s deploy to apps/dashboard-api’s deploy; worth splitting back into its own Worker later if analytics-processing volume needs to scale independently of dashboard traffic.


Choice: Astro (already built) — lives in a separate repo with its own deploy target, not part of this monorepo.


Choice: Vite, reusing an existing frontend-only service you’d previously built.

Status: Stack settled, adopted from the Niobi web rewrite’s frontend architecture:

Piece Choice
Component organization packages/ui-web (generic, app-agnostic, shadcn-based primitives, ships with Storybook stories) vs. apps/dashboard/src/components/ (Niobi-domain-bound, reaches for app assets, co-located Vitest tests instead of stories)
Component system shadcn/ui (Radix + Tailwind)
Server state TanStack Query
Client state Zustand
HTTP client axios
Testing Vitest + React Testing Library (unit), Playwright (e2e — prioritized for the credit top-up/balance flow)
Feature flags Lightweight, env/config-based, no dedicated flagging service
Client-side routing TanStack Router
Preview deploys Cloudflare Pages preview deployment per PR

Explicitly not adopted: manually-typed API client / no OpenAPI codegen; Sanctum bearer token, pusher-js/laravel-echo, Jenkins→Docker→Rancher pipeline; shared RBAC/permission model with a mobile app; two-tier code-splitting strategy; TanStack Start.


Problem: Niobi’s payments integration (used for the credit top-up flow) requires server-to-server calls from a static, allowlist-able IP. Cloudflare Workers don’t have one by default.

Choice: A small, separate relay service (apps/niobi-proxy) on the existing K3s droplet. apps/dashboard-api calls this internal endpoint over a private connection (e.g. Cloudflare Tunnel); the relay makes the actual outbound call to Niobi using the droplet’s stable IP.

Scope, deliberately minimal: a relay, not a service with business logic. Its job is: verify the caller, forward the request to Niobi with credentials, return the response. Business logic (what a top-up means, ledger effects) stays in apps/dashboard-api.

Language: TypeScript — enables sharing request/response types with apps/dashboard-api via packages/types, and keeps Router.Africa’s own code (excluding the adopted Bifrost binary) to one language.

Security requirements:

  • Caller verification — a shared secret or mTLS over the Cloudflare Tunnel connection, not an open HTTP endpoint.
  • Niobi credentials held here, not in apps/dashboard-api — separate secrets scope.

Staging support: must be able to mock Niobi calls (env-flag-driven mock mode within the same service) so staging doesn’t need live payment credentials or risk real transactions.

Niobi request signing: Niobi’s official reference implementation/SDK for request signing exists and will be ported into apps/niobi-proxy (integration details to follow).

Containment requirement: all Niobi signing logic — the algorithm itself, and the credentials it depends on — lives exclusively inside apps/niobi-proxy. No other service in the stack sees or handles it directly.


Choice, split by component:

  • External API Worker (apps/api), dashboard-backend Worker (apps/dashboard-api, also handling the queue consumer): Cloudflare Workers.
  • Bifrost, apps/niobi-proxy: K3s (Rancher’s lightweight Kubernetes distribution) on the existing droplet, managed via Helm charts.

Limitation to keep in mind: K3s/Helm provide deployment flexibility (scaling pod replicas, declarative rollouts) but do not make a single droplet infinitely scalable — it remains one machine with a fixed CPU/RAM/network/disk ceiling. Scaling past that requires adding nodes.


Choice: Single Turborepo monorepo.

router-africa/
├── apps/
│ ├── api/ # Worker — external API: ingest, hold/settle, calls Bifrost (api.router.africa)
│ ├── dashboard-api/ # Worker — Better Auth, dashboard + admin backend, and queue consumer (app.router.africa, admin.router.africa)
│ ├── niobi-proxy/ # TS relay on the K3s droplet — static-IP calls to Niobi payments
│ ├── dashboard/ # Vite frontend, served alongside dashboard-api (app.router.africa)
│ └── admin/ # Vite frontend, staff only, separate build and deploy (admin.router.africa)
├── packages/
│ ├── types/ # shared request/event schemas (Zod)
│ ├── db/ # PlanetScale schema/migrations, query helpers
│ └── config/ # shared eslint/tsconfig
├── infra/
│ └── bifrost/ # K3s/Helm config for Bifrost deployment (config only, not source)
├── turbo.json
└── package.json

Bifrost’s source isn’t part of this repo — it’s an adopted binary; only its deployment config lives here if tracked as infra-as-code.

apps/dashboard and apps/dashboard-api deploy together.


Layer Tool
Unit tests Vitest
Integration tests (Worker-specific) @cloudflare/vitest-pool-workers (runs inside the actual Workers runtime via Miniflare)
DB integration tests PlanetScale branching (real Postgres, not mocked)
Concurrency correctness Dedicated test: N concurrent requests against one account — the single highest-priority test in the suite
Load testing k6

Cross-reference the Decisions Log for the reasoning behind any choice above, and for anything still open.