Quickstart
From an API key to browser uploads in three steps.
This guide takes you from nothing to a working upload — first server-side, then straight from the browser with your secret key safely on the server.
Prerequisites
- A TypeScript or JavaScript project on a runtime with the Web
fetch/RequestAPI (Node 18+, Bun, Deno, Cloudflare Workers, or any modern bundler for the browser). - An API key from the dashboard (see below).
Install
npm i s3deliveryGet an API key
In the dashboard, open API Keys → Create key, give it a name, and copy the secret. It is shown once — only a hash is stored, so save it now.
Keys look like s3d_live_…. Set it on your server as an environment variable:
S3DELIVERY_TOKEN=s3d_live_xxxxxxxxxxxxxxxxxxxxxxxxKeep the key server-side
An API key has full access to your files. Never ship it to the browser or commit it. The browser talks to a route handler you mount (step 2) — never to the API directly.
Option A — Upload from your server
If your bytes already live on the server (a generated PDF, a fetched image, an agent's output), one call does everything:
import { S3Delivery } from "s3delivery";
const s3 = new S3Delivery(); // reads S3DELIVERY_TOKEN
const blob = new Blob(["hello world"], { type: "text/plain" });
const file = await s3.upload(blob, { name: "hello.txt", visibility: "public" });
console.log(file.url); // stable link that redirects to a signed bucket URLupload reserves a record, sends the bytes straight to your bucket, and confirms
the object landed — returning the stored file. See the
server client reference for every method.
Option B — Upload from the browser
Browser uploads keep your key on the server by routing the small create/complete handshake through a route handler you mount. The bytes still go directly to the bucket.
1. Mount a route handler
The handler holds your secret key and brokers signed URLs. It is a plain
(request: Request) => Promise<Response>, so it drops into any framework that
speaks the Web Request/Response API.
// app/api/s3delivery/route.ts (Next.js App Router)
import { S3Delivery, createRouteHandler } from "s3delivery";
const s3 = new S3Delivery(); // reads S3DELIVERY_TOKEN
export const POST = createRouteHandler(s3, {
// Enforced server-side before any upload URL is minted.
constraints: {
maxFileSize: 10 * 1024 * 1024, // 10 MB
allowedFileTypes: ["image/*", "application/pdf"],
},
// Authenticate the request and constrain the upload.
beforeUpload: async ({ request }) => {
const user = await getUser(request);
if (!user) throw new Error("Unauthorized");
return { visibility: "private" };
},
// Runs inline when an upload is confirmed — no webhook or tunnel.
onUploadComplete: async ({ file }) => {
await db.files.insert({ id: file.id, url: file.url });
},
});Other frameworks
Mount the same handler anywhere. In Hono:
app.post("/api/s3delivery", (c) => handler(c.req.raw)). The same handler works
in Remix, SvelteKit, Express (with an adapter), Bun, Deno, and Cloudflare
Workers. See Framework integrations.
2. Upload from the browser
The React hook gives you a clean, predictable state machine:
import { useUploadFiles } from "s3delivery/react";
function Uploader() {
const { upload, uploadedFiles, progress, isPending, error } = useUploadFiles({
maxFiles: 10,
maxFileSize: 10 * 1024 * 1024,
allowedFileTypes: ["image/*", "application/pdf"],
onUploadComplete: (files) => console.log(files),
});
return (
<>
<input type="file" multiple onChange={(e) => upload(e.target.files!)} />
{isPending && <progress value={progress} max={100} />}
{error && <p>{error.message}</p>}
{uploadedFiles.map((f) => (
<a key={f.id} href={f.url}>
{f.name}
</a>
))}
</>
);
}Not using React? uploadFiles is the same engine without the hook:
import { uploadFiles } from "s3delivery";
const [file] = await uploadFiles(input.files, {
onProgress: (p) => setProgress(p.percent),
});
console.log(file.url);Or drop in a prebuilt component:
import { UploadButton, UploadDropzone } from "s3delivery/react";
<UploadButton accept="image/*" onSuccess={(files) => console.log(files)} />;
<UploadDropzone onSuccess={(files) => console.log(files)} />;3. That's it
The file is in your bucket, the metadata is in the database, and file.url is a
stable link that redirects to a freshly-signed URL on each request.
Bonus: give an AI agent file tools
s3delivery/ai hands a Vercel AI SDK agent the ability to
store, find, link, and delete files in one line:
import { S3Delivery } from "s3delivery";
import { createFileTools } from "s3delivery/ai";
import { generateText } from "ai";
const tools = createFileTools(new S3Delivery());
await generateText({
model,
tools, // uploadFile, listFiles, getFileUrl, deleteFile, getUsage
prompt: "Save these notes as notes.txt and give me a shareable link.",
});Pass { readOnly: true } to drop the mutating tools, or requireApproval to gate
destructive actions behind a human. See the AI agents guide.