# payload-seed

URL: /docs/plugins/payload-seed

Type-safe, dependency-ordered seeding for Payload CMS. Declare seed data in files and let the plugin order, upload, and create it.

`@pro-laico/payload-seed` is an automatic, type-safe helper for writing seed values for your
collections, globals, and more. It rides the same `payload generate:types` you already run, so the
typing stays accurate and up to date on its own, which means you **and** your AI tools get real
feedback while writing seed content, instead of guessing at shapes. It also handles relationships
and assets for you, so you never have to worry about upload order or ending up with empty references.

It also ties into the other Pro Laico plugins, so uploading assets (and working with those plugins
in general) just falls into place.

Think of it as a **bootstrap** tool. It's for the initial data a site ships with: the content you'd
otherwise hand-enter once to stand a project up, or that you let an AI scaffold for you. Running the
seed uploads that content into your real storage and database, so it ends up where it belongs (your
DB, S3, Mux, and so on) instead of living in your repo. It's not meant for incremental, day-to-day
content changes.

```bash
pnpm add @pro-laico/payload-seed
```

## What's included

Everything you need to seed Payload CMS as easily as possible.

- **Typed seed helper functions:** payload-seed automatically generates types for the collections and globals you use. With one of this package's helper functions, you can write type-safe seed data for anything.
- **Relationship handling:** You don't have to deal with empty references in your seeded docs anymore. payload-seed handles reference seed order automatically.
- **Asset handling:** Dedicated asset upload handlers can be attached to payload-seed from other plugins, on top of a standard Payload upload handler built into our type-safe helper functions.
- **Seed anywhere:** Seed locally via the CLI, by pinging the seed endpoint, or with the included seed button right in your Payload admin dashboard.

## Quickstart

**Add the plugin**

Register `seedPlugin` in your Payload config. Leave `definitions` empty for now; you'll fill it in
once your seed files exist.

```ts
import { buildConfig } from 'payload'
import { seedPlugin } from '@pro-laico/payload-seed'

export default buildConfig({
  plugins: [
    seedPlugin({
      definitions: [],
      adminButton: true,
    }),
  ],
  // ...
})
```

> Using `adminButton: true`? The button is registered by string path, so regenerate the admin import
> map with `pnpm payload generate:importmap` (skip it if you only run the seed via the CLI /
> endpoint).

**Define your data**

Write one `seed.ts` per collection or global with `defineSeed` (it infers which from the slug). Each
collection record needs a `_key` (a local handle other files point at); upload collections carry their
file on `_file`, and relationship fields point at other records with `ref()`.

```ts
// src/collections/Media/seed.ts
import { defineSeed } from '@pro-laico/payload-seed'

export default defineSeed('media', ({ file }) => [
  {
    _key: 'serviceImg',
    _file: file('service-a.jpg'), // assets/media/service-a.jpg
    alt: 'Consulting',
  },
])
```

```ts
// src/collections/Services/seed.ts
import { defineSeed } from '@pro-laico/payload-seed'

export default defineSeed('services', () => [
  {
    _key: 'consulting',
    title: 'Consulting',
  },
])
```

```ts
// src/collections/Posts/seed.ts
import { defineSeed } from '@pro-laico/payload-seed'

export default defineSeed('posts', ({ ref }) => [
  {
    _key: 'launch',
    title: 'We launched',
    heroImage: ref('media', 'serviceImg'),
    relatedService: ref('services', 'consulting'),
  },
])
```

**Add them to the plugin**

Import each `seed.ts` export and hand the whole set to `definitions`. The same array drives the run
and the injected types.

```ts
import { buildConfig } from 'payload'
import { seedPlugin } from '@pro-laico/payload-seed'
import media from './collections/Media/seed'
import services from './collections/Services/seed'
import posts from './collections/Posts/seed'

export default buildConfig({
  plugins: [
    seedPlugin({
      definitions: [media, services, posts],
      adminButton: true,
    }),
  ],
  // ...
})
```

