Quotas, ownership, and cleanup

You need: storage composed, and pithy provision run for the sweep.

The quota is checked when an upload starts

The object model is what is being counted, letting users upload is what trips the check, big files is the case that leaves the most behind, and provisioning storage is what deploys the sweep.

And the sum counts pending rows alongside stored ones.

That is the whole design, and it is not an optimization.

Making the client declare its size up front, and having the pending row reserve those bytes the moment it is written, turns the quota into something a burst cannot walk past: the second init reads the first’s reservation.

The reservation only holds if the sum and the insert are one statement

A SELECT sum(size) followed by a separate INSERT reproduces the very race it was meant to close, one round trip later. Ten concurrent inits all read a total none of them has contributed to yet, all pass, and all insert.

D1 has no interactive transactions, so a BEGIN/COMMIT wrapper is not available.

The mechanism instead is a conditional INSERT … SELECT … WHERE — the sum is evaluated inside the write, and a row that would breach the limit simply does not appear.

A cheap pre-check still runs first, so an obviously-over request fails before anything is created. It is not the guarantee; the conditional insert is.

Which is why size is required

A null size reserves nothing, so the handler requires one whenever a quota is configured.

The declared size is confirmed against R2 at completion, and the row is corrected to what actually arrived.

Ownership

ownerId comes from the auth seam. Null for system-owned objects — an object created by a scheduled job legitimately has no owner.

Quotas are per owner. A null-owner object belongs to no quota.

Every read and every mutation route is scoped to the authenticated caller, so listing returns your objects and nobody else’s.

Three states, and the middle one is the one to watch

pendingThe row and its quota are reserved. No bytes yet
storedR2 confirmed the bytes
failedAbandoned

The sweep reclaims what nobody finished

A pending row past its TTL is what the orphan sweep collects — the deployed Worker pithy provision stands up alongside the bucket.

Without it, a client that starts an upload and disappears leaves a reservation eating somebody’s quota forever.

It also cleans up R2’s side. An incomplete multipart upload holds parts, and R2 refuses to delete a bucket that still holds them — which is why teardown empties a bucket rather than just calling delete on it.

Deleting

DELETE /storage/<id>

Removes the row and the object.

Shares against it stop working — a revoked or orphaned share answers 410 Gone or 404, and the difference is deliberate. See sharing files.

Teardown keeps your files by default

pithy provision down brings the sweep Workers down. The buckets and everything in them stay unless you explicitly ask.

Those files are your data, no deploy restores them, and the flag is the confirmation.

ESC