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
- Go to Admin → Tenants → select your tenant → Theme.
- 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.
- 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
| Command | Description |
|---|---|
pnpm dev | Start the development server |
pnpm build | Build for production |
pnpm start | Start the production server |
pnpm run generate:types | Regenerate Payload TypeScript types after schema changes |
pnpm run generate:importmap | Regenerate admin import map after changing custom components |
pnpm run ts | TypeScript type check (tsc --noEmit) |
pnpm run lint | Run ESLint |
pnpm run test | Run 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_SECRETenv 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:typesThis 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:importmapThis 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/googleYou 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 buildThis skips the @t3-oss/env-nextjs validation at build time. Env vars are still validated at runtime.
Stripe webhooks not firing
- Check the webhook URL in the Stripe Dashboard — it should be
https://your-domain.com/api/stripe/webhooks. - Verify the signing secret matches
STRIPE_WEBHOOKS_ENDPOINT_SECRET. - For local dev, use the Stripe CLI :
stripe listen --forward-to localhost:3000/api/stripe/webhooks. - 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_URIat build time andDATABASE_PRIVATE_URIat runtime. For local dev, both can be the same.