Local disk

Store uploads on your own server's disk, for self-hosted apps, VPSs and local development.

import { createFileSystemStorage } from "@uploadcn/server/fs"

createFileSystemStorage implements the same storage interface as S3, so the browser side doesn't change: s3Adapter uploads through signed URLs, with multipart and resume. Bytes are streamed to disk, never buffered whole in memory.

Node.js servers only

Serverless platforms (Vercel functions, Lambda, Workers) have no persistent disk. Use it on a VPS, a container with a volume, or locally.

Create the storage

lib/storage.ts
import { createFileSystemStorage } from "@uploadcn/server/fs"

export const files = createFileSystemStorage({
  directory: "./uploads",
  baseUrl: "/api/files",
  secret: process.env.UPLOAD_SIGNING_SECRET!, // a long random string
})

Mount the routes

The upload route signs requests. The file handler receives the bytes and serves signed downloads.

app/api/upload/route.ts
import { createUploadRoute } from "@uploadcn/server"

import { files } from "@/lib/storage"

export const { POST } = createUploadRoute({ storage: files.storage })
app/api/files/route.ts
import { files } from "@/lib/storage"

export const PUT = files.handler
export const GET = files.handler

Point components at it

const adapter = s3Adapter({ endpoint: "/api/upload" })

Public files

By default, getUrl returns a short-lived signed download URL. To serve files publicly, for example from public/uploads, set publicUrl:

createFileSystemStorage({
  directory: "./public/uploads",
  publicUrl: "/uploads",
  baseUrl: "/api/files",
  secret: process.env.UPLOAD_SIGNING_SECRET!,
})

Security

  • Upload and download URLs are HMAC-signed, expire, and are bound to one method, so a download URL can't be used to upload.
  • Object keys can't escape directory: .., absolute paths and the internal parts folder are rejected.
  • A signed single upload must match its declared size exactly; anything larger is rejected while streaming.
  • Files are written to a temporary path and renamed when complete, so readers never see half-written files.
  • Downloads are served with x-content-type-options: nosniff.

Options

Prop

Type

On this page