No description
  • TypeScript 94.5%
  • CSS 4.5%
  • JavaScript 0.8%
  • HTML 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-30 21:41:59 +08:00
dev feat: recognise the hooks Payload adds itself and show what they do 2026-10-30 21:41:56 +08:00
docs feat: publish as @freefoote/payload-schema-explorer on the Forgejo npm registry 2026-10-30 13:41:34 +08:00
scripts feat: publish as @freefoote/payload-schema-explorer on the Forgejo npm registry 2026-10-30 13:41:34 +08:00
src feat: recognise the hooks Payload adds itself and show what they do 2026-10-30 21:41:56 +08:00
test/integration feat: publish as @freefoote/payload-schema-explorer on the Forgejo npm registry 2026-10-30 13:41:34 +08:00
.gitattributes feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
.gitflow feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
.gitignore feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
.prettierignore feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
.prettierrc feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
AGENTS.md feat: publish as @freefoote/payload-schema-explorer on the Forgejo npm registry 2026-10-30 13:41:34 +08:00
CHANGELOG.md chore(release): 0.2.0 2026-10-30 21:41:59 +08:00
CLAUDE.md feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
cliff.toml feat: publish as @freefoote/payload-schema-explorer on the Forgejo npm registry 2026-10-30 13:41:34 +08:00
docker-compose.yml feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
eslint.config.mjs feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
LICENSE feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
package.json chore(release): 0.2.0 2026-10-30 21:41:59 +08:00
pnpm-lock.yaml feat: publish as @freefoote/payload-schema-explorer on the Forgejo npm registry 2026-10-30 13:41:34 +08:00
pnpm-workspace.yaml feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
README.md feat: recognise the hooks Payload adds itself and show what they do 2026-10-30 21:41:56 +08:00
tsconfig.json feat: publish as @freefoote/payload-schema-explorer on the Forgejo npm registry 2026-10-30 13:41:34 +08:00
tsup.config.ts feat: publish as @freefoote/payload-schema-explorer on the Forgejo npm registry 2026-10-30 13:41:34 +08:00
vite.config.ts feat: Payload CMS plugin that shows collections, links, hooks and access rules as an interactive diagram 2026-10-30 13:24:45 +08:00
vitest.config.ts feat: publish as @freefoote/payload-schema-explorer on the Forgejo npm registry 2026-10-30 13:41:34 +08:00

payload-schema-explorer

A Payload CMS 3 plugin that shows a project's data model as an interactive diagram: collections and globals, how they link, their fields, their hooks in the order Payload runs them, and who may do what. It is for developers, and for technically minded customers who want to see how their system fits together.

  • Graph: collections and globals grouped by admin group, with relationship, upload and join links.
  • List: searchable and sortable, for large schemas.
  • Detail panel: the field tree, links in and out, hooks by stage with their names, descriptions and file:line, and access rules per operation. Links are clickable and build a breadcrumb trail.
  • Access matrix: roles × collections × operations.
  • View as role: fades everything a role cannot read.

It runs as a page in the Payload admin (/admin/schema), and can also be exported as one self-contained HTML file to hand to a customer.

The design and decisions are in docs/PLAN.md.

Installing

The package is published to a public npm registry on Forgejo, not to npmjs.org. Point the @freefoote scope at it in the project's .npmrc. No login or token is needed:

@freefoote:registry=https://git.freefoote.net/api/packages/open-source/npm/

Then install it:

pnpm add @freefoote/payload-schema-explorer

A Docker build needs the same .npmrc copied in before pnpm install.

pnpm 11 and later hold back versions published within the last day (minimumReleaseAge). To install a new release straight away, add the scope to minimumReleaseAgeExclude in pnpm-workspace.yaml:

minimumReleaseAgeExclude:
  - '@freefoote/*'

Releases and their notes are on the Forgejo releases page and in CHANGELOG.md.

Peer dependencies: payload, @payloadcms/ui and @payloadcms/next 3.90+, next 15.4+ or 16, react and react-dom 19.

Setting it up

Add the plugin to payload.config.ts:

import { schemaExplorerPlugin } from '@freefoote/payload-schema-explorer';

