Sharing files

You need: storage composed.

Mint one

How a share works is why it is a row rather than a presigned URL, serving files is the private alternative, the object model is what a share points at, and quotas and cleanup is what happens to the object underneath.

POST /storage/<id>/shares
{ "expiresAt": "2026-09-30T00:00:00.000Z" }
→ { "token": "…" }

Omit expiresAt and it never expires — only revocation ends it.

GET /storage/share/<token>

Public. The token is the credential, and requiring a session as well would defeat the point of a link you can send somebody.

Take it back

DELETE /storage/<id>/shares/<token>

Effective on the next request, because the token is looked up on every fetch.

Why this is a row and not a presigned URL

A presigned URL cannot be revoked, only expired.

Once minted it is valid until its signature lapses, and nothing the owner does can call it back. So share this file, and let me take it back is not buildable on presigning alone — not with a shorter expiry, not with a second signature, not with anything short of rotating the credential every file in the bucket was signed with.

A share is therefore a row.

Revoking does not delete

revokedAt is a timestamp, not a DELETE.

The holder getsWhen
410 Gone — storage/share_revokedThe owner withdrew it
storage/share_expiredIt had a date and the date passed
404 — storage/not_foundNo such token

That difference is for the person holding the link. This existed and was withdrawn is a fact they can act on. A 404 leaves them wondering whether they mistyped it, and sends them to ask you.

Choosing an expiry

Set a date when the link has a natural life — an invoice somebody needs this quarter, a download in a receipt email. The expiry is the cleanup you never have to remember to do.

Leave it null when the link is meant to last and the owner will decide — a portfolio image, a file shared with a collaborator. Revocation is the whole exit, and it is available on the day you need it.

A share is not the same as making an object public

Public means anyone with the object id can read it.

A share is a separate credential over a private object.

Revoking a share does not make a public object private, and making an object private does not invalidate outstanding shares — they are two independent decisions, and a UI that presents them as one will eventually surprise somebody.

Deleting the object

Removes the row and the bytes. Shares against it stop working, and a fetch answers not-found.

Auditing what is out there

The shares for an object are yours to list from your own D1 — it is a plain table, with the token, the object, the dates and the revocation.

Which is the reconciliation query worth running: live shares with no expiry, on objects nobody has touched in a year.

ESC