File storage
Covers 8 API operations in the storage category.
| Method | Path | Description |
|---|---|---|
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/storage | Object storage status (runlot storage usage) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/storage | Turns storage back on after it was turned off |
| DELETE | /v1/orgs/{orgSlug}/projects/{projectName}/storage | Remove project storage (runlot storage delete) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/storage/public | The published key prefixes (runlot storage public list) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/storage/public | Publish a key prefix (runlot storage public add) |
| DELETE | /v1/orgs/{orgSlug}/projects/{projectName}/storage/public | Unpublish a key prefix (runlot storage public rm) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/storage/objects | List objects (runlot storage ls) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/storage/objects/{key} | A short-lived signed URL for one object (runlot storage download) |
GET /v1/orgs/{orgSlug}/projects/{projectName}/storage
Whether object storage is enabled, and its prefix, quota and usage. usedBytes is the
last reported value, so it lags by the reporting interval — the quota can be
slightly exceeded in the meantime.
Object listings and signed URLs are not part of this API; use env.storage from the
worker.
Requires viewer or higher.
operationId getStorage
| Status code | Description | Response body |
|---|---|---|
| 200 | Storage status | StorageStatus |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — the service is temporarily unavailable | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/storage
This is idempotent — calling it twice leaves the prefix unchanged, so a retry can never move objects out of view.
There is no request body. The prefix is fixed per project and the quota is set by the plan, so neither is a value you set.
Once enabled, env.storage is connected to the worker shortly afterwards. As with
env.db, no redeploy is needed.
Requires member or higher. Audited as storage.grant.
operationId grantStorage
| Status code | Description | Response body |
|---|---|---|
| 200 | Grant status (existing value if it was already there) | StorageStatus |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — the service is temporarily unavailable | Error |
DELETE /v1/orgs/{orgSlug}/projects/{projectName}/storage
Objects are not deleted. Turning storage off detaches it from the project and is recorded in the audit log; the stored objects remain.
Disabling is also idempotent. It returns 204 even when storage was not enabled.
Requires admin or higher — billing stops, but the objects are left behind. Audited as
storage.revoke.
operationId revokeStorage
| Status code | Description | Response body |
|---|---|---|
| 204 | detached | — |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — the service is temporarily unavailable | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/storage/public
The key prefixes this project serves unsigned at
https://objects.runlot.io/<org>/<project>/<key>.
A stored prefix is normalized: assets becomes assets/, and / becomes the empty
string, which means every key in the project's storage. Matching is a plain string
prefix test, so the trailing slash is what keeps assets from also opening
assets-private/.
Requires viewer or higher.
operationId listStoragePublic
| Status code | Description | Response body |
|---|---|---|
| 200 | The list | StoragePublicList |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — the service is temporarily unavailable | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/storage/public
Makes every key under this prefix readable by anyone, with no signature, over HTTPS. The bucket itself stays private: every request is checked against this list before the object is read.
It is idempotent, and it answers with the whole list after the change.
At most 20 prefixes per project.
Requires admin or higher. Publishing makes a project's bytes readable by the
internet. Audited as storage.public.add, recording both what was sent and what was
stored, since normalization can make the two differ.
operationId addStoragePublic
Request body: application/json · StoragePublicRequest
| Status code | Description | Response body |
|---|---|---|
| 200 | The list after the change | StoragePublicList |
| 400 | The prefix is empty, not normalizable, or would leave the project's slice | Error |
| 403 | — | — |
| 404 | — | — |
| 409 | storage_not_granted, or too_many_public_prefixes | Error |
| 503 | no_core — the service is temporarily unavailable | Error |
DELETE /v1/orgs/{orgSlug}/projects/{projectName}/storage/public
The prefix travels in the request body rather than the path, because a prefix can contain slashes.
Idempotent — removing one that is not there is a 200 with the unchanged list.
The world keeps reading for up to 60 seconds. The published list is cached with that TTL, so this call is not a revocation that takes effect the instant it returns.
Requires admin or higher. Audited as storage.public.remove.
operationId removeStoragePublic
Request body: application/json · StoragePublicRequest
| Status code | Description | Response body |
|---|---|---|
| 200 | The list after the change | StoragePublicList |
| 400 | The prefix is empty or not normalizable | Error |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — the service is temporarily unavailable | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/storage/objects
One page of the project's objects, grouped by / the way a file browser groups
them: entries are the objects directly under prefix, and prefixes are the
directories one level down.
Keys are the ones the worker writes — env.storage.put(key, …) — never the
internal p/<projectId>/ prefix, which is an isolation boundary and not part of
any address.
public is true when the key sits under a prefix this project has published
(…/storage/public), and url is then the unsigned address anyone can read. A
private object carries no url: this field means "anyone can read this", and a
signed URL in it would show a private object as a public one. For those, ask
…/storage/objects/{key}.
Paging is by cursor, and the boundary lands between groups — no directory is
listed twice and none is skipped. Feed cursor back unchanged; its absence is the
end.
Requires viewer or higher.
operationId listStorageObjects
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
prefix | query | No | string | Key prefix to list under. It is normalized the same way a published prefix is: assets becomes assets/, and empty means the project root. |
cursor | query | No | string | the cursor of the previous page |
limit | query | No | integer | entries plus prefixes on one page |
| Status code | Description | Response body |
|---|---|---|
| 200 | One page | StorageObjectPage |
| 400 | prefix is not normalizable, or limit is out of range | Error |
| 403 | — | — |
| 404 | — | — |
| 409 | storage_not_granted — this project has no file storage | Error |
| 502 | objects_unreachable — the object store did not answer | Error |
| 503 | objects_unavailable — this installation has no object store; or no_core | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/storage/objects/{key}
A GET URL valid for five minutes, which the browser or the CLI then fetches directly from the object front. The bytes never pass through this API.
The object is looked up before it is signed, so a key that holds nothing is a 404 here rather than a URL that 404s minutes later somewhere you cannot correlate it.
Only GET is signed. Uploading and deleting are not on this surface — write through
env.storage from the worker.
Requires viewer or higher.
operationId signStorageObject
| Status code | Description | Response body |
|---|---|---|
| 200 | The signed URL and the object's metadata | StorageObjectSigned |
| 400 | the key is empty, escapes the project prefix, or is over 1024 bytes | Error |
| 403 | — | — |
| 404 | — | — |
| 409 | storage_not_granted — this project has no file storage | Error |
| 503 | objects_unavailable — this installation has no object store; or no_core | Error |