# @uploadcn/server (/docs/reference/server)



<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 install @uploadcn/server
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @uploadcn/server
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @uploadcn/server
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @uploadcn/server
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Runs on Node.js, Bun, Deno, Cloudflare Workers and edge runtimes. Signing uses
[aws4fetch](https://github.com/mhart/aws4fetch) (Web Crypto, \~2.5 KB), no AWS SDK.

## createUploadRoute [#createuploadroute]

```ts
const { POST } = createUploadRoute(options)
// POST(request: Request): Promise<Response>
```

<TypeTable
  type="{
  storage: { type: &#x22;UploadStorage&#x22;, required: true, description: &#x22;s3Storage, r2Storage, createMemoryStorage or your own.&#x22; },
  authorize: { type: &#x22;({ request, action, key? }) => TAuth | Promise<TAuth>&#x22;, description: &#x22;Throw UploadRouteError to deny. The return value is passed to getKey and onUploadComplete.&#x22; },
  maxFileSize: { type: &#x22;number&#x22;, description: &#x22;Bytes. Checked before signing and locked into single-PUT signatures; verified after multipart.&#x22; },
  allowedTypes: { type: &#x22;string | string[]&#x22;, description: &#x22;accept syntax: MIME types, wildcards, extensions.&#x22; },
  getKey: { type: &#x22;({ file, meta, auth, request }) => string&#x22;, default: &#x22;`${uuid}/${sanitized name}`&#x22; },
  expiresIn: { type: &#x22;number&#x22;, default: &#x22;3600&#x22;, description: &#x22;Signed URL lifetime, in seconds.&#x22; },
  onUploadComplete: { type: &#x22;({ key, url, file, meta, auth, request, scan? }) => unknown&#x22;, description: &#x22;Save metadata, start processing. The return value is sent to the client as result.data.&#x22; },
  verify: { type: &#x22;boolean&#x22;, default: &#x22;true&#x22;, description: &#x22;HEAD the object before completing.&#x22; },
  scan: { type: &#x22;{ scanner, onUnknown?, maxFileSize?, onResult? }&#x22;, description: &#x22;Scan uploads before they complete. Infected files are deleted and returned as rejected. See [Virus scanning](/docs/guides/virus-scanning).&#x22; },
}"
/>

Errors thrown as `UploadRouteError(message, status, code?)` are returned as `{ error }`
(plus `code`) with that status. `code: "rejected"` marks the file itself as refused: the
client shows it as rejected and never retries. Anything else becomes a generic `500` and is logged, internal details
never reach the client.

## Storage [#storage]

### s3Storage [#s3storage]

```ts
s3Storage({
  bucket, region, accessKeyId, secretAccessKey,
  sessionToken?, endpoint?, forcePathStyle?, publicUrl?, readUrlExpiresIn?,
})
```

Without `publicUrl`, `url` in results is a signed GET URL.

### r2Storage [#r2storage]

```ts
r2Storage({ accountId, bucket, accessKeyId, secretAccessKey, publicUrl?, jurisdiction? })
```

### createMemoryStorage [#creatememorystorage]

```ts
const { storage, handler } = createMemoryStorage({ baseUrl: "/api/storage", secret })
```

A development storage implementing the full presigned and multipart flow with
HMAC-signed URLs. Bytes are discarded (only size and ETag are kept). Mount `handler`
at `baseUrl` for `PUT` requests. Not for production.

### Custom storage [#custom-storage]

Implement `UploadStorage` (`presignPut`, `createMultipart`, `presignPart`, `listParts`,
`completeMultipart`, `abortMultipart`, `headObject`, `deleteObject`, `getUrl`) to support
any backend. Add the optional `getObject(key)` (a `ReadableStream`) to enable scanning and
OCR of stored files.

## Virus scanning [#virus-scanning]

```ts
import { clamavScanner, createScanner, httpScanner, virusTotalScanner } from "@uploadcn/server/scan"
```

Each returns a `Scanner` whose `scan({ stream, file, key?, signal? })` resolves to
`{ status: "clean" | "infected" | "unknown", threats, scanner, details? }`. See
[Virus scanning](/docs/guides/virus-scanning).

## OCR [#ocr]

```ts
import {
  awsTextractOcr, azureDocumentIntelligenceOcr, createOcrProvider, createOcrRoute,
  googleVisionOcr, httpOcr, mistralOcr,
} from "@uploadcn/server/ocr"
```

Each provider's `recognize({ data, type, name?, signal? })` resolves to an `OcrResult`.
`createOcrRoute({ provider, authorize?, maxFileSize?, allowedTypes?, storage?, onResult? })`
returns `{ POST }`. See [OCR](/docs/guides/ocr).

## Protocol [#protocol]

One `POST` endpoint with a JSON body, dispatched on `action`. Implement it in any language
to use `s3Adapter` with a non-JavaScript backend.

| Action               | Request                                                         | Response                                          |
| -------------------- | --------------------------------------------------------------- | ------------------------------------------------- |
| `presign`            | `{ file: { name, type, size }, meta? }`                         | `{ key, url, method: "PUT", headers }`            |
| `complete`           | `{ key, file, meta? }`                                          | `{ key, url, etag?, data? }`                      |
| `create-multipart`   | `{ file, meta? }`                                               | `{ key, uploadId }`                               |
| `sign-part`          | `{ key, uploadId, partNumber }`                                 | `{ url }`                                         |
| `list-parts`         | `{ key, uploadId }`                                             | `{ parts: [{ partNumber, etag, size }] \| null }` |
| `complete-multipart` | `{ key, uploadId, parts: [{ partNumber, etag }], file, meta? }` | `{ key, url, etag?, data? }`                      |
| `abort-multipart`    | `{ key, uploadId }`                                             | `{ ok: true }`                                    |

Errors: any non-2xx status with `{ "error": "message" }`. `408`, `429` and `5xx` are
retried by the client.
