Source: https://www.hype-stack.dev/docs/backend/storage

# Storage

The template includes an S3-compatible object storage client. Locally it talks to RUSTFS (or MinIO-compatible) from
Docker Compose. On deploy, Fly uses Tigris and Railway uses an S3-compatible bucket. The app code stays the same: change
the endpoint and keys, not the upload calls.

## Client

Initialized at backend startup from `apps/backend/src/libs/storage/storage.ts`:

```ts
s3 = new S3Client({
  endpoint: process.env.RUSTFS_ENDPOINT,
  region: "us-east-1",
  credentials: {
    accessKeyId: process.env.RUSTFS_ACCESS_KEY,
    secretAccessKey: process.env.RUSTFS_SECRET_KEY,
  },
  forcePathStyle: true,
});
```

Helpers on that module cover upload, download, signed URLs, and delete. The client auto-creates the bucket on first
upload when the provider allows it.

## Environment variables

| Variable            | Purpose                                              |
| ------------------- | ---------------------------------------------------- |
| `RUSTFS_ENDPOINT`   | S3 API endpoint (local Docker URL or cloud endpoint) |
| `RUSTFS_ACCESS_KEY` | Access key                                           |
| `RUSTFS_SECRET_KEY` | Secret key                                           |

Bucket names are chosen by the code that uploads: each feature passes its bucket to the storage helpers, and the client
creates it on first use. The AI chat pack adds `AI_STORAGE_BUCKET` for its private bucket.

[`deploy`](/docs/cli/deploy) wires these from the provider you pick for `--bucket`. For a database or bucket you already
run, use the `external` block in `stack.json` (same pattern as bring-your-own Postgres).

## Upload middleware

HTTP uploads go through `apps/backend/src/middleware/file/`:

```ts
import { fileUpload, memoryStorage } from "@backend/middleware/file";

app.post(
  "/avatar",
  fileUpload({
    storage: memoryStorage(),
    limits: { fileSize: 5 * 1024 * 1024, files: 1 },
  }).single("file"),
  async (c) => {
    const file = c.get("file");
    // uploadFile({ bucket, key, body: file.buffer, contentType: file.mimetype })
    return c.json({ ok: true });
  },
);
```

Prefer `diskStorage` for larger files. Always set `limits.fileSize` and a `fileFilter` when only images (or another MIME
set) are allowed.

## What packs use it for

After a starter or feature pack install, storage typically backs:

- User avatars
- Organization logos
- Team / project icons
- Board or media images in collaboration packs

Those features import `@backend/libs/storage/storage` and the file middleware. They do not bring their own storage
provider.

## Local vs production

| Environment    | Typical backend                                           |
| -------------- | --------------------------------------------------------- |
| Local Docker   | RUSTFS / MinIO-compatible service in `docker-compose.yml` |
| Fly.io deploy  | Tigris (S3 API)                                           |
| Railway deploy | Railway's S3-compatible bucket / RustFS service           |

Keep `forcePathStyle: true` unless your provider documents otherwise. Path-style addressing is what the local and
deployed setups expect.

## Related

- [Env variables](/docs/backend/env-variables)
- [Deploy](/docs/cli/deploy)
- [Going to production](/docs/getting-started/going-to-production)
