Short answer: use Node.js to upload the untouched AI-generated image as audit evidence, compress a separate preview, and apply cloud retention to each object independently.
The least complex design for AI-generated receipt images is to compress only the derived display copy, preserve the original as immutable audit evidence, and make deletion a recorded policy decision rather than an accidental side effect of an upload job. That distinction matters in a marketplace: a small image bill is easy to celebrate, while an irreproducible receipt is expensive to explain.
The useful accounting question is not "How do I make every image smaller?" It is: which bytes must survive an audit, for how long, and which bytes are merely a convenient rendering?
What is the storage bill actually buying?
For a receipt workflow, retained bytes are usually the dominant term that compression can change. Request traffic, metadata, and failed retries still matter, but a marketplace that stores every generated image forever has chosen an ever-growing retention obligation before it has chosen an image format. Compression does not fix that decision. It only changes the slope.
I separate each generated receipt into three records: the original file, a normalized preview, and the audit metadata. The original gets a content hash, media type, byte count, creation timestamp, order identifier, and retention class. The preview can be regenerated or discarded. The metadata records the deletion decision and the reason, so an operator can tell the difference between policy expiry, a user request, and a failed processing attempt.
That creates a deliberately uneven policy:
| Artifact | Purpose | Retention decision |
|---|---|---|
| Original bytes | Audit evidence | Keep for the applicable retention period; delete through a controlled job |
| Preview bytes | Product display | Rebuild or remove independently |
| Hash and event record | Integrity and accountability | Keep as long as policy and audit needs require |
The trade-off is plain. Keeping originals consumes storage and may increase privacy exposure. Deleting them reduces that exposure but removes the ability to inspect the exact input later. A checksum can show that two files differ; it cannot recreate the deleted file.
Retention is a product boundary.
How should compression and deletion share one boundary?
Compression belongs before the upload of a derived preview, not in a path that silently replaces the audit original. For AI-generated images, the application should inspect the decoded image, reject unreasonable dimensions, choose an agreed output format, and record what transformation occurred. The original upload and preview upload should have distinct object identities.
A small implementation can make that boundary explicit. The storage client below is intentionally generic: its put method represents a provider-neutral object write, and its delete method is called only by the retention worker after the policy check. The example uses Pillow for a preview; the object store contract is left outside the article because its details vary by deployment.
from dataclasses import dataclass
from hashlib import sha256
from io import BytesIO
from PIL import Image
@dataclass
class StoredObject:
key: str
media_type: str
size: int
sha256_hex: str
def digest(data: bytes) -> str:
return sha256(data).hexdigest()
def make_preview(original: bytes, max_edge: int = 1600, quality: int = 82) -> bytes:
with Image.open(BytesIO(original)) as image:
image.load()
image.thumbnail((max_edge, max_edge))
output = BytesIO()
image.convert("RGB").save(output, format="JPEG", quality=quality, optimize=True)
return output.getvalue()
def process_receipt(storage, receipt_id: str, original: bytes) -> tuple[StoredObject, StoredObject]:
original_key = f"receipts/{receipt_id}/original"
preview_key = f"receipts/{receipt_id}/preview.jpg"
preview = make_preview(original)
storage.put(original_key, original, media_type="application/octet-stream")
storage.put(preview_key, preview, media_type="image/jpeg")
return (
StoredObject(original_key, "application/octet-stream", len(original), digest(original)),
StoredObject(preview_key, "image/jpeg", len(preview), digest(preview)),
)
The important part is not the quality value. It is the ownership of the two objects. A preview encoder may change later; an audit record should still point to the bytes that were actually received. I would also cap the input size before decoding and enforce a pixel limit, because compressed dimensions can conceal expensive allocations. Those limits are operational controls, not evidence that a file is valid.
Keep the original.
How should Node.js compress and upload AI-generated images to cloud storage?
Compression belongs before the upload of a derived preview, not in a path that silently replaces the audit original. For AI-generated images, the application should inspect the decoded image, reject unreasonable dimensions, choose an agreed output format, and record what transformation occurred. The original upload and preview upload should have distinct object identities.
What can go wrong after a successful upload?
A successful HTTP response is not a complete workflow result. The application can upload the original, crash before recording its hash, retry the preview and create a second object, or mark the database row complete while the preview write is still pending. These are different failure modes and need different repair paths.
I use an idempotency key derived from the receipt identifier and an explicit processing state such as received, original_stored, preview_stored, and recorded. A retry should address the missing state, not blindly repeat every side effect. The metadata write should include the original hash and object key, and a reconciler should find records whose database state and object inventory disagree.
Deletion has a sharper edge. If the retention worker deletes the original first and then crashes before recording completion, a retry needs an idempotent delete operation and a durable deletion event. If the worker deletes the database row first, the object can become orphaned and harder to locate. I prefer retaining a minimal tombstone with receipt identifier, original hash, policy version, deletion timestamp, and actor or job identity. Whether that tombstone itself must expire is a policy question, not an implementation detail.
Which retention rule survives an audit?
A good rule answers four questions: what event starts the clock, which artifact it applies to, who can suspend or shorten it, and what evidence remains after deletion. "Delete after 180 days" is incomplete unless the system defines whether the clock starts at generation, settlement, dispute closure, or another business event. It is also incomplete if a legal hold can pause deletion but no one can see that hold.
For a marketplace, I would model retention as data rather than burying it in a scheduled script. A policy record can contain an effective version, a duration or expiry timestamp, an allowed hold state, and the artifact class. The worker evaluates that record at deletion time. This makes policy changes reviewable, but it does not make them retroactive by magic; existing records still need an explicit migration decision.
The cost of keeping only the preview is lower storage and less sensitive material, but it weakens dispute investigation. The cost of keeping the original is the ongoing storage and access-control burden. My decision rule is conservative: if the original is the evidence needed to establish what was processed, retain it for the defined period; if it is only a product convenience, make it reproducible and disposable.
What should the upload path expose to the client?
A browser or service that uploads directly to object storage still needs a narrow authorization boundary. CORS controls which browser origins may read or send certain cross-origin requests; it does not grant permission to write an object, and it is not an audit policy. The server should issue a scoped, short-lived upload capability or accept the bytes itself, validate the resulting object, and bind the object key to the receipt record.
Do not infer a durable audit fact from a client-side success message. The server-side record should be the source of workflow state, while object metadata and hashes provide the evidence needed to reconcile it. Observability should include receipt identifier, state transition, object key, byte size, transformation version, retry count, and deletion outcome, with sensitive content excluded from logs.
A useful test matrix has small and large valid images, malformed files, an image whose dimensions exceed the limit, a preview write timeout, a duplicate retry, a database failure after the original write, and a retention job interrupted between object deletion and tombstone creation. Those tests expose the system's actual contract. A screenshot of a successful preview does not.
The preview is disposable.
The design deliberately stops keeping the original after the declared retention period. When that happens, future investigation can use the tombstone and hash, but it cannot inspect the pixels. That is the cost of a deletion policy. Pretending otherwise is how storage systems acquire facts they never recorded.
Top comments (0)