**Enable, then run**

The seed is gated by the `ENABLE_SEED` kill switch (off by default). Set it, then run the seed one
of three ways: the bundled command, the admin button (`adminButton: true`), or the endpoint:

```bash
ENABLE_SEED=true pnpm payload seed   # CLI (Local API)
# or set ENABLE_SEED=true in your env, then:
#   click "Seed your database" in the admin header
#   or POST /api/seed  (any authenticated user)
```

> The seed is **destructive**. It clears and recreates every seeded collection. `ENABLE_SEED` is
> off by default: opt in per-environment while iterating, and never set it in production.

## Plugin options

`seedPlugin(options?)` is the single entry point. One call registers the `payload seed` command,
the `POST /api/seed` endpoint, the optional admin button, and the type augmentation that checks your
refs. It does nothing useful without `definitions`.

The **Reference** tab is the interactive view; **TypeScript** is the same shape in code, every option
at its default.

**Reference**

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `definitions` | `SeedDefinition[]` |  | The seed definitions: your defineSeed exports. The same array feeds the seed run and the typed SeedRegistry injected into payload-types.ts. Omit it and the seed runs but warns there is nothing to do. |
| `assetsDir` | `string` | `'assets'` | The assets root where _file source files live, relative to the project root. A native upload's file is looked up under its per-collection subdir (see assetSubDirs) then the root; providers look under their own subdir. |
| `assetSubDirs` | `Partial<Record<CollectionSlug, string>>` | `{}` | Per-collection subdirectory (under assetsDir) for a collection's _file source files. Defaults to the collection slug (media resolves under assets/media/), so a folder named after the collection just works. Set an entry to use a different folder name, e.g. { media: 'images' } or { fontOriginal: 'font' }. |
| `adminButton` | `boolean` | `false` | Render the Seed your database button in the admin header. |

**TypeScript**

```ts
import { seedPlugin } from '@pro-laico/payload-seed'

// Every option at its default.
seedPlugin({
  // definitions: [media, services, posts], // no default; your seed.ts exports
  assetsDir: 'assets',
  assetSubDirs: {}, // default folder per collection is its slug; override here, e.g. { media: 'images' }
  adminButton: false,
})
```

## `defineSeed`

Declares the seed data for one collection or global, one `export default` per `seed.ts` file. It
infers which from the slug: a **collection** slug means the builder returns an **array of records**; a
**global** slug means it returns a **single data object** (no `_key`). Returns a seed definition you
add to `seedPlugin({ definitions })`.

The **Reference** tab lists the parameters; **TypeScript** is a minimal example.

**Reference**

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `slug` | `CollectionSlug \| GlobalSlug` |  | The collection or global to seed into. Typed against your Payload types, so an unknown slug is a TypeScript error. The slug also picks the builder's return shape: an array of records for a collection, a single object for a global. _(required)_ |
| `build` | `(tokens) => Record[] \| GlobalData` |  | Returns the data to create: an array of records for a collection, or one data object for a global. Receives the { ref, file } tokens as its argument, so you point at other docs and attach files inline, with nothing to import. _(required)_ |
| `opts.disabled` | `boolean \| string` |  | Skip this definition at seed time without unregistering it, so the generated seed-ref types don't change (a string becomes the warning's reason). Rarely set by hand — a collection that declares custom.seedDisabled (e.g. payload-mux without credentials) is skipped automatically. See Disabled seeds. |

**TypeScript**

```ts
// src/collections/Services/seed.ts
import { defineSeed } from '@pro-laico/payload-seed'

// slug + a builder returning the data (an array of records for a collection).
export default defineSeed('services', () => [
  {
    _key: 'consulting',
    title: 'Consulting',
  },
])
```

### Collections

