Files
agentic-dev/packages/cms-core/AGENTS.md

8.6 KiB

@repo/cms-core -- ALL Payload CMS Configuration

Purpose

This package contains all Payload CMS configuration: payload.config.ts, collections, globals, hooks, and access control. The apps/cms application is a thin shell that imports from this package -- it contains no CMS logic of its own.

Hard Rules

  • ALL collections, globals, hooks, and access patterns live here, never in apps/cms
  • Can import from @repo/core/application (for use case delegation in hooks)
  • NEVER import from @repo/core/infrastructure
  • NEVER import from any apps/* package
  • Keep hooks thin (5-10 lines max) -- delegate to use cases for business logic

File Structure

packages/cms-core/
  src/
    payload.config.ts                          # Main Payload config (db, editor, collections, globals)
    collections/
      users/
        index.ts                               # Users CollectionConfig (auth: true)
      articles/
        index.ts                               # Articles CollectionConfig (versions, hooks)
        fields.ts                              # Article field definitions
        hooks/
          before-change.ts                     # Auto-generate slug from title
      media/
        index.ts                               # Media CollectionConfig (upload)
    globals/
      site-settings.ts                         # SiteSettings GlobalConfig
    index.ts                                   # Package entry: exports all configs
  package.json
  AGENTS.md

Existing Collections

Users (auth collection)

  • Slug: users
  • Auth: true (provides login, password hashing, session management)
  • Fields: displayName (text), role (select: admin/editor/author, default: author)
  • Admin title: email

Articles (content with versioning)

  • Slug: articles
  • Versions: { drafts: true } -- enables draft/published workflow
  • Hooks: beforeChange: [autoGenerateSlug] -- generates slug from title if slug is empty
  • Fields: title (text, required), slug (text, unique, sidebar), content (richText), status (select: draft/published, default: draft), author (relationship to users, required), featuredImage (upload to media), publishedAt (date)
  • Admin title: title, default columns: title, status, author, updatedAt

Media (file uploads)

  • Slug: media
  • Upload: Accepts image/* and application/pdf
  • Fields: alt (text, required)
  • Admin title: filename

Existing Globals

SiteSettings

  • Slug: site-settings
  • Admin group: Settings
  • Fields: siteName (text, required, default: "My App"), siteDescription (textarea)

Hook Rules

Category Where it lives Description Examples
CMS-operational Stays in the hook file Logic tied to CMS data shaping, not business rules Slug generation, image resizing, setting default values, formatting fields
Business logic Delegated to @repo/core use case Logic that enforces a business rule or triggers side effects Sending notifications, validating against external data, cross-domain updates

DO

  • Keep hooks to 5-10 lines max
  • Import use cases from @repo/core/application (application layer only)
  • Map Payload hook arguments (data, operation, req) to use case input types
  • Return data from beforeChange / beforeValidate hooks

DON'T

  • Import from @repo/core/infrastructure -- violates Clean Architecture
  • Put business validation logic directly in hooks
  • Call external services (email, analytics, APIs) directly from hooks
  • Duplicate logic that already exists in a use case
  • Access req.payload for cross-collection operations (delegate to use case instead)

Rule of thumb: If deleting the hook would break a business requirement, the logic must live in a use case in @repo/core.

Recipe: Adding a New Collection

This example adds a Tags collection.

Step 1: Create the collection folder and fields

Create src/collections/tags/fields.ts:

import type { Field } from "payload";

export const tagFields: Field[] = [
  {
    name: "name",
    type: "text",
    required: true,
    unique: true,
    maxLength: 100,
  },
  {
    name: "slug",
    type: "text",
    unique: true,
    admin: {
      position: "sidebar",
      description: "Auto-generated from name if left empty",
    },
  },
  {
    name: "description",
    type: "textarea",
  },
];

Step 2: Create the CollectionConfig

Create src/collections/tags/index.ts:

import type { CollectionConfig } from "payload";
import { tagFields } from "./fields";
import { autoGenerateSlug } from "./hooks/before-change";

export const Tags: CollectionConfig = {
  slug: "tags",
  admin: {
    useAsTitle: "name",
    defaultColumns: ["name", "slug", "updatedAt"],
  },
  hooks: {
    beforeChange: [autoGenerateSlug],
  },
  fields: tagFields,
};

Step 3: Add a CMS-operational hook (slug generation)

Create src/collections/tags/hooks/before-change.ts:

import type { CollectionBeforeChangeHook } from "payload";

function generateSlug(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "");
}

export const autoGenerateSlug: CollectionBeforeChangeHook = ({
  data,
  operation,
}) => {
  if (operation === "create" || operation === "update") {
    if (data && data.name && !data.slug) {
      data.slug = generateSlug(data.name);
    }
  }
  return data;
};

Step 4: (Optional) Add a business-logic hook delegating to a use case

Create src/collections/tags/hooks/after-change.ts:

import type { CollectionAfterChangeHook } from "payload";
import { syncTagToSearchIndex } from "@repo/core/application";

export const syncTagAfterChange: CollectionAfterChangeHook = async ({
  doc,
  operation,
}) => {
  // Delegate to use case -- this hook is just a thin bridge
  await syncTagToSearchIndex({
    id: doc.id,
    name: doc.name,
    slug: doc.slug,
    operation,
  });
  return doc;
};

Step 5: (Optional) Add access control

Create src/collections/tags/access/is-admin.ts:

import type { Access } from "payload";

export const isAdmin: Access = ({ req: { user } }) => {
  return user?.role === "admin";
};

Then reference it in the CollectionConfig:

export const Tags: CollectionConfig = {
  slug: "tags",
  access: {
    create: isAdmin,
    update: isAdmin,
    delete: isAdmin,
    // read is open by default
  },
  // ...rest
};

Step 6: Register in payload.config.ts

Edit src/payload.config.ts:

import { Tags } from "./collections/tags";

export default buildConfig({
  collections: [Users, Articles, Media, Tags],   // <-- add Tags
  // ...rest
});

Step 7: Export from package entry

Edit src/index.ts:

export { Tags } from "./collections/tags";   // <-- add export

Recipe: Adding a New Global

This example adds a Navigation global.

Step 1: Create the GlobalConfig

Create src/globals/navigation.ts:

import type { GlobalConfig } from "payload";

export const Navigation: GlobalConfig = {
  slug: "navigation",
  admin: {
    group: "Settings",
  },
  fields: [
    {
      name: "mainMenu",
      type: "array",
      fields: [
        {
          name: "label",
          type: "text",
          required: true,
        },
        {
          name: "url",
          type: "text",
          required: true,
        },
      ],
    },
  ],
};

Step 2: Register in payload.config.ts

import { Navigation } from "./globals/navigation";

export default buildConfig({
  globals: [SiteSettings, Navigation],   // <-- add Navigation
  // ...rest
});

Step 3: Export from package entry

export { Navigation } from "./globals/navigation";   // <-- add export

Payload Config Overview

The payload.config.ts uses:

  • Database: @payloadcms/db-postgres (PostgreSQL via DATABASE_URL env var)
  • Editor: @payloadcms/richtext-lexical (Lexical rich text editor)
  • Secret: PAYLOAD_SECRET env var (required for production)
  • TypeScript output: Generates payload-types.ts in this package's src/ directory

Dependencies

Dependency Purpose
payload Payload CMS core
@payloadcms/db-postgres PostgreSQL database adapter
@payloadcms/richtext-lexical Lexical rich text editor

Cross-References

  • CMS app shell: apps/cms/ -- see apps/cms/AGENTS.md
  • Use cases for hooks: packages/core/src/application/use-cases/ -- see packages/core/AGENTS.md
  • CMS client for querying: packages/cms-client/ -- see packages/cms-client/AGENTS.md