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.
| Preset | Re-hosts a remote URL? | Production-ready? |
|---|---|---|
| Fetch endpoint | Yes | Yes |
| Supabase | No | Yes |
| Presigned URLs | No | Yes |
| Cloudinary | Yes | Yes |
| IndexedDB | No | No |
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
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl | string | (required) | Origin of your upload service, e.g. "https://api.myapp.com". |
field | string | "file" | Multipart field name the endpoint reads for POST /upload. |
headers | Record<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
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
| Option | Type | Default | Description |
|---|---|---|---|
bucket | string | ((kind: AssetKind) => string) | "blok" | Bucket name, or a function of the asset kind for per-kind buckets. |
path | (file: File, ctx: UploadContext) => string | random 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
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
| Option | Type | Default | Description |
|---|---|---|---|
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
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
| Option | Type | Default | Description |
|---|---|---|---|
cloudName | string | (required) | Your Cloudinary cloud name. |
uploadPreset | string | (required) | Must be an UNSIGNED upload preset. A signed one would need a server. |
folder | string | undefined | 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
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
| Option | Type | Default | Description |
|---|---|---|---|
dbName | string | "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
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);