AI-powered creative workspace for generating brand-correct marketing assets. Operators describe what they need in natural language; a single creative agent produces pixel-ready HTML (social posts, website sections, one-pagers, carousels) that looks and sounds like the configured brand.
cd canvas && npm install && npm run dev- Canvas app: http://localhost:5174/app/
- Template library: http://localhost:5174/
- Brand assets served at:
/fluid-assets/*
The agent runtime uses @anthropic-ai/claude-agent-sdk. Two auth paths are supported:
Option A — API key (CI + production):
# Add to .env at repo root (or export in your shell)
ANTHROPIC_API_KEY=sk-ant-...Option B — Claude CLI login (local dev, one-time setup):
# Install the Claude CLI if not already present, then:
claude login
# Follow the OAuth flow. Credentials are saved to ~/.claude/.credentials.json
# No API key provisioning needed; the SDK picks up the session automatically.API key takes precedence if both are set. GET /api/health returns anthropic: 'ok'
for either path.
VITE_FLUID_DAM_TOKEN— optional, for DAM picker integrationGEMINI_API_KEY— required for AI image generation (generate_image tool)FLUID_AGENT_MODEL— override the Claude model (default:claude-sonnet-4-6)FLUID_DISPATCH_TRUSTED— set totrueto bypass ask-first tool permission promptsFLUID_DAILY_COST_CAP_USD— daily spend cap for image generation (default:10.00)
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript 5.6, Zustand 5, Vite 6 |
| Backend | Vite middleware plugin (no Express) |
| Database | SQLite (better-sqlite3, WAL mode) |
| AI | Claude Agent SDK (@anthropic-ai/claude-agent-sdk) with MCP tool servers, SSE streaming |
| Testing | Vitest + Playwright |
| Styling | Plain CSS (no Tailwind in the app itself) |
canvas/ # The app (React + Vite + SQLite)
src/
App.tsx # Main app shell + navigation state machine
components/ # React components (CampaignDashboard, ChatSidebar, etc.)
store/
campaign.ts # Nav state machine + data cache + race-condition guard
chat.ts # Chat sessions + SSE streaming + tool-call UI
editor.ts # Slot editing UI state
server/
watcher.ts # Vite plugin: all API routes + file watcher + HMR
agent.ts # Creative agent: tool-use loop + SSE streaming + cancellation
agent-tools.ts # Agent tool implementations
agent-system-prompt.ts # Tier 1 rules + UI context layering
brand-brief.ts # Assembles Tier 2 Brand Brief from DB
chat-routes.ts # /api/chats/* handlers
db-api.ts # Campaign/Creation/Slide/Iteration CRUD
brand-seeder.ts # Seeds DB from voice-guide/ + pattern-seeds/
render-engine.ts # Playwright render for agent self-critique
validation-hooks.ts # Post-save validation
observability.ts # Chat event logging
lib/
db.ts # SQLite singleton (WAL, FK constraints)
campaign-types.ts # TypeScript interfaces for the data model
template-configs.ts # Template metadata + slot schemas
hooks/ # useFileWatcher, useRouteSync, useAssets, …
__tests__/ # Vitest tests (real SQLite, no mocking)
mcp/
server.ts # MCP server (stdio) — push_asset tool
tools/push-asset.ts # Sole MCP tool implementation
fluid.db # SQLite database (gitignored)
vite.config.ts # Base /app/, fluidWatcherPlugin, port 5174
tools/ # CLI validation tools (Node.js CommonJS)
assets/ # Brand assets (SVGs, fonts, textures, logos, photos)
archetypes/ # Brandless structural layout patterns (filesystem, not DB)
SPEC.md # Authoritative format specification
components/ # Reusable design component patterns (pattern.html + README)
{archetype-slug}/ # One dir per archetype (index.html + schema.json + README)
templates/ # Template library (social/, gold-standard/, one-pagers/)
pattern-seeds/ # Clean markdown pattern files (seeded into DB)
patterns/ # Legacy visual pattern page (archival — DB is source of truth)
voice-guide/ # Brand voice docs (seeded into DB)
feedback/ # Agent-written usage data for learning loop
Reference/ # Archival source material (NEVER load directly)
skills/marketing/ # Marketing domain skills (distributed globally via sync.sh)
.claude/
skills/ # Claude Code skills (brand-intelligence, brand-compliance-check, …)
settings.json # Permissions + PostToolUse validation hooks
.fluid/ # Runtime output (generated HTML + working files)
campaigns/{cId}/{creationId}/{slideId}/{iterationId}.html # Canonical output paths
working/{sessionId}/ # Scratch space
.planning/ # GSD project management (roadmap, phases, state) — gitignored
.mcp.json # MCP server registration (stdio)
Campaign (organizing unit — "Q1 Social Push")
└── Creation (single deliverable — "Instagram Post", "LinkedIn Carousel")
└── Slide (one page; carousels have multiple slides)
└── Iteration (one generated version of that slide)
└── Annotation (pin or sidebar note on iteration)
Key fields on Iteration:
htmlPath— relative path to HTML file on diskgenerationStatus— AI lifecycle:pending→generating→completestatus— user review:unmarked|winner|rejected|finalaiBaseline— immutable original AI values (JSON)userState— current user-edited slot values (JSON)source—ai(generated) ortemplate(from gallery)
Path: canvas/fluid.db (override with FLUID_DB_PATH env var)
SQLite with WAL mode (concurrent reads from MCP + Vite), foreign keys ON.
Tables:
campaigns,creations,slides,iterations,annotations— campaign hierarchychats,chat_messages— agent conversation historyvoice_guide_docs— brand voice rules (seeded fromvoice-guide/*.md)brand_patterns— visual design tokens + patterns (seeded frompattern-seeds/*.md)templates— template definitions with slot schemastemplate_design_rules— per-template brand rules with weightsbrand_assets— asset registry (scanned fromassets/directory)campaign_assets,saved_assets— per-campaign and DAM-linked assetscontext_map,context_log— brand-section routing + audit trail
Seeding: On first app startup, brand-seeder.ts populates voice_guide_docs from voice-guide/*.md and brand_patterns from pattern-seeds/*.md. Also auto-imports brand config from canvas/seed-data.json if present (brand data only — no user data like campaigns/creations).
Sharing DB state between teammates:
fluid.db is gitignored — each developer has their own local copy. To share brand data, templates, and design rules:
node tools/db-export.cjs # Export DB → canvas/seed-data.json (commit to git)
node tools/db-import.cjs [--merge] # Import seed-data.json into local DBOn first startup, if canvas/seed-data.json exists and the DB is empty, it auto-imports. For existing DBs, run db-import.cjs --merge manually. If things are broken, delete canvas/fluid.db and restart — it rebuilds from seeds + seed-data.json.
Integrity: On every startup, PRAGMA foreign_key_check runs to detect and clean orphaned records automatically.
Brand data lives in the SQLite database, managed through the app UI. There are NO brand files to read from disk.
Six pages in the app manage brand data and configuration:
- Voice Guide — voice rules, messaging frameworks, identity docs
- Patterns — two sections: Foundations (colors, typography) and Rules (layout archetypes, brushstroke rendering, circle emphasis, opacity patterns, etc.). Each pattern has a weight and optional
is_coreflag. - Assets — brand assets in 4 categories: Fonts, Images, Brand Elements, Decorations. Served at
/fluid-assets/URLs. Optional description field per asset. DAM sync auto-categorizes by mime type. - Templates — reference templates with per-template design rules (scope: global-social, platform, archetype)
- Styles — CSS layer system and component groups
- Settings — context map editor for configuring which brand sections are injected per (creation_type, stage) combination. Shows token budgets and injection priorities.
Navigation: LeftNav with Create, My Creations, Assets, Templates, Patterns, Voice Guide, Settings (gear icon at bottom). Chat sidebar toggles independently.
Do NOT read from brand/ files (that directory does not exist). Do NOT duplicate brand doc content in prompts.
Brand rules carry weights 1-100:
- 81-100 = must follow (brand-critical)
- 51-80 = should follow (strong preference)
- 21-50 = recommended (flexible)
- 1-20 = nice-to-have (optional)
Archetypes are brandless structural layout patterns — content skeletons that define spatial hierarchy without any brand expression. They live on the filesystem (archetypes/), not in the database.
Key architectural norms:
- Brand-neutral: No brand fonts, colors, assets,
text-transform: uppercase, vertical side labels, or any brand convention. Casing, decoration, and styling are brand-layer decisions applied at generation time. - Background/content/foreground split: Archetypes define content layout only. Two layers bracket the content:
.background-layer(z-index 0) receives textures, brushstrokes, and gradient washes;.foreground-layer(z-index 10) receives borders, frames, and watermarks. Content sits between them at z-index 2. archetypeId, nottemplateId: Archetype schemas usearchetypeIdto avoid collision withTEMPLATE_SCHEMASresolution intemplate-configs.ts.brushis alwaysnull— the brand layer provides decorative transform targets.- Identical output shape: Both templates and archetypes produce renderable HTML + SlotSchema. The agent can select either; the editor sidebar works with both.
- Components are patterns, not runtime includes: Design components (
archetypes/components/) are reference HTML/CSS patterns. When building an archetype, copy the markup structure and SlotSchema fields — there is no partial/import system.
See archetypes/SPEC.md for the authoritative format specification.
A single Anthropic tool-use loop (canvas/src/server/agent.ts) handles the whole flow. There are no staged sub-agents.
Flow:
- User sends a message to
/api/chats/:id/messages - Server builds the system prompt: Tier 1 universal rules + Tier 2 Brand Brief + UI context
- Agent loop runs: model emits text, calls tools, reads results, continues until it's done
- Assistant text and tool events stream to the UI over SSE (
chat_delta,tool_start,tool_result,creation_ready,done) - When the agent calls
save_creation, the server writes the iteration to SQLite + disk, runs validation hooks, and emits acreation_readyevent
System prompt layering (agent-system-prompt.ts):
- Tier 1 (static): universal rules — structural HTML rules, intent gating, platform dimensions. Cacheable.
- Tier 2 (static): Brand Brief assembled from
voice_guide_docs,brand_patterns, andbrand_assets. Cacheable. - Dynamic: current UI context (active campaign/creation/iteration). Not cached because it changes every request.
Agent tools (agent-tools.ts):
- Brand discovery:
list_voice_guide,read_voice_guide,list_patterns,read_pattern,list_assets,list_templates,read_template,list_archetypes,read_archetype - Creation writing:
save_creation,edit_creation,save_as_template,get_creation,get_campaign - Brand editing (gated on explicit user intent):
update_pattern,create_pattern,delete_pattern,update_voice_guide,create_voice_guide - Visual self-critique:
render_preview(Playwright-rendered screenshot)
Hard rules (enforced in the Tier 1 prompt, not in DB):
- All CSS in
<style>blocks with class selectors — never inlinestyle=""attributes - Self-contained HTML — no external CDN links or stylesheet references
- Decorative elements use
<div>withbackground-image: url()— never<img>tags - Only fonts listed in the Brand Brief's Asset Manifest are allowed
- Every creation must include a complete SlotSchema based on an archetype
- Use the background-layer / content / foreground-layer structure from archetypes
All routes served from Vite middleware (canvas/src/server/watcher.ts):
| Route | Method | Purpose |
|---|---|---|
/api/campaigns |
GET, POST | List/create campaigns |
/api/campaigns/:id |
GET, PATCH | Read/update campaign |
/api/campaigns/:cId/creations |
GET | List creations in campaign |
/api/creations/:id/slides |
GET | List slides in creation |
/api/slides/:id/iterations |
GET | List iterations in slide |
/api/iterations/:id |
GET, PATCH | Read/update iteration |
/api/iterations/:id/html |
GET | Serve iteration HTML (with path fallback + slot application) |
/api/iterations/:id/status |
PATCH | Update review status |
/api/chats |
GET, POST | List/create chats |
/api/chats/:id |
GET, DELETE | Read/delete chat |
/api/chats/:id/messages |
POST | Send message (SSE stream of agent output) |
/api/chats/:id/cancel |
POST | Cancel in-flight generation |
/api/context-map |
GET, POST, PUT/:id, DELETE/:id | CRUD for brand-section routing |
/api/context-log |
GET | Audit trail of injected context per generation |
/api/templates |
GET, POST | List/create templates |
/api/uploads/chat-image |
POST | Upload image to chat |
/ |
GET | Template library (static HTML) |
/app/* |
GET | React canvas app |
/fluid-assets/* |
GET | Brand assets |
HTML serving uses a 4-strategy fallback: stored path → .fluid/ relative → canonical path → templates fallback. After loading, the server rewrites asset paths, injects <base href>, applies userState slot values, and adds a postMessage listener for live editing.
Registered in .mcp.json, runs as stdio process (npx tsx canvas/mcp/server.ts).
Tool: push_asset — creates Iteration record + writes HTML to canonical path. Used by external Claude Code sessions that want to push generated output into the canvas from outside the app.
The canvas's own creative agent does NOT go through MCP — it calls its tool set directly in the same process.
API base: http://localhost:5174 (override with MCP_API_BASE env)
Validation tools in tools/:
node tools/brand-compliance.cjs <file> # Validate HTML against brand rules
node tools/schema-validation.cjs <file> # Validate .liquid against Gold Standard schema
node tools/dimension-check.cjs <file> --target <type> # Check dimensions (instagram, linkedin_landscape, linkedin_tall)
node tools/scaffold.cjs <section-name> # Generate Gold Standard .liquid skeleton
node tools/validate-archetypes.cjs # Validate archetype SPEC conformance
node tools/db-export.cjs # Export DB → canvas/seed-data.json (brand data only)
node tools/db-import.cjs [--merge] [--force] # Import seed-data.json into DB
node tools/verify-context-sizes.cjs # Check pattern sizes
node tools/feedback-ingest.cjs [--dry-run] # Analyze feedback, generate proposalsValidators read from the SQLite database. The app must run at least once to seed the DB.
cd canvas
npm test # Run all tests
npm test -- db.test # Run specific test file
npm run test:watch # Watch mode- Uses Vitest with real SQLite (no mocking)
- Each test gets an isolated temp DB via
FLUID_DB_PATHenv var - Tests cover components, API, DB, brand context, routing
Zustand stores in canvas/src/store/:
campaign.ts— navigation state machine + data cache + race condition guard (_requestIdcounter)chat.ts— active chat + message stream + tool-call UI + cancellationeditor.ts— slot editing mode + modified values
HMR integration: Server sends fluid:file-change custom Vite event → useFileWatcher hook refreshes store (debounced 200ms, paused during generation).
- Canonical HTML paths:
.fluid/campaigns/{campaignId}/{creationId}/{slideId}/{iterationId}.html - Working directory:
.fluid/working/{sessionId}/ - Asset URLs in HTML: use
/fluid-assets/...(server rewrites../../assets/automatically) - IDs: nanoid-generated (e.g.,
cmp_xxx,cre_xxx,sld_xxx,itr_xxx) - No Express: all backend routes are Vite middleware in
watcher.ts - No Tailwind in app: plain CSS. (Tailwind only appears in generated output HTML)
- Do NOT read from
brand/files — that directory does not exist. All brand data is in the DB. - Do NOT duplicate brand doc content in prompts; the Brand Brief is assembled from the DB.
Reference/is archival only — never load directly.archetypes/SPEC.mdis the authoritative format reference for building archetypes. Do not invent format conventions — follow the spec.- Archetypes must be 100% brand-neutral: no
text-transform: uppercase, no rotated side labels, no brand fonts/colors/assets. Casing and decoration are brand-layer decisions. feedback/is for agents to write usage data back (learning loop).voice-guide/*.mdandpattern-seeds/*.mdare seed sources — the DB is the live copy.- Pattern content is clean markdown with code snippets — never raw HTML or base64. All assets referenced via
/api/brand-assets/serve/URLs. seed-data.jsoncontains brand config only (no campaigns/creations/slides/iterations). User data stays local to each developer's DB.- The app must be running (
npm run dev) for the chat agent, MCP push_asset, and API endpoints to work.