Studio Docs

Production Deployment Edges

Studio works on every common React deploy target. The edges show up around caching (compositions change without code changes, caches need to invalidate), edge-runtime weight limits, and monorepo patterns. This page is the deployment playbook.

Do It With a Skill

deploy-studio-site ships to Vercel or Netlify and handles the traps this page catalogues: production env vars, composition Deploy, Live Preview gating, region and host config, cache and 401s.

curl -fsSL https://studio-documentation.contentstackapps.com/install.sh | sh

ISR + SSG: Cache Invalidation When Compositions Change

Studio compositions are content, not code. Caching them as if they were code means stale layouts persist even after a publish.

The pattern: short revalidation + on-publish webhook

For ISR / on-demand SSG (Next.js, Astro), use a short revalidate AND a Contentstack webhook to invalidate on publish:

// app/blog/[slug]/page.tsx
export const revalidate = 60;   // safety net — 60s max staleness

export default async function BlogPost({ params }) {
  const specOptions = await csStudio.fetchCompositionData({ /* … */ });
  return <StudioComponent specOptions={specOptions} />;
}

Then configure a Contentstack webhook on the compositions content type to hit your deploy platform’s revalidation API on every publish:

// app/api/revalidate-composition/route.ts
import { revalidatePath, revalidateTag } from "next/cache";

export async function POST(request: Request) {
  const { event, data } = await request.json();
  if (event !== "publish") return Response.json({ ok: true });

  const compositionUid = data.entry.uid;
  const linkedTemplate = data.entry.linked_content_type;   // e.g. "blog_post"
  revalidateTag(`composition:${compositionUid}`);
  revalidatePath(`/${linkedTemplate}/[slug]`);
  return Response.json({ revalidated: true });
}

This combination gives you fast cache misses on publish (the webhook) AND a backstop in case the webhook fires late or misses (the 60s revalidate).

What NOT to do

Don’tWhy
revalidate = false (cache forever) without a webhookA composition publish doesn’t invalidate the cache. Layout changes never appear
revalidate = 0 (always fresh) on every visitor routeDefeats ISR. Every visitor pays a full SSR render
Tag-based caching without tagging the composition UIDrevalidateTag can’t target specific routes when compositions change

CDN Cache Invalidation

If you’re behind a CDN (Cloudflare, Fastly, CloudFront), the deploy-platform revalidation isn’t enough: the CDN’s edge cache must also flush.

The pattern: webhook hits your origin’s revalidate endpoint AND purges the CDN. Most platforms have an API for surgical purge by URL or tag:

async function purgeCDN(paths: string[]) {
  await fetch("https://api.cloudflare.com/client/v4/zones/<zone>/purge_cache", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.CF_API_TOKEN}` },
    body: JSON.stringify({ files: paths.map(p => `https://yoursite.com${p}`) }),
  });
}

Trigger this from the same webhook handler as the deploy-platform revalidation.

Edge Runtime Compatibility

Studio’s SDK currently exceeds the Vercel Edge / Cloudflare Workers 1 MB limit for many configurations. For edge-deployed routes:

  • Use Node runtime for Studio routes. Mark the route file:
    export const runtime = "nodejs";   // Next.js App Router
  • Keep edge-deployed routes minimal (auth checks, redirects, simple JSON APIs) and proxy Studio renders to Node-deployed routes.
  • For full edge SSR: wait for the SDK’s edge-runtime story to mature OR ship hand-coded routes for the edge-critical pages.

The SDK team is actively shrinking the edge-runtime footprint. This constraint relaxes over time.

Monorepo Patterns

For monorepos where Studio is one of several apps:

Shared component library

Components live in a shared package. Both the Studio-rendered app and other apps consume them. The Studio-rendered app’s register-components.tsx imports from the shared package, no duplication.

Monorepo layout: packages/ui/ holds shared Hero, Card, Button components. The Studio-rendered app's register-components.tsx imports from ui/ (Studio-managed). A non-Studio marketing-app's src/ also imports from ui/. One source of truth, two consumers, no duplication.

