# Buckets

> S3-compatible object storage. Create a bucket, upload and download with presigned URLs, attach it to an app for direct access, set CORS for browser uploads.

A bucket holds objects and speaks the S3 protocol. Your app reaches it in two ways: ask the Rytiv API for a presigned URL per object, or attach the bucket to the app, which gives it a key scoped to that bucket and the `S3_*` environment variables to use any S3 client directly.

## 1. Create a bucket

**CLI**

```bash
ry bucket check my-uploads
ry bucket create my-uploads
```

**curl**

```bash
curl -X POST "https://api.rytiv.app/v1/buckets" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-uploads"}'
```

**TypeScript**

```ts
await fetch("https://api.rytiv.app/v1/buckets", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ name: "my-uploads" }),
});
```

**Python**

```python
import os, requests

requests.post(
    "https://api.rytiv.app/v1/buckets",
    headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"},
    json={"name": "my-uploads"},
)
```

Bucket names are 3 to 63 characters of lowercase letters, digits and hyphens, unique across Rytiv; names starting with `rytiv` are reserved. `GET /v1/bucket-names/:name` (`ry bucket check`) tells you whether a name is free and, if not, why.

## 2. Upload and download with presigned URLs

A presigned URL is valid for one object and one method, for a limited time (default 15 minutes, at most 7 days). Nobody who holds it needs a key.

**CLI**

```bash
ry bucket presign my-uploads reports/2026-10.pdf --put --content-type application/pdf
ry bucket presign my-uploads reports/2026-10.pdf
```

**curl**

```bash
curl -X POST "https://api.rytiv.app/v1/buckets/my-uploads/presign" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"op": "put", "key": "reports/2026-10.pdf", "contentType": "application/pdf", "expiresInS": 900}'
```

**TypeScript**

```ts
const res = await fetch("https://api.rytiv.app/v1/buckets/my-uploads/presign", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ op: "put", key: "reports/2026-10.pdf", contentType: "application/pdf" }),
});
const { url, method, requiredHeaders } = await res.json();
await fetch(url, { method, headers: requiredHeaders, body: fileBytes });
```

**Python**

```python
import os, requests

signed = requests.post(
    "https://api.rytiv.app/v1/buckets/my-uploads/presign",
    headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"},
    json={"op": "put", "key": "reports/2026-10.pdf", "contentType": "application/pdf"},
).json()
with open("2026-10.pdf", "rb") as f:
    requests.put(signed["url"], headers=signed["requiredHeaders"], data=f)
```

The answer carries `url`, `method`, `expiresAt` and `requiredHeaders`: headers the call must send exactly as given, because they are part of the signature. For a `put` with `contentType`, that is the `Content-Type` the upload must use.

Keys are at most 1024 bytes, with no leading or trailing `/`, no empty, `.` or `..` segments.

## 3. Attach the bucket to an app

```bash
ry bucket attach my-uploads --to web
ry bucket attach my-uploads --to web --as uploads
```

Attaching gives the app a key that opens exactly this bucket (read and write, no owner rights) and injects it as managed secrets. From its next start the app sees:

| Variable | Meaning |
| --- | --- |
| `S3_BUCKET` | The bucket's name |
| `S3_ENDPOINT` | The S3 endpoint to talk to, `https://s3.<region>.<domain>` |
| `S3_REGION` | The region name to sign with |
| `S3_ACCESS_MODE` | `key`: the app holds a key and speaks S3 directly |
| `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | The key scoped to this bucket |

With `--as uploads` every variable gets the prefix `UPLOADS_` (`UPLOADS_S3_BUCKET`, ...); from the second bucket on an app the prefix is required. The key is shown once, in the attach answer; attaching again rotates it. `ry bucket detach my-uploads --from web` removes the variables and revokes the key; the objects stay.

The endpoint supports virtual-host addressing (`<bucket>.s3.<region>.<domain>`). With the AWS SDK for JavaScript 3.729 or newer, set `requestChecksumCalculation: "WHEN_REQUIRED"`, or presigned uploads fail with `InvalidDigest`.

## 4. Allow uploads from the browser

A browser that uploads straight to a presigned URL sends a preflight first; without a CORS rule the storage refuses it, and nothing of it reaches your server.

```bash
ry bucket cors-set my-uploads --allow-origin https://www.example.com
ry bucket cors my-uploads
```

`ry bucket cors-set` writes one rule with the given origins, methods `GET,PUT,HEAD`, headers `*,authorization`, and `ETag` exposed, so an upload client can read the answer of its own `PUT`. `PUT /v1/buckets/:name/cors` takes the whole list of rules and replaces what was there; an empty list removes them. CORS is not authentication: the presigned URL's signature is what protects the object.

## 5. List and delete objects

```bash
ry bucket ls my-uploads --prefix reports/
ry bucket rm my-uploads reports/2026-10.pdf
```

`GET /v1/buckets/:name/objects?prefix=&limit=&cursor=` lists up to 1000 objects per page (default 100) with a `nextCursor` for the next page. `DELETE /v1/buckets/:name/objects?key=<url-encoded key>` deletes one object; the key goes in the query string.

## 6. Delete the bucket

```bash
ry bucket destroy my-uploads
ry bucket destroy my-uploads --force
```

A bucket that still holds objects is refused with `409`; `--force` (`?force=1` on `DELETE /v1/buckets/:name`) deletes the objects first, then the attachments' variables, then the bucket. The CLI asks you to type the name; pass `--yes` in scripts.
