ARTICLE
Projects

A complete business system on Cloudflare D1: five services, one database

5 August 2026Majed Alandajani

Most of what is written about Cloudflare D1 is a getting-started guide: create a database, run a query, print the result. I could not find anything answering the question I actually had: can it run a real business, with contracts, invoices and customer data?

This is what I built, and my answer after operating it.

What actually runs

Five independent services sharing one database:

  • a static bilingual site
  • a services store
  • a blog rendered from the database
  • an API taking orders and payments
  • an operations panel behind an identity gate

The database: 33 tables built through 18 ordered migrations, with 7 triggers enforcing constraints the application cannot bypass.

Decision one: one database, not one per service

The usual alternative is a database per service. I rejected it for one reason: the truth fragments. The order lives in one place, the customer in another, the contract in a third, and then you need synchronisation between them, and synchronisation fails quietly.

With one database, a client's state is a single fact every service reads. The price I paid: every service can technically read everything, so protection comes from the access gate rather than from database isolation.

Decision two: guarantees in the database, not in the code

My audit log cannot be updated or deleted. Not because the code declines to, but because a trigger refuses:

I covered this in detail in an audit trail the application cannot rewrite: a trigger in the database aborts any update or delete against the log.

The difference matters: a constraint in code falls with the first bug. A constraint in the database survives the application above it being entirely wrong.

Decision three: the partial unique index

Some rows carry an external key and some do not. A plain unique index will reject the second row holding NULL on some engines. The fix:


CREATE UNIQUE INDEX ux_posts_sheet_key ON posts(sheet_key) WHERE sheet_key IS NOT NULL;

Any number of NULLs coexist, and the key stays unique wherever it exists.

The traps that cost me time

Do not mix ? and ?1 in one statement. SQLite reuses index 1 and you get a wrong binding with no error.

Two time formats exist between the CLI and the evidence tables, and the comparison is textual, not chronological.

Rehearse every migration. I keep a staging database with the same schema and no customer data; the migration runs there and the schema is checked before production is even offered.

What it is not for

D1 is SQLite. Not for heavy concurrent writes, not for very large data, not for heavy analytical queries. It is right for what SQLite is right for: many reads, reasonable writes, structured data, which describes most business systems.

The conclusion

Yes, it works. Provided your guarantees live in the schema rather than in your intentions, and every migration is rehearsed before production.

What choosing an edge database actually decides

The decision is not purely technical. A database at the edge means you do not run a server, which removes from your schedule: upgrades, security patches, disk monitoring, capacity reservation, and a fixed monthly bill you pay whether it is used or not.

Its price: you work inside constraints you do not control. Execution time is capped, there is no filesystem, the runtime is not complete, and some libraries will not run. So verify before you build, not after.

The thirty-three tables: how they actually divide

Not thirty-three arbitrary tables, but five groups:

Commerce: orders, proposals, contracts, invoices, instalments, payments. Relationship: customers, their records, their private links. Operations: bookings, availability rules and their exceptions. Correspondence: inbound and outbound mail, the notification log. Governance: the audit trail, the approvals record, system settings.

The last group is what turns an application into a business system, because it answers "who did what and when" months later.

How eighteen migrations are managed without disaster

Each migration is a file numbered with leading zeros, so alphabetical order gives chronological order.

And an applied migration is never edited. A correction is a new migration, or your database diverges from every other environment's.

And each is rehearsed on a staging database before production, with a schema check afterwards, because a migration can succeed and leave a database that cannot answer.

When this choice is wrong

Very heavy concurrent writes to the same row. Long heavy processing. Data at a scale SQLite does not suit.

If one of those describes you, the choice is wrong rather than the implementation.

Related reading

Have a project in mind?
Tell me what you want to build. Your first 15 minutes of consulting are free.
Book a consultation