Skip to content
Ironflow is pre-1.0: APIs can change between releases. Pin a version. What is ready →

Files

The file storage API stores files for your app. You create a bucket, put files in it by path, and read them back. You can give an end user a short-lived URL to upload or download one file. You can also start a function when a file changes.

The API is the same in the Go, Node, browser and Python SDKs. It is a REST API under /api/v1/files.

For the mental model, see File Storage.

  • A database catalog holds the file records: path, size, content type, SHA-256 and metadata.
  • The bytes live in the blob backend (blobs.url). The backend key is an opaque object ID. Your path never reaches the backend.
  • Every byte goes through Ironflow, also on S3. Ironflow checks the credentials, the size and the content type first.
  • Each file has an ETag. It changes on every write.

A bucket holds files and the rules for them. Create a bucket before you put a file in it.

import { createClient } from "@ironflow/node";
const ironflow = createClient({ apiKey: process.env.IRONFLOW_API_KEY });
const files = ironflow.files();
await files.createBucket("inbox", {
maxObjectBytes: 10 * 1024 * 1024,
allowedContentTypes: ["image/*", "application/pdf"],
emitEvents: true,
allowSignedUrls: true,
});
const inbox = files.bucket("inbox");
OptionDefaultMeaning
namerequired1 to 63 characters. Lowercase letters, digits, - and _. The first character is a letter or digit.
maxObjectBytesthe server capThe largest file in this bucket. It can be lower than the server cap. It cannot be higher.
allowedContentTypes[] (any type)A list of patterns such as image/* or application/pdf. A pattern ignores media type parameters.
emitEventsfalseWhen true, file changes in this bucket emit events.
allowSignedUrlsfalseWhen true, you can create signed URLs for this bucket.

You can change these options later (updateBucket, files_patch_buckets). A lower cap or a narrower type list applies to new writes only. Old files stay.

You can delete a bucket only when it is empty. A bucket with files returns 409.

A path has 1 to 1024 bytes. It is valid UTF-8. It has no control characters, no leading /, no empty segment, and no . or .. segment. Use / to make folders. A folder is only a prefix of the path. You do not create it.

const info = await inbox.put("scans/2026/a.pdf", pdfBytes, {
contentType: "application/pdf",
metadata: { source: "scanner-1" },
});
console.log(info.etag, info.size, info.sha256);
const file = await inbox.get("scans/2026/a.pdf");
const bytes = await file.arrayBuffer(); // or file.text(), or read file.body as a stream
await inbox.delete("scans/2026/a.pdf");

put takes a Uint8Array, ArrayBuffer, Blob or string. It also takes a Node Readable or a ReadableStream. A stream needs contentLength:

import { createReadStream, statSync } from "node:fs";
await inbox.put("big.bin", createReadStream("big.bin"), {
contentType: "application/octet-stream",
contentLength: statSync("big.bin").size,
});

Rules for every upload:

  • The request must have a Content-Length. Without it, the server returns 411.
  • The size must be at most the effective cap: the lowest of the server cap, the bucket cap and (for a signed upload) the token maxBytes. A larger file returns 413. A body longer than its Content-Length also fails.
  • Content-Type is required. It must match the bucket allowedContentTypes, or the server returns 415.
  • User metadata keys use a-z, 0-9 and -. All metadata together is at most 2 KiB.
  • Two writes to one path at the same time: the last commit wins. A reader sees the old version or the new version, never a mix.

Use a precondition to make a write safe:

  • ifNoneMatch creates the file only when the path is free.
  • ifMatch: etag overwrites only that version.

A failed precondition returns 412 (PreconditionFailedError in TypeScript, ErrPreconditionFailed in Go).

await inbox.put("a.txt", bytes, { contentType: "text/plain", ifNoneMatch: true });
const current = await inbox.info("a.txt");
await inbox.put("a.txt", newBytes, { contentType: "text/plain", ifMatch: current.etag });

You can also pass ifMatch to a download. Ironflow then returns 412 if the file changed, so you read the exact version you expect. A download also supports range.

An upload that you can send again is retried after a retryable failure (for example 503). This means bytes, a Blob, and (Go and Python) a seekable reader that the SDK rewinds. A stream cannot be sent again. A stream upload is sent once and fails with a clear message on the first error. Python rejects a body that is not bytes and not seekable before it sends a request.

The two sign calls are retried in every SDK. A sign call only creates a URL, so a second attempt does no harm. Create, move and copy are sent once, because a retry after a lost response could repeat the change.

Byte transfers have no fixed timeout in the SDKs. Cancel a Go upload with its context.

A listing is ordered by path. Use prefix to select a folder. Use delimiter: "/" to get one level of folders.

let cursor: string | undefined;
do {
const page = await inbox.list({ prefix: "scans/", delimiter: "/", cursor, limit: 100 });
for (const folder of page.prefixes) console.log("folder", folder); // "scans/2026/"
for (const f of page.files) console.log(f.path, f.size);
cursor = page.nextCursor;
} while (cursor);

limit is 100 by default and 1000 at most. Without delimiter, the listing returns every file under the prefix.

OperationWhat it doesBytes move?
move(from, to, { toBucket?, ifMatch? })Renames one file. It can go to another bucket. Returns 409 if to exists.No
copy(from, to, { toBucket? })Makes a second file with the same bytes. Returns 409 if to exists.Yes
movePrefix(from, to)Renames every file under a prefix, in one transaction. Returns the count.No
await inbox.move("scans/a.pdf", "archive/a.pdf");
await inbox.copy("archive/a.pdf", "backup/a.pdf", { toBucket: "backups" });
const count = await inbox.movePrefix("scans/2026/", "archive/2026/");
_, err := inbox.Move(ctx, "scans/a.pdf", "archive/a.pdf", ironflow.MoveFileOptions{})
_, err = inbox.Copy(ctx, "archive/a.pdf", "backup/a.pdf", ironflow.CopyFileOptions{ToBucket: "backups"})
count, err := inbox.MovePrefix(ctx, "scans/2026/", "archive/2026/")
fmt.Println(count)
client.files_buckets_move("inbox", body={"from": "scans/a.pdf", "to": "archive/a.pdf"})
client.files_buckets_copy("inbox", body={"from": "archive/a.pdf", "to": "backup/a.pdf", "toBucket": "backups"})
res = client.files_buckets_move_prefix("inbox", body={"from": "scans/2026/", "to": "archive/2026/"})
print(res["count"])

Directory move rules:

  • from and to end in /. Neither one can start with the other.
  • It works inside one bucket.
  • It moves at most 10,000 files. A larger prefix returns 422. Nothing moves.
  • The whole move succeeds, or nothing changes. If a target path exists, you get 409. If a new path would be longer than 1024 bytes, you get 400.
  • It emits one ironflow.file.prefix_moved event, not one event for each file. A prefix with no files returns a count of 0 and emits no event.
  • Directory copy is not available in this release.

A signed URL lets an end user upload or download one file with no Ironflow credentials. The token in the URL is the credential. Your backend creates the URL. The browser uses it.

  1. Set allowSignedUrls: true on the bucket.
  2. Your backend signs a URL.
  3. The browser sends the file with a plain fetch.
const { url, expiresAt } = await inbox.signUpload("avatars/user-42.png", {
ttlSeconds: 600,
maxBytes: 2 * 1024 * 1024,
contentType: "image/png",
});
// Send `url` to the browser.
const download = await inbox.signDownload("avatars/user-42.png", 3600);

Signed URL rules:

  • The default TTL is 900 seconds. The maximum is 604800 seconds (7 days).
  • An upload URL can carry maxBytes and contentType. The request must follow both. A wrong content type returns 415.
  • A signed upload cannot set user metadata. A request with X-Ironflow-Meta-* headers returns 400.
  • An upload URL can carry createOnly: true. The PUT then acts as If-None-Match: *: it returns 412 when the path already exists, so the URL cannot replace a file.
  • A URL works many times until it expires. Without createOnly, a second upload overwrites the first. There is no revocation for one URL.
  • A download URL is bound to the path, not a file version. Replacing the file, or deleting and re-uploading it at the same path, lets an existing URL serve the new content until it expires. Deleting the file returns 404 while the path is absent.
  • To revoke every URL of a bucket, set allowSignedUrls: false. Ironflow checks this flag on every use. If you delete a bucket and make a new bucket with the same name, the old URLs stay dead.
  • A bad, tampered or expired token returns 403. A bucket with allowSignedUrls: false returns 403 with code SIGNED_URLS_DISABLED.
  • Set files.publicBaseURL when a proxy rewrites the request host or adds a path prefix. Without this setting, the URL uses the request host. Behind a TLS proxy, send X-Forwarded-Proto: https.
  • The signing key comes from auth.jwtSecret. If you change the JWT secret, every URL stops working.

The browser makes cross-origin requests to Ironflow. The server answers with Access-Control-Allow-Origin: * and no credentials, so a credential-free fetch works.

When a bucket has emitEvents: true, a file change emits an event. A function can use the event as a trigger.

EventWhen
ironflow.file.createdA new path is written, or a file is copied.
ironflow.file.updatedA path is overwritten.
ironflow.file.deletedAn existing file is deleted.
ironflow.file.movedA single-file move. Emitted if the source bucket has emitEvents.
ironflow.file.prefix_movedA directory move that moved at least one file.
ironflow.bucket.createdA bucket is created.
ironflow.bucket.updatedA bucket is updated.
ironflow.bucket.deletedA bucket is deleted.

Bucket events are always emitted. emitEvents controls file events only.

Payloads:

file.created / file.updated / file.deleted
{ bucket, path, size, contentType, sha256, etag, metadata,
previousEtag, // file.updated only
via: "api" | "signed_url" }
file.moved { bucket, toBucket, from, to, etag, size, contentType }
file.prefix_moved { bucket, from, to, count }
bucket.created { bucket, bucketId, config }
bucket.updated { bucket, bucketId, config, previous } // previous: changed fields only
bucket.deleted { bucket, bucketId }
config = { maxObjectBytes, allowedContentTypes, emitEvents, allowSignedUrls }

Use a wildcard trigger and a CEL expression to choose a bucket:

const client = createClient({ apiKey: process.env.IRONFLOW_API_KEY });
const onInbox = ironflow.createFunction(
{
id: "process-inbox-file",
triggers: [{ event: "ironflow.file.created", expression: "data.bucket == 'inbox'" }],
},
async ({ event, step }) => {
const { bucket, path, etag } = event.data as { bucket: string; path: string; etag: string };
return step.run("read-file", async () => {
// ifMatch pins the version that caused the event.
const file = await client.files().bucket(bucket).get(path, { ifMatch: etag });
return { size: file.size, text: await file.text() };
});
},
);

Delivery rules:

  • The event and the file change commit in one transaction. Both commit, or neither does.
  • A recovery sweep runs every 30 seconds. It dispatches events that a crash left unprocessed. It waits at least 30 seconds and retries for up to 24 active hours from the committed file event. Persisted intentional instance-freeze intervals pause that age budget; ordinary downtime does not. New file execution events are refused while frozen. Ten genuine per-function failures quarantine an event sooner. Frozen or dispatch-wide failures do not consume that failure budget.
  • Pending accepted file events survive ordinary unreferenced-event retention until processing or explicit quarantine. Quarantine preserves its reason and attempts for the event’s lifetime; terminal events remain subject to ordinary retention.
  • This clock starts with migration 122 for newly committed file events. Older rows retain the previous 24 wall-hour policy. Upgrade cannot recover deleted events or previously pruned attempt history.
  • Function triggering is therefore at-least-once within that recovery window. A function can run more than once for one event.
  • Make your function idempotent. Use the etag as the key. The etag is new on every write.
  • The sweep does not recover wait-for-event wake-ups.
  • Pass ifMatch: etag when you read the file. If someone overwrote the file, you get 412. The newer write has its own event.

The names ironflow.file. and ironflow.bucket. are reserved. Only the platform emits events with these prefixes. A client emit of such a name is rejected. This also applies to entity events, webhook event prefixes and the NATS ingest path. The whole ironflow. prefix belongs to Ironflow. A client can emit only the writable defined name ironflow.invoke.<fn>. All reserved names are listed in Reserved names.

Platform events do not pass through emit. Event schema registration rejects every name that a client cannot emit, ironflow.file.* and ironflow.bucket.* included, with invalid_argument. A platform event never has a schema.

The dashboard has a Files page. Use it to browse buckets, upload and download files, and view bucket settings. It uses the same REST API as the SDKs. Open Files in the Store group of the sidebar. The route is /files. The route /files/<bucket> opens one bucket.

If the server has no blob backend, the page shows “File storage is not enabled”. See Storage backend and configuration.

  • Left: bucket tree. It lists the buckets. Select a bucket to open it. The folders of the open bucket show below it. If a folder level has more entries than one page, select More… at the end of that level. The New bucket button shows only with files:manage.
  • Center: files. Buttons show the bucket and the open folders. Select a name in the path to go back to that level. The toolbar has Events and, with files:write, Upload. A tile shows each folder and each file. Select a tile to see it in the inspector. Double-click a folder tile, or press Enter on it, to open it. With no bucket selected, the center shows a card for each bucket. If a folder has more entries than one page, use Load more.
  • Right: inspector. It shows the item that you select:
    • A bucket: the bucket settings.
    • A file: a preview, the file details and the file actions.
    • A folder: the Open and Move folder actions. The inspector does not show a file count for a folder.
  1. Select New bucket.
  2. Enter a name. The name rules are the same as in Create a bucket.
  3. Set the maximum object size in MiB. The dialog shows the server cap beside this field. A value above the server cap returns 400.
  4. Optional: enter allowed content types, separated by commas. Leave the field empty to allow any type.
  5. Set Emit ironflow.file.* events and Allow signed URLs. The dialog starts with events on and signed URLs off.
  6. Select Create bucket.

Existing bucket settings are read-only in the inspector. Change them through the SDK or REST API. A lower cap or narrower types apply to new writes only. Old files stay.

Open a bucket and a folder. Then do one of these:

  • Select Upload and choose one or more files.
  • Drag files from your computer onto the center pane. The dashboard does not upload folders. Drop files.

The file goes to the open folder, with its own name as the last path segment. If a file exists at that path, the dashboard refuses the upload. The row shows “A file with this name already exists in this folder. Move or delete it first.” The dashboard sends If-None-Match: *, and the server returns 412. The API itself replaces a file when you send no precondition. Next to Upload, the page shows the largest size that you can upload. This size is the lower of the bucket cap and the server cap.

Before it sends a request, the dashboard checks the file size and the content type against the bucket rules. The dashboard does not send a file that fails a check. A row shows the reason. A file that passes the checks uploads, and its row shows the progress. Each file has its own row. Select the X on a row to dismiss it.

The dashboard uploads at most four files at the same time. The other files wait in their rows. If the server is busy with other uploads, it returns 503. The dashboard tries that file again one time, after the delay that the server sends in Retry-After. If you open a different bucket or leave the page, the dashboard stops the uploads that are not complete.

If the server still rejects a file, the row shows the message from the server. A file that is too large returns 413. A content type that the bucket does not allow returns 415. See Limits and errors.

Select a file. The inspector shows its size, content type, SHA-256, ETag, dates and metadata. Download saves the file to your computer. For a file over 25 MiB, the dashboard creates a signed download URL that expires after 60 seconds, and the browser downloads from it. Thus the file does not go into the memory of the page. If the bucket does not allow signed URLs, the dashboard does not download a file over 25 MiB and shows a message. Use an SDK or the API.

The inspector previews a file only for these types:

  • Images: PNG, JPEG, GIF, WebP and AVIF
  • PDF. The preview shows inside the page in browsers that support it. Other browsers show a short note. Use Download then.
  • Plain text (text/plain). The preview shows the first 256 KiB. The dashboard asks the server for only these bytes.
  • Audio and video

A file over 25 MiB does not preview. The inspector shows “Preview is off for large files. Download to view.” Use Download to view it.

The preview request sends the ETag from the file list in If-Match. If the file changed after the list loaded, the server returns 412. The inspector then shows “This file changed after the list loaded. Reload to preview it.”

Every other type shows “No inline preview. Download to view this file.” This includes text/html and image/svg+xml. These files can run script. The dashboard never renders them. The server also serves them as attachment. See Serving rules.

A signed download URL lets a person without Ironflow credentials download one file. For the rules, see Signed URLs.

  1. Select a file in a bucket that has Allow signed URLs on. If the bucket does not allow signed URLs, the Copy signed URL button does not show.
  2. Select Copy signed URL.
  3. Set Expires in (seconds). The default is 900 (15 minutes). The maximum is 604800 (7 days). You can also select a preset: 15m, 1h, 24h or 7d. The dialog changes a value outside the range to the nearest limit.
  4. Select Generate, then Copy.

The dialog shows the URL until you close it or change the expiry. The dashboard does not store the URL. When you close the dialog, the URL is gone. If you change the expiry, the dialog clears the URL. Generate a new one.

  • Move renames a file inside its bucket. Enter the new path.
  • Copy makes a second file. Choose the target bucket and enter the new path.
  • Delete asks you to confirm. Then it deletes the file. You cannot undo this.
  • Move folder renames every file under a folder, in one operation. The dialog shows how many files it moves. The count is exact up to 1,000. For a larger folder, the dialog says “more than 1,000 files”. The dialog also states the limit of 10,000 files. A larger folder returns an error and nothing moves. See Move, copy and directory move.

Move, copy and delete send the ETag of the file you inspected in If-Match. If the file changed, the server returns 412 and the dashboard shows “This file changed. Reload and try again.” A destination path keeps its leading or trailing spaces until you edit it.

If a target path exists, the server returns 409 and the dashboard shows the message.

When a bucket is open, the toolbar has an Events link. It opens the Events page with a filter for these names: ironflow.file.created, ironflow.file.updated, ironflow.file.deleted, ironflow.file.moved and ironflow.file.prefix_moved. See Events.

The list shows these events for the whole environment. It does not show only the events of the open bucket, because the Events API cannot filter by bucket. A bucket sends file events only when Emit ironflow.file.* events is on.

Select the bucket, then select Delete bucket in the settings. You can delete only an empty bucket. A bucket with files returns 409, and the settings show the message. Move or delete the files first.

The dashboard hides a control that you cannot use. See Permissions.

PermissionControls
files:readBrowse buckets and folders. Preview and download files. Create a signed download URL.
files:writeUpload, move, copy, delete and move folder.
files:manageNew bucket and Delete bucket.

A user with only files:read sees Download and Copy signed URL for a file, and Open for a folder. Copy signed URL shows only when the bucket has Allow signed URLs on. Bucket settings are a read-only summary for all users.

A platform user sees file controls only while they impersonate an organization. Then platform:impersonate:read gives the files:read controls, and platform:impersonate gives all controls.

Ironflow serves files from the same origin as the dashboard. To keep a stored file from running script in that origin, every download has these rules:

  • X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox.
  • Content-Disposition: inline only for image/png, image/jpeg, image/gif, image/webp, image/avif, application/pdf, audio/*, video/* and text/plain.
  • Every other type, including text/html and image/svg+xml, gets attachment. The browser saves the file. It does not render it.

To show HTML or SVG to a user, serve it from your own origin.

LimitValue
Server cap per file100 MiB by default. Set files.maxObjectBytes.
Bucket capAt most the server cap.
Path1 to 1024 bytes.
User metadataKeys [a-z0-9-], 2 KiB in total.
List page100 by default, 1000 at most.
Directory move10,000 files.
Signed URL TTL900 s by default, 604800 s at most.
Upload idle time30 s without a byte ends the upload (408). A slow, steady upload is not cut off by this alone. A single upload or copy may also run at most 45 minutes in total, however steadily the client sends, including the time the storage backend takes to finish the write. After that the request fails with an upload-timeout error.
Concurrent uploads32 per node, with at most 8 per principal even when the node has spare capacity. Either cap returns 503, code TOO_MANY_UPLOADS, and Retry-After: 5.

Each API key or authenticated user has one upload allowance per node, shared across environments and buckets. Direct uploads and signed upload URLs issued by that principal share the same allowance. If a backend issues URLs for different end users with one API key, all those URLs share that key’s quota.

Older signed URLs without an issuer identity remain valid and share an 8-upload allowance per environment, separately from identified callers. These URLs can remain valid for up to 7 days after the last one is issued. Each node enforces its own quotas; upgrade all serving nodes to apply the principal limit throughout a cluster.

StatusCauseTypeScriptGo
400Bad path, bucket name, metadata or prefix. A bucket cap above the server cap. A directory move that makes a path too long.IronflowError*IronflowError
401No valid credentials.UnauthenticatedErrorErrUnauthorized
403No permission. A bad or expired token.UnauthorizedErrorErrForbidden
403Signed URLs are off for the bucket (code SIGNED_URLS_DISABLED).IronflowError, code SIGNED_URLS_DISABLEDErrForbidden, Code == "SIGNED_URLS_DISABLED"
404Bucket or file not found.IronflowError, code not_found*IronflowError, Code == "not_found"
409Bucket not empty. A move or copy target exists. A directory move conflict.ConflictErrorErrConflict
408The upload sent no byte for 30 s, or ran longer than 45 minutes in total (code UPLOAD_TIMEOUT).IronflowError, code UPLOAD_TIMEOUT*IronflowError, Code == "UPLOAD_TIMEOUT"
411No Content-Length.IronflowError*IronflowError
412If-Match, If-None-Match or a createOnly URL failed.PreconditionFailedErrorErrPreconditionFailed
413Larger than the effective cap.PayloadTooLargeErrorErrPayloadTooLarge
415Content type not allowed.UnsupportedMediaTypeErrorErrUnsupportedMediaType
422A directory move of more than 10,000 files.IronflowError*IronflowError
503The blob backend is unavailable, or it could not serve a read (code BLOB_BACKEND_UNAVAILABLE). No shared signing key in a cluster. The node or principal upload cap is full (code TOO_MANY_UPLOADS, with Retry-After: 5).IronflowError*IronflowError

In Python, every error is an IronflowError with a status_code. PreconditionFailedError, PayloadTooLargeError and UnsupportedMediaTypeError are subclasses that you can import from ironflow.

In Go, test an error with errors.Is(err, ironflow.ErrPreconditionFailed) (and the same for the other sentinels).

A DELETE of a path that does not exist is not an error.

PermissionRoutesAdminDeveloperViewer
files:readGet, info, list, bucket reads, signed-urls/downloadyesyesyes
files:writePut, delete, move, copy, move-prefix, signed-urls/uploadyesyesno
files:manageBucket create, update, deleteyesyesno

The signed routes (/api/v1/files/signed) have no permission check. The token is the control.

A move or copy into another bucket (toBucket) also needs files:write on that bucket and files:read on the source bucket.

A Rego policy sees the bucket in input.request.resource as irn:ironflow:{org}:{project}:file:{env}:{bucket}. The IRN names the bucket. It does not name the file. A rule cannot match a path in this release.

Files use the same backend as blob overflow: blobs.url (IRONFLOW_BLOB_URL). A multi-node cluster needs an s3:// backend. Ironflow stores file bytes under the key prefix files/. Do not add a lifecycle rule that expires that prefix.

The server cap is a separate setting:

ironflow.yaml
spec:
files:
maxObjectBytes: "100MiB"

Or set IRONFLOW_FILES_MAX_OBJECT_BYTES=100MiB. The environment variable overrides the YAML value. A plain byte count such as 104857600 also works.

When a reverse proxy rewrites Host, configure the address clients use to reach Ironflow:

ironflow.yaml
spec:
files:
publicBaseURL: "https://files.example.com/ironflow"

Or set IRONFLOW_FILES_PUBLIC_BASE_URL=https://files.example.com/ironflow. The environment variable overrides the YAML value on both the flags-only and -f paths. The URL must use http or https, include a host, and omit credentials, a query string and a fragment. An optional path prefix is preserved; trailing slashes are removed. The example generates signed URLs under https://files.example.com/ironflow/api/v1/files/signed. Configure the proxy to forward that prefix to Ironflow’s /api/v1/files/signed route.

When unset, Ironflow keeps the request host and uses HTTPS if the request uses TLS or carries X-Forwarded-Proto: https.

Deleting an environment deletes its buckets, file records and file bytes.

Ironflow records retirement when a file is overwritten or deleted, or when a definite failed upload or copy can record its target for cleanup. Cleanup requires that committed evidence and checks that no current file references the object. Failed deletions retry hourly. Objects left by crashes, temporary files, and legacy objects without evidence remain in storage regardless of age. Database restores preserve unexplained bytes that still exist; they cannot recover bytes already deleted.

files.reconcile defaults to on, which retries recorded retirements. dry-run reports the queue and inspects every prefix page, distinguishing eligible retirements, live objects, conflicts, and unexplained objects without changing bytes or queue state. A backend without object-info listing supports queue-only dry-run and logs that prefix inspection is unavailable. off never starts the background worker. Explicit API overwrite and delete cleanup runs in all three modes. IRONFLOW_FILES_RECONCILE overrides the YAML value.

Keep each deployment on its own bucket prefix. Distinct foreign IDs are protected after an accidental overlap, but writable catalog clones sharing the same physical object IDs remain unsupported. See File storage for the cleanup contract and metrics.

  • Multipart or resumable upload. Upload without Content-Length.
  • Object versions. Directory copy. A directory move of more than 10,000 files.
  • Per-file access control. Single-use or per-URL revocable signed URLs.
  • Quotas for a bucket or an environment.