File storage that speaks TypeScript
Presigned uploads, browser helpers, framework adapters, webhooks, and client-side encryption — one type-safe package for Node, Bun, and the edge.
import { createStorage } from "sonuslab-storage/server"
const storage = createStorage({ apiKey: process.env.SONUS_KEY! })
// Mint a short-lived upload URL.
// The API key never touches the browser.
const { uploadUrl, fileId } = await storage.createUpload({
key: `avatars/${user.id}.png`,
contentType: "image/png",
maxBytes: 5_000_000,
})avatar.png
1.2 MB · image/png
Drop-in adapters for your framework
Everything included
One package. The whole upload pipeline.
From the presigned URL on your server to the encrypted byte in the bucket — typed end to end.
Server SDK
Mint presigned URLs, list, move, and delete files, abort multiparts. Runs on Node, Bun, and edge runtimes.
Browser uploads
Stream bytes straight from the browser through a short-lived upload URL. Progress, retries, and aborts built in.
Framework adapters
Idiomatic bindings for Vue, Svelte, and Next.js over one core client — same upload flow everywhere.
Signed webhooks
Get notified on upload, delete, and multipart completion with signature verification you can trust.
Client-side encryption
Encrypt bytes in the browser before they ever leave the device. Zero-knowledge storage, opt-in per file.
Multipart & migrations
Chunked uploads for huge files and background jobs to migrate an existing bucket into your app.
Bring your stack
The same upload, everywhere you ship.
A server route hands out the URL. A one-liner on the client does the upload. Pick your framework.
import { createStorage } from "sonuslab-storage/server"
const storage = createStorage({
apiKey: process.env.SONUS_KEY!,
})
export default defineEventHandler(async (event) => {
const { name, type } = await readBody(event)
// Hand the browser a one-time upload URL
return storage.createUpload({
key: name,
contentType: type,
})
})<script setup lang="ts">
import { useUpload } from "sonuslab-storage/vue"
const { upload, progress } = useUpload()
async function onFile(e: Event) {
const file = (e.target as HTMLInputElement).files![0]
await upload(file)
}
</script>
<template>
<input type="file" @change="onFile" />
<progress :value="progress" max="1" />
</template>import { createStorage } from "sonuslab-storage/server"
const storage = createStorage({
apiKey: process.env.SONUS_KEY!,
})
export async function POST(req: Request) {
const { name, type } = await req.json()
// Hand the browser a one-time upload URL
return Response.json(
await storage.createUpload({
key: name,
contentType: type,
}),
)
}import { useUpload } from "sonuslab-storage/react"
export function Avatar() {
const { upload, progress } = useUpload()
return (
<input
type="file"
onChange={(e) => upload(e.target.files![0])}
/>
)
}import { json, type RequestHandler } from "@sveltejs/kit"
import { createStorage } from "sonuslab-storage/server"
const storage = createStorage({
apiKey: process.env.SONUS_KEY!,
})
export const POST: RequestHandler = async ({ request }) => {
const { name, type } = await request.json()
// Hand the browser a one-time upload URL
return json(
await storage.createUpload({
key: name,
contentType: type,
}),
)
}<script lang="ts">
import { Upload } from "sonuslab-storage/svelte"
const uploader = new Upload()
</script>
<input
type="file"
on:change={(e) => uploader.send(e.currentTarget.files![0])}
/>
<progress value={uploader.progress} max={1} />Client-side encryption
Your bytes, sealed before they leave the browser.
Flip one flag and files are encrypted on the device. What lands in the bucket is ciphertext — zero-knowledge storage without wiring up your own crypto.
Keys never leave the client
Bytes are encrypted in the browser with a key you control. The server only ever sees ciphertext.
Zero-knowledge by design
We can't read your files, and neither can anyone who breaches the bucket. Opt in per upload.
Zero to your first upload in five minutes.
Create an app in the dashboard, drop in the key, and ship type-safe uploads today.