**Each record** is the collection's own data plus a required **`_key`** (a local handle other seed
files target with `ref(slug, key)`) and, for upload / provider collections, an optional **`_file`**
carrying its source file. Point relationship fields at other records with `ref()`; attach a file with
`file()` on `_file`:

**Media (an upload collection)**

```ts
// src/collections/Media/seed.ts: each doc carries its file on `_file`
import { defineSeed } from '@pro-laico/payload-seed'

export default defineSeed('media', ({ file }) => [
  {
    _key: 'serviceImg',
    _file: file('service-a.jpg'), // assets/media/service-a.jpg
    alt: 'Consulting',
  },
])
```

**Posts (references them)**

```ts
// src/collections/Posts/seed.ts: point at other seeded docs by ref
import { defineSeed } from '@pro-laico/payload-seed'

export default defineSeed('posts', ({ ref }) => [
  {
    _key: 'launch',
    title: 'We launched',
    heroImage: ref('media', 'serviceImg'),          // upload-field relationship, resolved by ref
    relatedService: ref('services', 'consulting'),  // typed dependency edge across files
  },
])
```

Records are typed against your generated Payload types (`RequiredDataFromCollectionSlug<slug>`), with
relationship fields widened to also accept `ref()` tokens. Change a collection's fields and the seed
file shows a TS error until it matches; unknown fields are rejected too. The producing side (`_key`)
stays a free string; only the consuming side (`ref`) is checked.

### Globals

