Guides
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
Section titled “1. Create a bucket”ry bucket check my-uploadsry bucket create my-uploadscurl -X POST "https://api.rytiv.app/v1/buckets" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "my-uploads"}'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" }),});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
Section titled “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.
ry bucket presign my-uploads reports/2026-10.pdf --put --content-type application/pdfry bucket presign my-uploads reports/2026-10.pdfcurl -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}'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 });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
Section titled “3. Attach the bucket to an app”ry bucket attach my-uploads --to webry bucket attach my-uploads --to web --as uploadsAttaching 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
Section titled “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.
ry bucket cors-set my-uploads --allow-origin https://www.example.comry bucket cors my-uploadsry 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
Section titled “5. List and delete objects”ry bucket ls my-uploads --prefix reports/ry bucket rm my-uploads reports/2026-10.pdfGET /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
Section titled “6. Delete the bucket”ry bucket destroy my-uploadsry bucket destroy my-uploads --forceA 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.