Skip to main content
The Openinary File Uploader is a shadcn/ui-compatible React component with drag & drop, per-file progress, previews, retry, cancel and client-side validation. It works against Cloud unchanged, only the configuration differs. This page covers the Cloud wiring. For the full props table, styling and the component’s own troubleshooting, see the component guide.

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.json present), with the shadcn CLI v3+ for namespaced registries.
Nothing else. No 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

The first installs the component and its hook into components/openinary/; the second installs lib/upload-token.ts, a thin client for POST /upload/sign.
3

Set your environment variables

.env.local
Unlike a self-hosted full-stack instance, there is no /api suffix on Cloud. The API is served at the root of cdn.openinary.dev.

Create a signing endpoint

Protect this route with your own authentication, and derive folder from the authenticated user server-side. A folder sent by the browser is a folder any visitor can choose, including someone else’s.
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 with 402 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.
See Errors and Plans and limits.

Troubleshooting

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.
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.
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.
A plan allowance ran out. feature in the body says which one. Storage is cumulative, deleting files frees it back; everything else resets monthly.
On Cloud the destination comes from the signature, not the component. Fix the folder you pass to signUpload on your backend.
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.