Tenant Export Boundaries: Node.js Streams for AI-Generated Images in Object Storage A developer outlined a Node.js streaming design for storing AI-generated images in object storage that makes tenant identity a server-owned input to the storage contract rather than a caller-assembled key prefix. The approach pairs a bounded byte counter with an export manifest naming exactly which objects are eligible, and separates four failure classes (policy rejection, input/generator handling, retry or abandoned-upload cleanup, and manifest/data-integrity failure) so each gets distinct remediation. The writeup compares direct streaming, multipart uploads and a private spool-then-promote boundary, arguing the prefix remains useful for organization and lifecycle rules but is not the authorization decision. Short answer: make tenant identity an input to the storage contract, not a prefix assembled by the caller. A Node.js image stream should pass through a server-owned object key, a bounded byte counter, and an export manifest that names the exact objects allowed in the export. That gives a fintech image service a reviewable boundary for both writes and reads. The useful design question is not “which S3-compatible endpoint can accept this stream?” It is “what must remain true when generation stops halfway, a retry arrives, or an export is requested by the wrong tenant?” I would make those answers part of the application contract before choosing a storage adapter. Generated images have two different lifecycles. The upload path handles a stream whose end may never arrive. The export path handles a set of completed objects whose membership must be stable. Treating both as a generic put key, bytes operation hides the most important distinction. For each image operation, record an immutable tenant context, an application-owned image identifier, an operation or idempotency identifier, and the final byte count. The client may provide display metadata such as a label. It must not provide the complete authorization-bearing key. There are 4 failure classes worth keeping separate: These failures need different remediation. The first is a policy rejection. The second is input or generator handling. The third needs retry or abandoned-upload cleanup. The fourth is a manifest or data-integrity failure. One generic storage error makes all four harder to investigate. I use three invariants for this scenario: The prefix is still useful for organization and lifecycle rules. It is not the authorization decision. A signed download URL can deliver an already-authorized export, but its expiry does not replace the ownership check performed when the export is created. The contract also needs a visibility rule. An image should be considered complete only after the upload consumer reaches end-of-stream, the byte limit remains valid, the digest or checksum is recorded if the storage adapter supports it, and the completion record is committed. Until then, an object identifier can exist without being eligible for an export. This makes partial work boring. A retry can reuse the operation identifier, inspect the completion record, and either return the existing result or create a new attempt according to one documented policy. It should not guess from a prefix listing. The options below are different operational contracts, not interchangeable upload APIs. | Boundary | What it makes explicit | New state to operate | Appropriate use | |---|---|---|---| | Direct stream to one object | Completion and byte-limit transition | An incomplete object or failed operation | Small, retryable generated images | | Multipart stream | Part identity and final completion | Upload ID, part records, expiry, and completion state | Larger or variable-length image output | | Private spool then promote | Complete-file inspection before visibility | Disk quota, cleanup, and promotion state | Format checks or repeated reads | The decision axis is tenant governance. A multipart transfer can reduce the amount repeated after a failed part, but it adds state that must carry the same tenant context as the object. A private spool can support validation that requires the complete file, but it moves the pressure point from memory to disk. Neither boundary fixes a caller-controlled key. The direct stream is the smallest contract when the image is explicitly bounded and the adapter already participates in backpressure. It is not a promise that every upload is atomic from the first byte. The application still needs a completion transition and a cleanup policy for work that never reaches it. This design is a poor fit when complete-file inspection must happen before any storage write, when the generator cannot expose a bounded stream, or when the storage adapter cannot report failed and abandoned transfers. In those cases, use a private spool or a different adapter boundary and accept the disk, latency, and cleanup costs. No storage interface removes that trade-off. The following deliberately keeps authorization and byte accounting visible. It is Python-shaped pseudocode for the portable part of a Node.js Readable to storage-writer flow; the concrete Node adapter should preserve the same stop condition and backpressure behavior. python from dataclasses import dataclass from hashlib import sha256 from typing import BinaryIO @dataclass frozen=True class TenantContext: tenant id: str actor id: str def server key context: TenantContext, image id: str - str: image id has already passed application identifier validation. return f"tenants/{context.tenant id}/images/{image id}.bin" def copy image source: BinaryIO, sink: BinaryIO, limit: int - tuple int, str : total = 0 digest = sha256 while chunk := source.read 1024 1024 : total += len chunk if total limit: raise ValueError "image exceeds the operation limit" digest.update chunk sink.write chunk sink.flush return total, digest.hexdigest The important line is not the 1 MiB read size. That is a local example, not a universal tuning value. The important line is the limit check before the next write can be treated as accepted data. A real Node.js implementation should stop consuming the readable source when the limit is crossed, destroy or abort the destination according to the adapter contract, and mark the operation incomplete. Holding the whole generated image in a Buffer makes concurrency a tenant-isolation concern: one noisy tenant can consume memory needed by unrelated jobs. I don't treat the chunk size as a promise about throughput; it is a capacity choice that must be tested against concurrent jobs and the adapter's backpressure behavior. The completion record should contain 6 fields: the tenant, image identifier, object key, byte count, digest when available, and final status. The export worker selects records for one tenant and one export identifier, verifies each record again, and writes its output to a destination derived from that same tenant context. That second check is intentional. Export code is a separate trust boundary from generation code. I reject a client-supplied full key as the primary interface because it combines naming, authorization, and storage location in one unreviewed string. A display filename remains useful; store it as metadata and keep it out of the object key function. I also reject prefix discovery as the authoritative definition of an export. It is tempting because a first demo can list a tenant-looking prefix and copy whatever appears there. The set can change during the export, and the prefix says nothing about the ownership of each record. A manifest provides a stable selection that can be audited and retried. There is a valid use for both rejected ideas. An operator diagnostic view can show a prefix after authorization, and a client filename can appear in a UI. Neither should decide which bytes a tenant may export. Tests should cross the boundaries deliberately: two tenants with the same image identifier, a byte limit exceeded by one chunk, a generator that ends without completion, a repeated idempotency identifier, a failed multipart completion, and a manifest containing one foreign object. Browser delivery adds another check: CORS governs which browser origins may read a response; it does not grant object ownership. Keep that browser policy separate from tenant authorization, as MDN's CORS guidance describes. Structured logs should carry tenant ID, operation ID, image ID, byte count, and classified final status. They should not carry image bytes, credentials, or a complete signed URL. Metrics should distinguish authorization rejection, size rejection, generator failure, transfer failure, and export assembly failure. The durable decision is modest: server-owned tenant context controls the key, a bounded stream controls memory, and a manifest controls export membership. Once those contracts are tested, the S3-compatible adapter is replaceable implementation detail rather than the place where tenant policy accidentally lives.