Site Studio is a Cloudflare-native AI website builder for academics and researchers. Users describe a site or change in natural language, the agent works directly in a sandboxed Dynamic Worker, and the app updates preview and published output from R2-backed project storage.
- Frontend: SvelteKit 5, Tailwind CSS v4, CodeMirror 6
- App runtime: Cloudflare Workers + Hono
- Agent runtime: Cloudflare Agents SDK +
@cloudflare/ai-chat - Sandbox execution: Dynamic Worker Loader +
@cloudflare/codemode - Model access: CAIL model proxy → Cloudflare Workers AI (
@cf/zai-org/glm-5.2default) - Storage: Cloudflare R2 + KV + Durable Objects
site-studio/
├── packages/
│ ├── app/ # Cloudflare Worker app + AI chat agent
│ │ ├── src/
│ │ │ ├── agents/ # SiteBuilderAgent
│ │ │ ├── routes/ # Projects, files, preview, publish, agents
│ │ │ ├── storage/ # R2-backed project storage
│ │ │ └── lib/ # Auth, paths, templates, HTTP helpers
│ │ └── wrangler.jsonc
│ ├── frontend/ # SvelteKit dashboard/editor
│ ├── serving-core/ # Shared published/preview HTTP behavior
│ └── worker/ # Legacy-domain published-site Worker
- Project CRUD against existing R2 data
- Legacy anonymous-session recovery for returning users
- Live preview at
/preview/:projectId/* - Public published sites at
/u/:handle/:slug/*(legacy/sites/:userId/:slug/*serves or 301s) - Cloudflare Agents chat transport with persisted messages
- Dynamic Worker sandbox execution for multi-step project edits
- Clarification questions when a request is materially ambiguous
bun install
bun run devThis starts:
- App: http://localhost:8792
- Frontend: http://localhost:5173
All packages share one Bun workspace and lockfile. To use different local ports, run the package commands directly and pass Vite/Wrangler --port flags.
Local Worker secrets live in:
Local identity verification material is ops-managed (see cail-gateway docs/INTEGRATION.md):
CAIL_IDENTITY_JWKS={"keys":[...]}Site Studio accepts identity only in X-CAIL-Identity-JWT and verifies it as
RS256 against CAIL_IDENTITY_JWKS, with audience cail:site-studio and the
canonical/staging issuer allowlist. A presented token rejects when the JWKS is
missing or malformed or verification fails. CAIL_REQUIRE_IDENTITY=true
rejects requests that do not carry a verified identity.
Identity enforcement also requires a bounded legacy-account import window:
CAIL_SSO_SWITCHED_AT=2026-07-13T14:00:00Z
CAIL_ACCOUNT_IMPORT_UNTIL=2026-07-27T14:00:00ZBoth values must be ISO 8601 instants with an explicit UTC offset. The import
window is half-open (CAIL_SSO_SWITCHED_AT <= now < CAIL_ACCOUNT_IMPORT_UNTIL),
the end must not precede the start, and the duration cannot exceed 30 days.
When CAIL_REQUIRE_IDENTITY=true, missing or invalid values fail protected
requests with 500 invalid_account_import_configuration. While identity is
optional, bad or absent window configuration disables legacy import.
Site Studio holds no provider API keys — model calls go through the CAIL model proxy, which attaches credentials itself.
The Worker also reads these vars from packages/app/wrangler.jsonc:
APP_PUBLIC_DOMAINPUBLISHED_BASE_URLCAIL_API_BASECAIL_MODEL(Workers AI@cf/...id only — CAIL policy is Cloudflare models only)CAIL_REQUIRE_IDENTITYCAIL_SSO_SWITCHED_ATCAIL_ACCOUNT_IMPORT_UNTIL
For production, configure secrets with Wrangler / Cloudflare, not by committing env files.
The app stores CSRF tokens under the csrf/ prefix in the existing private R2
bucket. Match their 30-day session lifetime with a bucket lifecycle rule:
bunx wrangler r2 bucket lifecycle add site-studio delete-expired-csrf csrf/ --expire-days 30 --forceGET /api/healthGET /api/projectsPOST /api/projectsGET /api/projects/:id/filesPOST /api/projects/:id/filePOST /api/projects/:id/uploadGET /preview/:id/*POST /api/projects/:id/publish(409handle_requireduntil the user claims a handle)POST /api/projects/:id/unpublishGET /api/handle,GET /api/handle/check?handle=…,POST /api/handleALL /api/agents/site-builder/:projectIdGET /u/:handle/:slug/*(canonical published sites)GET /sites/:userId/:slug/*(legacy: 301s to/u/…when the owner has a handle, else serves)
- The new app is a static-file site builder. Runtime build tools are out of scope.
- The normal chat path is execute-first, not approve-first.
- The built-in gallery includes blank, CV, course, portfolio, publication, event, photo, resource, timeline, and data-visualization templates.
- Canonical published URLs are
/u/:handle/:slug/, keyed by a user-chosen handle so the owner id never appears in a public URL. Old published sites remain readable permanently from the same R2 bucket via the legacy/sites/:userId/:slug/*shape, which 301s to the/u/…equivalent once the owner has a handle and otherwise serves content directly. This is a permanent compatibility exception: temporary account-import cleanup must retain/sitesrouting, direct serving, redirects, and.migrated.jsonforwarding-pointer resolution.
See docs/legacy-account-import-removal.md
for the temporary import telemetry and deletion follow-up.
MIT