Plan mode: If you are planning work, this entire skill is ONE plan step: "Invoke /vibes:vibes". Do not decompose the steps below into separate plan tasks.
Display this ASCII art immediately when starting:
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░░▒▓███████▓▒░
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓█▓▒▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓█▓▒▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░░▒▓██████▓▒░ ░▒▓██████▓▒░
░▒▓█▓▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓█▓▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓██▓▒░ ░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░▒▓███████▓▒░
Quick Navigation
- Terminal or Editor - Choose how to build (ask first!)
- Pre-Flight Check - Validate Connect setup before coding
- Core Rules - Essential guidelines for app generation
- Generation Process - Design reasoning and code output
- Assembly Workflow - Build the final app
- UI Style & Theming - OKLCH colors and design patterns
- Fireproof API - Database operations and hooks
- AI Features - Optional AI integration
- Common Mistakes - Avoid these pitfalls
- Deployment Options - Where to deploy
Vibes DIY App Generator
Generate React web applications using Fireproof for local-first data persistence.
Step 0: Terminal or Editor UI?
This is the very first question — ask before anything else. DO NOT check .env, credentials, or project state before asking this question. DO NOT invoke /vibes:connect or any other skill before asking this question. If Editor is chosen, skip ALL pre-flight checks — the editor handles everything.
Ask the user:
"How do you want to build? Editor (opens a browser UI with live preview, chat, and deploy button) or Terminal (I'll generate and deploy from here)?"
Present Editor as the first/recommended option.
-
If Editor: Start the editor server. Resolve the plugin root first, then launch:
PLUGIN_ROOT=$(find ~/.claude/plugins/cache/vibes-cli -name "preview-server.js" -path "*/scripts/*" 2>/dev/null | head -1 | xargs dirname) node "${PLUGIN_ROOT}/preview-server.js" --mode=editor --prompt "USER_PROMPT_HERE"If no prompt was given, omit
--prompt:PLUGIN_ROOT=$(find ~/.claude/plugins/cache/vibes-cli -name "preview-server.js" -path "*/scripts/*" 2>/dev/null | head -1 | xargs dirname) node "${PLUGIN_ROOT}/preview-server.js" --mode=editorTell the user: "Open http://localhost:3333 — the editor handles everything from here: describe your app, preview it live, switch themes, and deploy with one click." Then stop. The editor UI takes over the entire workflow (setup, generation, preview, deploy). Do not continue with the steps below.
-
If Terminal: Continue with the pre-flight check and normal generation workflow below.
Pre-Flight Check: Connect Status
MANDATORY: Complete these steps BEFORE generating any app code.
Run this command first to validate all required credentials:
if test -f "./.env" && \
grep -qE "^VITE_CLERK_PUBLISHABLE_KEY=pk_(test|live)_" ./.env 2>/dev/null && \
grep -qE "^VITE_API_URL=" ./.env 2>/dev/null && \
grep -qE "^VITE_CLOUD_URL=" ./.env 2>/dev/null; then
echo "CONNECT_READY"
else
echo "CONNECT_NOT_READY"
fi
If output is "CONNECT_NOT_READY", Connect setup is required:
Connect with Clerk authentication is required for Vibes apps.
Invoke /vibes:connect to deploy Connect, then return here when complete.
Platform Name vs User Intent: "Vibes" is the name of this app platform (Vibes DIY). When users say "vibe" or "vibes" in their prompt, interpret it as:
- Their project/brand name ("my vibes tracker")
- A positive descriptor ("good vibes app")
- NOT as "mood/atmosphere" literally
Do not default to ambient mood generators, floating orbs, or meditation apps unless explicitly requested.
Import Map Note: The import map points use-fireproof to /fireproof-vibes-bridge.js, a bridge module that wraps the raw Fireproof bundle with sync status forwarding and an onTock kick effect. Your code uses import { useFireproofClerk } from "use-fireproof" and the browser resolves this through the bridge → ./fireproof-clerk-bundle.js. This is intentional—the bridge ensures useLiveQuery subscribers see synced data and that SyncStatusDot gets live sync status via a window global.
Core Rules
- Use JSX - Standard React syntax with Babel transpilation
- Single HTML file - App code assembled into template
- Fireproof for data - Use
useFireproofClerkfor database + sync - Auto-detect Connect - Template handles Clerk auth when Connect is configured
- Tailwind for styling - Mobile-first, responsive design
Generation Process
Step 0.5: Check for Design Reference
If the user provides a reference image (local file path or URL) or a theme.html file alongside their app description:
- Image reference — Read the image (local path via Read tool, or URL via WebFetch). Analyze: extract colors, typography, layout structure, spacing, component patterns. Use these observations to guide your design reasoning and theme selection. Map extracted colors to
--comp-*token values. - theme.html reference — Read the file. Look for a
<!-- VIBES-THEME-META ... -->comment block with pre-selected themes and token values. Use these directly instead of the catalog selection in Step 1.75. - No reference — proceed normally to Step 1.
Step 1: Design Reasoning
Before writing code, reason about the design in <design> tags:
<design>
- What is the core functionality and user flow?
- What OKLCH colors fit this theme? (dark/light, warm/cool, vibrant/muted)
- What layout best serves the content? (cards, list, dashboard, single-focus)
- What micro-interactions would feel satisfying? (hover states, transitions)
- What visual style matches the purpose? (minimal, bold, playful, professional)
</design>
Step 1.5: Read Design Tokens (MANDATORY)
You MUST read this file before generating code:
Read file: ${CLAUDE_PLUGIN_ROOT}/build/design-tokens.txt
The token catalog defines all available CSS custom properties: colors, radius, shadows, spacing, typography, vibes-core, vibes-buttons, vibes-grid. It also includes the VIBES_THEME_CSS with .btn button classes, the grid/frame page styles, and a Component Catalog with bare HTML structures (card, input, badge, table, tabs, accordion, dialog, etc.).
In your generated code:
- ALWAYS wrap your App in a full-page container div with
min-height: 100vhand an explicitbackground-color— never leave the page background transparent or unstyled - Use
var(--token-name)references — NOT hardcoded color values - Use
--color-*for semantic colors,--radius-*for border-radius,--shadow-brutalist-*for neo-brutalist shadows - Use
className="btn"for buttons (pre-styled neo-brutalist) - Use
className="grid-background"on your app's root container for the default content grid background - Pick components from the catalog (card, input, badge, table, etc.), then write CSS for their class names using the design tokens
- Override
--color-*tokens in a:rootstyle block for per-app theming
Step 1.75: Select Theme
Read the theme catalog FIRST (it's small — just descriptions, not full theme files):
Read file: ${CLAUDE_PLUGIN_ROOT}/skills/vibes/themes/catalog.txt
Pick 1 theme based on the app's content type and purpose, using each theme's BEST FOR and NOT FOR lists. If the user explicitly requests a specific theme, always follow their choice.
ONLY THEN read the theme file you selected:
Read file: ${CLAUDE_PLUGIN_ROOT}/skills/vibes/themes/{selected-theme}.txt
Each theme file provides:
- Color token overrides (
:rootvalues — use these exactly, they define the mood) - Design principles (border style, typography, spacing, animation tempo)
- Reference CSS (study the aesthetic, then create your own interpretation)
- Personality notes (how the theme FEELS — guide your creative choices)
- Animation and SVG guidelines
Generate one layout using the selected theme's design principles. Do NOT add useVibesTheme() or theme branching — theme switching is handled by the live preview wrapper, not inside the app.
CREATIVE LIBERTY: Themes are mood boards, not templates. Two apps using the same theme should FEEL related but LOOK different. Use the color tokens exactly (they're the mood identity), follow the design principles, but invent unique layouts, card designs, hover effects, and decorative elements for each app. The reference CSS is ONE interpretation — don't copy it verbatim.
Step 1.9: Generate Design Preview (OPTIONAL)
Ask [Preview]: "Want to preview the design as a standalone HTML page before I build the app?"
- "Yes" → Generate
theme.html(see below), open in browser, iterate until the user is happy, then proceed to Step 2 - "No" → Skip directly to Step 2
If the user says yes, generate a standalone theme.html — a self-contained static page that demonstrates the visual design without React, Fireproof, or Clerk:
- Single HTML file with inline
<style>and<script>. No external dependencies except Google Fonts via@import. - CSS custom properties using
--comp-*token overrides from the selected theme. - Realistic placeholder content matching the app description (not lorem ipsum).
- Interactive elements — tabs switch, buttons have hover/active states, forms accept input. Wire with vanilla JS.
- Animations and inline SVGs following the theme's ANIMATIONS and SVG ELEMENTS guidelines.
- Mobile-responsive with
@mediabreakpoints.
Embed a metadata comment at the top for downstream reference:
<!-- VIBES-THEME-META
source: prompt
mood: "{theme mood}"
theme: "{theme-id}"
tokens: { "--comp-bg": "oklch(...)", "--comp-accent": "oklch(...)" }
layout: "{layout-type}"
-->
Write to ./theme.html. The user can open it in a browser, request changes, and iterate. When they're satisfied, proceed to Step 2 — use the design decisions from the preview to guide app.jsx generation.
Assembly: generate (preserve) —
assemble.jsinjects your code as-is. Import and export statements work because the import map intercepts bare specifiers at runtime. Code examples below include imports.If you're a launch/builder agent: Sell transforms vibes artifacts by stripping imports. When generating app.jsx for the launch pipeline, omit all imports — the sell template provides everything. Follow builder.md rules; use only the patterns from examples below, not the import lines.
Step 2: Output Code
After reasoning, output the complete JSX in <code> tags.
Theme Section Markers: Organize all theme-sensitive CSS and JSX into marked sections. This enables fast, targeted theme switching.
<code>
import React, { useState } from "react";
import { useFireproofClerk } from "use-fireproof";
const STYLE = `
/* @theme:tokens */
:root {
--comp-bg: oklch(0.15 0.02 280);
--comp-text: oklch(0.93 0.02 80);
--comp-accent: oklch(0.72 0.15 75);
}
/* @theme:tokens:end */
/* @theme:typography */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
/* @theme:typography:end */
/* @theme:surfaces */
.card-glass { backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.1); }
/* @theme:surfaces:end */
/* @theme:motion */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
/* @theme:motion:end */
/* Non-theme layout (outside markers) */
.app-grid { display: grid; gap: 1rem; }
`;
export default function App() {
const { database, useLiveQuery, useDocument, syncStatus } = useFireproofClerk("app-name-db");
// ... component logic
return (
<>
<style>{STYLE}</style>
<div className="min-h-screen bg-[var(--comp-bg)] text-[var(--comp-text)] p-4">
{/* @theme:decoration */}
<svg className="atmospheric-bg">...</svg>
{/* @theme:decoration:end */}
{/* App content (not theme-sensitive) */}
<div className="app-grid">...</div>
</div>
</>
);
}
</code>
Section rules:
@theme:tokens—:rootCSS variables (colors, spacing tokens)@theme:typography—@importfont URLs,font-familyrules@theme:surfaces— Shadows, borders, glass effects, gradient backgrounds@theme:motion—@keyframesand animation definitions@theme:decoration— SVG elements, atmospheric backgrounds (in JSX)- Everything else (layout, structure, logic) stays outside markers
⚠️ CRITICAL: Fireproof Hook Pattern
The @necrodome/fireproof-clerk package exports ONLY useFireproofClerk. Always use this pattern:
// ✅ CORRECT - This is the ONLY pattern that works
import { useFireproofClerk } from "use-fireproof";
const { database, useDocument, useLiveQuery, syncStatus } = useFireproofClerk("my-db");
const { doc, merge } = useDocument({ _id: "doc1" });
// ❌ WRONG - DO NOT USE (old use-vibes API)
import { toCloud, useFireproof } from "use-fireproof"; // WRONG - old API
import { useDocument } from "use-fireproof"; // WRONG - standalone import
const { attach } = useFireproof("db", { attach: toCloud() }); // WRONG - old pattern
Sync Status: syncStatus provides the current sync state. Values: "idle", "connecting", "synced", "reconnecting", "error". Display it for user feedback.
Connect Configuration: Generated apps require Clerk authentication and cloud sync.
The assemble.js script populates window.__VIBES_CONFIG__ from your .env file.
Apps will show a configuration error if credentials are missing.
Assembly Workflow
-
Extract the code from
<code>tags and write toapp.jsx -
Optionally save
<design>content todesign.mdfor documentation -
Ask [Preview]: "Want to preview the app before deploying?"
- "Yes — open live preview" — Start the preview server for iterating on the design
- "No — deploy now" — Skip preview, go straight to deploy
If yes: run
node "${CLAUDE_PLUGIN_ROOT}/scripts/preview-server.js"and tell the user to openhttp://localhost:3333. They can chat to iterate on the design and switch themes. When satisfied, stop the server and continue. -
Run assembly:
node "${CLAUDE_PLUGIN_ROOT}/scripts/assemble.js" app.jsx index.html -
Deploy the app so the user can see it. Clerk auth requires a public URL — the app cannot be viewed locally. Auto-invoke /vibes:cloudflare to deploy, then present the live URL.
⚠️ DEPRECATED API: Never use the old
useFireproofwithtoCloud()pattern. See references/DEPRECATED.md for migration details if you encounter legacy code.
UI Style & Theming
OKLCH Colors (Recommended)
Use OKLCH for predictable, vibrant colors. Unlike RGB/HSL, OKLCH has perceptual lightness - changing L by 10% looks the same across all hues.
oklch(L C H)
/* L = Lightness (0-1): 0 black, 1 white */
/* C = Chroma (0-0.4): 0 gray, higher = more saturated */
/* H = Hue (0-360): color wheel degrees */
Theme-appropriate palettes:
{/* Dark/moody theme */}
className="bg-[oklch(0.15_0.02_250)]" /* Deep blue-black */
{/* Warm/cozy theme */}
className="bg-[oklch(0.25_0.08_30)]" /* Warm brown */
{/* Fresh/bright theme */}
className="bg-[oklch(0.95_0.03_150)]" /* Mint white */
{/* Vibrant accent */}
className="bg-[oklch(0.7_0.2_145)]" /* Vivid green */
Better Gradients with OKLCH
Use in oklch for smooth gradients without muddy middle zones:
{/* Smooth gradient - no gray middle */}
className="bg-[linear-gradient(in_oklch,oklch(0.6_0.2_250),oklch(0.6_0.2_150))]"
{/* Sunset gradient */}
className="bg-[linear-gradient(135deg_in_oklch,oklch(0.7_0.25_30),oklch(0.5_0.2_330))]"
{/* Dark glass effect */}
className="bg-[linear-gradient(180deg_in_oklch,oklch(0.2_0.05_270),oklch(0.1_0.02_250))]"
Neobrute Style (Optional)
For bold, graphic UI:
- Borders: thick 4px, dark
border-[#0f172a] - Shadows: hard offset
shadow-[6px_6px_0px_#0f172a] - Corners: square (0px) OR pill (rounded-full) - no in-between
<button className="px-6 py-3 bg-[oklch(0.95_0.02_90)] border-4 border-[#0f172a] shadow-[6px_6px_0px_#0f172a] hover:shadow-[4px_4px_0px_#0f172a] font-bold">
Click Me
</button>
Glass Morphism (Dark themes)
<div className="bg-white/5 backdrop-blur-lg border border-white/10 rounded-2xl">
{/* content */}
</div>
Color Modifications
Lighten/darken using L value:
- Hover: increase L by 0.05-0.1
- Active/pressed: decrease L by 0.05
- Disabled: reduce C to near 0
Fireproof API
Fireproof is a local-first database - no loading or error states required, just empty data states. Data persists across sessions and syncs in real-time when Connect is configured.
Setup
import { useFireproofClerk } from "use-fireproof";
const { database, useLiveQuery, useDocument, syncStatus } = useFireproofClerk("my-app-db");
Note: When Connect is configured (via .env), the template wraps your App in ClerkFireproofProvider, enabling authenticated cloud sync automatically. Your code just uses useFireproofClerk.
Choosing Your Pattern
useDocument = Form-like editing. Accumulate changes with merge(), then save with submit() or save(). Best for: text inputs, multi-field forms, editing workflows.
database.put() + useLiveQuery = Immediate state changes. Each action writes directly. Best for: counters, toggles, buttons, any single-action updates.
// FORM PATTERN: User types, then submits
const { doc, merge, submit } = useDocument({ title: "", body: "", type: "post" });
// merge({ title: "..." }) on each keystroke, submit() when done
// IMMEDIATE PATTERN: Each click is a complete action
const { docs } = useLiveQuery("_id", { key: "counter" });
const count = docs[0]?.value || 0;
const increment = () => database.put({ _id: "counter", value: count + 1 });
WARNING — merge() + submit() timing trap: Never merge() a computed value (like Date.now() or crypto.randomUUID()) and call submit() in the same event handler. React batches state updates, so submit() reads stale state and the merged field may be missing from the saved document. Use database.put() with explicit fields instead:
// BAD — ts may not be saved due to React batching
merge({ ts: Date.now() });
submit();
// GOOD — all fields written atomically
await database.put({ text: doc.text, ts: Date.now(), type: "item" });
reset();
useDocument - Form State (NOT useState)
IMPORTANT: Don't use useState() for form data. Use merge() and submit() from useDocument. Only use useState for ephemeral UI state (active tabs, open/closed panels).
// Create new documents (auto-generated _id recommended)
const { doc, merge, submit, reset } = useDocument({ text: "", type: "item" });
// Edit existing document by known _id
const { doc, merge, save } = useDocument({ _id: "user-profile:abc@example.com" });
// Methods:
// - merge(updates) - update fields: merge({ text: "new value" })
// - submit(e) - save + reset (for forms creating new items)
// - save() - save without reset (for editing existing items)
// - reset() - discard changes
useLiveQuery - Real-time Lists
// Simple: query by field value
const { docs } = useLiveQuery("type", { key: "item" });
// Recent items (_id is roughly temporal - great for simple sorting)
const { docs } = useLiveQuery("_id", { descending: true, limit: 100 });
// Range query
const { docs } = useLiveQuery("rating", { range: [3, 5] });
CRITICAL: Custom index functions are SANDBOXED and CANNOT access external variables. Query all, filter in render:
// GOOD: Query all, filter in render
const { docs: allItems } = useLiveQuery("type", { key: "item" });
const filtered = allItems.filter(d => d.category === selectedCategory);
Direct Database Operations
// Create/update
const { id } = await database.put({ text: "hello", type: "item" });
// Delete
await database.del(item._id);
Common Pattern - Form + List
import React from "react";
import { useFireproofClerk } from "use-fireproof";
export default function App() {
const { database, useLiveQuery, useDocument, syncStatus } = useFireproofClerk("my-db");
// Form for new items (submit resets for next entry)
const { doc, merge, submit } = useDocument({ text: "", type: "item" });
// Live list of all items of type "item"
const { docs } = useLiveQuery("type", { key: "item" });
return (
<div className="min-h-screen bg-[var(--app-bg)] text-[var(--app-text)] p-4">
{/* Optional sync status indicator */}
<div className="text-xs text-gray-500 mb-2">Sync: {syncStatus}</div>
<form onSubmit={submit} className="mb-4">
<input
value={doc.text}
onChange={(e) => merge({ text: e.target.value })}
className="w-full px-4 py-3 border-4 border-[var(--app-border)]"
/>
<button type="submit" className="mt-2 px-4 py-2 bg-[var(--app-accent)] text-white hover:bg-[var(--app-accent-hover)]">
Add
</button>
</form>
{docs.map(item => (
<div key={item._id} className="p-2 mb-2 bg-[var(--app-surface)] border-4 border-[var(--app-border)]">
{item.text}
<button onClick={() => database.del(item._id)} className="ml-2 text-[var(--vibes-red-accent)]">
Delete
</button>
</div>
))}
</div>
);
}
Common Pattern - Demo Data & Empty States
Every generated app should include a "Load Demo Data" button visible only when the database is empty. This lets users immediately see the app working with realistic data instead of staring at a blank screen.
import React from "react";
import { useFireproofClerk } from "use-fireproof";
export default function App() {
const { database, useLiveQuery, useDocument, syncStatus } = useFireproofClerk("my-db");
const { doc, merge, submit } = useDocument({ title: "", priority: "medium", type: "task" });
const { docs } = useLiveQuery("type", { key: "task" });
const seedDemo = async () => {
if (docs.length > 0) return; // guard: only seed when empty
await database.put({ title: "Design landing page", priority: "high", done: false, type: "task" });
await database.put({ title: "Write API documentation", priority: "medium", done: false, type: "task" });
await database.put({ title: "Fix mobile nav overflow", priority: "high", done: true, type: "task" });
await database.put({ title: "Add dark mode toggle", priority: "low", done: false, type: "task" });
};
return (
<div className="min-h-screen bg-[var(--app-bg)] text-[var(--app-text)] p-4">
{docs.length === 0 && (
<button
onClick={seedDemo}
className="mb-4 px-4 py-2 bg-[var(--app-accent)] text-white rounded hover:bg-[var(--app-accent-hover)]"
>
Load Demo Data
</button>
)}
{/* ... rest of app */}
</div>
);
}
Rules:
- Demo data names/content must be plausible for the app's domain (not "test1", "test2")
- Use
database.put()directly (notmerge/submit) — this is batch creation - Guard condition (
docs.length > 0) and render condition (docs.length === 0) must match - Seed 3–5 documents with enough variety to populate all views and demonstrate features
- If the app has related document types, seed them with cross-references using returned
idvalues:const { id: projectId } = await database.put({ name: "Website Redesign", type: "project" }); await database.put({ title: "Update hero section", projectId, type: "task" });
AI Features (Optional)
If the user's prompt suggests AI-powered features (chatbot, summarization, content generation, etc.), the app needs AI capabilities via the useAI hook.
Detecting AI Requirements
Look for these patterns in the user's prompt:
- "chatbot", "chat with AI", "ask AI"
- "summarize", "generate", "write", "create content"
- "analyze", "classify", "recommend"
- "AI-powered", "intelligent", "smart" (in context of features)
Collecting OpenRouter Key
When AI is needed, ask the user:
This app needs AI capabilities. Please provide your OpenRouter API key. Get one at: https://openrouter.ai/keys
Store the key for use with the --ai-key flag during deployment.
Using the useAI Hook
The useAI hook is automatically included in the template when AI features are detected:
import React from "react";
import { useFireproofClerk } from "use-fireproof";
export default function App() {
const { database, useLiveQuery, syncStatus } = useFireproofClerk("ai-chat-db");
const { callAI, loading, error } = useAI();
const handleSend = async (message) => {
// Save user message
await database.put({ role: "user", content: message, type: "message" });
// Call AI
const response = await callAI({
model: "anthropic/claude-sonnet-4",
messages: [{ role: "user", content: message }]
});
// Save AI response
const aiMessage = response.choices[0].message.content;
await database.put({ role: "assistant", content: aiMessage, type: "message" });
};
// Handle limit exceeded
if (error?.code === 'LIMIT_EXCEEDED') {
return (
<div className="p-4 bg-amber-100 text-amber-800 rounded">
AI usage limit reached. Please wait for monthly reset or upgrade your plan.
</div>
);
}
// ... rest of UI
}
useAI API
const { callAI, loading, error, clearError } = useAI();
// callAI options
await callAI({
model: "anthropic/claude-sonnet-4", // or other OpenRouter models
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Hello!" }
],
temperature: 0.7, // optional
max_tokens: 1000 // optional
});
// error structure
error = {
code: "LIMIT_EXCEEDED" | "API_ERROR" | "NETWORK_ERROR",
message: "Human-readable error message"
}
Deployment with AI
When deploying AI-enabled apps, include the OpenRouter key:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-cloudflare.js" \
--name myapp \
--file index.html \
--ai-key "sk-or-v1-your-key"
Sharing / Inviting Users
The template includes a built-in invite UI in VibesPanel (the slide-out menu). For custom sharing in user app code, use the useSharing hook:
const { inviteUser, listInvites, deleteInvite, findUser, ready } = window.useSharing();
// Invite by email
async function handleInvite(email) {
if (!ready) return;
const result = await inviteUser(email, 'read'); // 'read' or 'write'
console.log('Invited:', result);
}
The hook is available on window.useSharing after Clerk loads. Check ready before calling methods.
Common Mistakes to Avoid
- DON'T use
useStatefor form fields - useuseDocument - DON'T use
Fireproof.fireproof()- useuseFireproofClerk()hook - DON'T use the old
useFireproofwithtoCloud()- useuseFireproofClerkinstead - DON'T use white text on light backgrounds
- DON'T use
call-aidirectly - useuseAIhook instead (it handles proxying and limits) - DON'T use Fireproof's
_filesAPI for images — it has a sync bug where blobs arrive after metadata, causing 404s on other devices. Store image data as Uint8Array directly on documents:// Convert file to Uint8Array (with resize) async function fileToImageData(file, maxDim = 1200) { const bitmap = await createImageBitmap(file); const scale = Math.min(1, maxDim / Math.max(bitmap.width, bitmap.height)); const canvas = new OffscreenCanvas(bitmap.width * scale, bitmap.height * scale); canvas.getContext('2d').drawImage(bitmap, 0, 0, canvas.width, canvas.height); const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 }); return new Uint8Array(await blob.arrayBuffer()); } // Display from Uint8Array function StoredImage({ data, type = 'image/jpeg', alt, className }) { const [url, setUrl] = useState(null); useEffect(() => { if (!data) return; // Fireproof CBOR round-trips Uint8Array as plain objects with numeric keys const bytes = data instanceof Uint8Array ? data : new Uint8Array(Object.values(data)); const objectUrl = URL.createObjectURL(new Blob([bytes], { type })); setUrl(objectUrl); return () => URL.revokeObjectURL(objectUrl); }, [data, type]); return url ? <img src={url} alt={alt} className={className} /> : null; } // Usage: <StoredImage data={doc.imageData} type={doc.imageType} alt="Photo" /> - DON'T call
merge()andsubmit()in the same handler when adding computed fields (timestamps, UUIDs, derived values). React batches the state update frommerge(), sosubmit()writes the old state. Usedatabase.put()with explicit fields +reset()instead. - DON'T spread
useDocumentdoc intodatabase.put()— internal CRDT metadata contaminates the write and can corrupt the database (missing blockerrors). Build documents with explicit fields instead:// BAD — spreads internal metadata await database.put({ ...doc, completed: true }); // GOOD — explicit fields only await database.put({ _id: doc._id, type: doc.type, todo: doc.todo, completed: true }); - DON'T wrap your app in
VibeContextProvider- that's a vibes.diy platform-only component. Standalone apps useuseFireproofClerk()directly. - DON'T panic if you see "Cannot read properties of null (reading 'useContext')" - the template already handles the React singleton via
?external=react,react-domin the import map. Check that the import map wasn't accidentally modified. - NOTE: Apps use
/fireproof-vibes-bridge.js— this bridge module wraps the local Fireproof bundle with sync status forwarding + onTock kick. The bundle itself (/fireproof-clerk-bundle.js) is a temporary workaround that fixes a CID bug and includes sync improvements. Apps work correctly with it. - DON'T hand-write
app.jsxand assemble it manually — always generate through/vibes:vibes, even for test or diagnostic apps. The skill generates code that's compatible with the template by construction. Hand-written code may include imports or patterns that conflict with the template's runtime setup.
When to Read Extended Docs
The shipped default files contain detailed reference material. Read them when the user's prompt matches these signals:
| Need | Signal in Prompt | Read This |
|------|------------------|-----------|
| Design tokens & theming | colors, theme, tokens, brand colors, styling | ${CLAUDE_PLUGIN_ROOT}/build/design-tokens.txt |
| File uploads | "upload", "images", "photos", "attachments" | ${CLAUDE_PLUGIN_ROOT}/docs/fireproof.txt → "Working with Images" |
| Auth / sync config | "Clerk", "Connect", "cloud sync", "login" | ${CLAUDE_PLUGIN_ROOT}/docs/fireproof.txt → "ClerkFireproofProvider Config" |
| Sync status display | "online/offline", "connection status" | ${CLAUDE_PLUGIN_ROOT}/docs/fireproof.txt → "Sync Status Display" |
| Full Neobrute design details | detailed design system, spacing, typography | ${CLAUDE_PLUGIN_ROOT}/skills/vibes/defaults/style-prompt.txt |
| Advanced visual effects | "interactive", "animated", "3D", "particles", "shader", "canvas" | ${CLAUDE_PLUGIN_ROOT}/skills/vibes/defaults/advanced-effects-prompt.txt |
Deployment Options
After generating your app, deploy it:
- Cloudflare - Edge deployment with Workers. Use
/vibes:cloudflareto deploy.
What's Next?
After generating and assembling the app, present these options using AskUserQuestion:
Question: "Your app is live! Want to turn it into a product? The /sell skill adds multi-tenant SaaS with auth and billing. Or pick another direction:"
Header: "Next"
Options:
- Label: "Keep improving this app"
Description: "Continue iterating on what you've built. Add new features, refine the styling, or adjust functionality. Great when you have a clear vision and want to polish it further."
- Label: "Apply a design reference (/design-reference)"
Description: "Have a design.html or mockup file? This skill mechanically transforms your app to match it exactly - pixel-perfect fidelity with your Fireproof data binding preserved."
- Label: "Explore variations (/riff)"
Description: "Not sure if this is the best approach? Riff generates 3-10 completely different interpretations of your idea in parallel. You'll get ranked variations with business model analysis to help you pick the winner."
- Label: "Make it a SaaS (/sell)"
Description: "Ready to monetize? Sell transforms your app into a multi-tenant SaaS with Clerk authentication, subscription billing, and isolated databases per customer. Each user gets their own subdomain."
- Label: "Deploy to Cloudflare (/cloudflare)"
Description: "Go live on the edge. Deploy to Cloudflare Workers with a subdomain registry, KV storage, and global CDN. Fast, scalable, and always on."
- Label: "I'm done for now"
Description: "Wrap up this session. Your files are saved locally - come back anytime to continue."
After user responds:
- "Keep improving" → Acknowledge and stay ready for iteration prompts. After each round of changes to app.jsx, re-run assembly and re-deploy.
- "Apply a design reference" → Auto-invoke /vibes:design-reference skill
- "Explore variations" → Auto-invoke /vibes:riff skill
- "Make it a SaaS" → Auto-invoke /vibes:sell skill
- "Deploy" → Auto-invoke /vibes:cloudflare skill
- "I'm done" → Confirm files saved, wish them well
Do NOT proceed to code generation until: Connect setup is complete with valid Clerk credentials in .env (pre-flight check returns CONNECT_READY).