Skip to content
runlot

File storage

Covers 8 API operations in the storage category.

MethodPathDescription
GET/v1/orgs/{orgSlug}/projects/{projectName}/storageObject storage status (runlot storage usage)
POST/v1/orgs/{orgSlug}/projects/{projectName}/storageTurns storage back on after it was turned off
DELETE/v1/orgs/{orgSlug}/projects/{projectName}/storageRemove project storage (runlot storage delete)
GET/v1/orgs/{orgSlug}/projects/{projectName}/storage/publicThe published key prefixes (runlot storage public list)
POST/v1/orgs/{orgSlug}/projects/{projectName}/storage/publicPublish a key prefix (runlot storage public add)
DELETE/v1/orgs/{orgSlug}/projects/{projectName}/storage/publicUnpublish a key prefix (runlot storage public rm)
GET/v1/orgs/{orgSlug}/projects/{projectName}/storage/objectsList 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 codeDescriptionResponse body
200Storage statusStorageStatus
403——
404——
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
200Grant status (existing value if it was already there)StorageStatus
403——
404——
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
204detached—
403——
404——
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
200The listStoragePublicList
403——
404——
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
200The list after the changeStoragePublicList
400The prefix is empty, not normalizable, or would leave the project's sliceError
403——
404——
409storage_not_granted, or too_many_public_prefixesError
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
200The list after the changeStoragePublicList
400The prefix is empty or not normalizableError
403——
404——
503no_core — the service is temporarily unavailableError

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

ParameterLocationRequiredTypeDescription
prefixqueryNostringKey prefix to list under. It is normalized the same way a published prefix is: assets becomes assets/, and empty means the project root.
cursorqueryNostringthe cursor of the previous page
limitqueryNointegerentries plus prefixes on one page
Status codeDescriptionResponse body
200One pageStorageObjectPage
400prefix is not normalizable, or limit is out of rangeError
403——
404——
409storage_not_granted — this project has no file storageError
502objects_unreachable — the object store did not answerError
503objects_unavailable — this installation has no object store; or no_coreError

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 codeDescriptionResponse body
200The signed URL and the object's metadataStorageObjectSigned
400the key is empty, escapes the project prefix, or is over 1024 bytesError
403——
404——
409storage_not_granted — this project has no file storageError
503objects_unavailable — this installation has no object store; or no_coreError

On this page