Studio Docs

When to Use

Set up a locally-trusted HTTPS cert (via mkcert) for the canvas-app. NOT required for most local-dev: browsers treat localhost as trusted and allow Studio to iframe http://localhost:<port>.

Use ONLY when HTTPS-on-localhost is actually required: service workers, secure cookies, web-share/WebAuthn, strict corporate policies that block mixed content even for localhost, or production-parity smoke tests. Phrases: “I need https locally”, “mixed-content block on localhost”. Do NOT use for production (real HTTPS) or tunnels (Cloudflare/ngrok already ship certs).

Auth preflight: settle the credential before the first API call. Resolve it OAuth-first per authenticate-cma: CS_OAUTH_ACCESS_TOKEN, else the Contentstack MCP’s stored session. Never ask the user for a session authtoken. If nothing resolves, or a refresh fails with 400 invalid_refresh_token, hand them ! CONTENTSTACK_REGION=<code> npx @contentstack/mcp --auth (it needs a TTY and a browser, so it cannot be run for them) and wait. 403 error_code 316 is a valid credential aimed at another org: fix the org or the api_key, do not re-authenticate.

Set up Locally-Trusted HTTPS for the Canvas-App

Context: When This is Actually Needed

Studio is served from https://app.contentstack.com/#!/studio. To preview your canvas-app, Studio loads it inside an iframe.

For plain http://localhost:<port> the iframe just works in modern browsers: Chrome, Firefox, and Safari all treat localhost and 127.0.0.1 as “potentially trustworthy” origins per the W3C Secure Contexts spec, so mixed-content blocking does NOT apply. You do not need mkcert for a basic Studio canvas iframe.

HTTPS-on-localhost via mkcert is only needed when something else in your app requires HTTPS even at dev time:

  • Service workers: require secure context. An HTTP localhost dev server can’t register one
  • Secure cookies / SameSite=None+Secure: set only over HTTPS, even on localhost
  • WebAuthn / WebShare / Payment Request: gated on secure context
  • Strict corporate browser policy: some MDM policies override the localhost exemption
  • Production-parity local testing: to catch HTTPS-only bugs before deploy
  • Tunnel URLs (cloudflared / ngrok / lt): NOT localhost, so the secure-context exemption doesn’t apply. The tunnel already gives you a trusted HTTPS URL, so mkcert wouldn’t help anyway

If none of the above apply, skip this skill and use plain http://localhost:<port> as your Canvas URL origin / env base URL. Add mkcert later if a real need emerges.

Task

1. Install mkcert (one-time per machine)

Detect the OS and use the right command:

# macOS
brew install mkcert nss
# nss is needed if you use Firefox; harmless otherwise

# Linux (Debian/Ubuntu)
sudo apt install libnss3-tools
# Then grab mkcert: https://github.com/FiloSottile/mkcert/releases (or via Homebrew on Linux)

# Windows (PowerShell, as admin)
choco install mkcert
# or: scoop install mkcert

Verify:

mkcert -version

2. Install the local CA into the system trust store

mkcert -install

This adds mkcert’s root CA to:

  • macOS: Keychain (System trust)
  • Linux: NSS-using browsers (Chromium, Firefox). Also writes to /etc/ssl/certs if update-ca-certificates is available
  • Windows: Windows Certificate Store

You may be prompted for sudo / admin once. After this, every cert mkcert generates is automatically trusted by every browser on this machine (including the Playwright-MCP-driven one, if you’re using that).

3. Generate the cert for the canvas-app

Inside {{projectRoot}}:

mkdir -p certs
mkcert -cert-file certs/localhost.pem -key-file certs/localhost-key.pem localhost 127.0.0.1 ::1

This emits a cert valid for localhost, IPv4 loopback, and IPv6 loopback. The cert is locally-trusted (signed by the mkcert root CA you installed in step 2).

4. Wire the cert into your framework’s dev server

Vite (vite.config.ts / .js):

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { readFileSync } from 'node:fs';

export default defineConfig({
  plugins: [react()],
  server: {
    https: {
      key:  readFileSync('certs/localhost-key.pem'),
      cert: readFileSync('certs/localhost.pem'),
    },
    port: {{devPort}},
    // Studio iframes from app.contentstack.com — allow it
    allowedHosts: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Content-Security-Policy': "frame-ancestors *;",
    },
  },
  preview: {
    https: {
      key:  readFileSync('certs/localhost-key.pem'),
      cert: readFileSync('certs/localhost.pem'),
    },
    port: {{devPort}},
    allowedHosts: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Content-Security-Policy': "frame-ancestors *;",
    },
  },
});

