Skip to content

Storage presets

Ready-made uploaders from @bloklabs/presets. They give Blok somewhere to put uploaded files, so you do not have to write an upload handler. Each one plugs straight into the uploader config option.

PresetRe-hosts a remote URL?Production-ready?
Fetch endpointYesYes
SupabaseNoYes
Presigned URLsNoYes
CloudinaryYesYes
IndexedDBNoNo

Fetch endpoint

Talks to a backend you already have. Every upload goes through your server. Your server decides where the bytes actually land.

Re-hosting a remote URL

Yes. Your endpoint does the fetch server-side, so the browser never has to reach a third-party URL itself.

Configuration

OptionTypeDefaultDescription
baseUrlstring(required)

Origin of your upload service, e.g. "https://api.myapp.com".

fieldstring"file"

Multipart field name the endpoint reads for POST /upload.

headersRecord<string, string> | (() => Promise<Record<string, string>>)undefined

Extra headers on every request. Pass a function to mint a fresh access token per upload.

Storage-side setup

  • A server that answers POST {baseUrl}/upload (multipart) and POST {baseUrl}/upload-by-url ({ url }) with { url, fileName? } JSON.
  • The endpoint alone decides where the file is stored: S3, a disk volume, or Supabase. This preset never sees it.
  • The URL your /upload-by-url endpoint receives is user-supplied. Validate it server-side and block internal and private addresses before you fetch it. If you do not, you have built an SSRF vector.

Usage Example

TypeScript
import { fetchStorage } from '@bloklabs/presets';

new Blok({
  holder: 'editor',
  uploader: fetchStorage({ baseUrl: 'https://api.myapp.com' }),
});

Supabase

Uploads straight to a Supabase Storage bucket using the client you already initialize.

Re-hosting a remote URL

No. Re-hosting a remote URL needs a server-side fetch, and the browser cannot make one on Supabase's behalf. uploadByUrl is left undefined, so Blok stores the URL verbatim instead of pretending it was re-hosted.

Configuration

OptionTypeDefaultDescription
bucketstring | ((kind: AssetKind) => string)"blok"

Bucket name, or a function of the asset kind for per-kind buckets.

path(file: File, ctx: UploadContext) => stringrandom name, original extension kept

Object path within the bucket.

Storage-side setup

  • Create the bucket in the Supabase dashboard (default name "blok", or whatever `bucket` resolves to).
  • Make the bucket public, or add a Storage policy granting anon SELECT. This preset returns getPublicUrl() directly, so an unreadable object shows up as a broken image.
  • Add a Storage INSERT policy for whichever role the client authenticates as.

Usage Example

TypeScript
import { createClient } from '@supabase/supabase-js';
import { supabaseStorage } from '@bloklabs/presets';

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
new Blok({ holder: 'editor', uploader: supabaseStorage(supabase, { bucket: 'blok' }) });

Presigned URLs

For S3 and S3-compatible storage (R2, MinIO, GCS). Your backend mints a short-lived signed URL. The browser PUTs the file straight to it.

Re-hosting a remote URL

No. Re-hosting a remote URL needs a server-side fetch, and a presigned PUT URL cannot do that. uploadByUrl is left undefined, so Blok stores the URL verbatim instead.

Configuration

OptionTypeDefaultDescription
sign(request: SignRequest) => Promise<SignedTarget>(required)

Called with { fileName, mimeType, size, kind }. It must return { uploadUrl, publicUrl, headers? } from your backend.

Storage-side setup

  • A backend endpoint that creates a presigned PUT URL (e.g. S3 PutObjectCommand) and returns it as SignedTarget.
  • A CORS rule on the bucket that allows PUT from your app's origin. It must also allow the headers this preset sends, notably Content-Type. The browser PUTs directly to the bucket, so this is the step that actually blocks people.

Usage Example

TypeScript
import { presignedStorage } from '@bloklabs/presets';

new Blok({
  holder: 'editor',
  uploader: presignedStorage({ sign: (request) => api.sign(request) }),
});

Cloudinary

Uploads directly to Cloudinary using an unsigned upload preset. No backend required.

Re-hosting a remote URL

Yes. Cloudinary fetches the remote URL itself once the browser hands it over. Re-hosting works without any server of your own.

Configuration

OptionTypeDefaultDescription
cloudNamestring(required)

Your Cloudinary cloud name.

uploadPresetstring(required)

Must be an UNSIGNED upload preset. A signed one would need a server.

folderstringundefined

Optional folder to upload into.

Storage-side setup

  • A Cloudinary account and its cloud name.
  • An upload preset with signing mode set to Unsigned (Settings → Upload → Upload presets). A signed preset silently fails from the browser, because there is no server to sign the request.
  • Cloudinary sends audio assets through its video pipeline by its own design. This preset maps kinds onto resource types to match. You do not need to set anything up for that part.

Usage Example

TypeScript
import { cloudinaryStorage } from '@bloklabs/presets';

new Blok({
  holder: 'editor',
  uploader: cloudinaryStorage({ cloudName: 'my-cloud', uploadPreset: 'blok-unsigned' }),
});

IndexedDB

Stores uploaded files as blobs in the browser's own IndexedDB. Nothing leaves the device.

Re-hosting a remote URL

No. There is nothing to re-host to, so a remote URL just gets stored as-is. IndexedDB only has an uploadByFile.

Not for production

Demo and prototyping only. The bytes live in one visitor's browser. They are gone on another device, in another browser, or the moment the user clears site data.

This preset exists because Blok's built-in fallback (a blob: URL) doesn't even survive a page reload, which makes demos look broken before you've configured any real storage.

Configuration

OptionTypeDefaultDescription
dbNamestring"blok-assets"

IndexedDB database name.

Storage-side setup

  • None. This preset needs no external service, which is the point.
  • Call the exported resolveBlokObjectUrl(url) helper wherever you render an uploaded asset. uploadByFile returns a blok:asset/… reference, not a URL you can use directly. Nothing resolves it back into a blob: URL for you automatically.
  • If you pass a custom dbName to indexedDBStorage(), pass that same dbName to resolveBlokObjectUrl(url, { dbName }). A mismatch looks up the wrong database. It resolves to null with no error, and does not throw an exception.

Usage Example

TypeScript
import { indexedDBStorage, resolveBlokObjectUrl } from '@bloklabs/presets';

new Blok({ holder: 'editor', uploader: indexedDBStorage() });

// Wherever you render a stored asset's url, e.g. an <img src>:
const displayUrl = await resolveBlokObjectUrl(asset.url);