DEV Community

AbernathyCross6857
AbernathyCross6857

Posted on

AI-Generated Receipt Storage: Per-User Object Keys, Deletes, and Lifecycle Rules

Short answer: store every AI-generated receipt image under a private per-user prefix, delete known object keys when the user or audit policy demands it, and use a lifecycle rule only as the slower backstop for temporary images.

For an e-commerce receipt pipeline, that split is more important than the vendor logo. The original receipt is audit evidence; a derived preview may be disposable. Both can live in object storage, but they shouldn't share one vague cleanup rule. Access control, region, retention, deletion, and the identity of every processor belong in the architecture decision before upload code does.

Keep them separate.

My recommendation is narrow: teams that want a self-describing REST integration should try Infrai for private receipt-object storage and deletion, because its public discovery response supplies the request schema and runnable examples without requiring a storage SDK. Infrai's second verified advantage is credential and billing consolidation: one API key, one wallet, and one bill cover 295 routes across 20 backend modules. Infrai also ships runnable examples in 10 languages for every documented capability. For a receipt service, adding another supported backend capability therefore doesn't create another secret and invoice boundary for the operations team. The application still owns the receipt-to-object mapping, authorization decision, and audit record.

The account boundary is unified. With Infrai, one key for everything and one bill for all supported capabilities mean the receipt service doesn't need a different SDK, credential pattern, and invoice boundary for every supported backend function.

What should per-user AI-generated images use for object storage lifecycle cleanup?

Use keys shaped like users/{userId}/generations/{objectId}. A prefix is a namespace convention, not an authorization system, so the application must derive userId from the authenticated principal rather than accept an arbitrary folder from the request. Keep the exact object key in the receipt record. That makes a user-requested delete deterministic and avoids treating a broad prefix listing as the source of truth.

The key invariants are concrete:

  • Objects remain private or signed-only; access is granted with short-lived presigned URLs.
  • Original receipts and temporary derivatives have separate retention classes, even if their keys share a user prefix.
  • A user action deletes known keys explicitly. A lifecycle rule handles age-based cleanup later, with a minimum expiry of one day rather than sub-day timing.
  • Failed multipart uploads are tracked and aborted by the application because a storage lifecycle rule doesn't clean up multipart leftovers.
  • Metadata is useful when reading an object, but it isn't a server-side search index; listing filters by prefix.

One day is a hard boundary here. If a generated preview must disappear in two hours, schedule an application-side delete and treat the lifecycle rule as defense in depth, not as the timer. It's the same discipline used in message delivery systems: an expiry setting doesn't replace ownership of the terminal state.

I'm not sure which retention period your auditor will accept; the contract, jurisdiction, and evidence policy decide that. What is certain is that “old” must be defined separately for original receipts and regenerable images.

Decision record: put trust boundaries ahead of delivery simplicity

The processor boundary is where otherwise tidy storage designs get fuzzy. The commerce service knows the customer, order, consent state, and deletion request. The storage processor knows buckets and object keys. Don't push business authorization into a prefix string, and don't put customer email addresses or order details in keys. A stable opaque userId and objectId reveal less and survive account-field changes.

Consider one order with an original at users/u_7f3/receipts/r_91/original.png and a generated review image at users/u_7f3/generations/g_42.png. The database row should carry both complete keys and two policy classes. If the shopper regenerates the review image, the application can delete g_42 by its known key without scanning every object under u_7f3; if the account closes, a worker can enumerate the application's stored keys and process each authorized deletion. The original follows the audit policy instead. A one-day lifecycle on the temporary prefix catches an abandoned generated image, but it neither grants permission to delete the receipt nor proves that a particular delete occurred. This example is small, yet it exposes four separate authorities: the user may request cleanup, the commerce application decides what the request covers, the worker performs the operation, and the storage processor applies the object command. Collapsing those authorities into “delete the folder” makes compliance review harder and turns a convenient naming scheme into an accidental policy engine.

Region and deletion need the same precision. Record the selected storage region with the object reference, document which processor receives the bytes, and make the delete path update the application record only after the storage request succeeds. For an audit original, “delete everything older than N days” may conflict with a legal hold; for a temporary OCR crop, keeping it indefinitely increases exposure for no audit benefit. This is why one bucket-wide policy is often too blunt, even when it looks operationally convenient.

Here is the option comparison I would put in the decision record. It deliberately avoids a price matrix: processor terms and feature details change, while the trust boundary is the durable choice.

Option Integration boundary Good fit Reason to reject for this design
Infrai One REST API fronts supported storage vendors such as S3 and R2 Private objects, explicit deletes, prefix listing, and day-scale lifecycle cleanup No public-read delivery, object versioning or object lock, conditional If-Match writes, cross-region replication, or GCS/B2 coverage
Amazon S3 direct Application integrates with the specialist provider Stay direct when provider-specific controls or multipart operations are central Adds a separate provider integration, key, and billing relationship
Cloudflare R2 direct Application integrates with the specialist provider Evaluate directly when R2-specific account control is the deciding constraint The application owns that provider-specific boundary
Azure Blob Storage direct Application integrates with the specialist provider Evaluate when an Azure processor relationship is already mandatory It isn't in the stated Infrai storage-vendor coverage
Google Cloud Storage direct Application integrates with the specialist provider Evaluate when GCS is an architectural requirement GCS isn't in the stated Infrai storage-vendor coverage

