Files

Store, list, download and delete an app's files by path, over HTTP or from a function.

Docs menu
curl -s -X POST $SUBSTRIO_URL/v1/_files -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H 'content-type: application/json' \
  -d '{"path":"docs/report.pdf","data":"SGVsbG8=","encoding":"base64"}'
# {"path":"docs/report.pdf","size":5,"content_type":"application/pdf","cache_control":"","public":false,"hash":"...","created_at":1750000000,"updated_at":1750000000}

A path is 1 to 65535 bytes of valid UTF-8, with no leading or trailing /, and no empty, . or .. segment; there is no folder to create first.

Uploading

Operation Route Scope
file.upload POST /v1/_files curator

Send the file two ways: as JSON (path, data, and the fields below), or as raw bytes with the path in an X-Filename header and the type in Content-Type.

Field Type Default Description
path string required Where to store the file
data string required The file's bytes, as text or base64
encoding string text text or base64
content_type string guessed from path The stored MIME type
cache_control string none The stored Cache-Control value
public boolean false Serve without authentication
curl -s -X POST $SUBSTRIO_URL/v1/_files -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H "X-Filename: assets/logo.png" -H "Content-Type: image/png" \
  --data-binary @./logo.png

Both calls answer the file's metadata: {"path", "size", "content_type", "cache_control", "public", "hash", "created_at", "updated_at"} (hash is its content hash, created_at/updated_at are Unix seconds). A second upload to the same path replaces it.

Listing

Operation Route Scope
file.list GET /v1/_files read-only
curl -s "$SUBSTRIO_URL/v1/_files?prefix=assets/&limit=50" -H "Authorization: Bearer $SUBSTRIO_KEY"
# {"files":[{"path":"assets/logo.png","size":2048, ...}],"next_cursor":null,"count":1}

prefix narrows the listing to paths starting with it; cursor (from a prior page's next_cursor) continues a listing; limit caps the page size.

Downloading and deleting

Operation Route Scope
file.download GET /v1/_files/{path} read-only
file.delete DELETE /v1/_files/{path} operator
curl -s $SUBSTRIO_URL/v1/_files/docs/report.pdf -H "Authorization: Bearer $SUBSTRIO_KEY" -o report.pdf
curl -s -X DELETE $SUBSTRIO_URL/v1/_files/docs/report.pdf -H "Authorization: Bearer $SUBSTRIO_KEY"
# {"deleted":true,"path":"docs/report.pdf"}

file.download answers the file's bytes with its stored content type. Both take the path as the route's tail; an unknown path answers 404 FILE_NOT_FOUND.

Calling files from a function

A function reaches the same store directly through substrio.files, with no HTTP, JSON or base64 between the call and the stored bytes:

export default async function (req) {
  const meta = substrio.files.put("reports/" + req.params.id + ".json", JSON.stringify(req.body));
  const stat = substrio.files.stat(meta.path);
  const bytes = substrio.files.get(meta.path);
  return substrio.files.response(meta.path) ?? new Response("not found", { status: 404 });
}

put(path, data, options?) stores data (an ArrayBuffer, typed array, string or Blob), replacing the path if it exists, and returns its metadata. stat(path) and get(path) return null for a path that does not exist, rather than throwing. remove(path) deletes a file and returns whether it existed. response(path, init?) returns the file as a Response streamed from storage, honoring an init.range byte range; slice(path, offset, length, init?) returns one byte range of it as a whole response, with no Content-Range header.

Size limits and errors

A raw upload (X-Filename plus a body of bytes) is limited only by the app's storage quota. A JSON upload ({"path", "data"}) counts against the request body limit, 16 MiB; base64 data is checked against the same limit after decoding. Either form past the limit answers 413.

An invalid path (too long, leading or trailing /, an empty, . or .. segment, invalid UTF-8, or a control byte) answers 400 INVALID_ARGUMENT (or KEY_TOO_LARGE past 65535 bytes), naming the field that carried it (path or X-Filename). A storage quota refusal on upload answers with the quota's own error.

Files versus a site release

Files here are stored and replaced one path at a time, with no history. A static site is released as a whole, versioned, content-addressed deploy instead: see operations/static-sites.md.