npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-plugin

Globals 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![Our team](media:42)". 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/mcp

The 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:

![Schemalink logo](media:2)

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 ![Alt text](media:42).

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.