@schemalink/payload-plugin
v0.1.18
Published
The Payload CMS adapter plugin for schemalink.
Readme
@schemalink/payload-plugin
Connect a Payload CMS installation to Schemalink, the permission-aware MCP control plane for managing Payload content with AI clients.
Install
pnpm add @schemalink/payload-pluginGlobals and singleton pages
Schemalink discovers both Payload collections and globals, including homepages, headers, footers, and site settings. Global definitions stay in your Payload config; AI edits their content by global slug without a document ID.
Globals require plugin version 0.1.16 or newer. After upgrading, redeploy Payload, refresh the project schema from Getting Started, and reconnect your MCP client to discover the new tools. Globals appear beside collections in the AI availability controls and in MCP consent/connection permissions. A connection restricted to selected collections must explicitly include its required globals.
The MCP global tools cover discovery/schema inspection, reads, updates, draft
saves, translations, publishing/unpublishing, version listing/reading/restoring,
locale coverage/comparison, SEO checks, media attachment, and rich-text link
repairs. schemalink_batch_globals runs global operations independently;
schemalink_batch_content also accepts mixed global and collection operations.
{ "project_id": "your-project-id", "global": "homepage", "data": { "hero": { "heading": "Welcome" } } }Use that input with schemalink_update_global. For rich text, provide Markdown,
such as "body": "## Welcome\n\n". For translations, call
schemalink_create_global_translation_draft with source_locale, target_locale,
and translated data. For a batch:
{
"project_id": "your-project-id",
"operations": [
{ "action": "update_draft", "global": "homepage", "data": { "title": "Welcome" } },
{ "action": "update_draft", "collection": "pages", "id": "page-id", "data": { "title": "About" } }
]
}Globals use the existing content capability flags. Payload access rules and
project/member/connection resource restrictions are checked for every call.
Global permission selectors use global:<slug> in the existing JSON resource
policy, keeping them separate from same-named collections without a database
migration. UI labels show the global's normal name.
Updates save drafts when enabled, or edit live content when drafts are disabled.
Publishing is explicit and publishes the latest draft; content edits are not
accepted inside publishing operations. Historical restoration saves a fresh
draft when supported and never silently publishes a published snapshot. Writes
support expected_updated_at and dry_run, verify persisted fields, and preserve
omitted fields. Arrays/blocks should be supplied as complete replacement values.
Translation-only permission accepts localized fields, not shared fields or shared
array structure. Use edit permission to change shared content. Use locale: "all"
to publish or unpublish all locales explicitly, otherwise use a specific locale.
Globals cannot be created or deleted as definitions through content tools: Payload config defines them and its global content API has read/update operations. This does not prevent saving the first content of a configured global.
@payloadcms/storage-s3 is a peer dependency. Install the version matching your Payload version so the upload adapter uses the same S3 storage implementation and collection schema as the rest of your Payload app.
@payloadcms/plugin-cloud-storage is also a peer dependency, used for its supported
object-key utility. Install it at the same version as Payload and storage-s3.
The supported range is Payload 3.89.0 through 3.x; the delivery hooks and storage
tests have been verified against 3.89.0. Keep all Payload packages on matching
versions. No second storage adapter registration is needed.
Optional public media delivery
Uploads use Payload's file proxy and file-download authorization by default.
For files already accessible anonymously through an S3/R2 public domain, opt in
with uploads.publicUrl:
schemalinkPayloadPlugin({
credential: process.env.SCHEMALINK_PAYLOAD_CREDENTIAL!,
uploads: {
enabled: true,
provider: 'r2',
collection: ['media'],
publicUrl: process.env.R2_PUBLIC_URL,
clientUploads: true, // default for S3/R2; required in public mode
// Keep existing upload limits and env mappings here.
},
})For A&P, set R2_PUBLIC_URL=https://assets.antonovandpartners.com in Payload's
server environment. Existing R2 credentials use R2_BUCKET, R2_ENDPOINT,
R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY by default, or your existing env
mappings. The public URL is distinct from the private S3 API endpoint.
A document with prefix: 'cms-pages-20261004' and filename: 'page-logo.png'
then reads as
https://assets.antonovandpartners.com/cms-pages-20261004/page-logo.png.
Keep the asset domain in the consumer's next/image allowlist.
Alternatively use uploads.env.publicUrl: 'R2_PUBLIC_URL'. This mapping must be
explicit: merely setting R2_PUBLIC_URL or configuring a Cloudflare domain never
enables public delivery. A direct value takes precedence over a mapping. A missing
explicitly mapped variable is an error; an omitted/undefined direct value retains
private delivery. Empty or invalid values are errors. If deployment must require
public delivery, use the explicit env mapping to fail on a missing variable.
This mode is only for publicly accessible files. It bypasses Payload's file-download authorization. Every file and generated size in the selected collections must be safe for anonymous access, including files on draft or restricted documents. Payload document read/create/update/delete rules and SchemaLink project, integration and connection permissions still apply; public file bytes are accessible independently of those permissions. The plugin does not change bucket policies or object ACLs to make objects public.
The plugin passes supported per-collection generateFileURL and
disablePayloadAccessControl: true options to Payload's S3 adapter. In 3.89.0,
generateFileURL takes precedence in URL field beforeChange and afterRead
hooks even without the access-control option; disablePayloadAccessControl
also removes the ordinary storage download handler, retaining only the handler
for client-upload finalization when clientUploads is enabled. Using both
expresses public delivery explicitly. See Payload storage adapter documentation.
URLs join the validated absolute HTTP(S) base (with optional base path) and
Payload's actual normalized object key. Trailing base slashes are removed;
document prefixes and nested directories are retained. The same public callback
is installed on originals and every configured imageSizes URL, so thumbnails
using those sizes receive public URLs too. Each raw key segment is encoded once,
including spaces, Unicode, #, ?, &, + and %. A filename containing the
literal text %20 is a different key from one containing a space and becomes
%2520; filenames are never URI-decoded. Prefixes use Payload's own decoding
and sanitization, exactly as uploads/deletion do. An already encoded base path
is retained. Credentials, whitespace, queries and fragments are rejected in the
base URL.
No records are migrated, objects moved, or files re-uploaded. Existing documents
receive the new original and size URLs on normal Payload reads because its field
hooks recompute them from filename and prefix. Direct database reads and
cached responses do not run those hooks. Documents/sizes without a filename
retain their previous URL value; missing sizes or objects are not generated by
this setting. An incorrect/missing historical prefix cannot be inferred from an
old URL. Consumer URL field hooks that run after Payload's hook can override the
result. Existing sizes must still be included in the collection's imageSizes
configuration to receive the size URL hooks.
Public delivery requires enabled S3/R2 storage and clientUploads: true (the
default). It rejects uploads.signedDownloads: true or a signed-download options
object, and rejects collections with a pre-existing storage adapter or file
handler. Configure the selected collections through this plugin alone.
For private delivery, omit publicUrl and its env mapping. Optionally set
uploads.signedDownloads: true or { expiresIn: 60 } for authorized presigned
GET redirects through Payload; access checks precede signing. Signed PUT and
multipart uploads remain separate from delivery and remain signed in public
mode. Upload processing, object prefixes, finalization, deletion and document
authorization continue through the official adapter and SchemaLink integration.
Configure
Add the plugin to payload.config.ts and keep the credential server-side:
import { schemalinkPayloadPlugin } from '@schemalink/payload-plugin'
export default buildConfig({
plugins: [
schemalinkPayloadPlugin({
credentials: (process.env.SCHEMALINK_PAYLOAD_CREDENTIALS ?? process.env.SCHEMALINK_PAYLOAD_CREDENTIAL ?? '')
.split(',')
.map((credential) => credential.trim())
.filter(Boolean),
}),
],
})Create project API keys in the Schemalink dashboard, then set SCHEMALINK_PAYLOAD_CREDENTIALS in the Payload server environment as a comma-separated list. Keep both keys active while rotating and revoke the old key after the Payload deployment has been updated. The legacy single-key variable SCHEMALINK_PAYLOAD_CREDENTIAL remains supported. Never expose these values to the browser.
Connect your AI client at schemalink.app. The MCP endpoint is:
https://schemalink.app/api/mcpThe plugin adds the Payload-side adapter route at /api/schemalink/ai. It does not move your content or database credentials.
Media attachments
The attach_media operation accepts Payload upload fields and generic relationship fields when their relationTo points to the configured upload-enabled media collection. Polymorphic relationship fields are supported when their relationTo array includes that media collection. Other relationship targets are rejected; the normal Payload access and ownership checks still apply.
Rich text inputs and outputs use Markdown for document creation, updates, reads, and publishing. The adapter converts Markdown to Payload's internal Lexical format before writing and converts it back before returning content. Lexical JSON is never part of the Schemalink contract and is rejected as write input.
Images that point at an accessible Payload upload document are converted to Payload upload nodes, rather than stored as HTML or URL text. The adapter resolves normal Markdown image URLs against upload documents when their stored URL matches. For a deterministic reference after create_media_from_url or finalize_upload, use the returned richTextReference.markdown, for example:
The media:<media-id> reference is Schemalink's public Markdown syntax for a Payload media relationship; it is not a public URL. The previously shipped schemalink://<upload-collection>/<media-id> form and the existing ![media:2]() Payload placeholder remain supported for compatibility. Collections with Payload's native versions: { drafts: true } use _status for publishing. Collections without native drafts can publish when they expose a conventional status, publicationStatus, publishStatus, or state select with draft and published options.
Document links use the same Markdown contract with an explicit link namespace: [Read more](schemalink://link/<collection>/<document-id>). Schemalink resolves the referenced document during import and stores a portable custom Lexical link using the target's slug, for example /services/russian-visa. The consuming Payload app can render that link with Payload's normal rich-text renderer; it does not need a Schemalink renderer or internalDocToHref resolver. If the target does not exist or has no usable slug, the write fails with an actionable error rather than storing a link that renders as #. Ordinary https://, relative, mailto, query-string, fragment, and other supported URLs remain custom links.
Existing Payload internal Lexical links can be converted to portable URL links with the repair_rich_text_links operation. It resolves each relationship using the current target document slug and preserves the link label and other link fields. Relationship-preserving internal links remain available only to callers that explicitly provide a custom link resolver to the exported serialization helpers; they are not the default write representation.
The default document route is /<collection-slug>/<document-slug>. Applications with different public routes can configure links.routes, for example { pages: '/page' }, or provide links.resolveRoute({ collection, document, slug, locale }). When a route table or resolver is configured, an unmapped collection fails with LINK_ROUTE_UNRESOLVED; Schemalink never guesses a route. Configure links.localePrefixes when public URLs include locale prefixes.
Document references can preserve a query string or fragment, such as schemalink://link/blog/9#requirements. File links use a separate syntax: [Download](schemalink://media/media/42). Schemalink verifies the upload document and requires its stable url (or a configured links.resolveMediaUrl); it never turns a media document into a slug route or stores temporary signed URLs. Image embeds remain .
For existing internal Lexical links, repair_rich_text_links supports dryRun: true to report the portable URL changes without saving. Repairs are idempotent, preserve link text/behavior, resolve all rich-text fields before writing, and return every unresolved target instead of partially updating the document.
