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.

upload.ts
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

0%

Drop-in adapters for your framework

VueVue
SvelteSvelte
Next.jsNext.js

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.

Serverserver/api/upload.post.ts
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,
  })
})
ClientAvatar.vue
<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>
Serverapp/api/upload/route.ts
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,
    }),
  )
}
ClientAvatar.tsx
import { useUpload } from "sonuslab-storage/react"

export function Avatar() {
  const { upload, progress } = useUpload()

  return (
    <input
      type="file"
      onChange={(e) => upload(e.target.files![0])}
    />
  )
}
Serversrc/routes/api/upload/+server.ts
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,
    }),
  )
}
ClientAvatar.svelte
<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.

How encryption works

Zero to your first upload in five minutes.

Create an app in the dashboard, drop in the key, and ship type-safe uploads today.