The allowedHosts: true + CSP frame-ancestors * headers are required for Studio to iframe the app. Without them Vite returns “host not allowed” and the browser blocks the frame. They’re part of this same setup. Don’t split them into a separate step.

Next.js: pass your mkcert files to next dev via the --experimental-https-key + --experimental-https-cert flags:

next dev \
  --experimental-https \
  --experimental-https-key ./certs/localhost-key.pem \
  --experimental-https-cert ./certs/localhost.pem

Bare next dev --experimental-https generates its OWN self-signed cert (not signed by mkcert’s root CA). Studio’s iframe silently blocks it. Passing the key/cert flags points Next at the trusted mkcert pair. Fallback if the flags aren’t available on your Next version: a custom server.js booting Node’s https module with the mkcert files, or a prod build (next build && next start) behind a reverse proxy that terminates HTTPS with the mkcert cert.

Remix / Astro: follow the same pattern as Vite (both expose server.https config).

5. Update .gitignore

The private key is sensitive, never commit it:

# .gitignore
certs/

Confirm with:

git check-ignore -v certs/localhost-key.pem

6. Verify

# Start the dev (or preview) server
npm run dev   # or pnpm dev / yarn dev / bun dev

# In another terminal — confirm HTTPS works
curl -I https://localhost:{{devPort}}/
# Expect: HTTP/2 200 (or similar). No `(self signed certificate)` warning.

# Open in a normal browser tab — expect green lock, no cert warning.

Then test the Studio iframe:

  1. Wherever Studio asks for your canvas-app’s origin (the env-level setting that holds the canvas-app’s host:port, set per Contentstack environment), make sure it’s now https://localhost:{{devPort}}. The Studio Project’s Canvas URL field stays /canvas. Canvas URL is a separate setting and it’s just a path, never an origin.
  2. Open any composition. The canvas iframe should load cleanly (no blank frame, no console error).

Inputs Needed From the User

  1. projectRoot: the canvas-app directory.
  2. devPort: the port your dev/preview server listens on. Vite default is 5173. Next is 3000.

Acceptance

  • mkcert -version returns a version string.
  • mkcert -install completed (root CA in system trust store).
  • certs/localhost.pem + certs/localhost-key.pem exist in the project root.
  • Framework config has server.https + preview.https pointing at the cert pair, AND allowedHosts: true + frame-ancestors * header set.
  • .gitignore contains certs/.
  • curl -I https://localhost:<port>/ returns a 200 with no untrusted-cert warning.
  • Browser loads https://localhost:<port> with a green lock.
  • Studio canvas (with the matching Canvas URL configured) shows the app inside its iframe, not a blank frame.

Common Pitfalls

PitfallWhy it bitesFix
Skipping mkcert -installCert is generated but not trusted by the browser, silent iframe blockRun mkcert -install first. Re-open browsers after (Firefox especially caches trust).
Bare next dev --experimental-https (no key/cert flags)Next generates its own self-signed cert not signed by mkcert’s root CA, so the Studio iframe blocks silentlyPass the mkcert pair: next dev --experimental-https --experimental-https-key ./certs/localhost-key.pem --experimental-https-cert ./certs/localhost.pem
Cert files committed to gitPrivate key leakedAlways add certs/ to .gitignore before generating
Generating cert for a different hostname than the dev URLCert mismatch, which produces a browser warning, which blocks the iframemkcert ... localhost 127.0.0.1 ::1 covers all common localhost forms. Add other names if needed
allowedHosts set but headers missingVite serves the page but Studio can’t frame itBoth allowedHosts: true AND the frame-ancestors * CSP header must be set. They solve different layers
Cert expired after ~825 daysmkcert certs are short-lived for securityRegenerate the cert pair. The root CA stays trusted
Re-installing mkcert after a system upgrade and re-running mkcert -installOld cert pair was signed by the previous mkcert root CA which is no longer trustedRegenerate the cert pair after re-installing mkcert
Using Mac App Store ChromeSandboxed. Sometimes ignores Keychain trust for system-installed CAsUse the standalone Chrome download from google.com/chrome

See Also

  • setup-section-preview: wires Studio’s preview channel. This skill is its HTTPS prerequisite
  • install-playwright-mcp: its smoke test depends on a trusted Studio canvas, which depends on this cert
  • troubleshoot-canvas: blank-iframe symptom row maps back here