---
name: sell
argument-hint: "[app-name]"
description: Self-contained SaaS automation — invoke directly, do not decompose. Transforms a Vibes app into a multi-tenant SaaS with subdomain-based tenancy. Adds Pocket ID authentication, subscription gating, and generates a unified app with landing page, tenant routing, and admin dashboard. Use when the user wants to monetize an app, add auth and billing, create a SaaS product, add subscriptions, or turn an app into a business.
license: MIT
allowed-tools: Read, Write, Bash, Glob, AskUserQuestion
metadata:
author: "Marcus Estes"
---
> **Plan mode**: If you are planning work, this entire skill is ONE plan step: "Invoke /vibes:sell". Do not decompose the steps below into separate plan tasks.
**Display this ASCII art immediately when starting:**
```
░▒▓███████▓▒░▒▓████████▓▒░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓██████▓▒░░▒▓██████▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓███████▓▒░░▒▓████████▓▒░▒▓████████▓▒░▒▓████████▓▒░
```
## Quick Navigation
- [Critical Rules](#-critical-rules---read-first-) - Read this first
- [Step 1: Pre-Flight Checks](#step-1-pre-flight-checks) - Verify prerequisites
- [Step 2: App Identity](#step-2-app-identity) - Set up app name and deploy URL
- [Step 3: App Configuration](#step-3-app-configuration) - Collect app settings
- [Step 4: Assembly](#step-4-assembly) - Build the unified app
- [Step 5: Deployment](#step-5-deployment) - Deploy to Cloudflare Workers
- [Step 6: Post-Deploy Verification](#step-6-post-deploy-verification) - Confirm everything works
- [Key Components](#key-components) - Routing, TenantContext, SubscriptionGate
- [Troubleshooting](#troubleshooting) - Common issues and fixes
---
> **Assembly: transform (strip)** — `assemble-sell.js` receives a vibes-generated app.jsx and adapts it for the sell template. It strips `import` statements, `export default`, React destructuring, and template constants — because the sell template already provides all of these. All dependencies (`React`, TinyBase hooks, `useTenant`, `useState`, etc.) are available as globals.
## ⛔ CRITICAL RULES - READ FIRST ⛔
**DO NOT generate code manually.** This skill uses pre-built scripts:
| Step | Script | What it does |
|------|--------|--------------|
| Assembly | `assemble-sell.js` | Generates unified index.html |
| Deploy | `deploy-cloudflare.js` | Deploys to Cloudflare Workers with registry |
**Script location:**
```bash
VIBES_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "${CLAUDE_SKILL_DIR}")")}"
bun "$VIBES_ROOT/scripts/assemble-sell.js" ...
bun "$VIBES_ROOT/scripts/deploy-cloudflare.js" ...
```
**NEVER do these manually:**
- ❌ Write HTML/JSX for landing page, tenant app, or admin dashboard
- ❌ Generate routing logic or authentication code
**ALWAYS do these:**
- ✅ Complete pre-flight checks before starting
- ✅ Run `assemble-sell.js` to generate the unified app
- ✅ Deploy with `deploy-cloudflare.js`
---
# Sell - Transform Vibes to SaaS
This skill uses `assemble-sell.js` to inject the user's app into a pre-built template. The template contains security checks, Pocket ID auth integration, and TinyBase data patterns.
Convert your Vibes app into a multi-tenant SaaS product with:
- Subdomain-based tenancy (alice.yourdomain.com)
- Pocket ID authentication (with passkeys, automatic on deploy)
- Subscription gating (Stripe billing is phase 2)
- Per-tenant data isolation via TinyBase rooms (Durable Objects)
- Marketing landing page
- Admin dashboard
## Architecture
The sell skill generates a **single index.html** file that handles all routes via client-side subdomain detection:
```
yourdomain.com → Landing page
*.yourdomain.com → Tenant app with auth
admin.yourdomain.com → Admin dashboard
```
This approach simplifies deployment - you upload one file and it handles everything.
---
### Terminal or Editor UI?
Detect whether you're running in a terminal (Claude Code CLI) or an editor (Cursor, Windsurf, VS Code with Copilot). **Terminal agents** use `AskUserQuestion` for all input. **Editor agents** present requirements as a checklist comment, wait for user edits, then proceed. See the vibes skill for the full detection and interaction pattern.
## Step 1: Pre-Flight Checks
**Before starting, verify these prerequisites. STOP if any check fails.**
### 1.1 Auth Check
Auth is automatic — on first deploy, a browser window opens for Pocket ID login. Tokens are cached at `~/.vibes/auth.json` for subsequent deploys. No `.env` credential setup is needed.
### 1.2 Detect Existing App
```bash
ls -la app.jsx 2>/dev/null || echo "NOT_FOUND"
```
**If output shows `NOT_FOUND`:**
Check for riff directories:
```bash
ls -d riff-* 2>/dev/null
```
**Decision tree:**
- Found `app.jsx` → Proceed to Step 2
- Found multiple `riff-*/app.jsx` → Ask user to select one, then copy to `app.jsx`
- Found nothing → Tell user to run `/vibes:vibes` first
**STOP HERE** if no app exists. The sell skill transforms existing apps.
### 1.3 Pre-Flight Summary
After checks pass, confirm:
> "Pre-flight checks passed:
> - ✓ App found (app.jsx)
> - ✓ Auth is automatic via Pocket ID (browser login on first deploy)
>
> Now let's configure your app settings."
---
## Step 2: App Identity
### 2.1 Collect App Name (Needed for Deploy URL)
Collect the app name for deployment.
Use AskUserQuestion:
```
Question: "What should we call this app?"
Header: "App Name"
Options: Provide 2 suggestions based on context + user enters via "Other"
Description: "Used for database naming and deployment URL (e.g., 'wedding-photos')"
multiSelect: false
```
Store as `appName` (URL-safe slug: lowercase, hyphens, no special chars).
The app will be deployed via the Deploy API (`/vibes:cloudflare`), which assigns the domain automatically. Store `{appName}.vibes.diy` as `domain`.
---
## Step 3: App Configuration
**Use AskUserQuestion to collect all config in 2 batches.**
### Batch 1: Core Identity
App name and deploy domain were already resolved in Step 2.2. Custom domains can be configured later (Step 5.2).
Use the AskUserQuestion tool with these 2 questions:
```
Question 1: "Do you want to require paid subscriptions?"
Header: "Billing"
Options: ["No - free access for all", "Yes - subscription required"]
Description: "Billing via Stripe is planned for phase 2. Choose 'No' for now unless you have a custom Stripe integration."
Question 2: "Display title for your app?"
Header: "Title"
Options: Suggest based on app name + user enters via "Other"
Description: "Shown in headers and landing page"
```
### Batch 2: Customization
**When billing is enabled** (`billingMode === "required"`): These fields appear on a pricing section visible to potential customers before signup. Write them as marketing copy — benefit-driven, not technical.
Use the AskUserQuestion tool with these 3 questions:
```
Question 1: "Tagline for the landing page headline?"
Header: "Tagline"
Options: Generate 2 suggestions based on app context + user enters via "Other"
Description: "Bold headline text. Can include
for line breaks (e.g., 'SHARE YOUR DAY.
MAKE IT SPECIAL.'). When billing is on, this is the sales headline — make it benefit-driven."
Question 2: "Subtitle text below the tagline?"
Header: "Subtitle"
Options: Generate 2 suggestions based on app context + user enters via "Other"
Description: "Explanatory text below the headline (e.g., 'The easiest way to share wedding photos with guests.'). When billing is on, this is the value proposition — answer 'why should I pay?'"
Question 3: "What features should we highlight on the landing page?"
Header: "Features"
Options: User enters via "Other"
Description: "Comma-separated list (e.g., 'Photo sharing, Guest uploads, Live gallery'). When billing is on, these appear as a visual checklist on the pricing section. Each should be a compelling benefit statement, not technical jargon. Aim for 3-5 items."
```
### After Receiving Answers
1. Domain is `{domain}` (resolved in Step 2.2). Custom domains can be added post-deploy (Step 5.2).
2. Admin User IDs default to empty (configured after first deploy - see Step 6)
3. **Proceed immediately to Step 4 (Assembly)**
### Config Values Reference
| Config | Script Flag | Example |
|--------|-------------|---------|
| App Name | `--app-name` | `wedding-photos` |
| Domain | `--domain` | `myapp.marcus-e.workers.dev` |
| Billing | `--billing-mode` | `off` or `required` |
| Title | `--app-title` | `Wedding Photos` |
| Tagline | `--tagline` | `SHARE YOUR DAY.
MAKE IT SPECIAL.` |
| Subtitle | `--subtitle` | `The easiest way to share wedding photos with guests.` |
| Features | `--features` | `'["Feature 1","Feature 2"]'` |
| Admin IDs | `--admin-ids` | `'["user_xxx"]'` (default: `'[]'`) |
---
## Step 4: Assembly
**CRITICAL**: You MUST use the assembly script. Do NOT generate your own HTML/JSX code.
### 4.1 Auth Note
Auth is automatic via Pocket ID — no `.env` credential setup is needed. On first deploy, a browser window opens for login. Tokens are cached at `~/.vibes/auth.json`.
### 4.2 Update App for Tenant Context
The user's app needs to use `useTenant()` for tenant-scoped data. TinyBase uses a single store per app with rooms via Durable Objects for multi-tenant isolation — no database name parameter is needed.
```jsx
// TinyBase hooks are globals — no initialization call needed.
// useTenant() provides tenant context for routing/display.
const { subdomain } = useTenant();
// Use TinyBase hooks directly:
const rowIds = useRowIds('items');
```
`useTenant()` is a **template global** (injected by AppWrapper in the sell template), NOT an importable module. Call it directly — do NOT write `import { useTenant } from ...` anywhere in app.jsx.
**Template-Provided Globals — do NOT redeclare these in app.jsx:**
| Category | Globals |
|----------|---------|
| React | `React`, `useState`, `useEffect`, `useRef`, `useCallback`, `useMemo`, `createContext`, `useContext` |
| Template utilities | `useTenant`, `useMobile`, `useIsMobile` |
| UI components | `HiddenMenuWrapper`, `VibesSwitch`, `VibesButton`, `VibesPanel`, `BrutalistCard`, `LabelContainer`, `AuthScreen` |
| Color constants | `BLUE`, `RED`, `YELLOW`, `GRAY` |
Do NOT destructure from React (e.g., `const { useState } = React;`) or import React hooks — they are already in scope from the template.
### 4.3 Run Assembly Script
Before running assembly, check the project `.env` for a cached admin user ID:
```bash
grep ADMIN_USER_ID .env 2>/dev/null
```
**If found**, offer to include it (mask the middle, e.g., `user_37ici...ohcY`):
```
AskUserQuestion:
Question: "Include stored admin user ID in this deploy? (user_37ici...ohcY)"
Header: "Admin"
Options:
- Label: "Yes, include"
Description: "Pass --admin-ids with the cached user ID"
- Label: "No, skip admin"
Description: "Deploy without admin access (can add later in Step 6)"
- Label: "Enter different"
Description: "I'll paste a different user ID"
```
If "Yes, include": pass `--admin-ids '[""]'`. If "Enter different": collect new ID, save to `.env`, then pass it. If "No, skip admin": pass `--admin-ids '[]'`.
**If not found**: use `--admin-ids '[]'` (admin setup happens post-deploy in Step 6.4).
Run the assembly script with all collected values:
```bash
VIBES_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "${CLAUDE_SKILL_DIR}")")}"
bun "$VIBES_ROOT/scripts/assemble-sell.js" app.jsx index.html \
--app-name "wedding-photos" \
--app-title "Wedding Photos" \
--domain "{domain}" \
--tagline "SHARE YOUR DAY.
MAKE IT SPECIAL." \
--subtitle "The easiest way to share wedding photos with guests." \
--billing-mode "off" \
--features '["Photo sharing","Guest uploads","Live gallery"]' \
--admin-ids '[]'
```
### 4.4 Validation Gate: Check for Placeholders
After assembly, verify no config placeholders remain:
```bash
grep -o '__VITE_[A-Z_]*__' index.html | sort -u || echo "NO_PLACEHOLDERS"
```
**If any placeholders found:** Re-run assembly with the correct flags. Auth credentials are managed automatically — no `.env` setup needed.
### 4.5 Customize Landing Page Theme (Optional)
The template uses neutral colors by default. To match the user's brand:
```css
:root {
--landing-accent: #0f172a; /* Primary button/text color */
--landing-accent-hover: #1e293b; /* Hover state */
}
```
**Examples based on prompt style:**
- Wedding app → `--landing-accent: #d4a574;` (warm gold)
- Tech startup → `--landing-accent: #6366f1;` (vibrant indigo)
- Health/wellness → `--landing-accent: #10b981;` (fresh green)
---
## Step 5: Deployment
**Deploy Target: Cloudflare Workers.** SaaS apps always deploy to Cloudflare Workers. The KV registry and subdomain routing require the CF Worker runtime.
### 5.1 Deploy to Cloudflare Workers
```bash
VIBES_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "${CLAUDE_SKILL_DIR}")")}"
bun "$VIBES_ROOT/scripts/deploy-cloudflare.js" \
--name wedding-photos \
--file index.html
```
On first deploy, a browser window opens for Pocket ID authentication. Tokens are cached at `~/.vibes/auth.json` for subsequent deploys.
### 5.2 DNS Configuration (For Custom Domains)
The app is immediately available at `{appName}.{subdomain}.workers.dev`. For a custom domain:
1. In the Cloudflare dashboard, go to **Workers & Pages** → your worker → **Settings** → **Domains & Routes**
2. Add a custom domain (e.g., `cosmicgarden.app`)
3. For wildcard subdomains (e.g., `*.cosmicgarden.app`), add a wildcard route
**Note:** Until a custom domain with wildcard SSL is configured, use the `?subdomain=` query parameter for tenant routing (e.g., `https://{domain}?subdomain=alice`).
### 5.3 Optional: AI Features
```bash
VIBES_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "${CLAUDE_SKILL_DIR}")")}"
bun "$VIBES_ROOT/scripts/deploy-cloudflare.js" \
--name wedding-photos \
--file index.html \
--ai-key "sk-or-v1-your-provisioning-key"
```
### 5.4 Validation Gate: Verify Registry
After deployment, verify the registry is working:
```bash
curl -s https://{domain}/registry.json | head -c 100
```
**Expected output:** `{"claims":{},"reserved":["admin","api","www"]...`
**If you see HTML instead of JSON:**
- The Worker may not have deployed correctly
- Check `bunx wrangler tail --name {appName}` for errors
---
## Step 6: Post-Deploy Verification
### 6.1 Test Landing Page
```bash
curl -s -o /dev/null -w "%{http_code}" https://{domain}
```
**Expected:** `200`
### 6.2 Test Tenant Routing
Open in browser: `https://{domain}?subdomain=test`
Should show the tenant app (may require sign-in).
### 6.3 Auth Verification Checklist
Present this checklist to the user:
> **Authentication Checklist**
>
> Verify these for your deployment:
>
> **Pocket ID Auth**:
> - [ ] Auth token cached at `~/.vibes/auth.json` (created on first deploy)
> - [ ] Sign-in flow works on the deployed URL
>
> **If using custom domain**:
> - [ ] Add the custom domain as an allowed origin in Pocket ID
### 6.4 Billing Verification (if `--billing-mode required`)
Note: Stripe billing integration is planned for phase 2. For now, billing mode "required" gates access but Stripe checkout is not yet wired up. Verify the paywall UI appears correctly:
1. **Check landing page**: Open `https://{domain}` and confirm the landing page is visible
2. **Test auth gate**: Open `https://{domain}?subdomain=test`, and confirm unauthenticated users see the auth screen
3. **Verify access**: After signing in, confirm the user can access the tenant app
### 6.5 Admin Setup (After First Signup)
Guide the user through admin setup:
> **Set Up Admin Access**
>
> 1. Visit your app and sign up: `https://{domain}`
> 2. Complete the signup flow (email or passkey via Pocket ID)
> 3. Find your User ID from the Pocket ID admin panel or application logs
> 4. Re-run assembly with admin access:
>
> ```bash
> VIBES_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "${CLAUDE_SKILL_DIR}")")}"
> bun "$VIBES_ROOT/scripts/assemble-sell.js" app.jsx index.html \
> --app-name "{appName}" \
> --app-title "{appTitle}" \
> --domain "{domain}" \
> --admin-ids '["user_xxx"]' \
> [... other options ...]
> ```
>
> 5. Re-deploy:
> ```bash
> VIBES_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "${CLAUDE_SKILL_DIR}")")}"
> bun "$VIBES_ROOT/scripts/deploy-cloudflare.js" \
> --name {appName} \
> --file index.html
> ```
After collecting the user ID, save it to the project `.env` for reference:
```bash
grep -q ADMIN_USER_ID .env 2>/dev/null && \
sed -i '' 's/^ADMIN_USER_ID=.*/ADMIN_USER_ID=/' .env || \
echo "ADMIN_USER_ID=" >> .env
```
---
## Key Components & Troubleshooting
For routing internals (getRouteInfo, TenantContext, SubscriptionGate), testing routes, import map details, and troubleshooting common errors, read `${CLAUDE_SKILL_DIR}/references/components-and-troubleshooting.md`.
---
## What's Next?
After Step 6 verification completes, present options:
```
Question: "Your SaaS is deployed and verified! What would you like to do?"
Header: "Next"
Options:
- Label: "Set up admin access (Recommended)"
Description: "Sign up on your app, get your user ID, and enable admin dashboard access."
- Label: "Customize landing page"
Description: "Adjust colors, refine tagline, or update feature descriptions."
- Label: "I'm done for now"
Description: "Your app is live at https://{domain}"
```