Cloudflare Worker powering the SaaS website delivery engine at
projectsites.dev. Built with Hono framework, Cloudflare D1/KV/R2/Workflows/AI.Template repo: https://github.com/HeyMegabyte/template.projectsites.dev
Authoritative: docs/architecture/cloudflare-first.md (mirror: ~/.agentskills/rules/projectsites-cloudflare-first.md).
- Allowed default infra: Cloudflare + Neon + Upstash + Fly.io. NO Cloud Run / AWS / GCP / Azure / Vercel / Supabase / Render / Railway by default (explicit override only).
- Neon = Postgres escape hatch (only when D1 can't). Upstash = Redis escape hatch (NOT the default cache — KV/R2 first). Fly.io = stateful-VM escape hatch (only when no CF primitive can).
- Hot path (public site): CF DNS / custom hostname → Worker dispatch → KV manifest → R2 asset → Analytics Engine sample → async Queue. Must NOT touch Neon/Upstash/Fly/Sentry/PostHog/Browserbase/Skyvern/external-AI unless truly dynamic.
- Browser automation:
browser.projectsites.dev(product abstraction, CF Browser Run + Playwright + Stagehand) → Browserbase fallback (managed session/replay/proxy only) → Skyvern is internal-only (skyvern.megabyte.space, behind CF Access), never the default product layer. Worker primitive:src/services/browser_gateway.ts. - AI Gateway is mandatory for every model call. Analytics Engine (not PostHog) is the default high-volume metrics backend.
These invariants are enforced programmatically in src/services/build_validators.ts and run between R2 upload and published status. Each maps to a gate in ~/.agentskills/15-site-generation/quality-gates.md. A violation flips the site to error once we move from report → strict mode.
| Invariant | Rule | Code |
|---|---|---|
| Required files | site.webmanifest, robots.txt, humans.txt, sitemap.xml, browserconfig.xml, .well-known/security.txt, favicon.ico, favicon-16x16.png, favicon-32x32.png, apple-touch-icon.png all exist |
manifest.required_file_missing |
| Asset existence | Every internal src= / href= / url(...) in HTML resolves to a file in R2; external hosts must be in allowlist |
asset.missing, asset.external_host_not_allowed |
| Image format | No PNG > 200KB except favicons (re-encode WebP/JPEG) | image.png_too_large |
| OG image | og-image.* exists, ≤100KB, branded card (1200×630) |
og.missing, og.too_large |
| apple-touch-icon | 180×180 at root | icon.apple_touch_missing |
| Meta lengths | <title> 50-60 chars, <meta description> 120-156 chars |
meta.title_length, meta.description_length |
| JSON-LD | 4+ blocks per HTML page (WebSite + Organization + WebPage + BreadcrumbList minimum) | jsonld.count_below_threshold |
| H1 in shell | Exactly 1 <h1> in HTML shell (prerender) — outside script/style |
html.h1_count |
| color-scheme | <meta name="color-scheme"> present |
meta.color_scheme_missing |
| Sitemap lastmod | Every <url> in sitemap.xml has <lastmod> |
sitemap.missing_lastmod |
| Banned slop | No "limitless", "revolutionize", "cutting-edge", "leverage", "world-class", etc. | copy.banned_word |
| JS chunk size | No JS chunk > 750KB raw (~250KB gzip) — code-split by route | js.chunk_too_large |
| Lightbox | JS bundle contains data-zoomable AND data-gallery strings |
lightbox.zoomable_missing, lightbox.gallery_missing |
Mode flag (in workflow validate-build step): currently report (logs to D1 audit, never throws). Flip to strict once template ships clean across all benchmarks (megabyte-labs, njsk, nyfb, vito's, soup kitchen).
Run from a unit test:
import { validateBuild } from './src/services/build_validators';
const report = validateBuild(files);
if (!report.ok) console.error(report.errors);Our goal: Generate enterprise-grade, industry-leading websites that BEAT any existing website. We don't copy sites — we take any website and use AI to make it dramatically better. Every generated site must be more beautiful, more accessible, better structured, faster loading, and better optimized for SEO/conversions than the source. We specialize in information-dense sites: take sprawling, poorly-organized original websites and condense them into gorgeous, well-organized, modern designs that pack MORE useful information into FEWER, better-designed pages.
Quality bar: The generated website must be so good that the business owner would prefer it over their original. Think: "What if Stripe's design team rebuilt this local business's website?"
A perfect website CANNOT be created with a single prompt. It requires 20-30 iterative, specialized prompts — just like a Principal Software Engineer needs 20+ detailed prompts to deploy a normal website. The system must spread the load across many prompts to circumvent single-prompt limitations.
| Site | URL | Type | What We Improve |
|---|---|---|---|
| whitehouse.gov | https://www.whitehouse.gov | Government / Information | Condense sprawling navigation into a clean, gorgeous homepage. Keep ALL important content but present it more beautifully. More information above the fold. Better visual hierarchy. |
| njsk.org | https://www.njsk.org | Non-Profit / Soup Kitchen | Bundle 20+ scattered pages into 3-4 well-organized pages + a blog. Make it gorgeous with warm, dignified colors. Add donation CTA, impact counters, volunteer signup. Keep all original content. |
How to use benchmarks:
- Deep-crawl the original site (all pages, all content)
- Generate our version using the full pipeline
- Compare: our version must have ALL the original content, but better organized
- Visual comparison: our version must look more professional, more modern
- Lighthouse comparison: our version must score higher on all metrics
- SEO comparison: our version must have better meta tags, schema, keywords
For information-heavy sites (like whitehouse.gov):
- The homepage should pack MORE useful information than the original
- Page count = source sitemap count (1:N mapping, max 1000 pages). Do NOT collapse 200 source pages into 4 — recreate every URL the source serves. Navigation reorganization happens in the IA hierarchy (mega-menu, faceted nav, search), not by deleting pages
- Content should be reorganized by user intent, not org structure
- Blog/news section should be auto-generated from scraped articles, with one route per post
For community/non-profit sites (like njsk.org):
- Bundle scattered pages into cohesive sections
- Generate a blog from existing news/updates content
- Add prominent donation/volunteer CTAs
- Use warm, inviting, professional colors (not generic)
See: memory/project_prompt_philosophy.md for the full 30-prompt pipeline specification.
| Phase | Prompts | Parallel? | Duration |
|---|---|---|---|
| 1. Research & Planning | 7 prompts (profile, brand, social, USPs, images, deep-crawl, structure) | 5 parallel + 2 sequential | ~3 min |
| 2. Asset Generation | 5 prompts (logo, favicon, hero images, section images, multimedia discovery) | All parallel | ~5 min |
| 3. Website Generation | 5 prompts (base HTML, animations, SEO meta, content, images) | Sequential | ~10 min |
| 4. Inspection & Fixes | 3 prompts (visual inspect via screenshot, fix issues, accessibility audit) | Sequential | ~5 min |
| 5. Quality & Safety | 5 prompts (quality gate, SEO audit, performance audit, safety check, final polish) | Sequential | ~3 min |
| 6. Domain-Specific | 1-3 prompts (donation CTA, menu, booking, child safety, medical compliance) | Conditional | ~2 min |
Key principles:
- NO API FALLBACK. Container-only builds. If it fails, it fails visibly with an error email.
- Each prompt has ONE focused job. Never combine multiple responsibilities.
- Parallelize aggressively — research + asset generation runs concurrently.
- Every prompt must measurably improve the website. If it doesn't, the prompt is wrong.
- Visual inspection is MANDATORY — screenshot the rendered page, analyze with GPT-4o vision.
- Domain-specific features are decided by AI based on research data, not hardcoded categories.
- Safety check is ALWAYS the last step regardless of business type.
- Optimize every feature for bottom-line impact (conversions, SEO, retention).
The container is a STATELESS Claude Code executor that runs a single orchestrator prompt. That orchestrator delegates to specialist subagents in parallel via the Task tool. It does not access D1 or R2 directly.
The container's ~/.claude/CLAUDE.md @-imports the upstream ~/.agentskills/CLAUDE.md, AGENTS.md, and _router.md — ProjectSites inherits the entire Emdash Skills meta surface (15 skill categories, 18 universal agents, 32 platform variants, conventions, profiles). Project-specific orchestrator instructions layer on top via the same file. Updates land within 10 minutes via container-server.mjs git-pull + syncAgents().
| Source | Agents | Role |
|---|---|---|
~/.agentskills/agents/ (synced to ~/.claude/agents/) |
visual-qa, seo-auditor, accessibility-auditor, performance-profiler, completeness-checker, content-writer, security-reviewer, test-writer (+10 more) | Universal audit/build specialists |
apps/project-sites/.claude/agents/ (COPY'd in Dockerfile) |
domain-builder, validator-fixer | Project-specific (no upstream equivalent) |
domain-builder creates donation/menu/booking/medical/child-safety/local-business sections as new files in src/components/sections/. validator-fixer runs scripts/run-validators.mjs and surgically fixes the 13 violation codes from build_validators.ts.
Step 1-3: Research (parallel)
→ Profile, Brand, Social, Images, Scrape, Structure plan
→ Workers AI (Llama 3.1) + external APIs
Step 4: container-build (single call, ~25-40 min)
→ Container runs ONE Claude Code orchestrator prompt
→ Orchestrator loads ~/.agentskills/_router.md + skill 15 in full
→ Customizes template directly (template -> dist)
→ npm run build, fix errors
→ PARALLEL FAN-OUT (single Task message, multiple subagents):
- domain-builder (writes section components)
- visual-qa (audit: screenshots + GPT-4o)
- seo-auditor (audit: title/meta/JSON-LD/OG)
- accessibility-auditor (audit: axe-core 6 bp)
- performance-profiler (audit: Lighthouse + bundles)
→ Routes findings:
- copy/voice → content-writer
- HTML/asset/meta/JSON-LD/sitemap/lightbox/js-chunk → validator-fixer
- a11y/perf remediation → validator-fixer (uses reports as input)
→ Loop until run-validators.mjs reports blockers === 0
→ completeness-checker as final gate (loop back if NOT_DONE)
→ node /home/cuser/upload-to-r2.mjs
Step 5: visual-inspection-final (non-blocking)
→ Screenshot published site, GPT-4o score, log to D1
- Subagents have isolated context windows — fan-out is free, no context pressure.
- File partitioning prevents merge conflicts — domain-builder owns sections, validator-fixer owns shell + public/ + config.
- Audit and build run concurrently — visual-qa screenshots while domain-builder writes, both finish ~5 min instead of 15 min sequential.
- Single container call — no R2 upload between stages, no workflow step boundaries to lose state across.
When DEEPSEEK_API_KEY is set in the Worker env AND BUILD_LLM_PROVIDER is NOT 'anthropic', the /build POST carries _deepseekKey, _anthropicBaseUrl: 'https://api.deepseek.com/anthropic', and _anthropicModel: 'deepseek-chat'. container-server.mjs then exports ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN (set to the DeepSeek key), and ANTHROPIC_MODEL into the shell that runs Claude Code — Claude Code reads these env vars and routes all API calls through DeepSeek's Anthropic-compatible endpoint. ANTHROPIC_API_KEY remains set as a passive fallback in case the DeepSeek endpoint errors. To force Anthropic regardless of key availability, set BUILD_LLM_PROVIDER=anthropic in the Worker secrets. For higher-order reasoning use deepseek-reasoner as the model value.
- Boot:
git pullon~/.agentskills+~/template, thensyncAgents()copies*.mdfrom~/.agentskills/agents/to~/.claude/agents/(project agents already on disk are preserved). - Every 10 min:
maybeRefreshSkills()re-pulls + re-syncs. - HTTP server on 8080.
POST /buildaccepts{ prompts, existingFiles, contextFiles }, runsclaude -pascuser, HMAC-signs callback to/api/internal/build-status. - Returns all non-underscore files from the build directory after completion.
API Credit Discipline (NON-NEGOTIABLE):
- NEVER waste API credits on speculative or debugging builds
- If there's an error, reduce to the simplest reproducible state first (e.g., 2+2 math test)
- Fix issues as separate, temporary tests — not by running full production builds
- Only trigger full builds when the pipeline is proven working end-to-end
- For infrastructure debugging, use mock responses or static data — never real API calls
- Each Claude Code prompt costs ~$0.50-2.00. Each full build costs ~$5-15. Treat credits as scarce.
Key constraints:
- Entrypoint must be under 10KB (CF Containers limit)
- Each workflow step must complete within 25 minutes
- Container uses
node:22-slimimage (CF Containers don't support custom large images) - Claude Code needs
--dangerously-skip-permissions -pflag for stdin→stdout mode - Shell scripts need
String.fromCharCode(10)for newlines (not\\nin template literals)
Templates are pre-built project skeletons stored in R2. Instead of generating from scratch every time, Claude Code starts from a template and customizes it.
Template repo: https://github.com/HeyMegabyte/template.projectsites.dev
How templates work:
- Templates are Vite + React + Tailwind + shadcn/ui project skeletons
- Each template has a
package.jsonwith all dependencies pre-configured - The template includes shadcn/ui components, layout patterns, responsive grids
- Templates are categorized by industry: restaurant, non-profit, retail, professional, etc.
- Claude Code receives the template as
existingFilesand customizes it with business-specific content - This reduces build time from 15 min to ~5 min (Claude Code edits vs. creates from scratch)
Template stack:
- Vite — build tool
- React 18+ — UI framework
- Tailwind CSS 3+ — utility-first CSS
- shadcn/ui — accessible component library (Radix UI primitives)
- Inter / Satoshi — typography (Google Fonts)
Templates evolve: Each successful build improves the template for that category. If Claude Code adds a pattern that improves quality, it gets folded back into the template.
Current templates stored at: R2: templates/{category}/ with files like package.json, vite.config.ts, tailwind.config.ts, src/App.tsx, etc.
Claude Code is INCAPABLE of designing a perfect website with a single prompt. The prompts MUST be broken into as many focused prompts as necessary — forming a web of Principal Software Engineer-level prompts that always take what's available and improve it. Each prompt should improve the site tremendously if there's nothing there, or polish incrementally if quality is already high.
The prompt system should:
- Use however many prompts are necessary (12 is the floor, not the ceiling)
- Include small, surgical prompts for specific improvements (e.g., "fix color contrast in section 3")
- Leverage GPT-4o visual inspection heavily — screenshot after each major change, analyze, fix
- Use Playwright E2E TDD to validate DOM correctness programmatically
- Parallelize whenever possible (run independent prompts concurrently)
- Target under 1 hour total build time
- Evolve — the prompt chain should get better over time as new patterns emerge
Color Contrast & Palette Quality:
- Every color combination in the design MUST be checked for WCAG AA contrast (4.5:1 for text, 3:1 for large text)
- UI default colors, component colors, and backgrounds must use industry-appropriate, legendary, gorgeous, professional palettes
- NEVER use colors that look washed out, muddy, or generic
- The AI should generate 3-5 palette options per business type and select the most professional one
- Brand colors from the original website are extracted via AI vision, then enhanced to be more vibrant if needed while keeping the hue family
- If brand colors have poor contrast combinations, the AI must adjust them (lighter/darker variants) while maintaining brand recognition
Logo Luminance Drives Theme (BUILD-BREAKING):
- During brand research, compute the dominant-color luminance of the brand logo (sRGB → relative luminance per WCAG)
- Dark logo (luminance < 0.4): build a LIGHT theme (cream/white/off-white background, dark text, accent colors pulled from logo)
- Light logo (luminance > 0.6): build a DARK theme (the existing dark-first preference applies here)
- Mid-luminance logo (0.4-0.6): default to dark theme but verify logo contrast against header/hero/footer backgrounds — if any combination falls below 4.5:1, flip to light theme
- Logo legibility outranks the dark-theme aesthetic preference. The site's primary background MUST contrast the logo by ≥4.5:1
- Set
_brand.json.theme = "light" | "dark"BEFORE template selection. The orchestrator must read this and pick a light or dark template variant accordingly - Examples: njsk.org (dark burgundy wordmark) → light theme. Stripe (light-on-dark wordmark) → dark theme. Linear (dark monochrome) → light theme
Source-Site Theme Preservation (BUILD-BREAKING):
- GPT-4o-score the source homepage screenshot on aesthetic polish (0-10) during brand research
- If score ≥7 (polished site), preserve the source theme polarity: light → light, dark → dark. Do NOT flip a polished light site to dark just because dark is the platform default
- Set
_brand.json.preserve_source_design = truewhen source ≥7/10 — orchestrator clones source layout patterns before adding our improvements - Example: lonemountainglobal.com is a light-themed bespoke WordPress site → MUST stay light. Flipping to dark destroys brand recognition
Logo Extraction Priority Chain (BUILD-BREAKING):
- Source HTML must be parsed BEFORE the LLM brand step. Extract logo via deterministic crawler with this priority chain:
- Header
<img>with class/alt containing "logo" → 2.site.webmanifesticons[] → 3. WordPresscropped-*-icon-*.pngand*-icon-512x512*.pngpatterns → 4.<link rel="apple-touch-icon">180×180 → 5.<link rel="icon">32×32 → 6.og:image(last resort) → 7. Logo.dev / Brandfetch API → 8. Ideogram A/B/C generation as ABSOLUTE last resort
- Header
- Persist BOTH
_brand.json.logo.original_url(full horizontal/wordmark) AND_brand.json.logo.original_icon_url(square icon-only, no text) - Container orchestrator MUST verify
original_icon_urlHEAD-200s before declaring brand-research complete
real-favicongenerator Pipeline (MANDATORY every build):
- Run RFG (
https://realfavicongenerator.net/api/favicon) against the icon-only logo to produce the FULL 11-file favicon set:favicon.ico,favicon-{16,32,48}.png,apple-touch-icon.png(180×180),android-chrome-{192,512}.png,mstile-150x150.png,safari-pinned-tab.svg,site.webmanifest(icons[] populated),browserconfig.xml - If RFG unavailable, fall back to
sharp-cli+realfaviconnpm package inside the container - Build fails if any of the 11 files missing from
public/after the step
Media Augmentation 1.4–2.0× (BUILD-BREAKING):
- Extract ALL original media from source: homepage hero, sliders, galleries, team headshots, og:image, every
<img>src, every CSSbackground-image:URL. Persist under_assets.json.original[] - Augment via Pexels + Unsplash + Pixabay + Google CSE + DALL-E 3 (heavy use) + Ideogram + YouTube + Sora. Persist under
_assets.json.augmented[] - Target count:
augmented.length >= original.length * 1.4and<= original.length * 2.0 - Per-page minimum: 6+ images on homepage, 4+ images on every sub-page
- Group preservation: source
[data-gallery="services"]MUST round-trip to new site[data-gallery="services"]with same images + new ones validator-fixerfails build if any page has <4 images or ifaugmented.length < original.length * 0.4
Original Document Preservation:
- Parse all
<a>href endings in.(pdf|docx|pptx|xlsx|doc|ppt|xls|zip)during scrape - Download each, host under
public/docs/{filename}, persist_assets.json.documents[]with{original_url, filename, linked_from_page, anchor_text} - Surface on the same page topic in the rebuilt site (CV/resume on About, brochure on Services, etc.)
Hero Asset From Logo (Brand Convergence):
- GPT-4o-compare brand logo against source homepage hero image. If cosine similarity of dominant shapes > 0.7, set
_brand.json.hero_extracted_from_logo = trueand persist asset URL - Container orchestrator MUST use that extracted asset as the hero background AND apply the logo's font family to H1 + nav
- Example: lonemountainglobal.com
mountain-background-splash.pngis the mountain shape from the logo blown up — pairing logo + hero + Poppins (logo font) creates instant brand convergence
Homepage-Clone-When-Source-Excellent:
- When source aesthetic score ≥7/10, the rebuild MIRRORS source homepage layout (hero structure, section order, color scheme, typography pairings) before adding our polish/animations/content density
- Sub-pages then inherit the homepage's design language for consistency
- "Clone" means structure + color + type — not literal pixel-copy. Improvements layer on top: scroll animations, denser content, better SEO, AI-augmented imagery
Typography Extraction From Source (NEVER substitute):
- During brand research, parse source CSS for
font-familydeclarations (homepage stylesheets, Google Fonts URL params,<link rel="stylesheet" href="*fonts.googleapis.com*">) - Persist
_brand.json.fonts.{logo, heading, body}withsource: "extracted"flag and the exact font names - Use those EXACT fonts in the rebuild — do not substitute with "modern equivalents" (Inter, Space Grotesk) when source has chosen with intent
- Example: lonemountainglobal.com uses Poppins (logo + headings) + Hind (body). Generic "Inter + Space Grotesk" substitution destroys brand identity
Every generated website must implement aggressive, intelligent SEO that targets low-hanging fruit:
- Primary keyword:
{business type} in {city}(e.g., "soup kitchen in Newark NJ", "grocery store Hell's Kitchen NYC") - Secondary keywords: Each page targets 2-3 long-tail phrases derived from services + location
- Keyword placement: Primary in H1, title tag, meta description, first paragraph, and at least 2 H2s. Secondary in H3s, image alt text, internal link anchor text.
- Keyword density: 1-2% natural density. Never stuff. Always readable.
<title>under 60 chars:{Primary Keyword} | {Business Name}<meta description>under 160 chars: compelling, includes primary keyword + CTA<link rel="canonical">on every page- JSON-LD
LocalBusinessschema with: name, address, phone, geo coordinates, opening hours, price range, image, URL, sameAs (social links) robots.txtallowing all crawlerssitemap.xmllisting all pages with lastmod dates- Open Graph + Twitter Card meta tags for social sharing
- Semantic HTML: one H1, logical H2→H3 hierarchy,
<article>,<section>,<nav>,<main>
- Internal linking: Every page links to at least 2 other pages with keyword-rich anchor text
- FAQ schema: FAQ section uses
FAQPagestructured data for rich snippets in Google - Breadcrumbs: Multi-page sites include breadcrumb navigation with
BreadcrumbListschema - Image SEO: Every image has descriptive alt text containing a relevant keyword. File names are descriptive (not
img-1.jpg). - Content length: Homepage 1000+ words, about page 500+ words. Real content, not filler.
- Google Maps embed with exact address
LocalBusinessschema includesgeocoordinates,areaServed,serviceArea- NAP consistency (Name, Address, Phone) appears in header, footer, contact section, and schema
- Location mentioned naturally in content: "serving {neighborhood} since {year}"
Two dedicated SEO prompts run during the build:
- Keyword research + placement: Analyze the business type + location, identify 5-8 target keywords, ensure they're placed in all required positions
- Technical SEO audit: Verify all meta tags, schema, sitemap, canonical, heading hierarchy, alt text, internal links. Fix any gaps.
Every page must be a COMPELLING MULTIMEDIA EXPERIENCE. Everything should be BREATHTAKINGLY GORGEOUS and VIVIDLY + MASTERFULLY ANIMATED.
Tech stack: shadcn/ui + Tailwind CSS + React (Vite). Every generated site uses this stack.
Visual requirements:
- 8+ @keyframes animations, glassmorphism, gradient text, scroll reveals
- 10+ unique images per site minimum
- Actual brand colors extracted via AI vision from original website
- Creative typography using premium fonts (Inter, Satoshi, DM Sans, Cabinet Grotesk)
- Dark theme preferred — darker + colorful is trending. Use dark backgrounds with vibrant accent colors
- Sites must look award-winning — Stripe / Linear / Vercel level polish
Multi-page architecture (page count = source sitemap, 1:N up to 1000 max — BUILD-BREAKING):
- Page count is NEVER capped at 5–8. Recreate every URL the source serves: 12 → 12, 80 → 80, 750 → 750. Cap at 1000 only as a sanity ceiling for runaway crawls
- Source sitemap discovery (priority chain): (1)
/sitemap.xml+/sitemap_index.xml+ nested<sitemap><loc>indexes, (2)/wp-sitemap.xml(WP) //sitemap-index.xml(Squarespace), (3)robots.txtSitemap: lines, (4) Wayback Machine CDX API for full historical URL set, (5) breadth-first crawl from homepage with depth ≤ 6 + same-host rule + dedupe by canonical - Persist EVERY discovered URL to
_scraped_content.json.routes[]with{url, title, h1, body_word_count, internal_links, depth, og_image, hero_image}. Container builds one Vite+React route per entry - Floor for thin source sites: even a 1-page source = ≥4 page rebuild (Home + About + Services + Contact). Never go thinner than the 4-page floor
- Homepage = compelling marketing summary of the entire business mission. Sub-pages keep source URL slugs (
/about,/programs/scholarships,/blog/2024-03-annual-report) — never lose deep-link continuity - Blog: every post in source = one route in rebuild.
/blogindex +/blog/{slug}permalinks.BlogPostingJSON-LD per post - Proper navigation: mega-menu / faceted nav / on-site search when route count > 12. Tree-organize by URL hierarchy, not by flat sitemap dump
Brand Recreation Philosophy (CRITICAL — suped-up clone):
- The goal is a SUPED-UP CLONE — same brand, same content, dramatically more beautiful
- Logo: IF THE BUSINESS HAS A LOGO, IT MUST BE USED. Priority: scrape from site header/footer → Logo.dev → Brandfetch → scan merchandise photos → extract from favicon → AI-generate as LAST resort
- Logo font: Extract the font from the logo image using AI vision and reuse it in the design
- Logo graphics: The graphic elements and colors in the logo should influence the ENTIRE site design
- App icon: Find real app icon → extract from logo → upscale favicon with AI → generate
- Brand colors: Extract from LOGO first, then website design, then merchandise/signage in photos
- Content: Use ALL original website content. Crawl and scrape EVERY page. If pages are too small, combine with 301 redirects
- Images: Use ALL original images. IF ANY PICTURE IS TOO SMALL, UPSIZE IT WITH AI
- Page recreation: Recreate every URL from the original sitemap. Make each page more beautiful, compelling, multimedia-packed
- Every pass makes it better: Structure prompts so each Claude Code pass escalates beauty, adding: gorgeous, accessible, concise, integrated, informative, intuitive, stunning, creative qualities
Criticism integration (MANDATORY):
- Any criticism for a *.projectsites.dev site must be adhered to, patched in prompts, and re-deployed
- The prompt system evolves with every user feedback cycle — never ignore a criticism
Deep content integration:
- Scrape ALL pages from the source site, not just the homepage
- Combine with web search results
- Augment with research, facts, references to affiliated organizations
- Back up content with real facts, proven research, affiliated org references
- If the source has a blog, recreate it with external CMS capability
Multimedia everywhere:
- Every page should have rich media (images, videos, animations)
- Double the parallelized research and multimedia discovery
- Optimize for visual impact — dark overlays on text are OK if they look stunning
- Add interactive, gamification elements where appropriate
Google Maps embed:
- Must have proper CSP headers allowing Google Maps iframe sources
- Register the domain with Google Maps Platform for API key restrictions
- Address links should use Google Maps directions URL format:
https://www.google.com/maps/dir/?api=1&destination={encoded_address} - Include
LocalBusinessschema with geo coordinates matching the embedded map
Every generated website must embody Stripe / Linear / Vercel-level polish:
- Layout: Material Design spacing + layout principles
- Typography: Apple-level hierarchy — Inter or Satoshi, 48px hero / 24px section / 16px body
- Components: Tailwind CSS patterns + shadcn/ui-inspired accessible components (Radix UI patterns)
- Colors: Professional Tailwind-style palette, WCAG AA minimum contrast, brand colors extracted via AI vision
- Immediate clarity above the fold — visitor knows what the business does in 3 seconds
- Single primary CTA (scroll to contact/donate/book)
- No cognitive overload — clean, minimal, premium SaaS aesthetic
- Subtle gradients, soft shadows, light motion only (not garish)
- Must pass Lighthouse 90+ across all categories
- Minimal JS, fast load, lazy images, font-display: swap
- Reference: Stripe.com, Linear.app, Vercel.com level of polish
- Output: Production-ready code. No placeholders. No filler text. No lorem ipsum.
Collect 10x more assets than needed, then curate down via AI visual inspection.
- Collect ~100 candidate assets from ALL APIs: Unsplash, Pexels, Pixabay, Foursquare, Yelp, Google CSE, DALL-E, scraped images, YouTube videos
- AI visual inspection on EVERY asset — GPT-4o vision checks quality, professionalism, relevance, safety, resolution
- Score and rank — each asset gets a quality score (0-100) and a relevance score
- Select top 10-15 for the final website — the absolute best from the 100 candidates
- Include rich media generously — scale by route count: 6+ images per route home, 4+ per sub-route, plus 1–2 videos per content cluster. A 4-route site = 15–20 images; a 50-route site = 200+ images; a 500-route site = 2000+. Never cap at 4-page-site numbers when source is larger
- Every image: resolution, composition, relevance to business, professionalism, no watermarks
- Every video: quality, relevance, appropriate content, no ads
- Logos: clarity at multiple sizes, brand accuracy, text readability
- App icons: works at 512px AND 32px, distinctive, brand colors
- Use ALL available sources: Logo.dev, Brandfetch, website scrape, Google image search
- For logo: prefer horizontal/text-based version with business name readable
- For app icon: prefer square/monogram version, works at small sizes
- Use the best trendy fonts (Inter, Satoshi, DM Sans, Cabinet Grotesk, General Sans) customized to brand personality
- If no suitable logo exists: generate via DALL-E/Ideogram using brand colors + display font
Branded error pages: All HTTP errors (400-503) return gorgeous animated HTML pages with Fira Code debug info (for browsers). API clients get JSON.
Snapshot system: Each build auto-creates a snapshot (first = "initial", edits = AI-named). Access frozen versions at {slug}-{snapshot}.projectsites.dev.
49 API integrations deployed — see memory file for full list. Key: Unsplash, Foursquare, Yelp, YouTube, Pexels, Pixabay, DALL-E, Stability AI, Cloudinary, Mapbox, Brandfetch, Logo.dev, plus quality gates (PageSpeed, GTmetrix).
Build process reference: See docs/BUILD_PROCESS.md for the complete technical flow — from user click to published site, including all prompts, APIs, and quality gates.
Quality rules: See memory file website_quality_rules.md for the criticism registry — every user feedback item about generated websites, organized by category.
Every prompt's suggestions should be considered and possibly included in the multi-stage build process. When a user provides feedback about a generated site, the feedback should be generalized into rules that apply to all future builds.
Every site MUST have a logo and app icon. If none is uploaded or discovered:
- Logo.dev (
LOGODEV_TOKEN) — try first for established businesses with domains - Brandfetch (
BRANDFETCH_API_KEY) — full brand kit including logos - DALL-E 3 (
OPENAI_API_KEY) — generate a clean, modern text-based logo - Ideogram (
IDEOGRAM_API_KEY) — alternative AI logo generation (good for stylized text) - Stability AI (
STABILITY_API_KEY) — Stable Diffusion for logo variations
Logo style: Clean, simple, text-based with a geometric accent. Use the business name in a bold display font with brand colors. The logo should work at both large sizes (hero) and small sizes (favicon).
App icon: 512x512 PNG, simplified version of the logo. If the logo has text, the app icon should be just the first letter or monogram in the brand's primary color on a clean background.
ALL available multimedia APIs must be queried during the build:
| API | Key | Use In Build |
|---|---|---|
| Unsplash | UNSPLASH_ACCESS_KEY |
Hero images, section backgrounds, service card images |
| Pexels | PEXELS_API_KEY |
Stock photos + videos for hero/section backgrounds |
| Pixabay | PIXABAY_API_KEY |
Illustrations, vectors, supplementary photos |
| YouTube | YOUTUBE_API_KEY |
Business-specific videos for hero or featured section |
| Foursquare | FOURSQUARE_API_KEY |
Venue-specific photos |
| Yelp | YELP_API_KEY |
Business photos from Yelp listing |
| DALL-E 3 | OPENAI_API_KEY |
Generated section images, logos, hero art |
| Ideogram | IDEOGRAM_API_KEY |
Logo generation, stylized text art |
| Stability AI | STABILITY_API_KEY |
Supplementary images, backgrounds, patterns |
| Replicate | REPLICATE_API_TOKEN |
Image upscaling, background removal |
| Remove.bg | REMOVEBG_API_KEY |
Clean up logos, product photos |
| Cloudinary | CLOUDINARY_* |
Image optimization CDN (auto WebP, responsive sizing) |
The generated website should have minimum 10 unique images from these sources. No site should ever launch with placeholder or missing imagery.
cd apps/project-sites
npm install --legacy-peer-deps # NOT pnpm (electron-builder breaks it)
npm test # 902 unit tests across 49 suites
npm run typecheck # tsc --noEmit
npm run lint # eslint
npx wrangler dev # local dev server (port 8787)"We don't sell websites. We deliver them."
A small-business owner searches for their business, signs in, and receives a professionally built AI-generated website in under 15 minutes — hosted, SSL'd, and live.
Search → Select business → Sign In → Provide details + upload → AI builds → Live site
src/
├── index.ts # Hono app: middleware stack, route mounts, queue/scheduled handlers
├── types/env.ts # Env bindings (D1, KV, R2, AI, Queue, Workflow) + Variables
├── middleware/
│ ├── auth.ts # Bearer token → session → userId/orgId (does NOT reject unauthed)
│ ├── error_handler.ts # AppError → JSON, ZodError → 400, unknown → 500 + Sentry
│ ├── payload_limit.ts # 256KB max request body
│ ├── request_id.ts # X-Request-ID header generation/propagation
│ └── security_headers.ts # CSP, HSTS, X-Frame-Options, Permissions-Policy
├── routes/
│ ├── health.ts # GET /health (checks KV + R2 latency)
│ ├── search.ts # Business search, site lookup, create-from-search
│ ├── api.ts # Auth, sites CRUD, billing, hostnames, audit logs
│ └── webhooks.ts # POST /webhooks/stripe (signature verification + idempotency)
├── services/
│ ├── ai_crypto.ts # AES-GCM helpers (per-record IV) for secrets at rest
│ ├── ai_env_vars.ts # Per-org/site/MCP encrypted env vars; resolveEnvVarsForAI()
│ ├── ai_workflows.ts # Multi-phase AI pipeline + prompt registration
│ ├── analytics.ts # PostHog server-side event capture + captureLLMCall ($ai_*)
│ ├── audit.ts # Append-only audit log writes
│ ├── auth.ts # Custom auth: magic link, Google OAuth, D1 sessions (fallback rail)
│ ├── billing.ts # Stripe checkout, subscriptions, entitlements
│ ├── build_context.ts # Build context assembly for container builds
│ ├── build_limits.ts # Build rate limiting + concurrency
│ ├── chat_synthesis.ts # Chat context synthesis for AI
│ ├── confidence.ts # Confidence scoring for research data
│ ├── contact.ts # Contact form handling
│ ├── db.ts # D1 query helpers (dbQuery, dbInsert, dbUpdate, dbExecute)
│ ├── domains.ts # CF for SaaS custom hostname provisioning
│ ├── external_llm.ts # External LLM provider routing (OpenAI, Anthropic, etc.)
│ ├── google_places.ts # Google Places API integration
│ ├── image_discovery.ts # Multi-API image discovery (Unsplash, Pexels, etc.)
│ ├── image_generation.ts # AI image generation (DALL-E, Stability, etc.)
│ ├── media.ts # Unified media library (uploads + stock + DALL·E + Sora + TTS + send-to-bolt)
│ ├── notifications.ts # Email notifications (Resend/SendGrid)
│ ├── openai_research.ts # OpenAI-powered research pipeline
│ ├── rag.ts # Vectorize + AutoRAG (embed, indexChunk, semanticSearch, autoRagQuery)
│ ├── sentry.ts # Error tracking (Toucan SDK)
│ ├── site_serving.ts # R2 static file serving + top bar injection for unpaid
│ ├── task_inbox.ts # Task tray: postAskUser, resolveTask, applyExpiredDefaults
│ ├── template_cache.ts # R2 template caching for container builds
│ └── webhook.ts # Stripe signature verification, idempotency
├── prompts/
│ ├── index.ts # Registry initialization (registerAllPrompts)
│ ├── types.ts # PromptSpec interface, PromptKey type
│ ├── parser.ts # YAML frontmatter + # System/# User section parser
│ ├── renderer.ts # Template rendering with injection prevention
│ ├── schemas.ts # Zod I/O schemas per prompt (validatePromptInput/Output)
│ ├── registry.ts # Version resolution, A/B variants, KV hot-patching
│ └── observability.ts # LLM call logging, cost estimation, SHA-256 hashing
├── workflows/
│ └── site-generation.ts # Cloudflare Workflow: 6-step durable AI pipeline
└── lib/
├── posthog.ts # PostHog capture helper (fire-and-forget)
└── sentry.ts # Sentry client factory (Toucan)
| Method | Path | Purpose |
|---|---|---|
| GET | /health |
Health check (KV + R2 probe) |
| GET | /api/search/businesses?q=... |
Google Places proxy (max 10) |
| GET | /api/search/address?q=... |
Address search proxy |
| GET | /api/sites/search?q=... |
Pre-built site search (LIKE) |
| GET | /api/sites/lookup?place_id=...&slug=... |
Check if site exists |
| GET | /api/auth/google |
Start Google OAuth flow |
| GET | /api/auth/google/callback |
Google OAuth callback |
| GET | /api/auth/:provider/login |
Better Auth (default) / Better Auth IdP login → 302 to authorize (404 dark when BETTER_AUTH_*/BETTER_AUTH_* unset) |
| GET | /api/auth/:provider/callback |
IdP callback → verify CSRF state → exchange code → D1 session → 302 home |
| GET | /api/auth/magic-link/verify?token=... |
Email click verification |
| POST | /webhooks/stripe |
Stripe webhook (signature verified) |
| Method | Path | Purpose |
|---|---|---|
| POST | /api/sites/create-from-search |
Create site + start AI workflow |
| POST | /api/auth/magic-link |
Request magic link email |
| POST | /api/auth/magic-link/verify |
Verify magic link (programmatic) |
| GET | /api/auth/me |
Get current user session |
| POST | /api/sites |
Create site (manual) |
| GET | /api/slug/check |
Check slug availability |
| GET | /api/sites |
List user's sites |
| GET | /api/sites/:id |
Get single site |
| GET | /api/sites/:id/workflow |
Get workflow status |
| GET | /api/sites/:id/logs |
Get site audit logs |
| GET | /api/sites/:id/readiness |
Production-readiness grade for one site (#9; reads latest workflow.build_validation audit) |
| GET | /api/readiness?ids=a,b,c |
Batch readiness grades for ≤100 sites in one request (#9 follow-on; org-scoped, {id: data|null}) |
| POST | /api/sites/:id/reset |
Reset site (rebuild) |
| POST | /api/sites/:id/deploy |
Deploy zip to site |
| POST | /api/sites/:id/publish-bolt |
Publish from bolt editor |
| DELETE | /api/sites/:id |
Delete site |
| POST | /api/billing/checkout |
Create Stripe checkout session |
| POST | /api/billing/embedded-checkout |
Create embedded checkout |
| GET | /api/billing/subscription |
Get subscription status |
| GET | /api/billing/entitlements |
Get plan entitlements |
| POST | /api/billing/portal |
Create Stripe billing portal |
| GET | /api/sites/:siteId/hostnames |
List hostnames |
| POST | /api/sites/:siteId/hostnames |
Provision hostname |
| PUT | /api/sites/:siteId/hostnames/:hostnameId/primary |
Set primary hostname |
| POST | /api/sites/:siteId/hostnames/reset-primary |
Reset to default hostname |
| DELETE | /api/sites/:siteId/hostnames/:hostnameId |
Delete hostname |
| POST | /api/sites/:siteId/hostnames/:hostnameId/unsubscribe |
Unsubscribe hostname |
| POST | /api/sites/improve-prompt |
AI prompt improvement |
| POST | /api/sites/generate-prompt |
AI prompt generation |
| POST | /api/ai/categorize |
AI business categorization |
| POST | /api/contact-form/:slug |
Submit contact form |
| POST | /api/ai-actions/payment-command |
Safety-gated AI payment command (dry-run→confirm→charge); flag ai_payment_command |
| POST | /api/ai-actions/payment-refund |
Refund a prior payment-command charge (caller-owned idempotency key); same flag |
| GET | /api/ai-actions/payment-status/:paymentIntentId |
Read-only PaymentIntent status (no charge, no audit); same flag |
| GET | /api/ai-actions/payment-methods?customer=cus_… |
List a customer's saved cards (masked brand/last4/expiry, never PAN); same flag |
| GET | /api/ai-actions/customers?q=cus_…|email |
Resolve a Stripe customer by id or email (read-only); same flag |
| GET | /api/sites/by-slug/:slug/build-context |
Get build context |
| GET | /api/sites/by-slug/:slug/chat |
Get chat context |
| GET | /api/sites/by-slug/:slug/research.json |
Get research data |
| GET | /api/domains/search |
Search available domains |
| POST | /api/domains/purchase |
Purchase domain |
| GET | /api/admin/domains |
Admin: list all domains |
| POST | /api/publish/bolt |
Publish from bolt |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/media/assets |
List assets with kind/source/q filters |
| GET | /api/media/assets/:id |
Single asset metadata |
| GET | /api/media/assets/:id/raw |
Stream the R2 object |
| POST | /api/media/upload |
Multipart upload (25 MB cap; exempt from 256 KB global limit) |
| DELETE | /api/media/assets/:id |
Soft delete |
| POST | /api/media/stock/search |
Federated Unsplash/Pexels/Pixabay search |
| POST | /api/media/stock/save |
Persist a candidate to the library |
| POST | /api/media/generate/image |
DALL·E 3 generation |
| POST | /api/media/generate/video |
Queued Sora / Veo (workflow callback flips status) |
| POST | /api/media/generate/podcast |
ElevenLabs / OpenAI TTS |
| POST | /api/media/send-to-bolt |
Mint a signed URL the bolt iframe consumes |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/env-vars |
List org+site+mcp env vars (decrypted values hidden) |
| POST | /api/env-vars |
Create scoped env var (org / site / mcp) |
| PATCH | /api/env-vars/:id |
Update value or label |
| DELETE | /api/env-vars/:id |
Soft delete |
| POST | /api/env-vars/import |
Bulk import (dotenv-style payload) |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/inbox/tasks |
List open tasks for the caller's org |
| POST | /api/inbox/tasks/:id/resolve |
Resolve a task; fans event into waiting workflow |
{
"error": {
"code": "NOT_FOUND",
"message": "Site not found",
"request_id": "uuid"
}
}Error codes: BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT,
PAYLOAD_TOO_LARGE, RATE_LIMITED, VALIDATION_ERROR, INTERNAL_ERROR,
WEBHOOK_SIGNATURE_INVALID, WEBHOOK_DUPLICATE, STRIPE_ERROR,
DOMAIN_PROVISIONING_ERROR, AI_GENERATION_ERROR
requestId— Generate/propagateX-Request-IDpayloadLimit— Reject bodies > 256KBsecurityHeaders— CSP, HSTS, X-Frame-Optionscors(API only) — Allow bolt/sites domains + localhostauth(API only) — Bearer token → session → userId/orgIderrorHandler— Catch all, format as JSON
Step 1 (sequential): research-profile → business_type needed for others
Step 1b (optional): google-places-lookup → enrich with Places API data
Step 2 (parallel): research-social → social links
research-brand → logo, colors, fonts
research-selling-points → USPs, hero content
research-images → image strategies
Step 2.5 (sequential): move-uploaded-assets → move user uploads to R2
generate-logo → AI logo generation
generate-favicon-set → favicon from logo
generate-section-images → AI section images
discover-brand-images → brand image discovery
discover-videos → YouTube video discovery
store-build-context → persist research to R2
Step 2.5b (optional): scrape-website → deep-crawl existing site
Step 2.6 (optional): seed-site-data → seed per-site D1 tables
Step 3 (sequential): structure-plan → site structure plan (fast LLM)
Steps 4-5 (container build — single-prompt orchestrator):
build-orchestrator → Container call: orchestrator prompt fans out to
parallel subagents (visual-qa, seo-auditor,
accessibility-auditor, performance-profiler,
content-writer, security-reviewer, domain-builder,
validator-fixer) under one Claude Code session,
gates DONE on completeness-checker (~40min)
upload-final → upload to R2, update D1 status to 'published'
visual-inspection-final → GPT-4o final scoring (non-blocking)
Each step has automatic retry (3x) with exponential backoff. Container builds use
Cloudflare Containers (Durable Object SITE_BUILDER) with custom Dockerfile pre-baking
Claude Code, skills, and template repos.
15 prompt files in YAML frontmatter + Markdown format:
| File | Purpose |
|---|---|
research_profile.prompt.md |
Deep business profile research |
research_social.prompt.md |
Social media + website discovery |
research_brand.prompt.md |
Logo, colors, fonts, personality |
research_selling_points.prompt.md |
3 USPs + hero slogans |
research_images.prompt.md |
Image needs + search strategies |
generate_website.prompt.md |
Full website HTML from research data |
generate_multipage_site.prompt.md |
Multi-page site generation |
generate_legal_pages.prompt.md |
Privacy/terms pages |
plan_site_structure.prompt.md |
Site structure planning |
score_website.prompt.md |
8-dimension quality scoring |
site_copy.prompt.md |
Marketing copy (variant A) |
site_copy_v3b.prompt.md |
Marketing copy (variant B) |
research_business.prompt.md |
Legacy business research (v2) |
generate_site.prompt.md |
Legacy generation (v2) |
score_quality.prompt.md |
Legacy scoring (v2) |
All tables have: id (UUID), created_at, updated_at, deleted_at (soft delete).
Org-scoped tables include org_id.
Core: orgs, users, memberships, sites, hostnames
Auth: sessions, magic_links, oauth_states (phone_otps dropped in migration 0516_drop_phone_otps.sql — phone feature removed)
Billing: subscriptions
Infra: webhook_events, audit_logs, workflow_jobs
AI: research_data, confidence_attributes
Analytics: analytics_daily, funnel_events, usage_events
Site status machine: draft → collecting → imaging → generating → published | error | archived
dbQuery<T>(db, sql, params) // SELECT multiple → { data: T[] }
dbQueryOne<T>(db, sql, params) // SELECT one → T | null
dbInsert(db, table, record) // INSERT + auto timestamps
dbUpdate(db, table, updates, where, params) // UPDATE + auto updated_at
dbExecute(db, sql, params) // Raw execute- Base domain (
projectsites.dev) → servemarketing/index.htmlfrom R2 - Subdomain (
{slug}.projectsites.dev) → resolve from D1 → serve from R2 - Unpaid sites → inject top bar after
<body>tag - KV cache:
host:{hostname}→ site record (60s TTL)
CACHE_KV: KV namespace for cachingPROMPT_STORE: KV namespace for prompt hot-patchingDB: D1 databaseSITES_BUCKET: R2 bucket for static sites + per-org media (media/{orgId}/...)QUEUE: Queue (optional, commented out — not yet enabled on account)SITE_WORKFLOW: Cloudflare Workflow binding (site generation)DRIVE_SYNC_WORKFLOW: Resumable Google Drive ingestIMAGE_GENERATION_WORKFLOW: Resumable DALL·E → Stability fallbackSNAPSHOT_QUALITY_WORKFLOW: Screenshot + composition + SEO + a11y matrixSOCIAL_PUBLISH_WORKFLOW: Per-account fan-out for pulse_posts rowsSITE_BUILDER: Durable Object (Cloudflare Container) for Claude Code builds (production only)APP_RUNTIME: Durable Object for installed-app runtime (auto-restart 3/min, idle-30m hibernation)AI: Workers AI binding (Llama 3.3 70B FP8, BGE embeddings)RAG_INDEX: Vectorize binding (768-dim cosine,projectsites-rag). Create vianpx wrangler vectorize create projectsites-rag --dimensions=768 --metric=cosineplus metadata indexes onkindandorgId. Seedocs/DEPLOYMENT.md.AI_GATEWAY_ENABLED(var): when"true"+CF_ACCOUNT_IDset, routes OpenAI + Anthropic viagateway.ai.cloudflare.com/v1/{account}/projectsites/{provider}. Falls back to direct vendor URL on 5xx. Seedocs/AI_INTEGRATION.md.MCP_ENCRYPTION_KEY(secret): AES-GCM master key forai_env_vars+mcp_connections. Tier 1.5 data-at-rest — NEVER rotate without re-encryption.
npm test # 902 unit tests across 49 suites
npm run test:coverage # with coverage
npx playwright test # E2E tests (needs Chromium)Key specs include:
e2e/golden-path.spec.ts— Full user journeye2e/homepage.spec.ts— Homepage sections + auth screense2e/health.spec.ts— Health, CORS, auth gatese2e/site-serving.spec.ts— Serving, security, webhookse2e/inline-editing.spec.ts— Inline site editinge2e/ai-workflow.spec.ts— AI generation workflowe2e/domain-management.spec.ts— Domain provisioninge2e/admin-and-billing.spec.ts— Admin + billing flowse2e/auth-and-signin.spec.ts— Authentication flows- Plus 29 additional spec files covering UI polish, modals, search, etc.
Vito's Mens Salon — 74 N Beverwyck Rd, Lake Hiawatha, NJ 07034
App auth runs through the IdentityProvider port (platform/identity.ts):
- Better Auth (
auth/better-auth.ts) is the DEFAULT consumer-auth IdP (OIDC). - Better Auth (
auth/better-auth.ts) handles ENTERPRISE org-scoped SSO/SAML. getIdentityProvider(env, { enterprise? })(middleware/identity.ts) selects Better Auth for enterprise (whenBETTER_AUTH_*set), else Better Auth (whenBETTER_AUTH_*set), else null → the existing custom auth (magic-link + Google OAuth + D1 sessions inauth.ts) stays the live path.- Login routes (
routes/auth_idp.ts, mounted inindex.ts):GET /api/auth/:provider/login302s to the IdP authorize URL (storing a one-time CSRFstatein KV);GET /api/auth/:provider/callbackverifies+consumes the state, callshandleCallback, then issues the D1 session and 302s tohttps://projectsites.dev/?token=…&auth_callback=<provider>.:provider∈ {betterauth}; Better Auth is the enterprise path. Routes 404 (dark) when the provider's secrets are unset. - After
handleCallbackreturns a verifiedAuthenticatedUser, the EXISTING D1 session machinery (findOrCreateUser→createSession) issues our session — the IdP replaces "how the user proves identity", not the session model. - Ships DARK behind
BETTER_AUTH_*/BETTER_AUTH_*; enabling is a config flip, reversible by unsetting the secret. Activation:docs/runbooks/auth-better-auth.md.
Per-site connections to Mailchimp, Stripe, HubSpot, GitHub, Slack, Notion, Linear, Discord, Google Calendar, Calendly + paste-key fallback for Resend and any provider missing OAuth credentials.
| Method | Path | Behaviour |
|---|---|---|
| GET | /api/mcp/:provider/connect |
Build authorize URL with PKCE state; returns paste-key spec when provider lacks OAuth |
| GET | /api/mcp/:provider/callback |
Exchange code, encrypt + upsert into mcp_connections, redirect to dashboard |
| POST | /api/mcp/:provider/paste |
Paste-key flow for providers without OAuth (Resend, internal webhooks) |
Naming convention going forward: {PROVIDER}_OAUTH_CLIENT_ID + _OAUTH_CLIENT_SECRET.
Historical aliases (MAILCHIMP_CLIENT_ID, HUBSPOT_CLIENT_ID, STRIPE_CONNECT_CLIENT_ID,
GITHUB_CLIENT_ID, GOOGLE_CLIENT_ID) kept as fallbacks so existing secrets keep working.
When ALL configured keys are empty, /connect returns 501 oauth_not_configured so the
UI can render a paste-key form instead of opening a broken popup.
Tables: mcp_oauth_states (one-shot state + code_verifier per pending auth),
mcp_connections (one row per (site_id, provider), upsert on conflict).
- CSP: Homepage uses inline
<script>— CSP MUST include'unsafe-inline'in script-src - MIME bug: Use
marketingPathnotpathfor content-type detection (path='/' has no extension) - console.log blocked: Use
console.warnfor structured JSON logs - Payload format: Frontend sends nested v2 format, backend accepts both v1 (flat) and v2 (nested)
- Queues: Not yet enabled on CF account — binding is optional, code falls back to Workflows
- Jest config: Must be
.cjsnot.js(ESM module type) - Registry KV match: Uses
startsWith('prompt:${id}@')to avoid false partial matches - MCP OAuth fallback —
/api/mcp/:provider/connectreturns501 oauth_not_configuredwhen the provider's{PROVIDER}_OAUTH_CLIENT_IDis missing — surface a paste-key toast, never open a broken popup. - (resolved) The former
ai_admin_features.ts.bakbackup has been removed; the live module issrc/services/ai_admin_features.ts. No.bakfiles remain insrc/. - Zod-validation drift in
src/routes/features.ts(tracked 2026-06-02): the ~33 POST handlers in the "50 experimental features" grab-bag read bodies as(await c.req.json().catch(() => ({}))) as { … }— a TypeScriptas-cast with NO runtime validation, which [[zod-everywhere]] + [[contract-first-ai]] forbid at a boundary. Low live-risk today (every endpoint isrequireFlag-gated on adefault_enabled:falseflag, so they 404 in prod), but each should get a colocated Zod schema +zValidator(orsafeParse+badRequest) before its flag is promoted to beta/stable. Do NOT mass-retrofit blind — convert per-feature as each flag is enabled, with a unit test per endpoint. Sameas-cast pattern exists (smaller) inmedia.ts,env_vars.ts, and the un-validated portion ofai_admin.ts. Verified dormant (re-checked this wave):features.tshas 123 flag-gated handlers; cross-referencing the registry's 12 enabled flags (default_enabled:true or stage beta/stable — all core_ sentinels + speculation_rules / structured_data_autopilot / quotable_answer_block / llms_txt / mcp_server / public_api / cli_tool / accessibility_statement) against the features.ts gated set returns ZERO overlap. So every features.ts handler 404s in prod → the gap has no live exposure; the per-feature-on-promotion defer is correct, NOT a hidden live gap. Mass-retrofit remains forbidden.* - Worker test mock paths — a
jest.mock('…/src/…')insidelibs/features/<slug>/__tests__/needs FOUR../to reachsrc/('../../../../src/…'). Three resolves tolibs/features/→ "Could not locate module" → the WHOLE suite fails to load (not one test). Handlers, one dir up, use three correctly. (Fixed page_audio_summary + generative_ui_stream 2026-06-21.) @swc/jestmock hoisting needs the GLOBALjest— this repo transforms tests with@swc/jest(jest.config.cjs), which only hoistsjest.mock(...)above the imports when it sees the globaljestidentifier.import { jest } from '@jest/globals'leaves thejest.mockcall BELOW the handler import → the real module loads first → the mock silently no-ops (e.g. the realisFlagOnruns and blows up on an empty env). Dropjestfrom the@jest/globalsimport and use the injected global (every passing handler+flag test does). Symptom:mockX.mockResolvedValue is not a functionOR a real-code crash inside a "mocked" dep. Related pitfall: abuildApp(userId = 'x')default meansbuildApp(undefined)re-applies the default — usenullas the explicit "no value" sentinel. (Fixed figma_import 2026-06-21.)
4-screen state machine: search → signin → details → waiting
- Vanilla JS, no framework
- CDN deps: Uppy (file upload), Lottie (animations), Google Fonts (Inter)
- 300ms debounced search, min 2 chars
- Parallel API calls on search:
/api/search/businesses+/api/sites/search
After the initial Worker foundation is complete, every meaningful new product capability MUST be implemented as a feature module under libs/features/<slug>/.
Every feature module must include:
manifest.tswith 7 required fields:slug,name,description,flagKey,owner,stage,createdAt- Typed feature flag row in D1
feature_flagsmigration + KV cache binding - Colocated
schemas.ts(Zod),handlers.ts(Hono routes),service.ts,__tests__/(Jest),e2e/pointer - Playwright E2E specs under
apps/project-sites/e2e/<slug>/ - Sentry breadcrumbs on every critical path — all events carry
{ featureSlug }tag - Structured log lines carry
feature_slugfield so Axiom/Workers Tracing can filter per feature - A section in
libs/features/CATALOG.md(NOT a per-folderREADME.md) with: what it does, flag key, rollout defaults, safe disabled behavior. Project override of the global per-module-README doctrine — one catalog file keeps the tree minimal;validate:featuresgatesmanifest.ts, not READMEs.
Canonical example: libs/features/donations_engine/ (docs in libs/features/CATALOG.md)
Reference docs: docs/architecture/feature-modules.md · docs/architecture/feature-flags.md
See also: root CLAUDE.md § PART 23 + [[feature-flags]] rule
Every post-launch feature is dark-launched behind a typed flag. Ship enabled=0, rollout_percent=0, stage='experimental' always.
Required fields on every flag:
key— snake_case ≤32 chars matchingmanifest.slugdescription— one-sentence human explanationenabled/rollout_percent— 0 at launchstage—experimental | beta | stable | deprecated | killswitchowner_email— promotion gatekeeperrisk_notes— what breaks when disabled
Server guard: if (!await isFlagOn(...)) return 404 — never 403, never leak feature existence.
UI guard: if (!useFeatureFlag('key')) return null.
Flag auto-appears in /admin/feature-flags via migration seed. Admin UI at apps/project-sites/frontend/src/app/pages/admin/feature-flags.component.ts.
Reference impl: apps/project-sites/src/services/feature_flags.ts + libs/core/feature-flags/
Before adding a new major feature to the Worker:
npm run validate:features— surfaces existing drift- Grep
src/routes/for partial implementations that should be colocated in a module - Check
libs/features/*/manifest.ts— does the capability extend an existing module? - New module:
npm run gen:feature -- --slug <name>scaffolds the skeleton - Route handlers for the feature live in
libs/features/<slug>/handlers.ts, registered insrc/index.tsviaapp.route()
Avoid: API handlers scattered directly in src/routes/ without a libs/features/ home.
CI runs npm run validate:features (.github/workflows/feature-architecture.yml) on every push. Drift = merge blocked.
Drift conditions:
libs/features/<slug>/exists butmanifest.tsis missing or has <7 fields- API handler in
src/routes/references a feature with no flag in D1 seed migrations - Feature flag in D1 seeds has no corresponding
e2e/<slug>/directory - Sentry/PostHog events fired without
featureSlugcontext tag - Zod schemas for a feature duplicated in
src/services/instead of colocated inlibs/features/<slug>/schemas.ts e2e/FEATURES.mdrow missing for any module with amanifest.ts
Fix drift immediately — never defer to a follow-up PR.