Drop a secure, ready-to-use file uploader into any React app with a single shadcn/ui command. Uploads go straight to your Openinary instance using short-lived presigned signatures — no API key in the browser.
The Openinary File Uploader is a shadcn/ui-compatible React component that lets your users upload media directly to your self-hosted Openinary instance. It ships with drag & drop, per-file progress, previews, retry, cancel, and client-side validation.It installs the same way as any shadcn component:
The component never holds an API key. Instead it uses presigned upload signatures, minted by POST /upload/sign — the same HMAC-SHA256 pattern Openinary already uses for signed delivery URLs.
1
Your backend requests a signature
A protected endpoint on your server calls POST /upload/sign with your Openinary API key, scoping the request to a folder and an expiry window.
2
The browser gets the signature
The component calls your sign() function right before uploading. It returns { signature, expires, folder } — opaque values that expire in minutes.
3
Openinary verifies the signature
POST /upload checks the signature against the submitted folder and expires before accepting the file. A tampered or expired signature is rejected.
Your Openinary API key never leaves your backend — only your server should
call POST /upload/sign. The endpoint you create for the component must be
protected by your own auth and rate limiting; derive folder from the
authenticated user, not from the client.
API_SECRET set on the instance (64 characters — openssl rand -hex 32) — used internally to compute the HMAC signature. Same secret used for signed delivery URLs.
An API key for your backend to call POST /upload/sign with (see API Keys).
CORS_ORIGIN set to include the origin(s) of the app embedding the uploader. Multiple origins are comma-separated:
MAX_FILE_SIZE_MB if you need to raise the default 50 MB limit.
A project already set up for shadcn/ui (components.json present). See the shadcn install guide. Requires the shadcn CLI v3 or later for namespaced registries.
This installs the component and its hook into components/openinary/, pulls in the shadcn button and progress dependencies, and adds NEXT_PUBLIC_OPENINARY_URL to your .env.local.
This installs lib/upload-token.ts, a thin client for POST /upload/sign — it makes one fetch call and doesn’t compute anything cryptographic itself; Openinary does that.
4
Set your environment variables
.env.local
# Public — the browser calls this Openinary API directly for the upload# /api is required in full stack mode (nginx only exposes the API under /api); drop it for API-only deploymentsNEXT_PUBLIC_OPENINARY_URL=https://your-domain.com/api# Secret — server only, an Openinary API key, never exposed to the browserOPENINARY_API_KEY=your-openinary-api-key
The component needs a sign() function that returns a presigned signature. Back it with a small endpoint on your server using the helper you just installed.
Protect this endpoint with your own authentication and derive folder from
the authenticated user on the server. Never trust a folder sent by the
browser, or a user could upload into someone else’s scope.
Next.js (App Router)
Express
Cloudflare Workers
app/api/upload-token/route.ts
import { NextResponse } from "next/server";import { signUpload } from "@/lib/upload-token";// import { auth } from "@/lib/auth"; // your own authexport async function POST() { // const session = await auth(); // if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); const signed = await signUpload( process.env.NEXT_PUBLIC_OPENINARY_URL!, process.env.OPENINARY_API_KEY!, { expiresIn: 300, folder: "uploads", // e.g. `users/${session.user.id}` }, ); return NextResponse.json(signed);}
Returns { signature, expires, folder } from your signing endpoint. Called before each upload batch and again on retry, so it always fetches a fresh signature. The upload’s destination folder is whatever the signature is scoped to.
Client-side cap on the number of files. This is a UI convenience only — POST /upload/sign does not encode a file-count limit, so it isn’t enforced
server-side.
The component is built entirely with shadcn’s design tokens (border, muted, primary, destructive, ring…), so it automatically matches your theme and dark mode. Because the source is copied into your project, you own it — edit components/openinary/file-uploader.tsx freely. Use className for one-off overrides.
Add your app’s origin to CORS_ORIGIN on the Openinary instance
(comma-separated for multiple origins) and restart the API. The origin must
match exactly, including scheme and port.
401 'Presigned upload URL has expired'
The signature’s expires timestamp passed before the upload started.
Increase expiresIn slightly (keep it under the 3600s server cap) and check
for clock skew between your server and the API.
401 'Authentication required' on POST /upload
The signature didn’t verify — usually because the folder used to sign it
doesn’t exactly match the folder submitted with the upload, the signature
was tampered with, or the API_SECRET used by the Openinary instance
changed since the signature was minted. The component always uses the folder
returned by sign(), so this typically means your signing endpoint and the
Openinary instance disagree, or the request was hand-crafted outside the
component.
401/403 calling POST /upload/sign itself
That endpoint requires a real Openinary API key or session — check
OPENINARY_API_KEY on your backend, not the browser. It should never be
called directly from client code.
signUpload throws 'Failed to sign upload (HTTP 404)', or the response is a redirect to /login
NEXT_PUBLIC_OPENINARY_URL is pointing at a full-stack instance’s bare
domain. In full stack mode the API only lives under /api (nginx routes
everything else to the web dashboard), so append /api to the URL — see the
note in Set your environment variables. API-only
deployments don’t need the suffix.
File rejected before upload
Client-side validation mirrors the server. Check the file type is in
accept and the size is within maxSize (and the server’s
MAX_FILE_SIZE_MB).
Like Uploadcare’s File Uploader, this component uses a backend-signed, short-lived credential so the browser never holds a secret. The difference: uploads go to your self-hosted Openinary instance and storage — no third-party service, no per-file pricing.