Build the product

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:all

Setup 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.

SettingRequired meaning
CONTENT_DATABASE_URLSeparate Payload database, never the SaaS database
PAYLOAD_SECRETIndependent 32-byte editor-session secret
CONTENT_STUDIO_URLStudio origin; also supplied to SaaS Admin for its link
CONTENT_CORS_ORIGINSExact allowed browser origins including the Studio editor origin
SAAS_MARKETING_API_URLSaaS origin accepting signed marketing requests
CONTENT_MARKETING_SECRETExact same gateway secret as the SaaS
CRON_SECRETStudio 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=true

Fill 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:prod

Validation 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:studio

Payload'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 check

For 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-schedules

Or 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.