Why we chose Cloudflare D1 over Postgres for TaxCalc
Relational data at the edge is tricky. Here is how we handled consistency without sacrificing the roughly 10ms backend budget.
Axiom Labs
Field notes from the studio
What TaxCalc.ng really needed
We built TaxCalc.ng with a hard constraint: a typical calculation should feel instant, even on a shaky Nigerian 4G connection. End-to-end latency had to stay under ~200ms, while our own backend/database work for the calculation path had a roughly 10ms budget.
The product is a focused financial app: users, sessions, saved calculations, Paystack payments, and audit trails. It wants relational modeling, transactions, constraints, and low operational overhead.
The database checklist was simple: SQL with joins and transactions, global performance for Lagos and London, and enough consistency to keep money right even if some UI paths are optimistic.
Why 'just use Postgres' wasn't enough
A single Postgres primary region adds hundreds of milliseconds of round trips for Workers running in faraway POPs; that alone blows the roughly 10ms backend budget.
Managed clusters, poolers, tunnels, and multi-region stories are solvable but shift time from "build TaxCalc.ng" to "babysit infra."
Data volume is small-to-medium. Postgres headroom was overkill for v1 while the operational cost was very real.
Why D1 made sense for TaxCalc.ng
Cloudflare D1 mounts SQLite right next to the Worker, so reads and writes avoid the extra network hop to a central database.
No connection pooling, no long-lived sockets; just the D1 binding and Drizzle ORM. Our stack stays on one platform: Next.js on Pages, Workers for API, D1 for data.
We still get SQL, foreign keys, and per-request transactions, which covers TaxCalc.ng's data model without spinning up extra infra.
The consistency problem we had to solve
Writes do not instantly exist everywhere. A user could upgrade to Premium in one region and still look Free in another for a few seconds.
Overlapping writes from different Workers or retried Paystack webhooks can race each other. Financial data cannot tolerate silent duplication or lost updates.
We split invariants into "hard" (payments, entitlements) and "soft" (badges, counters) so we could design for eventual consistency without risking money.
Guardrails we layered on top of D1
Idempotent payment handling: every Paystack reference is unique and enforced in the schema. Webhook retries become no-ops instead of double-charges.
Application-level locks on critical flows: either routing certain writes to a single colo or using version/nonce columns so conflicting updates are rejected and retried.
Treating non-critical flags as caches: JWT premium flags or last-seen summaries can drift briefly; audit trails remain the source of truth.
What we gained and accepted
Single-digit millisecond DB calls in practice and a unified Cloudflare deployment story.
Sufficient relational power for TaxCalc.ng with minimal operational overhead.
We accepted eventual consistency quirks, added app-level safeguards, and acknowledged that migrating to a heavier database later is a worthwhile future project if growth demands it.
If this is your problem
A practical next step
If this article sounds like the thing you are trying to fix, these are the closest Axiom Labs services to start from.
Key takeaway
We did not pick "the best database"; we picked the one that met the backend budget and designed around its constraints.
Useful? Send it to someone scoping a similar build.
Related Signals
Stay in the loopAPI design for products that are starting to sprawl
APIs usually become painful when more systems depend on them than the original backend shape anticipated. Here is how to reset the boundary before it calcifies.
Studio Operating System v1
Defining the constraints for Axiom Labs. Why we are choosing a 'Systems First' approach over generalist agency work.
Follow the Studio
More field notes and build logs.
