Tenant isolation is the constraint that changes the file naming pattern for object storage. In a customer-support system that stores ticket attachments and serves image thumbnails, put the tenant boundary before an immutable upload version, keep multiple sizes and encodings inside that version, and publish it only after the original and required derivatives are complete.
Short answer: use one versioned key family for the original, each size, and each encoding; never make latest, original.jpg, or thumbnail.webp the identity of mutable content. Keep the current version in a tenant-scoped database record, and treat backup and restore as explicit workflows rather than as side effects of overwriting an object.
That choice is boring. Boring is useful here.
Start with a tenant-boundary audit
Before choosing a naming pattern, audit the operations that can touch an object: upload, thumbnail generation, agent preview, deletion, and restore. For each operation, write down the tenant, ticket, upload version, and actor it is allowed to use. This catches a more expensive mistake than an awkward filename: a worker that can be pointed at another tenant's prefix.
Three identifiers. Tenant, ticket, version.
I would keep those three fields in every job payload and log record, then derive the key on the server. A browser filename should not be able to select a storage location, and a cleanup job should not infer ownership from a substring in a path.
The audit also defines the failure boundary. Object storage can hold bytes, while the application must decide who may publish them, which version is current, and whether a restore is authorized. Put those decisions in a tenant-scoped record and make the storage key a consequence of that record.
What file naming pattern fits object storage image thumbnails, multiple sizes, WebP, AVIF, originals, and backups?
An object key should identify an artifact, not describe a changing business state. A key such as tenant-17/ticket-8042/latest.webp mixes ownership, rendition, and mutability into one string. A worker retry, a new upload, and a restore operation can all interpret that name differently. The storage layer then has no way to tell whether the last writer was authorized to replace the bytes.
Use an opaque upload version as the boundary for one complete image set. The exact token can come from the application; its important property is uniqueness within the tenant and asset scope. A useful family looks like this:
media/{tenant_id}/tickets/{ticket_id}/versions/{upload_version}/original/source.jpg
media/{tenant_id}/tickets/{ticket_id}/versions/{upload_version}/variants/320x180/webp/image.webp
media/{tenant_id}/tickets/{ticket_id}/versions/{upload_version}/variants/320x180/avif/image.avif
media/{tenant_id}/tickets/{ticket_id}/versions/{upload_version}/variants/1280x720/webp/image.webp
media/{tenant_id}/tickets/{ticket_id}/versions/{upload_version}/backup/source.jpg
The same version token appears in every related key. Size and format are attributes of a derivative, while the upload version identifies the source state from which that derivative came. Adding a new size creates another object in the same version family. Reprocessing an existing size can replace that derivative only if the application explicitly allows regeneration; it cannot replace the original or another tenant's artifact.
Tenant identity comes first because it gives authorization, listing, lifecycle, and incident review a natural partition. It is not authorization by itself. Every read and write still needs an application-level tenant check, and the worker needs a tenant-aware job payload rather than an untrusted path assembled from a browser filename.
The original filename is metadata, not identity. OWASP's file-upload guidance also treats uploaded names and content as untrusted input, which is the right boundary for a thumbnail service: validate the upload before processing it, then generate controlled keys from server-owned identifiers.
Make restore a pointer change, not a replacement
The application record should answer one mutable question: which upload version is currently published for this ticket? Storage should hold the bytes addressed by that answer. This separation gives a restore operation a reversible state transition instead of forcing it to overwrite a familiar filename.
A minimal state model has pending, ready, and published versions. A worker writes the original and the required derivatives under pending. The application checks that the expected set exists, records the version as ready, and changes the ticket's current pointer in a transaction. Readers resolve that pointer before constructing a key. A failed derivative job leaves an unpublished version that can be retried or discarded without changing what a customer sees.
The critical path is small enough to make concrete:
from dataclasses import dataclass
@dataclass(frozen=True)
class ImageVersion:
tenant_id: str
ticket_id: str
upload_version: str
original_key: str
required_keys: tuple[str, ...]
def publish_if_complete(
version: ImageVersion,
present_keys: set[str],
current_version: str | None,
) -> str:
if not version.required_keys <= present_keys:
raise ValueError("image version is not complete")
# The database transaction must also verify the tenant and ticket scope.
if current_version == version.upload_version:
return "already-published"
return "publish-in-transaction"
The function deliberately returns a transaction instruction rather than pretending that a Python check is an atomic publish. The compare-and-swap belongs in the database layer, where the record can reject a stale worker update. A queue can serialize work per ticket, but serialization does not replace tenant authorization or a durable current-version record.
Backups need a separate interpretation. Copying original/source.jpg to backup/source.jpg within the same version family can protect a workflow from an accidental derivative cleanup, but it is not automatically a durable backup. Recovery requirements may demand retention controls, independent failure domains, an audit trail, and a tested restore procedure. If those guarantees are required, select a backup or archival system that actually enforces them and record the restore decision outside the object key.
Compare the guarantees before choosing a storage design
The useful comparison is not a list of storage brands. It is a comparison of where each guarantee lives and what happens when a worker, an editor, or a cleanup task makes a wrong assumption.
| Design option | Useful for | Limitation to record in the ADR |
|---|---|---|
Mutable names such as latest.webp
|
Disposable cache output with accepted loss | A retry or restore can silently replace bytes that another operation still expects. |
| Versioned key families | Ticket attachments, multiple sizes, and format negotiation | The application must maintain the current pointer and clean up abandoned versions. |
| Storage-side object versioning | Recovery from selected replacements where the backend provides it | It does not define which tenant or editor is allowed to publish a version. |
| Dedicated backup or archive | Retention, audit, and recovery objectives | It adds a second lifecycle and restore path that must be exercised, not just configured. |
For customer support, versioned key families are the default fit when a ticket can receive a replacement attachment and agents must restore an earlier snapshot. A restore should select an existing version and publish that pointer; it should not copy old bytes into a new original.jpg name and hope that caches, workers, and readers converge.
The rejected option is a single mutable path per ticket. It looks attractive because URLs and database columns are short. It is valid for a cache whose contents can be regenerated and whose loss is acceptable. It is not suitable for source attachments or restore points. When the distinction matters, choose the versioned design and retain a record of who requested the transition, which tenant-owned asset it affected, and which version became current.
Test publication, isolation, and recovery together
The naming convention is only half of the design. The operational method must test the boundaries that naming cannot enforce.
First, test isolation with adversarial identifiers. A job for tenant A must not be able to read, write, list, or delete tenant B's prefix by changing tenant_id in its payload. Reject path components that are not generated by the service, and do not let a client choose the full key. The upload filename can be retained for display after validation, but it should never select the storage location.
Next, test publication under races. Run two uploads for one ticket, retry a derivative job, and request a restore while a new version is being prepared. The expected result is deterministic: one transaction wins the current-pointer update, an older worker cannot publish over a newer decision, and a partial version remains unpublished. A 409-style application conflict is clearer than silently accepting a stale transition, but the exact response belongs to the service contract rather than to object storage.
Then exercise format and size fallbacks. A client may accept AVIF, WebP, or the original; that negotiation should select from the published version's known derivatives. If a requested size does not exist, generate it under the same upload version or return a controlled absence. Do not fall back to a key from another version merely because its dimensions match. That is a data-integrity failure disguised as a cache hit.
Observability should include tenant, ticket, upload version, derivative size, encoding, job attempt, and publication decision as separate fields. Avoid logging raw customer filenames as the primary correlation value. Alert on versions that remain pending beyond the service's normal processing window, and measure restore completion rather than only upload success.
I'm not sure any naming pattern can satisfy a retention policy by itself. The unresolved question is which recovery point and recovery time the support organization promises; a restore drill and a written policy will settle that better than a storage benchmark.
Decision record: version the artifact family
Adopt tenant-first, versioned key families for originals and derivatives. Store WebP and AVIF as sibling representations under explicit size directories, keep the current upload version in a tenant-scoped application record, and publish only after the required artifact set is present. Keep backups and archival retention as explicit capabilities with their own tests.
Do not use mutable names as source identity. Keep them for disposable cache layers only, where regeneration and loss are accepted. The catch is operational overhead: abandoned versions need lifecycle management, and a restore needs a transactional pointer change. That overhead is the price of making tenant isolation and overwrite safety reviewable instead of implicit.
Top comments (0)