Skip to main content
The FileUploader component with files queued for upload
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.
On Openinary Cloud the same component works with a different configuration, see File Uploader on Cloud.
It installs the same way as any shadcn component:

Security model

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.

Prerequisites

  • A running Openinary instance (see Quickstart).
  • API_SECRET set on the instance (at least 16 characters; openssl rand -hex 32 gives you a strong 64-character one), 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.

Installation

1

Register the Openinary namespace

Add the @openinary registry to your project’s components.json:
components.json
2

Add the component

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.
3

Add the signing helper

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

Create a signing endpoint

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.
app/api/upload-token/route.ts
Keep expiresIn short, 60-300 seconds is plenty (the server clamps it to at most 3600). It only needs to live long enough to start the upload.

Usage

Props

() => Promise<SignedUpload> | SignedUpload
required
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.
string
Base URL of your Openinary API, e.g. https://media.example.com. Falls back to NEXT_PUBLIC_OPENINARY_URL.
boolean
default:"true"
Allow selecting more than one file.
boolean
default:"false"
Blocks file selection and drag and drop. Use it while a parent form is submitting, or before the user is allowed to upload.
Record<string, string[]>
MIME types mapped to allowed extensions. Defaults to the Openinary media types (JPEG, PNG, WebP, AVIF, GIF, HEIC/HEIF, PSD, MP4, MOV, WebM).
number
default:"52428800"
Maximum file size in bytes. Default is 50 MB. Must not exceed the server’s MAX_FILE_SIZE_MB.
number
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.
string[]
Transformation segments to pre-warm after upload, e.g. ["w_800,f_webp"]. See Upload & Pre-warm.
boolean
default:"false"
Start uploading as soon as files are added, hiding the explicit Upload button.
string
Additional classes for the root element.
(files: UploadedFile[]) => void
Called with the stored files after a successful batch.
(error: UploadFileError) => void
Called when a file fails to upload.
(file: FileUploadState, progress: number) => void
Called with upload progress (0-100) for each file.

Styling & theming

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.

Server response

On success, onSuccess receives the files stored by the API:
string
The stored filename.
string
The stored path. May be renamed if a file with the same name already exists (e.g. image (1).jpg).
string
The transformation URL prefix, e.g. /t/{path}. Prefix it with your baseUrl to build a full URL.
string[]
URLs of image variants pre-generated from transformations.
string[]
URLs of video variants queued for background processing.
HEIC/HEIF files are automatically converted to JPEG by the server, so the returned path may end in .jpg.

Troubleshooting

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.
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.
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.
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.
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.
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).

How it compares to Uploadcare

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.