Tanstack Start

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 / Request API (Node 18+, Bun, Deno, Cloudflare Workers, or any modern bundler for the browser).
  • An API key from the dashboard (see below).

Install

npm i s3delivery

Get 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_xxxxxxxxxxxxxxxxxxxxxxxx

Keep 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 URL

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

Next steps

On this page