Almost everything I build runs on the same small stack: Cloudflare Workers for code, D1 for the database, and Paddle for payments. It lets one person run several products without managing a single server.
This post explains the stack piece by piece: why I chose Cloudflare Workers for SaaS, how D1 behaves as a product database, why I use a merchant of record instead of Stripe, and where this setup is the wrong choice.
The stack in one paragraph
A Worker handles every request and holds the business logic. Data lives in D1, which is SQLite managed by Cloudflare. Files go to R2, small cached values go to KV. Static sites deploy from GitHub: a push to the main branch publishes the site. Payments, tax and invoices go through Paddle. There are no servers, no containers and nothing to patch.
Why Cloudflare Workers
No servers to look after
A Worker is a function that runs on Cloudflare's network. I write the code and deploy it with one command. There is no operating system to update and no machine that fills up at night. For a solo developer, the time not spent on maintenance is the biggest benefit of all.
Small running costs
Workers has a free plan, and the paid plan starts at five dollars a month. Several of my own tools still run on a free plan. A small SaaS does not need infrastructure that costs more than its first customers pay.
Fast everywhere
The code runs close to the person making the request, wherever they are. I sell to a global market from Serbia, so this matters: a customer on another continent is not served from one distant data center.
The constraints are real
Workers is not a normal server, and pretending otherwise causes problems later:
- No long-running processes. Each request has a time and CPU budget. Heavy jobs need to be split up or moved to a queue.
- Not a full Node.js environment. Most libraries work, some do not. I check before I depend on one.
- No local disk. Anything that must persist goes to D1, KV or R2.
I find these limits useful. They push a product toward small, clear request handlers, which is what I want anyway.
D1: SQLite as a product database
D1 is a SQL database built on SQLite. I use it for every product that stores data.
What I like about it:
- It is plain SQL. Tables, indexes, joins. Nothing new to learn and nothing that locks the data in.
- Migrations are files. Each schema change is a numbered SQL file that I apply with the command-line tool, first locally and then in production.
- Local development matches production. I run the same database engine on my laptop.
- It sits next to the code. A Worker queries D1 directly, with no connection pool to configure.
What to know before choosing it:
- It is SQLite. There are no Postgres extensions, and some advanced SQL features are missing.
- Each database has a size limit. For very large datasets, plan to split data across databases or use something else.
- It suits read-heavy products. Dashboards, content tools, notes and bookmarks fit well. A system with very heavy concurrent writes needs a careful look first.
For the kind of product I build, these limits are far away. A changelog tool, a notes app or a link manager will not outgrow D1 for a long time.
The supporting pieces
Three other Cloudflare services cover everything D1 should not do:
- R2 for files. In my notes app, images are stored in R2 and the note itself keeps only a short text reference.
- KV for small, fast lookups. Settings and cached values that are read often and changed rarely.
- Analytics Engine for counting events. In my QR code manager it records scans without filling the main database with rows.
The frontend
For apps, I mostly write plain HTML, CSS and JavaScript. It loads fast, has no build step to break, and any developer can read it.
This site is the exception. It is built with Next.js and exported as static files, because a portfolio with a blog benefits from components and content collections. It still deploys the same way: static files on Cloudflare, published on every push.
Paddle and the merchant of record model
Payments are where being outside the United States changes the decision.
Why not Stripe
My business is registered in Serbia. At the time of writing, Stripe does not offer a standard payments account to a business registered only in Serbia. The usual workaround is to open a company in another country, which adds cost and paperwork before the first sale.
What a merchant of record does
Paddle works differently from a payment processor. It is the legal seller of the product. The customer buys from Paddle, and Paddle pays me.
That means Paddle handles:
- sales tax and VAT in every country, including registration and filing
- invoices and receipts
- chargebacks and payment disputes
- subscription billing, renewals and failed payment retries
For a one-person company selling worldwide, this removes an entire category of work. I do not track tax rules in forty countries. I build the product.
The trade-offs
A merchant of record is not free of downsides:
- The fee is higher than a plain payment processor, because it includes tax handling.
- Your product must be approved. Paddle reviews what you sell and the site you sell it on.
- You have less control over the checkout and over how payouts are scheduled.
For me the trade is clearly worth it. If you run a company in a country that Stripe supports and you have an accountant who handles international tax, the math may be different.
How it connects
Lytmark, my changelog widget for SaaS, uses this setup with two paid plans. The pattern is simple:
- The customer picks a plan and pays in Paddle's checkout.
- Paddle sends a webhook to a Worker.
- The Worker verifies the webhook and updates the subscription status in D1.
- Every later request checks that status to decide what the account can do.
The whole billing integration is one webhook handler and one column in a table.
Where this stack is the wrong choice
I would not use it for everything. Choose something else when:
- The product does heavy computation. Video processing, large data jobs and model inference need real machines.
- You need a full relational database. Complex reporting, large joins across huge tables and Postgres-specific features belong in Postgres.
- The team already runs conventional servers well. Moving a working system to a new platform for its own sake is rarely worth it.
- The app is built around long-lived connections. It can be done on Cloudflare, but it is a different kind of project with different tools.
What this means if you hire me
A product built on this stack costs very little to run and needs almost no maintenance. There is no server to renew and no security patch to schedule. You own the Cloudflare account, the repository and the payment account, and a push to the repository publishes an update.
It also means I can tell you early if your idea does not fit. If it needs heavy computation or a large relational database, I will say so before any work starts.
Frequently asked questions
Is Cloudflare Workers good for building a SaaS?
Yes, for most small and medium SaaS products. It handles APIs, authentication, webhooks and scheduled jobs well. It is a poor fit for heavy computation and long-running processes.
Is D1 ready for production?
Yes. D1 has been generally available since 2024 and I run my products on it. It is SQLite, so check its limits against your data size and write volume before committing.
Should I use Paddle or Stripe?
If Stripe supports your country and you can handle international tax yourself, Stripe gives you lower fees and more control. If you sell worldwide as a small company, or Stripe is not available where you are registered, a merchant of record such as Paddle removes the tax and compliance work.
How much does this stack cost to run?
For a small product, very little: hosting starts free and the paid plan is a few dollars a month. The main cost is the payment provider's share of each sale.
Want a product built this way?
I build web apps and SaaS products on Cloudflare, including payments and MCP servers that connect them to AI assistants. Write to collab@vhirty.com and tell me what you have in mind.