Context
FileSystem#write takes one buffer, so an export for a large tenant is built whole in memory before it is stored.
Decision
FileSystem#writeStream(key, source, options)takes a webReadableStream<Uint8Array>or anAsyncIterable<Uint8Array | string>(strings are UTF-8).- The port’s own default buffers: it reads the stream and calls
write. The memory, local, GCS, Encore and Electron disks use it. - The
s3disk (S3, R2, MinIO) uploads in parts of a fixed 8 MiB (above S3’s 5 MiB floor, equal as R2 requires), holding one part in memory. A stream shorter than one part is one PUT. - Never a partial object. A stream that fails stores nothing: the buffered default never calls
write, and the multipart upload is aborted (the abort’s own failure never hides the stream’s error).
Consequences
- A large export streams on the managed buckets; a local or memory disk still holds it whole (development only).
- The part size is a constant, not configuration: it is a protocol limit, not a business answer.
Enforcement
planned —packages/data/filesystem/src/contracts/ports/__tests__/file-system.port.test.ts and
packages/data/filesystem/src/node/drivers/__tests__/s3-file-system.driver.test.ts.
Note 2026-10-27: streamed reads (STA-1259)
FileSystem#readStream(key) is the read side of decision 1. It resolves to a web ReadableStream<Uint8Array> once the
file is found, so a missing file rejects the call with FileNotFoundError, never the stream. Every disk has it: the
local and electron disks stream from an open file handle, s3 streams the GET body and gcs the download; the
memory and encore_bucket disks use the port default, which reads the file whole and serves it as one chunk
(development or small files, as decision 2 does for writes).
Enforcement of this note: planned — file-system.port.test.ts and the driver tests under
packages/data/filesystem/src/{node,desktop}/drivers/__tests__/ (written, run on ci-box).