# File storage

Every project has file storage, split into buckets. Each environment has its own files: a preview environment starts empty and never sees production's.

## Buckets and rules

Two buckets always exist:

| Bucket | Read | Write |
| --- | --- | --- |
| `public` | Anyone | The file's owner |
| `private` | The file's owner | The file's owner |

Define more, or change these two, in `flatstack/storage.ts`. Rules use the same words as [tables](data.md): `owner`, `authenticated`, `public`, `deny`, or a function.

```ts
import { bucket } from "@flatstack/server/storage";

export const avatars = bucket("avatars", {
  access: { read: "public", write: "owner" },
  contentTypes: ["image/*"],
  maxFileBytes: 5 * 1024 * 1024,
});

export const reports = bucket("reports", {
  access: { read: "authenticated", write: "deny" },
});

// A function rule gets the user and the file ({ bucket, key, size, contentType }).
export const shared = bucket("shared", {
  access: { read: (_ctx, file) => file.key.startsWith("open/"), write: "owner" },
});
```

For files, **`owner` means the first folder of the key is the user's ID**: user `u_123` owns `u_123/avatar.png` and everything under `u_123/`. Put each user's files under their ID.

A bucket without an `access` block is owner-only. Bucket names are up to 63 lowercase letters, numbers and hyphens. Every export of `flatstack/storage.ts` must be a `bucket()`, or the deploy fails.

Keys are paths like `u_123/photos/beach.jpg`: up to 1,000 bytes, no leading or trailing `/`, no `//`, and no `.` or `..` folders.

## Uploading

Browsers upload straight to storage with a signed URL, so large files never pass through your functions.

1. `POST /storage/<bucket>/uploads` with `{ "key", "size", "contentType"? }`. The bucket's write rule, content types and size limit are checked here.
2. For files up to 100 MB the answer is `{ "kind": "single", "url", "method": "PUT", "headers" }`. Send one `PUT` to `url` with the file as the body and exactly those headers. The URL only accepts a file of the size and type you gave, and lasts an hour.
3. Larger files, up to 5 GB, get `{ "kind": "multipart", "uploadId", "partSize", "parts": [{ "partNumber", "url" }] }`. Send each `partSize` slice of the file (the last one shorter) as a `PUT` to its part URL, keep the `ETag` header of each response, then `POST /storage/<bucket>/uploads/complete` with `{ "key", "uploadId", "parts": [{ "partNumber", "etag" }] }`. To give up, `POST /storage/<bucket>/uploads/abort` with `{ "key", "uploadId" }`. Uploads not completed within 24 hours are aborted for you.

## Downloading

`GET /storage/<bucket>/<key>` serves the file under the bucket's read rule. Files in a bucket anyone may read are cached at the edge for an hour (`cache-control: public, max-age=3600`); others are sent with `private, no-store`. Every response has an `ETag`, and `If-None-Match` gets `304`.

For private files you can also hand out a signed link: `POST /storage/<bucket>/signed-url` with `{ "key", "expiresIn"? }` returns `{ "url", "expiresAt" }`. Links last an hour by default, at most 7 days, and work without signing in.

### Image transforms

Add parameters to a download URL to resize and convert JPEG, PNG, WebP, AVIF and GIF images:

```
/storage/avatars/u_123/me.png?w=400&h=300&fit=cover&format=webp
```

| Parameter | Values |
| --- | --- |
| `w`, `h` | Width and height in pixels, 1 to 4096 |
| `fit` | `scale-down`, `contain`, `cover`, `crop`, `pad` |
| `format` | `webp`, `avif`, `jpeg`, `png`. Without it, the image keeps its format (GIFs become WebP). |
| `q` | Quality, 1 to 100 |

Each size is made once and cached: later requests for the same parameters, in any order, are served from the cache (`x-fs-transform-cache: hit`). Replacing the original makes new versions. Cached sizes don't count towards your file storage. Source images can be up to 50 MB.

## Listing and deleting

| Request | What it does |
| --- | --- |
| `GET /storage/<bucket>?prefix=&cursor=&limit=&delimiter=1` | Files the user may read, up to 100 per page (`limit` up to 1,000). With `delimiter=1`, keys below the next `/` are rolled into `prefixes`, like folders. Returns `{ files, prefixes, cursor }`; pass `cursor` back for the next page. In an `owner` bucket the listing is the user's own folder. |
| `DELETE /storage/<bucket>/<key>` | Deletes the file if the user may write it: `{ deleted }`. |

## In functions: `ctx.storage`

```ts
export const saveAvatar = fn({
  auth: "required",
  input: z.object({ png: z.string() }),
  handler: async ({ input, ctx }) => {
    const bytes = Uint8Array.from(atob(input.png), (c) => c.charCodeAt(0));
    const file = await ctx.storage.asUser().from("avatars").put(`${ctx.user.id}/me.png`, bytes, {
      contentType: "image/png",
    });
    return { url: ctx.storage.from("avatars").url(file.key, { w: 256, format: "webp" }) };
  },
});
```

`ctx.storage.asUser().from(bucket)` applies the bucket's rules for the calling user, like the HTTP routes. `ctx.storage.from(bucket)` skips them, like `ctx.db.asAdmin()`: use it in server-only code. Both offer `put`, `get` (with `{ transform }`), `head`, `delete`, `list`, `createUploadUrl`, `completeUpload`, `abortUpload`, `createDownloadUrl` and `url`. Streams passed to `put` need `{ size }`.

## Limits

| Limit | Value |
| --- | --- |
| Largest file | 5 GB |
| Single upload | up to 100 MB; larger files upload in 100 MB parts |
| Abandoned multipart uploads | aborted after 24 hours |
| Signed download links | 1 hour by default, at most 7 days |
| File storage | your plan's file storage, pooled across the account's projects |

When the account's file storage is full, uploads answer `507 file_storage_full`. Downloads and deletes keep working.

## Errors

| Code | Status | When |
| --- | --- | --- |
| `validation_failed` | 400 | A bad bucket name, key, size, content type or transform parameter, or a file the bucket doesn't accept. |
| `unauthenticated` | 401 | The rule needs a signed-in user. |
| `forbidden` | 403 | The rule doesn't allow this user, for example another user's folder in an `owner` bucket. |
| `not_found` | 404 | No such bucket, file or open upload. |
| `file_storage_full` | 507 | File storage is full. |