# Quickstart (/docs/quickstart)



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 [#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 [#install]

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm i s3delivery
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add s3delivery
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add s3delivery
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add s3delivery
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Get an API key [#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:

```bash
S3DELIVERY_TOKEN=s3d_live_xxxxxxxxxxxxxxxxxxxxxxxx
```

<Callout type="warn" title="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.
</Callout>

## Option A — Upload from your server [#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:

```ts
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](/docs/server-client) for every method.

## Option B — Upload from the browser [#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 [#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.

```ts
// 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 });
  },
});
```

<Callout title="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](/docs/frameworks).
</Callout>

### 2. Upload from the browser [#2-upload-from-the-browser]

The React hook gives you a clean, predictable state machine:

```tsx
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:

```tsx
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:

```tsx
import { UploadButton, UploadDropzone } from "s3delivery/react";

<UploadButton accept="image/*" onSuccess={(files) => console.log(files)} />;
<UploadDropzone onSuccess={(files) => console.log(files)} />;
```

### 3. That's it [#3-thats-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 [#bonus-give-an-ai-agent-file-tools]

`s3delivery/ai` hands a [Vercel AI SDK](https://ai-sdk.dev) agent the ability to
store, find, link, and delete files in one line:

```ts
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](/docs/ai-agents).

## Next steps [#next-steps]

<Cards>
  <Card title="How it works" href="/docs/architecture" icon="Network" />

  <Card title="Projects" href="/docs/projects" icon="FolderOpen" />

  <Card title="Server client" href="/docs/server-client" icon="Server" />

  <Card title="Browser uploads" href="/docs/browser-uploads" icon="Upload" />

  <Card title="React hooks" href="/docs/react" icon="Atom" />

  <Card title="Access control" href="/docs/access-control" icon="Shield" />

  <Card title="API keys" href="/docs/api-keys" icon="KeyRound" />
</Cards>
