Extras
Theming
Each tenant can have its own color theme. There are 22 built-in themes to choose from:
default, neutral, stone, zinc, gray, amber, blue, cyan, emerald, fuchsia, green, indigo, lime, orange, pink, purple, red, rose, sky, teal, violet, yellow
Setting a theme
- Go to Admin → Tenants → select your tenant.
- Choose a theme from the Theme dropdown.
- Save. The frontend updates immediately (via revalidation).
How it works
- The frontend wraps tenant pages in a
TenantThemeProvider(client component). - The provider sets a
data-tenant-themeattribute on the<html>element (e.g.data-tenant-theme="blue"). - CSS selectors in
src/styles/themes.cssoverride color variables (using oklch) for each theme. - The
defaulttheme (or no theme) uses the base color scheme fromglobals.css.
Dark mode
All themes support dark mode. The dark variant is activated by the .dark class on <html> (standard Tailwind dark mode). Theme overrides apply to both light and dark modes.
Custom themes
To add a custom theme:
- Add the theme name to
THEME_OPTIONSinsrc/lib/themes.ts. - Add a CSS selector in
src/styles/themes.css:
[data-tenant-theme='my-theme'] {
--primary: 60% 0.15 250;
--primary-foreground: 98% 0 0;
/* ... other color variables */
}- Run
pnpm run generate:typesto update the TypeScript types for the select field.
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/webhooks/stripe. - Verify the signing secret matches
STRIPE_WEBHOOKS_ENDPOINT_SECRET. - For local dev, use the Stripe CLI :
stripe listen --forward-to localhost:3000/api/webhooks/stripe. - 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, allow your IP address (or
0.0.0.0/0for Railway/Vercel) in the Atlas network access settings. - The app uses
DATABASE_PUBLIC_URIat build time andDATABASE_PRIVATE_URIat runtime. For local dev, both can be the same.