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.
Ingest / Auth / Orchestration Layer
Section titled “Ingest / Auth / Orchestration Layer”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).
Credit Balance / Hold-Settle Mechanism
Section titled “Credit Balance / Hold-Settle Mechanism”Choice: Direct atomic writes from the Worker to PlanetScale Postgres (UPDATE ... WHERE balance - held >= :amount).
Model Gateway
Section titled “Model Gateway”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).
Ledger Database
Section titled “Ledger Database”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).
Domain & Service Split
Section titled “Domain & Service Split”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 |
Dashboard Auth
Section titled “Dashboard Auth”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-apiWorker, not the external-facingapps/apiWorker. 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.
Async / Post-Response Work
Section titled “Async / Post-Response Work”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.
Marketing Site
Section titled “Marketing Site”Choice: Astro (already built) — lives in a separate repo with its own deploy target, not part of this monorepo.
Dashboard Frontend
Section titled “Dashboard Frontend”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.
Niobi Payments Static IP Handling
Section titled “Niobi Payments Static IP Handling”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.
Deployment Infrastructure
Section titled “Deployment Infrastructure”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.
Repository Structure
Section titled “Repository Structure”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.jsonBifrost’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.
Testing
Section titled “Testing”| 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.