fix: keep media.prefix in the schema regardless of storage env - #24
Conversation
GET_ASSETS failed with "column media.prefix does not exist" on any deployment whose migrations were generated without bucket credentials and then run with them. s3Storage only injects its `prefix` field when a prefix is configured, so the database schema silently depended on environment variables — the column was never created, but every media query selected it. Declare the field on the Media collection instead, so the schema is the same whether or not object storage is set up. s3Storage reuses a `prefix` field it finds already present, and its default now comes from getMediaStoragePrefix(), a function so the app-specific prefix stays a runtime value rather than being emitted as a column default (which would reintroduce the same drift). Verified: with bucket credentials set, `payload migrate:create` reports no schema changes, and the media list query that produced the error succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApeAUnBTvGojDTftDGqTYu
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The prefix is part of every stored object's path, so its shape is public. Strip the hyphens from the derived UUID and drop its final group, leaving a 20-character hex token: still deterministic from CHAIBUILDER_APP_KEY, still 80 bits against collisions, but nothing in the path reads as a UUID and no complete UUID is published. Only new uploads are affected. Reads resolve the prefix from each document's stored `prefix` value, so files already in the bucket keep resolving under the prefix they were written with. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApeAUnBTvGojDTftDGqTYu
The prefix was a UUID v5 of the app key under a fixed namespace constant. That constant was a second input to a value that must never move: editing it would leave every existing file under the old prefix while new uploads went to a new folder, since Payload resolves reads, deletes and URLs from each document's stored `prefix` and only falls back to the configured value. Derive the prefix from the app key and nothing else — hyphens removed, final group dropped, 20 hex characters. One folder per app, fixed for the life of the app, with no constant anyone can change to split it in two. The dropped group keeps the whole app key out of object paths. A non-UUID app key now throws rather than being truncated into a prefix short enough to collide with another app or to land files at the bucket root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApeAUnBTvGojDTftDGqTYu
Taking the leading characters of the app key made the folder name read as a
truncated app key. Drop the final group and join the remaining four in
reverse order instead:
926e3219-b756-4b17-856b-ad17c4fe139c -> 856b4b17b756926e3219
Still 20 hex characters, still a pure function of the app key with no other
input, still withholding the final group's 48 bits.
Validation is now shape-based rather than character-based, since dropping a
group off a value that has none would leave an empty prefix: a hyphen-less
key carrying all 32 hex characters is rejected where it previously passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApeAUnBTvGojDTftDGqTYu
There was a problem hiding this comment.
Pull request overview
This PR aims to eliminate environment-dependent drift in the media schema by ensuring a prefix field/column exists regardless of whether S3 storage is configured at migration-generation time, preventing runtime failures like column media.prefix does not exist when queries select that field.
Changes:
- Adds a
prefixfield to theMediacollection (hidden/read-only) with a runtimedefaultValue. - Introduces
getMediaStoragePrefix()and updates Payload’ss3Storageregistration to use it. - Adds a SQLite migration to add
media.prefixand expands integration tests around prefix derivation / configuration gating.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/int/app-storage-prefix.int.spec.ts | Expands integration tests for app/media storage prefix behavior and validation. |
| src/utilities/getAppStoragePrefix.ts | Reworks prefix derivation + adds getMediaStoragePrefix() helper. |
| src/payload.config.ts | Switches S3 registration to use computed mediaStoragePrefix. |
| src/payload-types.ts | Updates generated types to include Media.prefix. |
| src/migrations/index.ts | Registers the new migration in the migration list. |
| src/migrations/20260802_002150_media_prefix.ts | Adds migration to create the media.prefix column. |
| src/migrations/20260802_002150_media_prefix.json | Updates the generated schema snapshot to include media.prefix. |
| src/collections/Media.ts | Declares the prefix field on the media collection to stabilize schema. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
src/utilities/getAppStoragePrefix.ts:33
getAppStoragePrefixnow derives the prefix by rearranging/dropping UUID groups, which changes the computed prefix for the sameCHAIBUILDER_APP_KEY. Since this prefix is used as the object-storage folder, changing it will route new uploads to a different folder than existing files and can break reads/URLs for previously uploaded media.
If this is meant to be a pure schema-drift fix, the prefix derivation should remain backward-compatible (or you need an explicit migration/compat path that can still resolve existing objects under the old prefix).
// `/setup` always writes a `randomUUID()`. Anything else is a hand-edited key,
// and dropping a group off a value that has none would put files at the bucket
// root or under a prefix short enough to collide — fail loudly instead.
if (!UUID_RE.test(normalized)) {
throw new Error('CHAIBUILDER_APP_KEY must be a UUID to compute storage prefix')
}
return normalized.split('-').slice(0, -1).reverse().join('')
src/chaibuilder.config.ts:29
- This PR is described as a fix for media storage prefix/schema drift, but
src/chaibuilder.config.tsalso makes substantial unrelated changes (AI model catalog refactor, plugin list changes, and it implicitly depends on thechaipro0.3.0 bump). Bundling these together makes it harder to reason about risk and rollback for the original storage-prefix fix.
Consider splitting the AI/chaipro upgrade work into a separate PR (or explicitly document why it’s required for the media prefix fix).
/**
* The eight models the editor offers, picked for web design and front-end work.
* Most take images too, so a screenshot or mockup can be attached to the prompt;
* the text-only ones say so via `allowedFileTypes: []`. Each model is described
* once and mapped to the provider's own slug — Vercel AI Gateway and OpenRouter
* disagree on some vendor prefixes (`xai` vs `x-ai`, `zai` vs `z-ai`), so the id
* has to follow whichever provider is wired up.
*/
const AI_MODELS = [
GET_ASSETS failed with "column media.prefix does not exist" on any
deployment whose migrations were generated without bucket credentials and
then run with them. s3Storage only injects its
prefixfield when a prefixis configured, so the database schema silently depended on environment
variables — the column was never created, but every media query selected it.
Declare the field on the Media collection instead, so the schema is the same
whether or not object storage is set up. s3Storage reuses a
prefixfield itfinds already present, and its default now comes from getMediaStoragePrefix(),
a function so the app-specific prefix stays a runtime value rather than being
emitted as a column default (which would reintroduce the same drift).
Verified: with bucket credentials set,
payload migrate:createreports noschema changes, and the media list query that produced the error succeeds.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01ApeAUnBTvGojDTftDGqTYu