> ## Documentation Index
> Fetch the complete documentation index at: https://upstash.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Create a bucket, upload a file straight from the browser, and read it back.

Building with an AI coding agent?

Run `npx upstash` to install the Upstash [MCP server](/docs/agent-resources/mcp) and
[skills](/docs/agent-resources/skills) in Claude Code, Cursor, Codex, and other agents.

Upstash Blob is S3-compatible object storage. `@upstash/blob` has a `Bucket` client for your server, and an upload handler plus React hooks that upload from the browser straight to storage. This page builds a working file picker on Next.js App Router. Other frameworks work the same way; see [Other frameworks](/docs/blob/uploads/upload-handler#other-frameworks).

***

## Setup

```bash npm theme={"system"}
npm install @upstash/blob
```

```bash pnpm theme={"system"}
pnpm add @upstash/blob
```

```bash yarn theme={"system"}
yarn add @upstash/blob
```

```bash bun theme={"system"}
bun add @upstash/blob
```

Create a bucket in the [Upstash Console](https://console.upstash.com) and put its token in your environment.

```bash .env theme={"system"}
UPSTASH_BLOB_TOKEN=...
```

The console asks whether the bucket is public or private:

* **Public**: every object has a public URL. For avatars, product images, anything a page links to directly.
* **Private**: no public URL. Every read goes through a time-limited [signed URL](/docs/blob/bucket/reading#signedreadurl). For user documents, invoices, anything that must not be guessable.

***

## Upload from your server

```ts lib/reports.ts theme={"system"}
import { Bucket } from "@upstash/blob"

const bucket = Bucket.fromEnv()

export async function saveReport(file: Blob) {
  const blob = await bucket.put("reports/2026-01.pdf", file, {
    contentType: "application/pdf",
  })
  return blob.url
}
```

Bytes already on your server go to the bucket with `put`. `blob.url` is the public object URL, `undefined` on a private bucket. See [Writing](/docs/blob/bucket/writing).

***

## Upload from the browser

Files a user picks go straight from the browser to storage. Your server only authorizes the upload and records the result, so the bytes never pass through it.

**1. Write the upload handler**

```ts lib/uploads.ts theme={"system"}
import "server-only"
import { uploadHandler } from "@upstash/blob"

export const uploads = uploadHandler({
  constraints: {
    maxSize: "20mb",
    contentTypes: ["image/*", "application/pdf"],
  },

  onBeforeUpload: ({ file }) => ({ path: `images/${file.name}` }),
})
```

The handler runs on your server. It decides who may upload, where the object goes, and what happens once it lands. It never sees the bytes.

This one accepts anyone. [Upload handler](/docs/blob/uploads/upload-handler) adds the auth check and the completion callback.

**2. Mount it as a route**

```ts app/api/upload/route.ts theme={"system"}
import { uploads } from "@/lib/uploads"

export const { GET, POST } = uploads
```

`POST` runs the upload and `GET` serves the route's constraints. The hooks look at `/api/upload` by default, so nothing else has to name a URL.

**3. Bind the hooks to the handler**

```ts lib/upload-hooks.ts theme={"system"}
"use client"

import { uploadHooks } from "@upstash/blob/react"
import type { uploads } from "./uploads"

export const { useUpload } = uploadHooks<typeof uploads>()
```

`uploadHooks<typeof uploads>()` reads the handler's type, so route names and completion data are checked at compile time. The `import type` is erased at build time and never pulls server code into the browser bundle.

**4. Upload a file**

```tsx app/page.tsx theme={"system"}
"use client"

import { useUpload } from "@/lib/upload-hooks"

export default function Page() {
  const { start, upload, accept } = useUpload()

  return (
    <div>
      <input
        type="file"
        accept={accept}
        onChange={(e) => start({ file: e.target.files?.[0] })}
      />

      {upload && <p>{upload.status}</p>}
      {upload?.pending && <progress value={upload.percent} max={100} />}
      {upload?.status === "done" && (
        <a href={upload.blob.url}>{upload.blob.path}</a>
      )}
      {upload?.status === "error" && <p>{upload.error.message}</p>}
    </div>
  )
}
```

You get these without more code:

* **Multipart for large files.** Past 16 MB the SDK switches to parts, with pause, resume and per-part retry. See [Large files](/docs/blob/uploads/large-files).
* **Retries.** Failed parts back off and retry, and an expired signature is refreshed mid-upload.
* **A picker that matches the server.** `accept` comes from the route's own `GET`, so an oversized file is refused before any request goes out. See [Constraints](/docs/blob/uploads/constraints).
* **Progress.** `percent`, `status` and `pending` read the same for one PUT or 200 parts.
* **Types end to end.** Whatever `onUploadComplete` returns is `upload.blob.data` on the client.

***

## Next steps

> **[Upload handler](/docs/blob/uploads/upload-handler)**
>
> Auth, routes, and the completion callback in full.

> **[Upload client](/docs/blob/uploads/upload-client)**
>
> `useUpload`, the record it renders, and the non-React client.

> **[Writing](/docs/blob/bucket/writing)**
>
> `put`, metadata, conditional writes and multipart from the server.

> **[Recipes](/docs/blob/recipes/avatars)**
>
> Avatars, attachments and private documents, wired end to end.


## Related topics

- [Quickstart](/docs/realtime/overall/quickstart.md)
- [Realtime Quickstart](/docs/workflow/howto/realtime/basic.md)
- [Docs Search Quickstart](/docs/search/tutorials/buildsearchbar.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.