Skip to Content
Extras

Extras

Theming

Each tenant owns its own design system. Rather than picking one flat theme, you compose the look across a few independent axes — base color, color accent, border radius, and fonts — so two tenants on the same deploy can feel like completely different products.

Setting a tenant’s theme

  1. Go to AdminTenants → select your tenant → Theme.
  2. Either pick a named preset to fill every axis at once, or compose your own:
    • Base color — the foundation neutral that drives backgrounds, borders, and muted surfaces.
    • Color theme — the primary/accent color. Some base-bound options (e.g. Neutral, Stone, Zinc) only pair with their matching base color; the field guides you.
    • Radius — the border-radius scale, from sharp to fully rounded.
    • Body font and Heading font — heading can inherit the body font or use its own.
    • An optional independent chart palette, which otherwise inherits the color theme.
  3. Save. The change applies on save via revalidation, so the tenant’s frontend reflects it immediately.

Dark mode

Themes work in both light and dark mode out of the box — every axis carries both variants, so a tenant’s theme stays consistent whichever mode the visitor is in.


Commands reference

CommandDescription
pnpm devStart the development server
pnpm buildBuild for production
pnpm startStart the production server
pnpm run generate:typesRegenerate Payload TypeScript types after schema changes
pnpm run generate:importmapRegenerate admin import map after changing custom components
pnpm run tsTypeScript type check (tsc --noEmit)
pnpm run lintRun ESLint
pnpm run testRun integration and E2E tests

Cron / background jobs

Not included by default. If you need scheduled tasks (e.g. syncing external data, cleanup, sending digest emails), you can:

  • Add an API route (e.g. /api/cron/your-job) and call it from an external cron service (e.g. Railway cron, Vercel Cron Jobs, or cron-job.org ).
  • Protect the route with a CRON_SECRET env var and check it in the handler.

Troubleshooting

Tenant not found / blank page

The request host must exactly match a tenant’s domain field in the admin, including the port for local dev. For example, if your tenant domain is localhost:3000, visiting 127.0.0.1:3000 will not match.

Types out of date

After changing any collection, global, or field configuration:

pnpm run generate:types

This regenerates payload-types.ts so TypeScript stays in sync with your schema.

Admin components not found

After creating or modifying custom admin components:

pnpm run generate:importmap

This regenerates the import map that Payload uses to resolve component paths.

OAuth redirect mismatch

If Google sign-in fails with a redirect URI mismatch, ensure the redirect URI in Google Cloud Console matches the tenant domain exactly:

https://your-tenant-domain.com/api/auth/callback/google

You need one redirect URI per tenant domain where Google sign-in is used.

Media lost after deploy

Local file storage is ephemeral on Railway and Vercel. Use S3 storage for production so media uploads persist.

Build fails on environment variables

If the build fails because some env vars aren’t set (common in CI/Docker):

SKIP_ENV_VALIDATION=1 pnpm build

This skips the @t3-oss/env-nextjs validation at build time. Env vars are still validated at runtime.

Stripe webhooks not firing

  1. Check the webhook URL in the Stripe Dashboard  — it should be https://your-domain.com/api/stripe/webhooks.
  2. Verify the signing secret matches STRIPE_WEBHOOKS_ENDPOINT_SECRET.
  3. For local dev, use the Stripe CLI : stripe listen --forward-to localhost:3000/api/stripe/webhooks.
  4. Check the webhook logs in Stripe for error details.

Chrome redirects .dev domains to HTTPS

Chrome has .dev in its HSTS preload list, so it forces HTTPS for any .dev domain. For local development, use .local or .test TLDs instead. See Configure Tenants for details.

Database connection errors

  • Ensure your MongoDB instance is running and the connection string is correct.
  • For MongoDB Atlas, prefer a private/internal connection (Atlas private endpoint or VPC peering) so the cluster is never exposed to the public internet; otherwise allow your specific IP in the Atlas network access settings. Only as a last resort, when you can’t pin your host’s egress IPs, fall back to 0.0.0.0/0 — and then only with strong, rotated credentials.
  • The app uses DATABASE_PUBLIC_URI at build time and DATABASE_PRIVATE_URI at runtime. For local dev, both can be the same.