Shared Contentstack config

The lib/contentstack.ts factory lives in a shared package. Both apps init the SDK with their own credentials.

Independent deploys

Each app deploys independently: Studio renders don’t block deploys of other apps. Composition publishes flow through Contentstack webhooks to the Studio-rendered app’s revalidate endpoint only.

Deploy-Platform Specifics

PlatformStudio runtimeCachingNotes
VercelNode only (edge limit)ISR + on-demand revalidate + revalidateTagUse runtime = "nodejs" for Studio routes
NetlifyNodeOn-demand builders + cache tagsSame pattern, netlify-functions for the revalidate handler
Cloudflare PagesWorkers (size constrained)Cache rules + purge APIUse Pages Functions + Node runtime for Studio routes
AWS AmplifyLambda (no edge limit)CloudFront invalidation via webhookTrickier: CloudFront invalidation is path-based, plan paths up front
Self-hosted (Node)NodeWhatever you buildSame patterns. Just wire the webhook to cache invalidation yourself

Patterns to Avoid

PatternWhy it bites
Caching SSR output without including the variant + locale in the cache keyStale variant/locale content served to wrong visitors
Ignoring the publish webhook, so all caches go stale until the safety-net revalidate firesAuthors publish. Nothing changes for visitors for minutes
Deploying Studio routes to edge runtime without confirming size limitsBuild fails or the route OOMs at request time
Running the same Studio project against staging + production environmentsOne author edit in staging accidentally affects production
Monorepo + duplicated component registrations across appsTwo apps register the same type UID differently. Studio uses whichever loaded last

Secret Handling Per Host

The four CONTENTSTACK_* secrets need different handling per environment and per host. Never commit them to git. Use each host’s secret store.

HostWhere secrets livePreview vs production split
VercelProject, then Settings, then Environment Variables (per environment)Set the same key with different values for Preview and Production environments. NEXT_PUBLIC_CONTENTSTACK_ENVIRONMENT=preview on preview, =production on production.
NetlifyThe site’s Settings, under Build & deploy, then EnvironmentUse “Deploy contexts” to scope per-branch. Production branch gets production, deploy previews get preview.
Cloudflare PagesProject, then Settings, then Environment variables (Preview vs Production tabs)Same key, different values per tab.
AWS AmplifyApp, then Environment variables (per branch)Bind each branch to an environment name.
Self-hosted (Node / Docker).env files loaded by dotenv, or systemd EnvironmentFile=, or Docker --env-fileShip a .env.production and .env.preview. Never ship .env with real values into the container image, mount at runtime.

Public vs server-only:

  • NEXT_PUBLIC_CONTENTSTACK_* / VITE_CONTENTSTACK_* / PUBLIC_CONTENTSTACK_* are client-visible: the bundler inlines them into the JS bundle. Only put values here that are safe to see in the browser (API key + delivery token used from CSR + preview token).
  • Plain CONTENTSTACK_* stays on the server. Management tokens, auth tokens, and any secret used only in server-side fetchCompositionData() calls belong here: the bundler won’t inline them.

Preview vs Production Environment Split

Studio + Contentstack use environments (preview, staging, production, etc.) to gate what content is deployed where. The split matters for three reasons:

  1. Authors deploy to preview first for review, then production for launch. The same composition can be in different versions on each environment.
  2. Your app must configure the environment name explicitly: CONTENTSTACK_ENVIRONMENT=preview on the preview host, =production on the production host. The SDK reads the env at bootstrap and fetches composition versions matching that name.
  3. Never point staging + production at the same environment name. An author’s staging Deploy would surface on production instantly. Provision distinct environments in Contentstack (open Stack, then Settings, then Environments). Wire each host to the right one.

Preview URL split: Studio’s Canvas URL (Layer 2 setup) should point at your preview host, not production. Authors compose against fresh preview builds. Production stays untouched until Deploy.

