---
title: Supabase Storage
description: Supabase Storage via @supabase/storage-js. Pass an existing SupabaseClient to share auth/postgrest with the rest of your app.
related:
  - /guides/supabase-private-uploads-rls
---

## Installation

`@supabase/storage-js` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime.

```package-install
files-sdk @supabase/storage-js
```

## Usage

Supabase Storage via the official `@supabase/storage-js` SDK. Auto-loads the project URL and an API key from the standard env vars; pass `client` to share an existing `SupabaseClient` with the rest of your app (auth, postgrest).

```ts lineNumbers
import { Files } from "files-sdk";
import { supabase } from "files-sdk/supabase";

const files = new Files({
  adapter: supabase({
    bucket: "uploads",
    // Auto-loads url + key from SUPABASE_URL / NEXT_PUBLIC_SUPABASE_URL
    // and SUPABASE_SERVICE_ROLE_KEY / SUPABASE_KEY /
    // NEXT_PUBLIC_SUPABASE_ANON_KEY. Or pass an existing SupabaseClient
    // via `client` to share with auth/postgrest.
  }),
});
```

## Options

<AutoTypeTable
  path="../../packages/files-sdk/src/supabase/index.ts"
  name="SupabaseAdapterOptions"
/>

## Compatibility

| Method | Status | Notes |
| --- | :-: | --- |
| `upload` | ⚠️ | `cacheControl` maps to Supabase's seconds-based field, so only a `max-age=<n>` (optionally with `public`) or a bare number of seconds is accepted; other directives (`no-store`, `immutable`, `private`, …) throw rather than being dropped. Pause/resume via `control` uses Supabase's TUS endpoint (needs `url` + `key`; with a pre-built `client`, `capabilities.resumable` is `false`) in fixed 6 MiB chunks - Supabase requires that size, so `multipart.partSize` is ignored. `abortUpload()` rejects when Supabase refuses the cancel. |
| `download` | ⚠️ | Supabase's `download()` has no range primitive, so `range` throws. The download response carries no user metadata, so every download (buffered or streamed) also requests the object's info, for its size, ETag, last-modified, and metadata. If that lookup fails with anything other than `NotFound`, the download fails rather than return the body without its metadata. |
| `delete` | ✅ | A bulk delete goes out in batches of 1000 keys, Supabase's per-request cap; a refused batch fails only its own keys. |
| `list` | ✅ | Uses Supabase's V2 list API: a flat, recursive, string-prefix scan over full keys with a real server cursor. (The legacy V1 API is folder-scoped and non-recursive, so it could not back the unified `list` contract.) |
| `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). |
| `head` | ✅ | User `metadata` round-trips exactly, on `head()` and every download path (keys aren't camelCased), so [`encryption()`](/docs/plugins/encryption), [`compression()`](/docs/plugins/compression), and [`dedup()`](/docs/plugins/dedup) work on Supabase. |
| `exists` | ✅ |  |
| `copy` | ✅ | Server-side copy that replaces an existing destination (sent with `x-upsert: true`), so `copy` and `move` onto a key that already exists overwrite it like every other adapter. |
| `url` | ⚠️ | Default mints a signed read URL (1-hour default). With `public: true`, returns the permanent unsigned public object URL. With `publicBaseUrl`, returns `<publicBaseUrl>/<key>`. `responseContentDisposition` forces the signed path and maps onto Supabase's `download` option, which can only force an attachment: `attachment` and `attachment; filename="…"` work, while `inline` or any other disposition throws. |
| `signedUploadUrl` | ⚠️ | PUT URL only - Supabase has no POST policy equivalent. `maxSize` and `contentType` throw (Supabase signed upload URLs bind neither a `content-length-range` policy nor a Content-Type; set the bucket-level size limit and allowed MIME types in the dashboard instead). A positive `minSize` throws too - there's no minimum-size constraint (`minSize: 0` is accepted). `expiresIn` is silently ignored - Supabase fixes the TTL at 2 hours server-side. The returned headers include `x-upsert: true`. |

## Keys

Every request that puts a key in its URL (upload, download, `head`, `exists`, `url`, `signedUploadUrl`) percent-encodes each path segment, so the request names exactly the key given: a `?`, `+`, or space can't cut it short or change it. Supabase Storage itself only accepts keys made of ASCII letters and digits plus `_ / ! . * ' ( ) & $ = @ ; : + , ? -` and spaces; it rejects others (such as `#`, `%`, or non-ASCII characters) with an `Invalid key` error rather than storing a truncated key.
