# Storage

A private S3-compatible bucket per project, with a file browser and presigned URLs.

## Deploy

Click Deploy on the Storage card. CloudHike.dev creates a bucket named `proj-<slug>` and an access key that can only list, read, write and delete in that one bucket. The free plan includes 500 MB; Pro has no limit and is billed at $0.08 per GB-month stored.

| Variable | Meaning |
| --- | --- |
| `S3_ENDPOINT` | The S3 address to send requests to |
| `S3_REGION` | The region string SDKs expect |
| `S3_BUCKET` | Your bucket name. Put every object here |
| `S3_ACCESS_KEY_ID` | The access key |
| `S3_SECRET_ACCESS_KEY` | The secret key. Server only |

The card has Reveal and Copy for the secret, Copy as .env, Reset keys, Delete and a ready-made AWS SDK sample.

## Use the S3 SDK

Any S3 library works. Set `forcePathStyle` to true so the bucket name goes in the path.

*lib/s3.ts*

```ts
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

export const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  region: process.env.S3_REGION,
  forcePathStyle: true,
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID!,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
  },
});

const Bucket = process.env.S3_BUCKET!;

export async function upload(key: string, body: Buffer, contentType: string) {
  await s3.send(new PutObjectCommand({ Bucket, Key: key, Body: body, ContentType: contentType }));
}

// A link that works for one hour, for downloads or direct browser uploads.
export const downloadUrl = (key: string) =>
  getSignedUrl(s3, new GetObjectCommand({ Bucket, Key: key }), { expiresIn: 3600 });

export const uploadUrl = (key: string, contentType: string) =>
  getSignedUrl(s3, new PutObjectCommand({ Bucket, Key: key, ContentType: contentType }), { expiresIn: 3600 });
```

To let users upload from the browser, create the presigned upload URL on your server and have the browser `PUT` the file to it. The file never passes through your server.

*In the browser*

```ts
const { url } = await fetch("/api/upload-url", { method: "POST", body: JSON.stringify({ name: file.name, type: file.type }) }).then((r) => r.json());
await fetch(url, { method: "PUT", headers: { "content-type": file.type }, body: file });
```

> **Warning:** Buckets are private. Share files with presigned links, which expire, or serve them through your own server. Do not put the access key in browser code.

## Storage browser

Click Open storage on a ready Storage card to manage files from the dashboard.

- **Files**: folders and files with size, type and date. Sort, page 25 to 250 at a time, and search a folder and everything below it.
- **Preview**: images, video, audio and PDFs play inline, and text and JSON are shown (the first 256 KB). Download, Copy URL and Delete are there too.
- **Upload**: drop files on the list or click Upload. They go straight to the bucket three at a time with progress. Replacing a file asks first.
- **New folder**: creates an empty folder. Deleting a folder removes everything in it.
- **Stats**: object and folder counts, space used against your limit, a breakdown by type, and the largest files.

Copy URL copies a presigned link that expires after an hour.

## Limits and behavior

- Uploads that would go over your plan's limit are refused up front.
- A Free bucket over its limit for 3 days is paused: uploads and new folders are refused, deleting still works.
- Pro buckets have no size limit and are never paused for size; every GB stored is billed per GB-month.
- Database backups live in the bucket under `_backups/postgres/` and count toward the size.
- Browser uploads need a CORS rule on the bucket. New buckets get one automatically on AWS; local MinIO accepts uploads from any site.
- Reset keys creates a new access key and secret. Update your app's env file afterwards.

## Use with AI

Ask an agent to add file uploads and downloads to your app with your project's bucket.

```text
Read https://cloudhike.dev/docs/md/storage, then add file uploads to this app using my CloudHike.dev Storage bucket.

- S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY are in my env file (if not, follow https://cloudhike.dev/prompt.md to download them).
- Use @aws-sdk/client-s3 with forcePathStyle: true, from server code only.
- Upload straight from the browser using presigned PUT URLs created on the server, and show files with presigned GET URLs.
- Never expose the access keys to the browser and never make the bucket public.
```
