Running the seed
Run your seed from wherever you are — the admin button, an HTTP call, the CLI, or straight from code.
Four entry points run the same seed engine over the same definitions. Pick the one that fits 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. The fourth, seed(), is the in-code path and is deliberately not gated, which is what lets a test drive the real seed.
See the Quickstart for setting ENABLE_SEED.
Every entry point is destructive. It clears the seeded collections before recreating them — and
the queued Payload Jobs with them, since a job enqueued before the reseed references docs that no
longer exist (jobs enqueued by the seeding itself survive). Run it on purpose. Leave
ENABLE_SEED unset in production.
The four entry points
| Entry point | How you trigger it | ENABLE_SEED gate | Needs a user |
|---|---|---|---|
| Admin button | Click "Seed your database" in the admin header | Yes | Yes — access.run |
| HTTP endpoint | POST /api/seed | Yes | Yes — access.run |
| CLI | payload seed (Local API) | Yes | No |
| Programmatic | seed() in code | No | No |
Do you need a user first? The admin button and POST /api/seed run the options.access.run gate —
by default any logged-in Payload user (not just an admin), and yours to narrow to a role. 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. The plugin registers it
for you — set ENABLE_SEED=true, then click it. It POSTs to /api/seed as the logged-in user and
reports success (or the error) inline.
// payload.config.ts
seedPlugin({ definitions: [media, services, posts] })When ENABLE_SEED isn't set, the button doesn't render at all — environments where the endpoint would
refuse anyway never show it.
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_SEEDmust equal"true", or it returns403and does nothing (the primary safety — leave it unset in production and the route is inert).- The caller must pass
options.access.run, or403. By default that's any authenticated request — a Payload auth cookie or API key — and you can narrow it to a role. It never widens the kill switch:ENABLE_SEEDis checked first and independently. See Gating endpoints.
# 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": [ … ] }Responses
| Status | Body | When |
|---|---|---|
200 | { success, created, collections, globals, order, deferred, skipped } | Seed ran and committed. Carries a message instead of counts when no definitions are registered. |
400 | { error, issues } | Validation failed — bad ref, duplicate _key, unknown field or slug. The same named, collected issues the engine produces, so you can fix them without digging through server logs. |
403 | { error } | The gate is closed (ENABLE_SEED ≠ "true"), or the caller didn't pass access.run. The error string tells you which. |
500 | { error: 'Error seeding data.' } | Any other failure. It's logged server-side; internals never reach the client. |
Which 403 is it? Read the error string — the two denials word themselves differently.
"Seeding is disabled. Set ENABLE_SEED=true …" is the kill switch. "Seeding requires an authenticated Payload user …" is the access.run gate turning the caller away.
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:
// package.json
{ "scripts": { "seed": "payload seed" } }ENABLE_SEED=true pnpm seed
# [payload-seed] clearing collections...
# [payload-seed] seeding documents...
# [payload-seed] seed complete.payload seed boots through Payload's tsx-based CLI. On some Node + database-adapter combinations
that loader has bugs — if it dies with node:crypto?tsx-namespace on Node 24, see
Troubleshooting. Use the admin button or endpoint instead
(they run in the app's own runtime).
Best for: a terminal, or a pre-deploy CI step that seeds before the app serves traffic.
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:
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)SeedResult fields
seed() returns { created, collections, globals, order, deferred, skipped } — the same shape the
endpoint response carries (alongside success: true) and the source the CLI's completion log summarizes.
| Field | Type | What |
|---|---|---|
created | Record<slug, number> | Count of docs created per collection. |
collections | string[] | Slugs of the collections seeded this run. |
globals | string[] | Slugs of the globals seeded this run. |
order | string[] | Topo-sorted create order, e.g. ['media:hero', 'services:consulting', 'posts:launch']. |
deferred | — | Fields created null to break a ref cycle, then set in the second pass. |
skipped | [{ slug, reason }] | Definitions skipped this run (their own disabled, or a collection's custom.seedDisabled). |
Still destructive; call it deliberately. Best for: integration tests and migrations.
Writing seeds
Describe your seed data in typed seed.ts files — refs, files, collections, and globals all checked against your real Payload types as you write.
Advanced
Route assets through a collection's own ingest hook, skip seeds that can't run in an environment, and keep bulk seeding from firing revalidation — plus how the engine orders and creates everything.