Pass a global slug and the builder returns a single data object instead of an array (a global is a
singleton, so there's no `_key`). It gets the same tokens, and globals are **updated after** all their
dependencies exist:

```ts
// src/globals/SiteSettings/seed.ts
import { defineSeed } from '@pro-laico/payload-seed'

export default defineSeed('site-settings', ({ ref }) => ({
  /* SiteSettings data; ref('services', 'consulting') etc. */
}))
```

### Tokens

Inside a `build` callback you get two tokens. The engine resolves them at create time, and `ref`
doubles as the dependency edge that orders the run.

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `ref` | `(collection, key) => Ref` |  | Point at another seeded doc by its _key, e.g. ref('services', 'consulting'). Records a dependency edge so the target is created first. Both arguments are checked against the SeedRegistry once types are generated. Use it for any relationship / upload field. |
| `file` | `(name, options?) => FileToken` |  | Attach a source file to a doc, on its _file meta-key, e.g. file('hero.jpg'). Delivered as a native upload or a provider ingest depending on the collection (see Files). options is an optional bag for provider ingest (e.g. a font weight). |

You never import either; they're handed to every `build` callback (`({ ref, file }) => …`), so they
can't drift from the data they point at. `file` goes on the record's `_file` meta-key; everything
else is an ordinary field.

Run `payload generate:types` and every `ref()` is checked against your real `_key`s: rename or remove
a seeded item and each stale reference becomes a TS error (unknown collections error too). See
[How it works](#how-it-works) for the mechanism.

## Files (`_file`)

An asset is just a seeded doc that carries a file. Put it on the record's **`_file`** meta-key with
the `file()` token. How the file is delivered depends on the doc's collection. The engine decides,
you don't:

- **An upload collection** (e.g. `media`, `images`, `icon`) → the bytes are read and uploaded as the
  doc's file.
- **A `custom.seedAsset` collection** (e.g. Mux) → the resolved path + options are handed to the
  collection's own ingest hook (see [Custom ingestion](#custom-ingestion)).

```ts
// src/collections/Media/seed.ts
import { defineSeed } from '@pro-laico/payload-seed'

export default defineSeed('media', ({ file }) => [
  {
    _key: 'hero',
    _file: file('hero.jpg'), // assets/media/hero.jpg
    alt: 'Hero',
    focalX: 78,
    focalY: 32,
  },
])
```

`file(name, options?)`: `name` is the source filename (resolved below); `options` is an optional bag
merged into a `custom.seedAsset` collection's ingest source field (e.g. a Mux `playbackPolicy`) and
ignored for native uploads. The
doc's own fields (`alt`, focal point, `title`, …) are ordinary record fields, typed against the
collection, so an upload collection that requires `alt` makes the seed record require it too. Focal
points set the upload's focal point, so seeded images crop to the right subject the moment they're
served (e.g. by [`@pro-laico/payload-images`](/docs/plugins/payload-images)).

### Where files live

Every `_file` resolves to the same three-part path — the assets dir, a per-collection folder, then the
name you pass:

```text
<assetsDir> / <collection folder> / <file name>
assets      / media              / hero.jpg
```

The folder is the collection slug, so this needs no config. On disk:

```text
root/
  assets/                  ← assetsDir        (default 'assets', relative to project root)
    media/                 ← collection folder (the slug; rename via assetSubDirs)
      hero.jpg             ← file('hero.jpg')
      portraits/           ← a subpath in the name nests further
        jane.jpg           ← file('portraits/jane.jpg')
```

The `defineSeed` using those files:

```ts
// src/collections/Media/seed.ts
export default defineSeed('media', ({ file }) => [
  {
    _key: 'hero',
    _file: file('hero.jpg'), // assets/media/hero.jpg
    alt: 'Hero',
  },
  {
    _key: 'jane',
    _file: file('portraits/jane.jpg'), // assets/media/portraits/jane.jpg
    alt: 'Jane',
  },
])
```

A `_file` can also be an **absolute path** (`file('/tmp/logo.svg')`), used as-is for a file outside the tree.

> Consider **gitignoring your assets folder** in a real project. Since seeding is a one-time bootstrap
> that uploads these files into your real storage, the source originals don't need to live in the repo
> afterward, and they can be large enough to bloat it. (The example sandboxes in this repo commit
> theirs only so the demos run in CI.)

## Custom ingestion

Most collections take a plain Payload upload for their `_file` and seed natively. A few ingest their file
**through their own hook** instead — e.g. [`@pro-laico/payload-mux`](/docs/plugins/payload-mux)'s
`mux-video`, whose bytes go to Mux. Mark such a collection's own config and the engine hands the `_file`
to that hook (via a `sourceField`) rather than uploading bytes:

```ts
// the collection's own config — usually set by its plugin, not by you
{
  slug: 'mux-video',
  custom: {
    seedAsset: { sourceField: 'source' }, // or `seedAsset: true` for the defaults
  },
  fields: [
    // the field the engine writes `{ file, ...options }` to, read by the ingest hook
    { name: 'source', type: 'json', admin: { hidden: true } },
    // ...the rest of the collection
  ],
}
```

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `seedAsset` | `true \| { sourceField?, subdir? }` |  | Set on the collection's custom.seedAsset. true uses the defaults below; pass an object to override them. |
| `seedAsset.sourceField` | `string` | `'source'` | The field the engine sets to { file, ...options } for the collection's ingest hook to read. |
| `seedAsset.subdir` | `string` | `<collection slug>` | Source-file folder under assetsDir. Override per-collection with the plugin's assetSubDirs. |

The engine auto-discovers the marker from the live config — nothing to register, and the owning plugin
never imports payload-seed. The doc then seeds like any other (`_file` + `ref()`). Collections whose asset
is a plain upload (payload-fonts' `fontOriginal`, payload-images' `image`) need **no** marker. See
[payload-mux → Seeding](/docs/plugins/payload-mux) for a worked example.

## Disabled seeds

Some collections can't always seed — [`@pro-laico/payload-mux`](/docs/plugins/payload-mux)'s `mux-video`
needs API credentials to ingest a clip. Don't gate the definition on the environment (that shifts the
generated seed-ref types with it); the **collection declares it** instead:

```ts
// set by the owning plugin when it can't ingest — e.g. payload-mux without MUX_* env vars
{
  slug: 'mux-video',
  custom: { seedDisabled: 'Mux credentials not set (MUX_TOKEN_ID / MUX_TOKEN_SECRET)' },
}
```

The engine **skips** that definition (warning with the reason) and **drops** any optional field whose
`ref()` points at it — a *required* ref is a hard error. The definition stays registered, so types
don't change with the environment; set the env vars and the next run seeds it and fills the dropped
refs back in. To gate a definition yourself, `defineSeed` takes the same flag directly:
`defineSeed('reports', build, { disabled: !process.env.REPORTS_API_KEY })`.

## Running the seed

Four entry points run the **same engine** over the same definitions — pick by where you are. Three of
them (the admin button, the HTTP endpoint, and the CLI) sit behind the **`ENABLE_SEED` kill switch**: if
`ENABLE_SEED` isn't exactly `"true"`, they refuse to run — leave it unset in production. The fourth,
`seed()`, is the in-code path and is deliberately *not* gated (that's what lets a test drive it). Every
entry point is **destructive** — it clears the seeded collections before recreating them — so run it on
purpose. (See [Quickstart](#quickstart) for setting `ENABLE_SEED`.)

> **Do you need a user first?** The admin button and `POST /api/seed` require a logged-in Payload user
> (any user — not just an admin). The **CLI** and **`seed()`** run over the Local API with access control
> bypassed, so they need no user at all — reach for them to bootstrap an **empty** database (you can even
> seed your first admin user as part of the run).

**Admin button**

The friendliest path: a **"Seed your database"** button in the admin header. Turn it on with
`adminButton: true`, set `ENABLE_SEED=true`, then click it — it POSTs to `/api/seed` as the logged-in
user and reports success (or the error) inline.

```ts
// payload.config.ts
seedPlugin({ definitions: [media, services, posts], adminButton: true })
```

**Best for:** local development and demos — the quickest way to reseed while you build.

**HTTP endpoint**

The plugin registers **`POST /api/seed`** — a Payload REST route, and the very route the admin button
calls. Hit it from a script, a CI step, or curl against a running app. It's guarded twice; **both** must
pass:

- **`ENABLE_SEED` must equal `"true"`**, or it returns `403` and does nothing (the primary safety —
  leave it unset in production and the route is inert).
- **The request must be authenticated** — a Payload auth cookie or API key — or `403`. Any logged-in
  user qualifies, so gate by environment with `ENABLE_SEED`, not by role.

```bash
# Send your Payload auth cookie or API key (here: an API key on the `users` collection).
curl -X POST https://your-app.com/api/seed \
  -H 'Authorization: users API-Key <your-api-key>'
# 200 → { "success": true, "created": { "media": 3, "services": 2, "posts": 1 }, "order": [ … ] }
```

On success it returns `200` with `{ success, created, order }`. A validation failure (bad ref, duplicate
`_key`, unknown field/slug) returns `400` with `{ error, issues }` — the same named, collected issues the
engine produces, so you can fix them without digging through server logs. Any other failure logs
server-side and returns a generic `500` (`{ error: 'Error seeding data.' }`), so internals never reach the
client. **Best for:** CI/CD, or seeding a deployed environment over HTTP.

**CLI**

The plugin adds a **`payload seed`** command (a `bin` on the package) that runs over the Local API — no
HTTP, no auth, just a terminal. Wire a script and run it with the switch on:

```json
// package.json
{ "scripts": { "seed": "payload seed" } }
```

```bash
ENABLE_SEED=true pnpm seed
# [payload-seed] clearing collections...
# [payload-seed] seeding documents...
# [payload-seed] seed complete.
```

**Best for:** a terminal, or a pre-deploy CI step that seeds before the app serves traffic. Note that
`payload seed` boots through Payload's `tsx`-based CLI; on some Node + database-adapter combinations that
loader has bugs — if it trips, use the admin button or endpoint instead (they run in the app's own runtime).

**Programmatic**

Call the engine directly with **`seed()`** — for tests, migrations, or a custom script. It builds a Local
API `req` if you don't pass one and resolves the options you hand it. Unlike the other three it is **not**
behind `ENABLE_SEED` (the gate lives on the entry points), so a test can drive the real seed:

```ts
import { seed } from '@pro-laico/payload-seed'
import { getPayload } from 'payload'
import config from '@payload-config'

const payload = await getPayload({ config })
const result = await seed({ payload, options: { definitions: [media, services, posts] } })

result.created  // → { media: 3, services: 2, posts: 1 }              (created docs per collection)
result.order    // → ['media:hero', 'services:consulting', 'posts:launch']  (topo-sorted create order)
result.deferred // → fields created null to break a ref cycle, set in the second pass
result.skipped  // → [{ slug, reason }] definitions skipped this run (disabled / custom.seedDisabled)
```

`seed()` returns `{ created, order, deferred, skipped }` — `created` and `order` are the same shape
the endpoint response carries (alongside `success: true`) and the CLI logs on completion. Still
destructive; call it deliberately. **Best for:** integration tests and migrations.

## How it works

Every entry point runs the same engine. You don't need any of this to write seeds; it's here for when
you're debugging or curious.

### What a seed run does

Trigger a seed from any entry point and the engine runs the same fixed sequence — starting from your
in-memory `defineSeed` output and ending with a fully-populated database:

**Build the model**

Skip any [disabled definition](#disabled-seeds) (its own `disabled`, or the collection's
`custom.seedDisabled` — warning per skip), then run each remaining builder to produce the collection
records (with their `_file`) and the globals. Optional fields whose `ref()` points at a skipped
definition are dropped (required ones error).

**Validate**

Every `ref` targets a real collection and resolves to a seeded doc, every `_file` sits on an upload or
`custom.seedAsset` collection, no duplicate `_key` within a collection, no unknown top-level fields. All
issues are collected and thrown at once, naming the collection, `_key`, and field.

**Build the dependency graph & topo-sort**

Every `ref(collection, key)` becomes an edge; a depth-first sort orders each doc after the docs it
references. A cycle is broken by deferring an optional field; an all-required cycle is a hard error naming it.

**Clear the seeded collections**

Upload collections and any collection with delete hooks clear via `payload.delete` so those hooks fire
(e.g. external-asset cleanup); plain collections are wiped directly.

**Create docs in dependency order**

Resolve each doc's `ref` tokens to real ids and deliver its `_file` — a native upload, or a source-field
value for a `custom.seedAsset` collection's ingest hook. A field deferred to break a cycle is created null.

**Resolve deferred references**

If a cycle was broken, set each deferred field now that every doc exists — one `update` per deferred field.

**Update globals**

Last, after every doc exists — resolving their refs too.

### How typed refs are generated

The plugin injects a `SeedRegistry` into `payload-types.ts` via Payload's own `typescript.postProcess`
hook, riding the same `generate:types` command you already run. That makes the augmentation **global
with no import**, exactly like Payload's generated `GeneratedTypes`, so `ref('services', 'consulting')`
is checked against your real `_key`s. Without codegen, refs fall back to runtime validation: safe
either way, fully safe with it.

### Disable revalidation

The engine sets `context: { disableRevalidate: true }` on every create, update, and delete. Have your
`afterChange` / `afterDelete` revalidation hooks check for it and skip, so a bulk seed doesn't fire a
revalidation per doc:

```ts
export const revalidatePost: CollectionAfterChangeHook = ({ doc, context }) => {
  if (!context.disableRevalidate) revalidatePath(`/posts/${doc.slug}`)
  return doc
}
```

## Gotchas

A few things to keep in mind:

- **Every run is destructive.** A seed clears each collection it touches before recreating it — a reset,
  not an append. Re-running wipes and rebuilds, so never point it at data you want to keep.
- **Nothing runs without `ENABLE_SEED`.** The admin button, endpoint, and CLI all no-op (`403`) unless
  `ENABLE_SEED` is exactly `"true"`. The in-code `seed()` is the exception — it skips the gate, so it will
  wipe whatever database it's handed. Guard your script / test setup accordingly.
- **Circular references need an optional field.** Two docs that reference each other are fine — the engine
  breaks the cycle by deferring one side's `ref` and setting it in a second pass once both docs exist. That
  field just has to be **optional** (it's created null first). A cycle where every ref sits on a *required*
  field is a hard error naming it (`posts:a -> authors:b -> posts:a`) — make one side optional to break it.
- **A missing source file only warns.** If a `_file` isn't found under `<assetsDir>/<subdir>/`, the engine
  logs `_file '…' not found` and keeps going — the doc is still created (and an upload collection that
  requires a file may then error downstream). Watch the logs for typos. The subdir defaults to the
  collection slug; override it with `assetSubDirs`.
- **Don't env-gate a definition out of the array.** Conditionally registering a definition
  (`...(withMux ? [videos] : [])`) makes the generated seed-ref types depend on the environment — the dev
  server regenerates them on boot and every `ref()` at it flips between valid and error. Register it
  unconditionally and let it be [skipped at runtime](#disabled-seeds) instead (`custom.seedDisabled`, or
  `defineSeed`'s `disabled`).
- **Typed refs need `generate:types`.** `ref()` keys are only checked once the `SeedRegistry` is generated;
  before that they fall back to runtime validation. Rename a `_key` and you must regenerate types for the
  compile-time check to catch stale references.
- **Globals can't be referenced, and update last.** `ref()` points at collection docs only — a global can
  reference docs, but a doc can't reference a global. Globals are applied after every doc exists.
- **CLI dies with `ENOENT … node:crypto?tsx-namespace=…` on Node 24? That's tsx, not the seed.** Payload's
  CLI loads bin scripts through tsx's `tsImport()`, and tsx ≥ 4.21.1 mis-resolves a dependency's bare
  `require('crypto')` under Node 24's module hooks — killing any custom `payload <script>` at boot. Pin tsx
  below the regression until it's fixed upstream (pnpm: `overrides: { tsx: '4.19.4' }`). The admin button
  and `POST /api/seed` are unaffected.

## Exports

| Export                                                              | From                                            | What                                                                                                                                                                           |
| ------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `seedPlugin`                                                        | `@pro-laico/payload-seed`                       | The plugin factory.                                                                                                                                                            |
| `defineSeed`                                                        | `@pro-laico/payload-seed`                       | Declare a collection's seed records or a global's seed data (inferred from the slug; one default export per `seed.ts`).                                                        |
| `seed`                                                              | `@pro-laico/payload-seed`                       | Run the seed from a script / test / migration (see [Running the seed](#running-the-seed)).                                                                                     |
| `ref` / `file` / `isRef` / `isFileToken`                            | `@pro-laico/payload-seed`                       | The token constructors + type guards, for code that builds seed data *outside* a builder callback (unit tests, composed fragments). Inside a builder, use the supplied tokens. |
| `SeedTokens` / `WithRefs` / `CollectionSeedData` / `GlobalSeedData` | `@pro-laico/payload-seed`                       | Types for seed helpers composed across files: type a fragment's `{ ref, file }` parameter, widen generated data types to accept `Ref` tokens.                                  |
| `Ref` / `FileToken`                                                 | `@pro-laico/payload-seed`                       | The token value types themselves.                                                                                                                                              |
| `SeedRegistry`                                                      | `@pro-laico/payload-seed`                       | Augmentable interface `generate:types` fills in, so `ref()` keys are checked. Not imported by name.                                                                            |
| `SeedButton`                                                        | `@pro-laico/payload-seed/components/SeedButton` | The admin **Seed your database** button (wired via the import map when `adminButton` is on).                                                                                   |
