How it stays safe
The browser never holds your API key. Your backend mints a short-lived presigned signature, the browser uploads with that.1
Your backend requests a signature
A route you control calls
POST /upload/sign
with your Cloud API key, scoping the upload to a folder and an expiry.2
The browser gets the signature
The component calls your
sign() function right before uploading and gets
back { signature, expires, folder }, opaque values that die in minutes.3
Cloud verifies it
POST /upload reads the destination account, bucket and folder from the
token itself, never from the request. A tampered, replayed or expired token
is rejected.Because the token names its own bucket, an upload can’t be redirected into
another bucket, or another customer’s storage, by editing the form fields.
Prerequisites
- A Cloud account, see Cloud Quickstart.
- An API key pinned to the bucket you want uploads to land in.
- A project set up for shadcn/ui (
components.jsonpresent), with the shadcn CLI v3+ for namespaced registries.
API_SECRET, no CORS_ORIGIN allow-list, no
MAX_FILE_SIZE_MB, Cloud handles all of it, and POST /upload accepts
token-authenticated uploads from any origin.
Installation
1
Register the Openinary namespace
components.json
2
Add the component and the signing helper
components/openinary/;
the second installs lib/upload-token.ts, a thin client for
POST /upload/sign.3
Set your environment variables
.env.local
Create a signing endpoint
- Next.js (App Router)
- Express
- Cloudflare Workers
app/api/upload-token/route.ts
Usage
What comes back
onSuccess receives the stored files:
string
The stored path, e.g.
uploads/photo.jpg. May be de-duplicated if a file of
that name already existed (photo (1).jpg), so always use this value rather
than the name you sent.string
The delivery path,
/b/{bucketId}/t/{path}. Prefix it with
https://cdn.openinary.dev for a full URL, and insert transformations right
after /t/:string
The stored filename.
HEIC/HEIF files are converted to JPEG on upload, so
path may end in .jpg.Cloud-specific behaviour
Handling quota errors
An upload that would push the account past its storage allowance is refused with402 and a JSON body naming the feature. Surface it, don’t retry it, the retry
will fail identically until the plan or the usage changes.
Troubleshooting
signUpload throws 'Failed to sign upload (HTTP 404)'
signUpload throws 'Failed to sign upload (HTTP 404)'
NEXT_PUBLIC_OPENINARY_URL probably still ends in /api, that suffix is a
self-hosted full-stack thing. On Cloud the API is at the root:
https://cdn.openinary.dev.401 on POST /upload/sign
401 on POST /upload/sign
The API key is missing, disabled, expired, or you’re calling the endpoint from
the browser (where the key shouldn’t be at all). Check
OPENINARY_API_KEY on
your backend, and that the key is still enabled in Settings → API keys.401 'Invalid or expired upload signature'
401 'Invalid or expired upload signature'
The token’s
expires passed before the upload started, or the signature was
altered. Raise expiresIn a little (the server clamps it to 3600s). The
component always re-signs on retry, so this shouldn’t survive a retry.402 quota_exceeded
402 quota_exceeded
A plan allowance ran out.
feature in the body says which one. Storage is
cumulative, deleting files frees it back; everything else resets monthly.Files land in the wrong folder
Files land in the wrong folder
On Cloud the destination comes from the signature, not the component. Fix
the
folder you pass to signUpload on your backend.CORS error in the console
CORS error in the console
POST /upload accepts token-authenticated uploads from any origin, so a CORS
failure here usually means the request isn’t the one you think, e.g. your code
is calling /upload/sign or /storage from the browser. Those are
backend-only endpoints. Move the call server-side.