Overview
apps/cms is the dedicated home for Tuturuuu CMS. It should feel like a
consumer-grade site editing product for nontechnical editors, even though it
continues to use the existing external-project implementation contracts behind
the scenes.
- It owns the CMS UI, routing, workspace picker, and root-level
/{wsId}/projectslinking console. - It remains a satellite frontend. Protected APIs still live in
apps/web. - All CMS
/api/*traffic is forwarded toapps/web.
Product Vocabulary
Normal CMS product UI should use editor-friendly terms: site, site template, connection, content, section, URL path, custom details, media, publishing, preview, and team access. Avoid exposing implementation terms such as external project, canonical, adapter, binding, slug, schema, metadata, profile data, payload, or JSON in the primary UI. When staff genuinely need exact implementation values, place them behind a collapsed “Developer details” or internal root-console advanced panel. Code, database names, API paths, tests, and operational docs can keep the existingexternal-project* names so route and data contracts remain stable.
Routing
/{wsId}: overview/{wsId}/games: game-focused content library, visible only when CMS Games is enabled/{wsId}/landing: landing-page builder for hero/profile/page-section edits/{wsId}/library: content operations library for non-landing, non-game content/{wsId}/library/entries/{entryId}: compatibility redirect that opens the library fullscreen editor dialog/{wsId}/library/collections/{collectionId}: collection detail/{wsId}/members: workspace member and role management/{wsId}/preview: delivered-content preview/{wsId}/settings: legacy compatibility redirect to/{wsId}/members/internal/projects: internal site-template registry and workspace linking console/play/{wsId}/webgl/{assetId}: public WebGL package player. This route does not require a CMS login, but the proxied WebGL asset API still only serves the package when the owning CMS entry is published. WebGL player iframes must stay sandboxed withoutallow-same-origin.
wsId resolves to the internal/root workspace, /{wsId} and other workspace-local routes should bounce to /internal/projects instead of trying to render the bound-workspace CMS surfaces.
The previous /{wsId}/content, /{wsId}/collections/..., and /{wsId}/admin CMS routes are removed. New entry points should link directly to the CMS-native route map above.
Access Model
- Only workspaces with CMS access enabled appear in the CMS workspace picker.
- A workspace must still satisfy the existing external-project permissions to open CMS workspace routes.
- CMS Games is disabled by default for every workspace. Enable it from the CMS settings dialog, which writes
ENABLE_CMS_GAMES=trueto workspace configs and reveals the/{wsId}/gamessidebar route. - Only platform admins can open
/internal/projects. - Workspace member and role management lives on
/{wsId}/members. Read paths resolve through CMS workspace access so editors with CMS access can load people, invites, roles, member defaults, and guest defaults without requiring direct workspace settings-route access. Write paths still respect member and role management permissions.
API Ownership
apps/cmsrewrites/api/:path*toapps/web.- The rewrite target must resolve to the central web app, not the CMS app origin. Prefer
INTERNAL_WEB_API_ORIGIN,NEXT_PUBLIC_WEB_APP_URL, orWEB_APP_URLfor the central origin. If a deployment-levelNEXT_PUBLIC_APP_URLpoints atcms.tuturuuu.comor the current CMS Vercel preview URL, the CMS config ignores it and falls back tohttps://tuturuuu.comto avoid self-rewriting/api/*requests. - New CMS backend behavior should be added in
apps/web, then exposed throughpackages/internal-api. - The root linking console reads the workspace-indexed binding summary from
GET /api/v1/admin/external-project-bindings. - The root linking console may load its initial server-render payload directly with the request-scoped CMS Supabase admin path after
getCmsWorkspaceAccessproves root admin access. Avoid server-to-server Internal API fetches during that initial render, because production auth/origin forwarding failures turn/internal/projectsinto a hard RSC error page. Keep workspace-secret lookups batched; one.in('ws_id', allWorkspaceIds)query can exceed PostgREST/proxy URL limits and return400 Bad Requestin production. - Backend naming stays on
external-project*contracts even though the product surface is branded as Tuturuuu CMS. - Add or update the CMS product-copy hygiene test whenever new visible CMS strings are introduced. The test should protect the consumer vocabulary while allowing collapsed developer-detail and internal-only exceptions.
Content Model
- Collections remain the content-type container. Field definitions live in
workspace_external_project_field_definitions, while entry values continue to be stored in the existingprofile_dataandmetadataJSON payloads so delivery and sync payloads stay backward-compatible. - Field definitions support the manifest field contract:
string,markdown,number,boolean,date,datetime,json, andstring-array. Scope controls whether the value is saved underprofile_dataormetadata. - The CMS Library has a
Content modelsection with reusable templates for Profile, Blog Posts, Gallery, Shop Products, Writing Worlds, and Social Links. Applying a template creates the collection if needed, stores the collection schema, and creates any missing field definitions without deleting CMS-only fields. - Landing and Library must stay visibly distinct. Landing is a page-builder surface for landing-only sections, preview-first actions, and page readiness. Library is a content operations surface for search, status, bulk publishing, collection health, and non-landing/non-game content. Keep
landing,library, andgamescapability scopes mutually distinct in tests. - The Library command center is the consolidated authoring entry point for content, content models, workflow queues, and settings. Keep new CMS operations reachable from that surface before adding another isolated toolbar or settings panel.
- Entry detail forms render schema-aware controls for the active collection field definitions. Authors should edit those typed controls instead of hand-editing raw
profile_dataormetadataJSON for modeled fields. - Body and asset affordances should be driven by the collection schema when available. Naming heuristics for legacy Yoola collections remain as compatibility fallback only.
- External-project setup and sync import manifest
profileFieldsandmetadataFieldsinto DB field definitions. Sync snapshots are built from DB-managed field definitions, and field removals should only be applied during an explicit destructive sync.
Yashie Bridge
- Yashie is the proof case for the generic content model. Its manifest seeds profile, blog post, gallery, shop product, writing world, and social-link collections and field definitions.
- This phase does not change
../yashieruntime pages. Yashie can be seeded and managed from CMS, but the public Yashie app continues to read its local static data until a later integration pass consumes CMS delivery payloads. - Shop product entries are catalog-only in this phase. No checkout, payment, inventory, or commerce fulfillment behavior is implied by the template.
Client Architecture
- Keep route pages server-side only for auth and access gates, then hand off interactive CMS surfaces to client modules backed by TanStack Query.
- Keep
cms-studio-client.tsxfocused on query state, mutations, and routing. Library rendering should stay split across dedicated gallery, workflow, and settings modules rather than growing back into one large TSX file. - Keep CMS team access clients on
@tuturuuu/internal-apihelpers that target external-project-awareapps/webroutes. Do not call the standard workspace members or roles settings endpoints directly fromapps/cms. - Entry detail now opens as a fullscreen dialog from the library route so the CMS keeps the library cache warm instead of forcing a separate navigation for each entry.
- The Games route reuses the library client with a game-like collection scope and only exposes WebGL ZIP upload controls when
ENABLE_CMS_GAMESis explicitly enabled. - When a user creates the first Games entry, CMS should automatically create the workspace
Gamescollection and then open the new game entry instead of requiring a manual collection setup step. - Published game entries expose a public
/play/{wsId}/webgl/{assetId}link from the WebGL package card. Keep this route login-free in the CMS proxy, and keep publication checks in the centralapps/webWebGL asset route so drafts and archived entries do not leak assets. The player iframe and the served WebGL document must both use sandboxing that allows scripts but does not allow same-origin access. - CMS media uploads show per-file progress for cover images, gallery media, and WebGL ZIP packages. Cover and gallery media must use the external-project asset app-server upload route so Tuturuuu measures the actual bytes before writing storage objects, then store the returned Drive path on the CMS asset row. WebGL ZIP package uploads still upload directly to the signed storage URL returned by the self-hosted web API, then call the WebGL package finalize route so the ZIP can be extracted and mapped to a playable artifact. The WebGL asset-serving route must infer browser content types from the requested WebGL filename for known outputs such as
index.html,.js,.jpg,.wasm,.data, and compressed.gz/.brfiles, because extracted storage metadata can be too generic or incorrectly recorded astext/plain. Served WebGL files must include a CSPsandboxheader that omitsallow-same-origin; served HTML is also adjusted for the CMS player so Unity canvases fill the iframe viewport instead of keeping the default fixed-size template layout, hide the stock Unity footer chrome, resize the canvas backing buffer to the iframe viewport, inject a route-local<base>and artifact-map URL resolver soBuild/*andTemplateData/*load through the same WebGL package API route even when the Unity template creates scripts dynamically, and show in-frame resource download progress when the runtime fetches package assets. - CMS asset delivery must respect the workspace Drive provider. Supabase assets may use signed image transforms, while R2-backed assets should resolve through the workspace storage provider signed-read path and ignore Supabase-only transform parameters.
- CMS asset rows may only store workspace storage paths under
external-projects/; asset delivery and cleanup must not operate on paths from other workspace modules such as Drive or finance. - CMS does not register the shared Serwist service worker or emit the shared web app manifest. It is a Vercel-hosted satellite that proxies protected APIs to
apps/web, so PWA routes owned byapps/webmust not be advertised fromcms.tuturuuu.com. - Library taxonomy edits should stay lightweight: category/tag assignment, creation, and removal can happen directly from the library dialog, while deeper editorial changes still live in the entry sidebar taxonomy card.
Local Development
- Use
bun dev:cmsto run the CMS app with the shared packages andapps/web. - Use
bun devx:cmsorbun devrs:cmswhen you also need a reset local Supabase environment.
CI/CD
- Preview deployments run through
.github/workflows/vercel-preview-cms.yaml. - Production deployments run through
.github/workflows/vercel-production-cms.yaml. - Both workflows are gated through
ci-check.ymland thetuturuuu.tsworkflow allowlist. - The Vercel project must be configured through the
VERCEL_CMS_PROJECT_IDrepository secret.