WorldScript Studio is a static SPA (Vite β dist/). API keys stay client-side in IndexedDB; do not put Gemini/OpenAI secrets in host environment variables for inference.
| Target | Build command | Vite base |
Typical URL |
|---|---|---|---|
| GitHub Pages (default CI) | pnpm run build |
/WorldScript-Studio/ |
https://<user>.github.io/WorldScript-Studio/ |
| Vercel | pnpm run build:edge |
/ |
https://<project>.vercel.app/ |
| Cloudflare Pages | pnpm run build:edge |
/ |
https://<project>.pages.dev/ |
Edge builds run scripts/build-edge.mjs: sets DEPLOY_TARGET=edge, patches public/manifest.json, offline.html, 404.html, then vite build.
- Repo Settings β Pages β Build and deployment: source GitHub Actions.
- Push to
main; workflow.github/workflows/ci.ymlruns build + e2e, then deploy uploadsdist/(built withpnpm run build, subpath base). - Environment github-pages must exist (created on first successful deploy).
# Pages enabled?
gh api repos/:owner/:repo/pages 2>/dev/null || echo "Pages API: not configured or billing/plan blocked"
# Latest deploy workflow
gh run list --workflow="CI / CD" --limit 3
gh run view <run-id> --log-failedBilling / availability: If the deploy job is skipped or fails with Resource not accessible, check Settings β Billing (Actions minutes, Pages for private repos). Public forks get Pages on the fork ownerβs plan. The app remains buildable locally with pnpm run build && pnpm run preview.
- Import the Git repository; Root Directory = repo root.
- Framework: Other (or Vite). Settings are overridden by
vercel.json:- Install:
node scripts/dependency-state.mjs reconcile - Build:
pnpm run build:edge - Output:
dist
- Install:
- Node.js β₯ 22 (Project Settings β General).
- Environment variables (optional):
DEPLOY_TARGET=edgeβ redundant if usingbuild:edge; do not add AI API keys for end users. - SPA routing:
rewritesinvercel.jsonβindex.html. - Preview deployments: enabled per branch/PR by default.
After each successful merge to main, the exact resulting-main CI and CodeQL runs must be
successful before the authenticated Preview retention procedure runs. Follow
VERCEL-PREVIEW-RETENTION-POLICY.md for
the dry-run, three-Preview open-PR retention, protected Production aliases,
closed-PR/orphan classification, deletion, redaction, and post-cleanup proof.
The GitHub workflow prune-deployments.yml
prunes GitHub Deployment records only; it does not delete Vercel deployment
artifacts.
- Cloudflare Dashboard β Workers & Pages β Pages β Connect Git.
- Build command:
node scripts/dependency-state.mjs reconcile && pnpm run build:edge - Build output directory:
dist - Deploy command: leave completely empty β Cloudflare uploads
distafter a successful build. - Do not use
npx wrangler deploy(Workers) norwrangler pages deployin the deploy step β redundant and often fails on API token scope in the build container. - If the UI forces a deploy command, use:
pnpm run deploy:cloudflareβ it exits 0 on Cloudflare (CF_PAGES=1) without calling Wrangler. - Remove
CLOUDFLARE_API_TOKENfrom Pages build environment variables unless you have a dedicated manual deploy workflow; it is not needed for Git-based Pages. - Environment variables (build):
NODE_VERSION=22,PNPM_VERSION=11.22.0(or Corepack using the exactpackageManagerpin). - Root: repository root; Package manager: pnpm.
Status: Optional GitHub workflow
.github/workflows/deploy-cloudflare-pages.ymlis paused (if: false). Prefer dashboard-only Pages deploy.
Static extras in public/:
_redirectsβ SPA fallback/* β /index.html_headersβ cache + security headers
Local preview with Wrangler (optional):
pnpm run build:edge
pnpm exec wrangler pages dev distConfig: wrangler.toml.
Workflow .github/workflows/deploy-cloudflare-pages.yml runs only when secrets are set:
| Secret | Purpose |
|---|---|
CLOUDFLARE_API_TOKEN |
Pages deploy token |
CLOUDFLARE_ACCOUNT_ID |
Account ID from dashboard |
Without secrets the job is skipped (fork-safe).
Workflow .github/workflows/deploy-cloudflare-pages.yml can run on v* tags when CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are set. Prefer dashboard Git integration for day-to-day deploys; use tags for release snapshots.
pnpm run build:edge
pnpm run deploy:cloudflare # exits 0 on CF_PAGES=1; otherwise wrangler pages deploy dist# GitHub Pages-shaped build
pnpm run build && pnpm run preview
# Vercel / Cloudflare-shaped build (root base)
pnpm run build:edge && pnpm exec vite preview --base /HTTP response headers are not portable across targets β each host has its own config file, and one target can't set headers at all. Voice (hooks/useMicLevel.ts, hooks/useSpeechRecognition.ts) depends on Permissions-Policy: microphone=(self); an empty microphone=() allowlist silently breaks it even for same-origin calls.
| Host | Config file | Permissions-Policy | Content-Security-Policy |
|---|---|---|---|
| GitHub Pages (canonical upstream) | (none β platform has no header-injection mechanism) | β not settable at all (no meta-tag equivalent exists) | index.html only β this is the sole enforcement point on this host |
| Vercel | vercel.json headers[] |
microphone=(self) |
header set, mirrors the index.html meta CSP |
| Cloudflare Pages | public/_headers |
microphone=(self) |
header set, mirrors the index.html meta CSP |
Docker / nginx (.github/workflows/docker.yml image) |
nginx.conf |
microphone=(self) |
header set, mirrors the index.html meta CSP |
When both a header CSP and the index.html meta CSP are present, the browser enforces both simultaneously β a resource load must satisfy every active policy, so if the two diverge on an overlapping directive, the more restrictive result applies (not "the header wins and the meta tag is ignored"). The exception is frame-ancestors (and sandbox/report-uri): the CSP spec explicitly disallows these in a <meta>-delivered policy, so they only take effect via the header β that's why adding the header is a real hardening, not just a duplicate. If the two policies ever diverge on a directive both can express, keep them identical (see ADR-0004 and the regression tests in tests/unit/csp.test.ts / tests/unit/deploymentHeaders.test.ts) so the effective policy stays predictable rather than silently intersecting two different allowlists. New endpoint rule: add origins to config/csp-connect-src.json, run pnpm run csp:sync, and let pnpm run csp:verify prove all surfaces are synchronized; do not edit only index.html.
script-src on every surface (index.html, vercel.json, public/_headers, nginx.conf Γ3,
src-tauri/tauri.conf.json) carries 'self' 'wasm-unsafe-eval'. Not the broader
'unsafe-eval' keyword β that's forbidden. 'wasm-unsafe-eval' only lifts the restriction on
WebAssembly.compile/instantiate, required by WebLLM, ONNX Runtime Web, Transformers.js,
DuckDB-WASM, Whisper-STT, and Kokoro-TTS β the local-inference stack this app advertises. Before
2026-07-29 this token was absent everywhere, so WebAssembly.instantiate was blocked in every
Chromium browser on every deployment surface (F-01) β the advertised feature never worked in
production, and no test caught it because the CSP tests only checked cross-surface consistency,
never functional correctness. Full rationale, alternatives considered, and the 3-layer test
architecture that now guards this: docs/adr/0013-csp-wasm-and-blob-frames.md.
frame-src 'self' blob: was added alongside it (previously absent everywhere, falling back to
default-src 'self', which blocks blob: iframes) β required by the Binder-PDF-preview and
ManuscriptResearchSplit iframes, which render IndexedDB-backed assets via URL.createObjectURL.
Does this weaken the plugin sandbox? No β workers/plugin.worker.ts sets
self.WebAssembly = undefined before running untrusted plugin code and restores it afterward (both
success and error paths), a JS-level guard independent of CSP. See ADR-0013 for the full analysis.
- No server-side storage of manuscripts or API keys.
- CSP origins are declared in
config/csp-connect-src.jsonand generated intoindex.html, headers, and Tauritauri.conf.json; runtime preflight rejects unlisted configured endpoints before fetch. - Service worker: AI hosts are network-only (
public/sw.js); WASM/ONNX not precached.
Pricing / SLAs: Vendor pricing changes frequently β verify current Pages, Vercel, and Cloudflare plans before production commitments.