TL;DR
Tuturuuu CMS now lives in the dedicatedapps/cms satellite app.
New CMS-backed sites should use the global CMS site v1 contract. Add
site-specific structure through template and collection metadata instead of a
new adapter unless the integration has genuinely different authentication or
runtime behavior.
apps/cmsowns the CMS authoring UI, workspace picker, overview/content/preview/members routes, and the root/{wsId}/projectslinking console (reached as/internal/projectsfor the root workspace).apps/webremains the only backend/API owner for CMS and external-project traffic.apps/cmsrewrites/api/*toapps/web, so proxy behavior and bot protection stay centralized.
Product Vocabulary
The visible CMS experience is a site/content product, not an implementation console. Bound-workspace UI should say site, site template, connection, content, section, URL path, custom details, media, publishing, preview, people, invitations, and team access. Avoid implementation terms in normal UI copy, including external project, canonical, adapter, binding, slug, schema, metadata, profile data, payload, and JSON. If operators need exact implementation values, show them only in collapsed Developer details panels or internal root-console advanced controls. Keep code, API routes, database objects, migration names, and integration docs on the existingexternal-project* contracts.
Route Model
The route tree below mirrorsapps/cms/src/app/[locale]/(dashboard)/[wsId].
Workspace navigation is defined in
apps/cms/src/app/[locale]/(dashboard)/[wsId]/navigation.tsx. Every route is
workspace-scoped under /{wsId}; the root workspace uses the internal slug
(it resolves to ROOT_WORKSPACE_ID via normalizeWorkspaceId).
Root workspace
/{wsId}/projects(reached as/internal/projects): internal site-template registry, workspace-indexed connection list, and connection audit history. The internal workspace is not a bindable content workspace, so its overview, content, and/settingsroutes redirect into/{wsId}/projects.
Bound workspaces
/{wsId}: overview / continue-editing home./{wsId}/content: primary content operations surface (search, status filtering, publishing, reusable content)./{wsId}/content/collections/{collectionId}: collection (section) detail./{wsId}/content/entries/{entryId}: entry (content) detail./{wsId}/pages: page-builder surface for landing-style sections./{wsId}/media: media library./{wsId}/products: product catalog content./{wsId}/storefront: storefront configuration./{wsId}/taxonomy: taxonomy (terms, categories, tags)./{wsId}/games: game content surface, shown only when CMS Games is enabled for the workspace (getCmsGamesEnabled)./{wsId}/preview: site preview./{wsId}/members: workspace member search, invite, removal, role assignment, and role/default-permission management./{wsId}/settings: redirect-only route. It sends the internal workspace to/{wsId}/projectsand every other workspace to/{wsId}/members; CMS has no standalone settings screen.
Root Linking Console
- Treat
/{wsId}/projects(the root workspace’s/internal/projects) as a workspace-first control room. - The main left panel is the searchable workspace list.
- Each row shows the current connection state, site type, site template, and most recent change signal.
- The selected workspace panel owns connect and disconnect actions.
- The site-template registry is secondary and stays on the same page for create/edit flows.
- Binding audits stay anchored to the selected workspace instead of living as a detached global feed.
Workspace CMS
- Use
/{wsId}as the concise starting point for continue-editing tasks, launch readiness, attention queues, recent activity, and connected-site context. - Use
/{wsId}/contentfor broader content operations: search, status filtering, publishing queues, collection health, and reusable content. - Use
/{wsId}/pagesfor focused landing-style page edits, page readiness, and preview-first section work. - Use
/{wsId}/media,/{wsId}/products,/{wsId}/storefront, and/{wsId}/taxonomyfor the corresponding media, catalog, storefront, and taxonomy surfaces. - Use
/{wsId}/gamesfor game content when CMS Games is enabled for the workspace. - Use
/{wsId}/membersfor routine collaborator management without leaving CMS. - Keep collection configuration on
/{wsId}/content/collections/{collectionId}and entry authoring on/{wsId}/content/entries/{entryId}. - Use
/{wsId}/previewas the delivered-content surface backed by the existing preview delivery route. - CMS has no standalone settings screen;
/{wsId}/settingsis a redirect-only route to/{wsId}/members(or/{wsId}/projectson the internal workspace).
Member Management In CMS
- Workspace member search, invite, remove, and role-assignment controls now have a dedicated
/{wsId}/memberssidebar route. - The members surface also absorbs the essentials of the workspace roles page so CMS operators can manage named roles and default permissions without leaving CMS.
- This keeps standalone CMS usable without forcing operators back into the main platform app for routine collaborator changes.
- CMS members data must load through external-project-aware
apps/webAPI routes and@tuturuuu/internal-apihelpers. Reads should authorize by CMS workspace access so app-session users do not hit workspace settings-route 403s; writes still respect member and role management permissions. - Use CMS copy in this area: team access, people, invites, access levels, member defaults, guest defaults, and Advanced access.
Linked-Site Email Security
Outbound email requested throughPOST /api/v1/workspaces/{wsId}/external-projects/emails is denied unless the
workspace binding has an explicit settings.outboundEmail grant. A valid app
secret and matching workspace binding are necessary but no longer sufficient.
Workspace security managers configure the grant on the main platform workspace
settings page:
enabledmust betrue.allowedRecipientDomainsmust contain every exact recipient domain. A grant forexample.comdoes not includesub.example.com, and wildcards are rejected.useRootWorkspaceCredentialsallows the workspace to borrow the managed platform sender while retaining workspace rate limits, blacklist checks, and audit attribution. Only a root user withmanage_external_projectsmay change this flag.
TURNSTILE_SECRET_KEY_RICHFIELD when present,
falling back to the platform-wide TURNSTILE_SECRET_KEY only for backwards
compatibility. This lets operators add or rotate Richfield’s widget without
changing the Turnstile secret used by other platform surfaces.
Validate focused behavior from the repository root:
Implementation Notes
- Keep CMS as a pure satellite frontend. Do not move API ownership into
apps/cms. - Continue adding or extending client helpers in
packages/internal-apifor CMS-facing API calls. - Keep preview sourced from the existing delivery route instead of introducing a second preview payload contract.
- Backend/database naming remains
external-project*; the product surface is what changed. - Protect CMS-owned product copy with the CMS copy hygiene test, and add explicit admin/developer-detail exceptions only when a staff workflow truly needs an implementation value.
- Translation validation now treats
nullleaves as invalid so missing shared keys fail checks instead of silently passing parity. - External app admin dashboards should call
POST /api/v1/workspaces/{wsId}/external-projects/setupwith their sync manifest before diff/apply. The route uses the existing app-coordination token and requires manage access. If the workspace is already bound to an active canonical project for the same adapter, setup preserves that binding while refreshing the project schema and field definitions; otherwise it creates the canonical{adapter}-mainproject if needed and binds the configured workspace without requiring production Supabase keys in the external app. - External apps can mark manifest assets with
metadata.publicPath,metadata.localAssetPath, or a relativesourceUrlsuch as/media/hero.png. - For a registered branded app using the
customadapter, make its app ID equal to the linked canonical template key (for example,rennu). Token exchange and subsequent content requests match that exact key, not the sharedcustomadapter name. Built-in integrations still match their adapter. The registered app workspace allowlist, enabled binding, user membership, and scoped permissions are all still required. A mismatch producesApp is not linked to this workspace; do not fix it by sharing another site’s app credentials. - Before diff/apply, run the public-folder linker so the manifest stores deterministic Drive paths under
external-projects/{adapter}/{collectionSlug}/{entrySlug}/{filename}. - Diff/apply routes are workspace-scoped. They may persist manifest schema into the bound workspace’s collections and field definitions, but they must not update
canonical_external_projectsor other root/canonical registry state. - Before apply, upload those files through the external-project asset app-server upload route. Abort the apply if a local public asset is missing so CMS never receives storage paths for objects that were not uploaded.
- Asset
storagePath/storage_pathvalues must stay underexternal-projects/. Do not point CMS assets at Drive, finance, or other workspace storage namespaces; delivery and admin cleanup only operate on assets that satisfy the CMS namespace invariant.
Storage analytics cache
The authenticated external-project storage analytics endpoint reads Supabase project statistics from a private Postgres snapshot keyed by workspace and adapter. It calculates file totals and highlights fromstorage.objects metadata, with a
1,000-object cap and an explicit truncated flag. Archived versions and folder
placeholders do not contribute to file totals.
Snapshots expire after five minutes. Storage inserts, updates, deletes, and moves
invalidate workspace snapshots in the same transaction, including direct uploads.
A shared workspace lock serializes refresh and invalidation so a concurrent file
change cannot leave a newly stored stale snapshot. Workspace quota is read live.
The cache is available only through a service-role RPC, after endpoint access and
rate-limit checks.
R2 uses the bounded provider scan. A missing RPC during an additive migration
rollout also uses that scan; other database failures remain visible. Production
activation requires the normal gated Supabase migration workflow. No cache
backfill is required.
The first-class Next handler preserves bodyless HEAD responses and request-time
authorization. Its future Rust port mirrors the Supabase cache response with live
quota and falls through to Next for fully configured R2 providers and unported
methods. Rust is not deployed; satellite-session and rate-limit parity remain
tracked prerequisites for a future cutover.
Database contract gate
external-project-storage-analytics-contract.yaml checks the exact PR head with
all tracked Supabase schema inputs. Its verifier requires the reviewed migration
body under the root-reserved forward filename, runs all 15 strict SQL assertions,
generates actual public/private/storage types, and closes its owned disposable
stack. A skipped wrapper or local source test does not establish this DB gate.
After the 15-assertion sequential fixture, the gate uses distinct bounded psql
sessions for first-cache-row refresh versus insert/update. It observes the
contender waiting on the actual workspace advisory lock (namespace 73007), then
requires invalidation and exact recalculation after both sessions complete.
Authored Node wiring tests do not establish that race; its actual hosted receipt
is required before merge. Linux’s shared rootful Docker daemon is not a
qualified contained executor; do not run this verifier there to bypass that gate.
Team Guidance
- Link into the CMS-native route map directly.
- Prefer the root
/internal/projectsconsole (the root workspace’s/{wsId}/projects) for site-template and workspace-link operations. - Keep bound-workspace collaborator management on the dedicated
/{wsId}/membersroute, and do not surface the root projects console inside non-root workspace sidebars. - Update both
apps/cms/messages/en.jsonandapps/cms/messages/vi.json, then runbun i18n:sortwhenever CMS copy changes.