export default buildConfig({
  // ...
  plugins: [
    schemaExplorerPlugin({
      access: ({ req }) => isAdmin(req.user), // who may open it; default: any signed-in user
      roles: ROLE_OPTIONS, // { label, value }[]: the access matrix columns
    }),
  ],
});

Then run pnpm payload generate:importmap, because the plugin adds admin components.

Option Default Meaning
access any signed-in user ({ req }) => boolean: who may open the view and the endpoint.
roles none The roles shown as columns in the access matrix and in "View as role".
annotationsFile schema-explorer.annotations.json Where the source details are stored, relative to the working directory.
hideSourcePaths false Leave source file paths out of the model.
projectName name from package.json The name shown in the viewer.
route /schema The admin route of the view.
disabled false Turns the plugin off without removing it.

The plugin adds:

  • the admin view and a navigation link, shown only to users access allows;
  • GET /api/schema-explorer/model, which returns the model as JSON to the same users;
  • the CLI command pnpm payload schema-explorer:export.

The model only ever describes structure, never document data.

Describing access rules

At runtime, access functions are anonymous closures, so the explorer cannot tell what they allow. Tag them with the roles they let through. Tagging the factory that makes them covers every rule it builds:

import { tagAccess } from '@freefoote/payload-schema-explorer/access';

export function allowRoles(roles: Role[]): Access {
  return tagAccess<Access>(({ req }) => hasAnyRole(req.user, roles), { roles, label: 'Librarians' });
}

export const isAdminOrSelf: Access = tagAccess<Access>(
  ({ req }) => (isAdmin(req.user) ? true : { id: { equals: req.user?.id } }),
  { roles: ['admin', 'staff'], label: 'Admins: all; others: own record only', scope: 'filtered' },
);

Import it from @freefoote/payload-schema-explorer/access: access helpers are often shared with client components, and that entry has no server-only code. The main entry also exports it, for server-only files. tagAccess stores the details under Symbol.for('payload-schema-explorer.access'), so a project could also set that property itself, without importing the package.

Rules that are not tagged are still shown:

  • Payload default: no rule was set, so any signed-in user is allowed.
  • Public: () => true.
  • Denied: () => false.
  • Custom: anything else, with its name and TSDoc summary from the annotations file.

Hook and rule descriptions: the annotations file

The explorer shows each hook's and rule's name, the first sentence of its TSDoc block, and where it is defined. A production server has no TypeScript source, so these details are collected in development and committed:

pnpm payload schema-explorer:export

This writes schema-explorer.annotations.json. Run it again after changing hooks or access rules. To catch a stale file, add a unit test or CI step that runs:

pnpm payload schema-explorer:export --check

It exits with code 1 and lists the differences when the committed file no longer matches the source.

The command loads the config under Payload's CLI and asks Node's inspector where each function was defined. tsx's source maps map that back to the .ts file, and ts-morph reads the declaration's name and TSDoc. Rules made by a factory are named after the identifier the config uses (read: isAdmin).

Payload also adds some hooks of its own while it sanitises the config. These are shown with a Payload chip and what they do, rather than as anonymous functions:

  • a unique field's beforeDuplicate hook, which adds " - Copy" to text-like values and clears the others, so a duplicated document stays unique;
  • an orderable collection's beforeChange hook and its hidden _order field's beforeDuplicate hook, which manage the manual sort order.

Exporting for a customer

pnpm payload schema-explorer:export --html schema.html --hide-source

This writes the whole viewer, with the model embedded, as one HTML file that opens in any browser without a server. --json model.json writes only the model. Opening the standalone viewer without an embedded model shows a drop zone for a model file.

Developing

  • Node.js 24 (Volta) and pnpm 12. pnpm install.
  • docker compose up -d --wait postgres starts Postgres with PostGIS on port 5460. Copy dev/.env.example to dev/.env, then pnpm dev and open http://localhost:3000/admin. The first visit creates a user; give it the admin role to see the explorer.
  • dev/ is a small library-themed Payload app that covers every case the explorer handles. Extend it when adding one.
  • pnpm test runs the unit, viewer and integration tests. The integration tests start their own Postgres with Testcontainers, so Docker must be running.
  • pnpm build builds the package (tsup) and the standalone viewer (Vite) into dist/.

Agent and coding guidelines are in AGENTS.md.

Licence

MIT.