Shared databases

You need: more than one Worker.

Sharing is the default, not a mode

The data layer is what sits on top, migrations is what runs once across the set, pithy migrate is the command, and adding a second Worker is how you get two in the first place.

Two Workers that declare the same binding name share one database. There is nothing to switch on.

Most capabilities default to DB, the shared app database, precisely so that an admin Worker and an API Worker read the same rows without wiring.

Prefixes are what make it safe

Every capability’s tables are pithy_<name>_*.

That prefix is the same segment that is the pithy add argument, the migration namespace, the error-code domain, the audit-action domain and the catalog key — one rule, five places, so one capability’s tables cannot collide with another’s or with yours.

Your own tables carry no pithy_ prefix, which is what keeps them yours.

Migrations run per database, not per Worker

pithy migrate

--worker narrows what is reported and which databases are visited. It never narrows the registry a visited database runs — because a shared database’s ledger holds both Workers’ migrations, and a partial provider reads as corrupted state.

The registry, the ordering and the per-database runs are identical everywhere. Only the driver differs: locally through Miniflare against the same store wrangler dev reads; remotely over the D1 API against the database that environment’s config names. You pass no ids.

Ownership is stamped

A database carries an ownership stamp, so a second project pointed at somebody else’s database is caught rather than migrated into.

worker is the only thing telling events apart

Two Workers sharing a database write audit rows into one table.

project, environment and worker are stamped by the recorder from the Worker’s own vars — never by the emitter, which has no way to set them and no way to override them.

Without worker, two Workers’ events are indistinguishable. With it, what did the admin Worker do is a query.

Which capability composes where

Each Worker has its own config and composes its own capabilities.

A capability composed on one Worker and not the other still has its tables in the shared database — the tables are a property of the database, and the routes are a property of the Worker.

So an admin Worker can read payments’ tables through its own routes without composing payments at all, if that is what you want. It just does not get payments’ routes, its middleware, or its entitlement resolver.

When to give something its own database

When the isolation is the point.

The secrets capability keeps a per-environment database of its own, because the master key that opens its rows is per environment.

Email’s suppression database is deliberately shared across a project’s environments — because an unsubscribe has to apply everywhere, and somebody who opted out in prod has not agreed to receive staging’s mail either.

Two answers to one question, and each is about what the data means rather than about tidiness.

The parameter ceiling is shared too

D1 allows 100 bound parameters per query. That is a property of the database, not of a Worker — so a shared database does not raise it, and a query built by concatenating two Workers’ filters can hit it.

Leaderboard’s 80-member segment cap is exactly this arithmetic: 100, less the 10 the my-rank path spends on filters and the tiebreak.

ESC