Set Up Payload Content Studio and Marketing Email
Configure the separate Payload CMS, provision its first editor privately, run content jobs, and deliver reviewed marketing through the SaaS gateway.
Content Studio is the optional third application in the starter. It runs Payload/Next.js for pages, posts, briefs, media and campaign drafts. Web and Admin share SaaS data/auth; Studio has its own database, editor sessions, migrations and queue. The detached Sushi SaaS documentation website does not consume this CMS automatically.
Start locally and provision the first editor
From the starter root:
./scripts/setup.sh development
pnpm dev:doctor
pnpm dev:allSetup writes apps/content-studio/.env.development.local, creates sushi_content and applies Payload migrations without replacing existing values. To run authoring alone afterwards, use pnpm dev:studio, then open http://localhost:3002/admin.
The first account in an empty Studio database becomes administrator. Current code permits first-account creation without an existing editor session. Keep the deployment accessible only to trusted operators until that account is created; do not expose an empty Studio publicly. No bootstrap:admin CLI is shipped. Afterwards, user creation requires an administrator and later accounts default to writer.
Editor roles are writer, seo-manager, reviewer, publisher, admin. They are independent of SaaS admin_ro/admin_rw; the Admin publishing link grants no editor access.
Configure a separate production app
Run pnpm env:setup:prod interactively and opt into Studio. Only that opt-in creates apps/content-studio/.env.production.local; non-interactive setup prepares only the SaaS production profile. Alternatively copy the Studio .env.example to its production profile and fill it manually.
| Setting | Required meaning |
|---|---|
CONTENT_DATABASE_URL | Separate Payload database, never the SaaS database |
PAYLOAD_SECRET | Independent 32-byte editor-session secret |
CONTENT_STUDIO_URL | Studio origin; also supplied to SaaS Admin for its link |
CONTENT_CORS_ORIGINS | Exact allowed browser origins including the Studio editor origin |
SAAS_MARKETING_API_URL | SaaS origin accepting signed marketing requests |
CONTENT_MARKETING_SECRET | Exact same gateway secret as the SaaS |
CRON_SECRET | Studio scheduler secret, distinct from SaaS cron/auth |
CONTENT_STORAGE_* | Durable private media storage in production |
The current config uses the same allowlist for CORS and CSRF. Include the Studio origin itself and only trusted frontend origins that need browser API access. Locally use, for example, http://localhost:3002,http://localhost:3000; replace all local origins in production.
CONTENT_STUDIO_URL=https://studio.example.com
CONTENT_CORS_ORIGINS=https://studio.example.com,https://www.example.com
SAAS_MARKETING_API_URL=https://app.example.com
CONTENT_STORAGE_BUCKET=product-content
CONTENT_STORAGE_REGION=auto
CONTENT_STORAGE_ENDPOINT=https://ACCOUNT_ID.r2.cloudflarestorage.com
CONTENT_STORAGE_ACCESS_KEY=
CONTENT_STORAGE_SECRET_KEY=
CONTENT_STORAGE_FORCE_PATH_STYLE=trueFill the database and credentials privately. The storage plugin activates only when bucket, region, access key and secret key are all present. Local disk is a development fallback; serverless/read-only containers require durable object storage. Use a dedicated private Studio bucket and scoped credentials. Choose the correct region, endpoint and path-style setting for your provider.
openssl rand -hex 32 # PAYLOAD_SECRET
openssl rand -hex 32 # Studio CRON_SECRET
openssl rand -hex 32 # CONTENT_MARKETING_SECRET; copy once to SaaS and Studio
openssl rand -hex 32 # MARKETING_UNSUBSCRIBE_SECRET; SaaS only, different value
pnpm env:check:prodValidation checks an existing Studio production profile, including its separate database and matching marketing secret. It does not prove editor-origin CSRF, storage reachability or first-admin provisioning. See Environment Configuration for full SaaS startup requirements and explicit process loading.
Migrate before deployment
With Studio production values supplied to its release process:
NODE_ENV=production pnpm studio:migrate
pnpm build:studio
NODE_ENV=production pnpm start:studioPayload's CLI loads the Studio's Next-style production profile when NODE_ENV=production; exported host values take precedence. Containers never migrate on startup. After changing collections, generate and commit types, import map and the independent Payload migration:
pnpm studio:generate
pnpm studio:migrate:create
pnpm studio:migrate
pnpm --dir apps/content-studio checkFor Payload 3.90.2, apply 20261005_024231_payload_security_fields before promoting Studio. It adds nullable password-reset throttling and media-key fields; existing uploaded objects stay in place. Capture service-account API keys when generated, because later reads do not reveal them. Re-create pending scheduled publish/unpublish events after upgrading: their user reference now includes its authentication collection.
Draft, review, publish and automate
Pages/posts support five locales, drafts, autosave, previews, review, approval, scheduled publishing and version history. Registered blocks include hero, rich text, callout, FAQ, CTA and known tools; content cannot install arbitrary HTML, CSS, JavaScript or executable tools. Review locale, text, SEO fields and preview, then have a publisher/admin approve and publish the intended version.
Your product website must consume published Payload data or /api/content/v1/published; creating Studio does not replace this docs site's MDX loader. Create a Service Account with only needed scopes such as content:draft:create and content:read. Keep content:publish separate. Capture its API key once and store it in the automation host's secret manager.
Payload requires the exact Authorization: service-accounts API-Key <key> format. With CONTENT_API_KEY privately exported, create a draft:
curl --fail-with-body https://studio.example.com/api/content/v1/drafts \
-H "Authorization: service-accounts API-Key $CONTENT_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: first-product-update-v1' \
--data '{"collection":"posts","locale":"en","slug":"first-product-update","title":"First product update","summary":"What changed and why it helps.","blocks":[{"type":"richText","markdown":"A useful update for customers."}]}'Retry the same operation with the same key/body; use a new key for new work. Reusing a key with another payload is rejected. The API also supports draft read/replace, review submission, separately scoped publication, briefs and batch imports. Imports return job IDs; inspect /api/content/v1/jobs/:id with the corresponding scope. Queued does not mean published.
Run the Payload queue
SaaS pnpm jobs:work drains the SaaS queue, not Payload's queue. Studio's current timer runs up to five content jobs every minute while its process stays alive. In serverless hosting, configure a scheduler instead of relying on that timer.
With Studio production variables loaded, schedule this process every minute:
NODE_ENV=production pnpm --dir apps/content-studio jobs:run --all-queues --handle-schedulesOr schedule the authenticated HTTP runner, including all queues and scheduled publishing:
curl --fail-with-body \
-H "Authorization: Bearer $STUDIO_CRON_SECRET" \
'https://studio.example.com/api/payload-jobs/run?allQueues=true&limit=5'STUDIO_CRON_SECRET is the scheduler's private variable containing Studio's exact CRON_SECRET; it is not a new application setting or the SaaS cron key. Prove one import and one due publication finish, and monitor failures/delays.
Deliver marketing through the SaaS
Studio owns layout, copy, preview, approval and schedule. The SaaS owns subscriber addresses, consent, unsubscribe/suppression state, audience resolution, Resend credentials, recipient jobs and delivery audit. Automation uses marketing:* scopes and reviewed action endpoints; a generic collection update does not launch a campaign.
Share CONTENT_MARKETING_SECRET exactly. In SaaS, set distinct stable MARKETING_UNSUBSCRIBE_SECRET, RESEND_API_KEY, EMAIL_FROM and provider-issued RESEND_WEBHOOK_SECRET. Register https://app.example.com/api/marketing/webhooks/resend for delivery, bounce, complaint, failure and suppression events. Obtain that endpoint's signing secret from Resend; do not generate it with OpenSSL.
Create a template with a physical sender address, compose a campaign, preview/validate, inspect the consented audience count, send a test, approve/publish the exact version, then launch. SaaS deduplicates recipients, rechecks consent before Resend, adds postal/unsubscribe content and one-click headers. Repeated launch does not duplicate delivery. Cancel pending work through the campaign; provider-accepted mail cannot be recalled. Refresh status and prove unsubscribe prevents a later provider call.
Connect your website's revalidation handler
Optional PUBLIC_SITE_REVALIDATE_URL names your own receiving endpoint. Generate a separate CONTENT_REVALIDATION_SECRET and share it only between Studio and that handler. Studio signs timestamp.rawBody with HMAC-SHA256, using x-content-timestamp and x-content-signature. The adopting website must implement signature verification, freshness checks and cache invalidation. No automatic receiver is configured in sushisaas.com.
Launch evidence: first admin provisioned privately, isolated databases, editor-origin CSRF login, durable uploads, both migration histories current, one draft from API replay, imports/scheduled publishing executed, and consent/duplicate-launch/unsubscribe checks pass for a disposable recipient.
Related: Environment Configuration, Transactional Email, Jobs and Readiness, Deployment and Security.
Source: Studio contract, Payload configuration, first-user provisioning, marketing contract.