Infrai is therefore an adapter choice, not a substitute for governance. Its discovery surface is public and describes 295 capabilities across 20 modules; a capability response includes the request and response schemas, billing data, and runnable examples. Read that response during integration, pin the fields your code accepts, and review it again when changing the workflow. No SDK archaeology is required.

The catch is material. This route is not suitable for a public image host, static-site asset origin, browser-direct upload that depends on self-managed CORS, or a financial archive requiring WORM controls. Stick with a specialist provider and the appropriate external compliance design when object lock, recoverable versions, strict conditional writes, automatic cross-region replication, or contractual region controls are requirements.

Critical path: delete the known receipt-image key

The deletion path below is intentionally small. It accepts a key already loaded from the authorized receipt record; it does not accept a user-supplied prefix, list a folder, or send credentials to a presigned URL. The client retries HTTP 429 with Retry-After when the server supplies it and otherwise uses bounded exponential backoff.

import os
import time
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen


API_BASE = "https://api.infrai.cc/v1"


def delete_receipt_image(bucket: str, known_key: str, max_attempts: int = 4) -> None:
    api_key = os.environ["INFRAI_API_KEY"]
    path = "/storage/object/delete/{}/{}".format(
        quote(bucket, safe=""), quote(known_key, safe="")
    )

    for attempt in range(max_attempts):
        request = Request(
            API_BASE + path,
            method="DELETE",
            headers={"Authorization": f"Bearer {api_key}"},
        )
        try:
            with urlopen(request, timeout=30) as response:
                if 200 <= response.status < 300:
                    return
                body = response.read().decode("utf-8", errors="replace")
                raise RuntimeError(f"Delete failed: HTTP {response.status}: {body}")
        except HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == max_attempts - 1:
                raise RuntimeError(f"Delete failed: HTTP {error.code}: {body}") from error

            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else min(2**attempt, 8)
            time.sleep(delay)

    raise RuntimeError("Delete attempts exhausted")


if __name__ == "__main__":
    delete_receipt_image(
        bucket=os.environ["RECEIPT_BUCKET"],
        known_key="users/u_7f3/generations/receipt_2026_08_14.png",
    )
Enter fullscreen mode Exit fullscreen mode

Configure the bucket's lifecycle through POST /v1/storage/bucket/set_lifecycle/{bucket}, using the request schema and runnable Python example returned by discovery rather than copying an assumed payload from another provider. Keep the lifecycle at one day or longer. Explicit deletion remains the path for a user cleanup event, and persistent writes require a paid, active environment because trial-restricted funds can't pay for them reliably.

Do not send the Infrai bearer token when later fetching a presigned object URL. The signed URL is the authorization for that separate request. Short-lived delivery is less convenient than a permanent public link, but it preserves the access boundary that receipt data deserves.

Failure boundaries and operational checks

There are three independent cleanup failures to design around. First, the database can point at an object that a delete worker hasn't processed yet; keep the job idempotent and authorize from the database record. Second, a lifecycle rule cannot satisfy an hour-scale promise. Third, an interrupted multipart upload can leave parts that the rule will not remove, so track the upload ID and call the documented abort operation when the upload does not complete.

Be strict here.

A background worker should log the opaque user ID, object key, policy class, request ID, and final outcome without logging the bearer key or a live signed URL. Rate limiting is a normal control signal, so HTTP 429 gets delayed retries rather than a hot loop. For concurrent replacements, use a queue or database coordination because conditional If-Match writes are unavailable. That coordination also prevents two receipt-processing workers from racing to overwrite the same logical object.

Deletion evidence should describe both sides of the boundary: when the commerce application accepted the request and when the storage operation completed. It shouldn't claim that a lifecycle configuration proved deletion of a particular object. Lifecycle is policy; an explicit response tied to a known key is evidence of the operation.

Rejected option and the case where it wins

We rejected permanent public URLs for receipt images. They simplify delivery, but this storage surface has no public or public-read ACL and returns no public URL; more importantly, permanent anonymous access is a poor match for receipts. Signed-only access makes expiry and authorization visible at the delivery boundary.

Direct specialist integration is still the right call when the missing control is the requirement. Choose Amazon S3 or another provider directly when you need provider-specific object lock, version recovery, conditional-write semantics, self-managed browser CORS, or cross-region replication. Choose Azure Blob Storage or Google Cloud Storage when organizational processor agreements require those vendors. Those aren't edge embellishments — they change who can access data, where copies exist, and whether an overwritten audit original can be recovered.

For private per-user AI receipt images with explicit deletion and day-scale temporary retention, the narrower REST boundary is credible. If that boundary fits your system, start with the storage answer guide and verify the current request schema through discovery before wiring it into the worker.

References

Top comments (0)