Skip to content

feat(next): add config.admin.serverFunctions registry for plugins#16499

Draft
tsemachh wants to merge 1 commit into
payloadcms:mainfrom
shefing:feat/admin-server-functions-registry
Draft

feat(next): add config.admin.serverFunctions registry for plugins#16499
tsemachh wants to merge 1 commit into
payloadcms:mainfrom
shefing:feat/admin-server-functions-registry

Conversation

@tsemachh
Copy link
Copy Markdown
Contributor

@tsemachh tsemachh commented May 6, 2026

Closes #16497

Summary

Adds an optional serverFunctions?: Record<string, ServerFunction> field on Config['admin'] so that plugins can register custom server functions through payload.config.ts instead of asking every integrator to wire them into the serverFunctions prop of <RootLayout /> in (payload)/layout.tsx.

Motivation

Today, the only way a plugin can ship a server function is to ask the integrator to:

  1. Import the plugin's server function in app/(payload)/layout.tsx.
  2. Add it to the serverFunctions={{ ... }} prop on <RootLayout />.

This is hostile to "single-line install" plugin UX — the plugin's payload.config.ts factory can wire collections, globals, fields, components, endpoints, and admin routes, but it cannot wire a server function. A concrete consumer is a planned @shefing/review-button plugin (companion: #16496) that needs a 'render-review-diff' server function on the server side.

Changes

  • packages/payload/src/config/types.ts
    • Add serverFunctions?: Record<string, ServerFunction> to the admin block of Config, with a JSDoc that documents the lookup order and recommends namespaced keys (e.g. '@my-plugin/foo').
    • SanitizedConfig.admin.serverFunctions is automatically inherited via DeepRequired<Config['admin']>.
  • packages/payload/src/config/defaults.ts
    • Default admin.serverFunctions to {} in both the static defaults object and addDefaultsToConfig so callers can always do config.admin.serverFunctions[key] without a guard.
  • packages/payload/src/config/sanitize.ts
    • Inside sanitizeAdminConfig, validate keys are non-empty strings and values are functions. Throw InvalidConfiguration on misuse so plugin/integrator wiring errors fail fast at config build time.
  • packages/next/src/utilities/handleServerFunctions.ts
    • New lookup order:
      const adminServerFunctions = req.payload.config.admin?.serverFunctions
      const fn =
        extraServerFunctions?.[fnKey] ||
        adminServerFunctions?.[fnKey] ||
        baseServerFunctions[fnKey]
    • The integrator-supplied serverFunctions prop continues to win on key conflicts, preserving back-compat.
  • packages/payload/src/config/sanitize.serverFunctions.spec.ts (new — 4 unit tests)
    • Defaults admin.serverFunctions to {} when absent.
    • Preserves user-supplied entries through sanitize.
    • Rejects non-function values.
    • Rejects empty-string keys.

All 1329 existing unit tests still pass alongside the 4 new ones.

Lookup order

1. integrator-supplied `serverFunctions` prop on <RootLayout />   ← highest priority (back-compat)
2. config.admin.serverFunctions                                    ← the new registry
3. baseServerFunctions (built-ins)                                 ← lowest priority

The lookup-order behavior in handleServerFunctions itself is intentionally not unit-tested in this PR because that handler depends on the full Next/payload runtime stack (initReq, getPayload, request headers). It is exercised end-to-end by the existing test/server-functions/ suite.

Backward compatibility

  • Fully additive: no field is removed or renamed.
  • Existing wiring on <RootLayout serverFunctions={...} /> continues to win on key collisions.
  • Built-in server functions still resolve when nothing else is registered with the same key.
  • No runtime cost beyond a single object lookup per server-function call.

Cross-plugin key collisions

Plugins are expected to namespace their keys (e.g. '@my-plugin/foo'). I considered emitting a console.warn at sanitize time when two plugins register the same key, but plugins mutate Config one-by-one before sanitizeConfig runs, so by the time sanitize sees the merged map the colliding entry has already been overwritten — the warning can't be reliably produced from the central pipeline. Happy to add a per-plugin merge helper if the team prefers that direction.

Concrete consumer

A planned @shefing/review-button plugin will use this registry to ship a 'shefing/review-button:render-diff' server function without forcing every integrator to edit (payload)/layout.tsx.

Companion proposal

Companion PR #16498 (issue #16496) exposes the Versions diff surface as @payloadcms/next/views/diff. The two PRs are independent and can be reviewed/merged separately.

…gins

Adds an optional Record<string, ServerFunction> on Config['admin'] so
plugins can register custom server functions through payload.config.ts
instead of asking every integrator to wire them into the serverFunctions
prop on <RootLayout /> in (payload)/layout.tsx.

handleServerFunctions resolves fnKey with this lookup order:
  1. integrator-supplied serverFunctions prop on <RootLayout />
     (highest priority — preserves back-compat)
  2. config.admin.serverFunctions (the new registry)
  3. built-in baseServerFunctions

Sanitize-time validation:
  - Defaults admin.serverFunctions to {}.
  - Rejects empty-string keys and non-function values with
    InvalidConfiguration so misuse fails fast at config build time.

Tests:
  - packages/payload/src/config/sanitize.serverFunctions.spec.ts covers
    defaulting, preservation of user-supplied entries, and rejection of
    invalid keys/values. The lookup-order behavior in handleServerFunctions
    is exercised by the existing test/server-functions e2e suite.

100% additive and backward-compatible: integrator-supplied serverFunctions
continue to win on key conflicts; built-ins still resolve when nothing
else is registered.

Closes payloadcms#16497

Co-authored-by: Junie <junie@jetbrains.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add config.admin.serverFunctions registry so plugins can register server functions without editing (payload)/layout.tsx

2 participants