[v4 beta] Storage adapter API regression: signed upload context removed without an equivalent replacement

Closed
#18,763 4 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@nathanlentz is already working on this.

Since Oct 16, 2026.

  • #19032 by @nathanlentz — merged
  • #19033 by @nathanlentz — merged

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
8/100
Issue type
Feature
Clarity
Needs clarification
Activity status
Active
Tech stack
typescript
Domain
api, backend

Research direction

Start from the upload receipt code path: createClientUploadReceipt() and the receipt-validation change in commit db8423cc, plus the uploadReference refactor from PR #17312 and the path check from PR #18548. Trace how the submitted reference gets replaced with _objectKey, prefix and signedReceipt before the storage handler runs. This is a design question about adapter extension points, not a bounded fix, so confirm scope with maintainers before writing code. Done would mean an agreed adapter API that carries verified context and allows custom physical layouts while keeping core file tracking.

Written by the indexing model from the issue text.

Description

Bug created-by: Contributor

The below is AI generated, but there's been a fair bit of revision to edit pare it back a bit.

Summary

The v4 storage adapter API removes capabilities that were available in v3:

  • Carrying adapter-owned, verified data through signed upload receipts.
  • Using that data to implement a storage layout different from Payload’s default.

We cannot find an equivalent extension point that retains Payload’s standard original-file tracking, transformations, history and cleanup behaviour.

We maintain an internal adapter for our preferred storage provider. The concern is the loss of adapter API flexibility, rather than a request for one particular storage layout.

Versions

Package Previous Current
payload 3.90.1 4.0.0-beta.1
@payloadcms/plugin-cloud-storage 3.90.1 4.0.0-beta.1

Background: this started with a recent v3 change

Previously, uploads used a flat storage layout by default.

A recent v3 update introduced per-upload UUID directories alongside signed upload receipts. We noticed this after upgrading, when new files started appearing inside these directories.

The v3 adapter API still gave us the tools to restore our preferred layout while keeping signed upload verification.

The v4 changes remove those tools and introduce stricter path assumptions in the standard upload flow.

What changed in the API

1. Receipts no longer accept adapter-owned context

In v3, createClientUploadReceipt() accepted context. Payload verified the receipt and passed its authenticated context to the storage handler.

An adapter could use this to carry a verified provider object ID or other provider-specific data from upload completion into the document save.

The v3 receipt implementation supported this. The v4 receipt implementation instead accepts a fixed set of storage-identity fields, without adapter-owned context.

2. Core removes supplementary reference fields

The v4 upload-instructions refactor (#17312) replaced clientUploadContext with uploadReference.

With requiresUploadReceipt: true, the subsequent receipt-validation change replaces the submitted reference with:

{
  _objectKey: receipt._objectKey,
  prefix: receipt.filePrefix,
  signedReceipt,
}

Returning additional data from the client upload handler therefore does not provide an alternative:

return {
  ...originalUploadReference,
  providerObjectId,
  signedProviderContext,
}

Neither additional field reaches the server-side storage handler.

We are not asking Payload to trust client-supplied IDs. The adapter must verify them against the original upload authorisation before treating them as trusted data.

3. The standard receipt flow requires a particular path structure

The upload file-versioning implementation (#18548) added a check that an ordinary new upload’s original storage path ends in:

<filePrefix>/<objectKey>/<name-original.ext>

A path without the _objectKey directory is rejected before the storage handler runs.

The same PR also added UUID object-key generation for managed server-side uploads.

Example impact: flat storage

Our adapter previously used:

uploads/images/example.jpg

The standard v4 flow requires a structure such as:

uploads/images/<objectKey>/example-original.jpg

The extra directory is the concern here, not the -original suffix or UUID format.

Flat storage is one example of the flexibility lost. The broader issue is that adapters can no longer use signed provider context to implement their own verification and storage decisions through the standard flow.

Why opting out is not equivalent

Setting requiresUploadReceipt: false preserves supplementary reference fields and avoids the directory check.

However, in equivalent no-transform tests, Payload stopped populating original. This does not preserve the same automatic original-file tracking and cleanup behaviour.

Using private _payload… request-context fields to restore that behaviour would also create an unsupported dependency on core internals.

Expected behaviour

The adapter API should provide a supported way to:

  • Carry authenticated, adapter-owned upload context.
  • Resolve the provider object and its physical storage path.
  • Retain core receipt checks, original-file tracking, transformations, history and rollback support.

Restoring signed receipt context—or providing an equivalent verification/finalisation hook—would address the first part.

Adapters should also be able to choose their physical storage layout without opting out of core file-management support.

We understand the need for distinct upload identities and safe handling of retained files. The regression is the loss of extension points that previously let adapters meet those requirements without adopting Payload’s default directory structure.

Dominant language
TypeScript
Stars
45.6k
Forks
4.4k
Avg merge
1d 15h
Merged PRs (30d)
280

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from payloadcms/payload

All issues in payloadcms/payload

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.