POST-GA Walkthrough — Completed
Overview
All 9/9 main tasks in the POST-GA phase have been delivered, covering the backend, database schema, AI Copilot, frontend UI, and automated testing.
Task 1: [AI] A real LLM provider ✅
Replaced the mock analyzeIntent() with a real LLM provider abstraction.
New files
apps/cms/src/services/llm-provider.ts— the provider abstraction, with 4 implementations:OpenAIProvider— calls the OpenAI Chat Completions API (gpt-4o-mini by default), tool-calling formatAnthropicProvider— calls the Anthropic Messages API (claude-sonnet-4-20250514 by default), native tool_useWorkersAIProvider— calls the Cloudflare Workers AI REST API, OpenAI-compatible tool formatEchoProvider— the backward-compatible keyword matcher (legacy behaviour), used when no API key is present- A
createLLMProvider(env)factory — picks the provider from theLLM_PROVIDERenv var - A system prompt combining CMS context with safety rules
Modified files
apps/cms/src/env.ts— added 7 env vars:LLM_PROVIDER,OPENAI_API_KEY,ANTHROPIC_API_KEY,WORKERS_AI_ACCOUNT_ID,WORKERS_AI_API_TOKEN,WORKERS_AI_GATEWAYapps/cms/src/routes/ai.ts— fully rewritten:analyzeIntent()replaced withllmProvider.chat()→ parse tool_calls → execute via the harness
Task 2: [AI] Context memory ✅
Added conversation history to the AI Copilot — messages now persist across sessions.
New schema
packages/database/src/schema/ai.ts— 2 new tables:ai_conversations(id, siteId, userId, title, createdAt, updatedAt)ai_messages(id, conversationId, role, content, toolCalls jsonb, metadata jsonb, createdAt)
New routes
apps/cms/src/routes/ai.ts— added:POST /chatnow accepts an optionalconversationId, creates a conversation when absent, persists messages, and loads the last 20 messages as LLM contextGET /conversations— list conversations (sorted by updatedAt desc)GET /conversations/:id/messages— get the messages in a conversationDELETE /conversations/:id— delete a conversation and cascade its messages
Frontend
apps/studio/src/components/ai-assistant.tsx— added:- A
conversationIdstate, passed in the request body - A dropdown to pick a previous conversation (loaded from
GET /conversations) - A "New conversation" button
- A delete-conversation button
- Auto-loading messages when switching conversation
- A
Task 3: [AI] RAG skills ✅
Added the aiSuggestField and aiContentAssist skills plus an embedding service.
New files
apps/cms/src/services/embedding-service.ts— a provider abstraction:OpenAIEmbeddingProvider— text-embedding-3-small (1536 dims)WorkersAIEmbeddingProvider— @cf/baai/bge-base-en-v1.5 (768 dims)EchoEmbeddingProvider— deterministic pseudo-embeddings for testing- A
cosineSimilarity()helper - A
createEmbeddingProvider(env)factory
New schema
packages/database/src/schema/ai.ts— theai_embeddingstable:- JSONB vector storage (note: migrate to pgvector when ANN search is needed)
- Indexed by siteId + collection, itemId
Skills registry
packages/ai-skills/src/skills.ts— 2 new skill definitions:aiSuggestField— suggest a field from a description plus the existing schemaaiContentAssist— generate/edit field content using RAG context
Harness wiring
apps/cms/src/services/ai-harness.ts— added:- A
generateFieldSuggestions()helper — pattern matching across 16 field types - An
aiSuggestFieldhandler — wired to SchemaService - An
aiContentAssisthandler — a placeholder for the full LLM integration - Widened the service type:
'schema' | 'items' | 'ai'
- A
Task 4: [BE] Materialized collection writes ✅
Upgraded from physical DDL to automatic synchronisation on every item change.
New files
apps/cms/src/services/materialize-service.ts— the DDL operations:createPhysicalTable()— initialise themat_{target}table.refreshPhysicalTable()— synchronise viaTRUNCATE + INSERT INTO ... SELECTfrom items.dropPhysicalTable()— drop the physical table when the configuration is removed.installAutoRefreshTrigger()— a PG trigger that automates the refresh.queryPhysicalTable()— read straight from the physical table for the Delivery API.
Modified files
apps/cms/src/routes/materialize.ts— mount the endpoints and wire table creation in.apps/cms/src/services/item-service.ts— added the auto-refresh mechanism:- After every write (
create,patch,softDelete) the system checks and automatically triggers/enqueues a refresh for the matching materialized collections (completing Sub-task A).
- After every write (
Task 5: [BE] Multi-region DO sharding ✅
New files
apps/cms/src/realtime/shard-config.ts— region mapping:- 60+ IATA colo codes → 5 regions (wnam, enam, weur, eeur, apac)
getRegionFromColo(),getShardKey(),getLocationHint(),isShardingSupported()- Docker fallback: an
undefinedlocation hint → a single instance
Modified routes
apps/cms/src/routes/realtime.ts— added:- Region detection from the
cf.colorequest property - Shard key format
{siteId}:{region}for DO naming locationHintpassed tositeRoom.get(id, { locationHint })- The region param forwarded to the DO via query string
- Region detection from the
Task 6: [FE] Marketplace browser UI ✅ (Sub-task B)
Built the extension search-and-install page inside Studio.
New files
apps/studio/src/modules/settings/marketplace-page.tsx— the Marketplace UI:- A card grid showing each extension's details, publisher, version and type.
- Keyword search plus live filtering by category (Module, Layout, Display, …).
- A detail modal showing the description, capabilities, and the list of requested permissions.
- Display of the publisher's cryptographic signature (Verified Signature).
- An Install button calling the install API, switching to an "Installed" badge on success.
Modified files
apps/studio/src/router.tsx— lazy-loaded routing for/settings/marketplace.apps/studio/src/components/app-shell.tsx— added the Marketplace link to the Settings menu.
Task 7: [FE] Flows visual editor ✅ (Sub-task C)
Built a drag-and-drop automation graph editor.
Library added
@xyflow/react(React Flow) for graph rendering and connection events.
New files
apps/studio/src/modules/automation/flow-editor.tsx— the visual graph canvas:- A left palette of operation blocks (Condition, Transform, HTTP, Mail, Log, Sleep, Database CRUD).
- A drag-and-drop canvas where handles connect into success (Next) or failure (OnError) edges.
- A right-hand Config Panel that changes with the selected node type.
- Graph persistence as JSON via
PATCH /api/v1/flows/:id, plus a Test Run.
apps/studio/src/modules/automation/flow-node-types.tsx— registers the custom nodes with their icons and handles.
Modified files
apps/studio/src/modules/automation/flows-page.tsx— added create and edit buttons leading into the editor.apps/studio/src/router.tsx— added the/automation/flows/$idand/automation/flows/newroutes.
Task 8: [BE] SCIM token rotation + audit ✅ (Sub-task D)
Hardened SCIM security: authentication via database-hashed tokens, with rotation and activity logging.
New schema
packages/database/src/schema/access.ts— thescim_tokenstable:id,siteId,tokenHash(SHA-256),label,createdBy,expiresAt,revokedAt,lastUsedAt,createdAt.- Indexed on
siteId + tokenHash.
New files
apps/cms/src/routes/scim-admin.ts— the token management surface (requires a Logto JWT):POST /— mint a new token (returns the plaintext once only).GET /— list the metadata of existing tokens.DELETE /:id— revoke a token.POST /:id/rotate— mint a new token and expire the old one after a 24-hour grace period.
Modified files
apps/cms/src/routes/scim.ts— changed the auth middleware:- Hash the bearer token with SHA-256 before looking it up in
scim_tokens. - Take
siteIdstraight from the token, so anX-Lumi-Siteheader cannot spoof the tenant. - Write SCIM activity (
scim.user.create,scim.group.patch, …) to theactivitytable.
- Hash the bearer token with SHA-256 before looking it up in
apps/cms/src/index.ts— mount the/scim-tokensrouter.
Task 9: [OPS] Multi-tenant isolation testing (k6) ✅ (Sub-task E)
Measure data isolation and probe for tenant-leak holes.
New files
apps/cms/k6/helpers/setup-tenants.js— provisions two independent test sites with tokens/collections and tears them down automatically.apps/cms/k6/cross-site-leak.js— a k6 script running 5 isolation scenarios:- Scenario 1 — Data: write on site A, check whether site B can see it (expected: no).
- Scenario 2 — Auth: use site A's token against site B's API (expected: rejected, 403).
- Scenario 3 — SCIM: use site A's SCIM token with a spoofed site-B header to create a user (expected: forced back to site A).
- Scenario 4 — Search: a search on site A returns none of site B's results.
- Scenario 5 — Realtime: a WebSocket subscriber on site A receives none of site B's change events.
Modified files
apps/cms/src/routes/admin.ts— added APIs letting an admin create and delete a site, to support the k6 script.
🛠️ Test & build results ✅ (Sub-task F)
1. TypeScript typecheck
- Fixed every cast issue around Durable Objects, destructuring returned rows, missing import dependencies, and React Flow's
Node<any>generic. pnpm typecheckpasses across all 11 packages in the monorepo.
2. Unit/integration/property testing
- The full test suite (
pnpm test) passes, 62/62 tests green, with no regressions.