Config options
| Option | Default | Meaning |
|---|---|---|
basePath | /storage | Where the object routes mount |
sharePath | /s | Where share fetches mount. Kept short — the whole URL gets pasted into a chat window |
multipartThresholdBytes | 100 MiB | Above this, a resumable multipart upload. Cannot exceed the single-PUT ceiling |
partSizeBytes | 64 MiB | Bytes per part. At most 10,000 parts, so this fixes the largest object you can store |
defaultVisibility | private | For an upload that does not name one. Private, so a forgotten field can never publish a file |
pendingTtlSeconds | 86400 | How long an unfinished upload holds its quota reservation |
quota | — | Bytes per owner |
Those two size settings multiply into your maximum object size. Raising the part size raises the ceiling and costs a larger retry when one part fails.
Routes
Using Storage has the calls; standing the bucket up is pithy storage.
| Route | Purpose | Verification |
|---|---|---|
POST /storage | Start an upload; returns one PUT URL or the part URLs | bearer · session |
GET /storage | List your files by path prefix, cursor-paginated | bearer · session, owner |
POST /storage/:id/complete | Finalize, confirming the bytes against the store | bearer · session, owner |
POST /storage/:id/abort | Abandon an upload and drop its parts | bearer · session, owner |
GET /storage/:id/parts | Resume: stored parts, plus a fresh URL per missing one | bearer · session, owner |
POST /storage/:id/copy | Server-side copy | bearer · session, owner |
POST /storage/:id/shares | Mint a revocable share link | bearer · session, owner |
DELETE /storage/shares/:token | Revoke a share | bearer · session, owner |
GET /storage/:id/url | A presigned direct URL, valid five minutes | bearer · session, owner or public |
GET /storage/:id | Stream the bytes | public when the object is public, else bearer · session and owner |
HEAD /storage/:id | Metadata only | bearer · session, owner |
PATCH /storage/:id | Rename, or change visibility | bearer · session, owner |
DELETE /storage/:id | Delete the file and its row | bearer · session, owner |
GET /s/:token | Fetch via a share link | public — the token is the credential |
Error codes
| Code | Status | Meaning |
|---|---|---|
storage/not_found | 404 | No such object, or one you may not see |
storage/forbidden | 403 | A public object you do not own |
storage/quota_exceeded | 413 | The upload would put the owner over their byte limit |
storage/upload_incomplete | 409 | Still pending — the upload never finished |
storage/multipart_failed | 500 | The part plan or the reported part list cannot be completed |
storage/share_expired | 410 | The share token is past its expiry |
A not-found for an object you may not see is deliberate. A 403 there would confirm the object exists.
Response hardening
This is what keeps a user’s file from executing on your origin.
Every object response carries no-sniff and a locked-down content policy.
An active type is served as an attachment whatever was stored — markup, vector images, anything with an XML suffix, script.
Not blocked at upload. Served in a way a browser will not execute.
Bindings
| Binding | Type | Written by |
|---|---|---|
DB | d1 | pithy add |
STORAGE_BUCKET | r2 | pithy add, with the name provisioning will create |
STORAGE_SWEEP | workflow, optional | pithy storage provision |
Tables
pithy_storage_objects · pithy_storage_shares
These two carry the only foreign key in the kit — shares reference objects — because that one relationship is internal to the capability and both tables are certain to be in one database.
Secrets
storage-r2-credentials — the account id, the S3 key pair, a scoped token, and the bucket name.
The key pair is supplied, not minted. There is no API for creating one, so you make it by hand and hand it over. Passing one half without the other is refused.
Keys and paths
The store key is server-derived and opaque. A client can never name an object or guess the one beside it.
The logical path is yours — stored, indexed and listable by prefix. Renaming is a metadata change rather than a copy.
The sweep
Daily, reconciling in both directions: rows with no bytes, and bytes with no row.
Peer capabilities
Required: secrets.
Optional: auth — effectively required, since every owner-scoped route denies without it.
What is not here
No transcoding, thumbnails or AI. That is media, which presigns through this package’s object-store seam against its own bucket and its own credential name, and inherits none of these tables or routes.
No public CDN hosting. Objects are private by default and served by your Worker.
No versioning. A write to the same logical path replaces what was there.