Skip to content
All posts

Why Emberstack runs entirely on Cloudflare Workers

One platform for the app, both databases, media, sessions, email and AI. Here's what that buys you, and the two rules it forces on your code.

Roger
A small campfire glowing in the dark beside a stack of split logs

Emberstack started life as SaaSWind, a starter that ran on Node with Postgres somewhere else, email somewhere else, and a deploy target you picked from a list. It worked. It also meant a handful of accounts and dashboards before you'd written a line of product code.

So I moved all of it onto Cloudflare.

Today the whole thing is one Worker. The app, the marketing site, the blog, these docs and the AI assistant all ship together with bun run deploy (or a push to main, more on that in another post).

What lives where

Everything the Worker touches is a binding in wrangler.jsonc:

Binding

What it is

What uses it

DB

D1

Users, teams, notes, activity, assistant chats

CMS_DB

D1

EmDash: pages, posts, docs, menus

MEDIA

R2

Uploaded images

SESSION

KV

Sessions for the CMS admin

EMAIL

Email Sending

Invites, sign-in links, app email

AI

Workers AI

The assistant, and anything else you dream up

There are no connection strings in there. A binding is a handle the runtime gives your code, already authenticated. You import env from cloudflare:workers and call env.DB or env.AI.run(). That's it.

That's my favourite part. No DATABASE_URL in 3 places, no API key for the email provider, nothing to rotate when someone leaves.

Why I think it's the right default for a starter

The first deploy is short. Create 2 D1 databases, an R2 bucket and a KV namespace, paste their IDs into wrangler.jsonc, set 3 secrets, deploy. The Cloudflare setup doc is the whole list.

You can explore for free. Most of what Emberstack uses fits inside Cloudflare's and Stripe's free tiers while you figure out whether the idea has legs. Better to find out an idea won't fly before you've paid anyone for a database.

It's fast where your users are. Workers run in isolates close to the visitor, so there's no container to wake up on the first request of the morning. Pair that with edge caching on the public pages and the marketing site feels static even though it's server-rendered.

Fewer moving parts to break at 2am. One deploy, one set of logs (bunx wrangler tail), one place to look.

The 2 rules it forces on you

Running on Workers isn't free of trade-offs. It changes how you write code in 2 ways, and both are baked into the template.

1. No Node-only packages

Workers aren't Node. nodejs_compat covers a lot, but anything that opens a raw TCP socket or reads the filesystem won't work. That's why Emberstack hashes passwords with PBKDF2 through Web Crypto (built into the runtime) instead of bcrypt, and why the database client is drizzle-orm/d1 instead of a Postgres driver.

When you reach for a package, check that it runs on Workers first. Most modern ones do.

2. Keep CPU work small

A Worker gets a CPU budget per request. Waiting on the network (D1, an AI model, Stripe) doesn't count against it, but heavy computation does. In practice that means don't resize images in the request path or crunch big reports inline. Hand that work to a queue or a cron, or do it once and store the result.

It's also why password hashing uses PBKDF2 on Web Crypto: the runtime does that work natively, while a JavaScript bcrypt can eat a big share of the budget on its own.

Two databases, on purpose

One thing surprises people: there are 2 D1 databases. The CMS (EmDash) and Better Auth both create a table called users, and they mean very different things by it. Keeping them apart means a CMS editor and an app user are separate accounts in separate places, and neither system can trip over the other's schema.

It also means you never join across them. Drizzle only ever talks to DB. EmDash only ever talks to CMS_DB. I'll write more about the app side in the D1 post.

Where to start

If you want to poke at it, the quick start gets it running locally in a handful of commands. If you'd rather read first, project structure shows where everything lives.

And if something on this list doesn't fit your product, swap it. It's your code once you clone it.

Continue reading

All posts