Canvas Iframe Embedding: CSP / Allow-List

Studio renders your Canvas URL inside an iframe hosted at app.contentstack.com. Two headers can break this if misconfigured on your app:

Content-Security-Policy: if your app sends a CSP header, add frame-ancestors https://app.contentstack.com (plus regional hosts like https://eu-app.contentstack.com, https://au-app.contentstack.com per your region). Without this, the browser refuses to render your app in Studio’s iframe and the canvas shows a blank state or “refused to connect” error.

X-Frame-Options: legacy header. If set to DENY or SAMEORIGIN, remove it or explicitly whitelist Contentstack. Prefer frame-ancestors (CSP), modern browsers honour it over X-Frame-Options.

Regional hosts to allow-list:

RegionStudio app host
US (default)app.contentstack.com
EUeu-app.contentstack.com
AUau-app.contentstack.com
Azure USazure-na-app.contentstack.com
Azure EUazure-eu-app.contentstack.com

Add every region your team’s Studio project uses to the CSP frame-ancestors list.

Preview + production both need the allow-list. Authors previewing on staging need Studio to iframe the staging URL. Production canvases need production allow-listed.

Pre-Flight CI Checks

Every production Studio deploy should gate on these checks in CI. Each maps to a common failure mode we’ve seen ship: catch them at PR time, not at 3am.

1. Single React copy in the deployed bundle. Rule 1 from framework recipes.

npm ls react
# expect exactly one line under the root package

Fail the build if react appears twice: that’s the “Invalid hook call” bomb at runtime.

2. Every required env var present at build time. Miss one and the SDK falls back to defaults that don’t match production.

: "${CONTENTSTACK_API_KEY:?}" \
  "${CONTENTSTACK_DELIVERY_TOKEN:?}" \
  "${CONTENTSTACK_PREVIEW_TOKEN:?}" \
  "${CONTENTSTACK_ENVIRONMENT:?}" \
  "${CONTENTSTACK_REGION:?}"

(Bash ${VAR:?} fails fast with the var name if it’s empty.)

3. Framework-recipe curl assertions pass on a boot-and-fetch. After building + starting the app in CI, run:

URL=http://localhost:3000/some-known-path curl -s "$URL" > /tmp/out.html
grep -q '<main' /tmp/out.html || { echo "empty body"; exit 1; }
grep -q 'data-studio-ssr' /tmp/out.html || { echo "styles missing"; exit 1; }
grep -q '<title>' /tmp/out.html || { echo "metadata missing"; exit 1; }

Details + rationale: Framework recipes: Verification.

4. CSP frame-ancestors includes every regional Studio host you use.

CSP=$(curl -sI https://your-preview-host.example.com/ | grep -i content-security-policy)
echo "$CSP" | grep -q 'app.contentstack.com' || { echo "CSP missing Studio host"; exit 1; }

5. No accidental commit of .env files. Simple grep in CI:

git ls-files | grep -E '^\.env$|^\.env\.[^.]+$' && { echo "env file committed"; exit 1; } || true

6. Studio SDK version pinned or in a tight range. Major-version drift is the top cause of “worked yesterday, broken today.” Prefer ~1.5.0 or exact-pin over ^1.5.0 on the SDK packages.

Wire these six into your CI job. Every one has bitten a real deployment. Every one is a 30-second check.

What’s Still Unsolved

  • Edge-runtime SSR for Studio: a major open item. Until the SDK shrinks, Node-only is the answer.
  • CDN tag-based purge across multiple compositions: most CDNs purge by URL or by surrogate-key. If you have hundreds of routes and one section’s composition changes affecting all of them, surgical purge is non-trivial. Workaround: shorter revalidate for affected routes.
  • Multi-region deploys with composition replication: Contentstack’s CDA handles read-replicas across regions, but composition publishes propagate at content-stack speed (seconds). For sub-second cross-region consistency, you’re back to short revalidate.

See Also