---
name: sell
description: Transform a Vibes app into a multi-tenant SaaS with subdomain-based tenancy. Adds Clerk authentication, subscription gating, and generates a unified app with landing page, tenant routing, and admin dashboard.
license: MIT
allowed-tools: Read, Write, Bash, Glob, AskUserQuestion
---
**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: Clerk Configuration](#step-2-clerk-configuration-required) - Set up authentication (REQUIRED)
- [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) - Go live with exe.dev
- [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
---
## ⛔ 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-exe.js` | Deploys to exe.dev with registry |
**Script location:**
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/assemble-sell.js" ...
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" ...
```
**NEVER do these manually:**
- ❌ Write HTML/JSX for landing page, tenant app, or admin dashboard
- ❌ Generate routing logic or authentication code
- ❌ Deploy without `--clerk-key` and `--clerk-webhook-secret`
**ALWAYS do these:**
- ✅ Complete pre-flight checks before starting
- ✅ Collect ALL Clerk credentials BEFORE app configuration
- ✅ Run `assemble-sell.js` to generate the unified app
- ✅ Deploy with ALL required flags
---
# 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, proper Clerk integration, and Fireproof patterns.
Convert your Vibes app into a multi-tenant SaaS product with:
- Subdomain-based tenancy (alice.yourdomain.com)
- Clerk authentication with passkeys
- Subscription gating via Clerk Billing
- Per-tenant Fireproof database isolation
- 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.
---
## Step 1: Pre-Flight Checks
**Before starting, verify these prerequisites. STOP if any check fails.**
### 1.1 Check for Fireproof Connect
Run this command to check if Connect is configured:
```bash
cat .env 2>/dev/null | grep VITE_API_URL || echo "NOT_FOUND"
```
**If output shows `NOT_FOUND`:**
> "Fireproof Connect is not configured. Run `/vibes:connect` first to set up your sync backend, then return to `/vibes:sell`."
**STOP HERE** if Connect is not configured. The sell skill requires cloud sync for multi-tenant data isolation.
### 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 both checks pass, confirm:
> "Pre-flight checks passed:
> - ✓ Fireproof Connect configured (.env found)
> - ✓ App found (app.jsx)
>
> Now let's configure Clerk authentication. This is required for multi-tenant SaaS."
---
## Step 2: Clerk Configuration (REQUIRED)
**These credentials are REQUIRED. Do not proceed without them.**
### 2.1 Clerk Dashboard Setup Instructions
Before collecting credentials, the user must set up Clerk. Present these instructions:
> **Clerk Setup Required**
>
> Before we continue, you need to configure Clerk authentication:
>
> 1. **Create a Clerk Application** at [clerk.com](https://clerk.com)
> - Choose "Email + Passkey" authentication
>
> 2. **Configure Email Settings** (Dashboard → User & Authentication → Email):
> | Setting | Value | Why |
> |---------|-------|-----|
> | Sign-up with email | ✅ ON | Users sign up via email |
> | Require email address | ⬜ **OFF** | **CRITICAL** - signup fails if ON |
> | Verify at sign-up | ✅ ON | Verify before session |
> | Email verification code | ✅ Checked | Use code for verification |
>
> 3. **Configure Passkey Settings** (Dashboard → User & Authentication → Passkeys):
> | Setting | Value |
> |---------|-------|
> | Sign-in with passkey | ✅ ON |
> | Allow autofill | ✅ ON |
> | Show passkey button | ✅ ON |
> | Add passkey to account | ✅ ON |
>
> 4. **Create Webhook** (Dashboard → Configure → Webhooks):
> - Click "Add Endpoint"
> - Enter URL: `https://YOUR-APP.exe.xyz/webhook` (we'll confirm the name later)
> - Select events: `user.created`, `user.deleted`, `subscription.*`
> - Click "Create"
> - Copy the **"Signing Secret"** (starts with `whsec_`)
>
> See [CLERK-SETUP.md](./CLERK-SETUP.md) for complete details.
>
> **When you're ready, I'll collect your Clerk credentials.**
### 2.2 Collect Clerk Credentials
Use AskUserQuestion with these 3 questions:
```
Question 1: "What's your Clerk Publishable Key?"
Header: "Clerk Key"
Options: User enters via "Other"
Description: "From Clerk Dashboard → API Keys. Starts with pk_test_ or pk_live_"
Question 2: "What's your Clerk JWKS Public Key?"
Header: "JWKS Key"
Options: User enters via "Other"
Description: "From Clerk Dashboard → API Keys → scroll to 'Show JWT Public Key'. Starts with -----BEGIN PUBLIC KEY-----"
Question 3: "What's your Clerk Webhook Secret?"
Header: "Webhook"
Options: User enters via "Other"
Description: "From the webhook you created. Starts with whsec_"
```
### 2.3 Validation Gate
**Before proceeding, validate ALL credentials:**
| Credential | Valid Format | If Invalid |
|------------|--------------|------------|
| Publishable Key | Starts with `pk_test_` or `pk_live_` | Stop, ask for correct key |
| JWKS Public Key | Starts with `-----BEGIN PUBLIC KEY-----` | Stop, guide to JWT Public Key location |
| Webhook Secret | Starts with `whsec_` | Stop, guide to webhook creation |
**If ANY validation fails:** Stop and help user get the correct credential. Do not proceed to Step 3.
### 2.4 Save JWKS Key to File
After receiving and validating the JWKS key, save it to a file for the deploy command:
```bash
cat > clerk-jwks-key.pem << 'EOF'
-----BEGIN PUBLIC KEY-----
[THE USER'S KEY CONTENT HERE]
-----END PUBLIC KEY-----
EOF
```
Verify the file was created:
```bash
head -1 clerk-jwks-key.pem
```
**Expected output:** `-----BEGIN PUBLIC KEY-----`
### 2.5 Clerk Configuration Complete
Confirm to the user:
> "Clerk credentials validated and saved:
> - ✓ Publishable Key: pk_test_... (saved for assembly)
> - ✓ JWKS Key: Saved to clerk-jwks-key.pem (for deployment)
> - ✓ Webhook Secret: whsec_... (saved for deployment)
>
> Now let's configure your app settings."
---
## Step 3: App Configuration
**Use AskUserQuestion to collect all config in 2 batches.**
### Batch 1: Core Identity
Use the AskUserQuestion tool with these 4 questions:
```
Question 1: "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')"
Question 2: "What domain will this deploy to?"
Header: "Domain"
Options: ["Use exe.xyz subdomain", "Custom domain"]
Description: "exe.xyz is instant. Custom domains require DNS setup."
Question 3: "Do you want to require paid subscriptions?"
Header: "Billing"
Options: ["No - free access for all", "Yes - subscription required"]
Description: "Billing is configured in Clerk Dashboard → Billing"
Question 4: "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
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.')"
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.')"
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')"
```
### After Receiving Answers
1. If user selected "Custom domain", ask for the domain name
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.exe.xyz` |
| Billing | `--billing-mode` | `off` or `required` |
| Clerk Publishable Key | `--clerk-key` | `pk_test_xxx` |
| 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 Verify .env Exists
Before running assembly, verify the .env file exists:
```bash
test -f .env && echo "OK" || echo "MISSING"
```
**If MISSING:** Stop and run `/vibes:connect` first.
### 4.2 Update App for Tenant Context
The user's app needs to use `useTenant()` for database scoping. Check if their app has a hardcoded database name:
```jsx
// BEFORE: Hardcoded name
const { useLiveQuery } = useFireproofClerk("my-app");
// AFTER: Tenant-aware
const { dbName } = useTenant();
const { useLiveQuery } = useFireproofClerk(dbName);
```
If the app uses a hardcoded name, update it:
1. Find the `useFireproofClerk("...")` call
2. Add `const { dbName } = useTenant();` before it
3. Change to `useFireproofClerk(dbName)`
The template makes `useTenant` available globally via `window.useTenant`.
### 4.3 Run Assembly Script
Run the assembly script with all collected values:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/assemble-sell.js" app.jsx index.html \
--clerk-key "pk_test_xxx" \
--app-name "wedding-photos" \
--app-title "Wedding Photos" \
--domain "myapp.exe.xyz" \
--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:** The .env file is missing required values. Check:
- `VITE_CLERK_PUBLISHABLE_KEY` - must be set
- `VITE_API_URL` - must be set
- `VITE_CLOUD_URL` - optional but recommended
Fix the .env file and re-run assembly.
### 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
**Registry server credentials are REQUIRED for SaaS apps.**
### 5.0 Pre-Deploy Check: Verify JWKS Key File
Before deploying, verify the JWKS key file exists:
```bash
test -f clerk-jwks-key.pem && echo "✓ JWKS key file exists" || echo "✗ MISSING: clerk-jwks-key.pem"
```
**If output shows `MISSING`:**
> Return to Step 2.4 and save the JWKS key to `clerk-jwks-key.pem`
### 5.1 Deploy with ALL Required Flags
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" \
--name wedding-photos \
--file index.html \
--clerk-key "$(cat clerk-jwks-key.pem)" \
--clerk-webhook-secret "whsec_xxx"
```
**Required Flags for SaaS:**
| Flag | Source | Purpose |
|------|--------|---------|
| `--clerk-key` | clerk-jwks-key.pem file | Registry JWT verification |
| `--clerk-webhook-secret` | Clerk webhook | Subscription sync |
**Without these flags, the registry server will NOT be deployed and subdomain claiming will NOT work.**
### 5.2 DNS Configuration (For Custom Domains)
If using a custom domain (not just yourapp.exe.xyz), configure DNS:
| Type | Name | Value |
|------|------|-------|
| ALIAS | @ | exe.xyz |
| CNAME | * | yourapp.exe.xyz |
**Example for `cosmic-garden.exe.xyz` with custom domain `cosmicgarden.app`:**
| Type | Name | Value |
|------|------|-------|
| ALIAS | @ | exe.xyz |
| CNAME | * | cosmic-garden.exe.xyz |
This routes both the apex domain and all subdomains through exe.dev's proxy, which handles SSL automatically.
**Note:** If your DNS provider doesn't support ALIAS records, use the `?subdomain=` query parameter as a fallback.
### 5.3 Optional: AI Features
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" \
--name wedding-photos \
--file index.html \
--clerk-key "$(cat clerk-jwks-key.pem)" \
--clerk-webhook-secret "whsec_xxx" \
--ai-key "sk-or-v1-your-provisioning-key" \
--multi-tenant \
--tenant-limit 5
```
### 5.4 Validation Gate: Verify Registry
After deployment, verify the registry server is running:
```bash
curl -s https://wedding-photos.exe.xyz/registry.json | head -c 100
```
**Expected output:** `{"claims":{},"quotas":{},"reserved":["admin","api","www"]...`
**If you see HTML instead of JSON:**
- The registry server isn't running
- Check deployment logs
- Verify `--clerk-key` and `--clerk-webhook-secret` were provided
---
## Step 6: Post-Deploy Verification
### 6.1 Test Landing Page
```bash
curl -s -o /dev/null -w "%{http_code}" https://wedding-photos.exe.xyz
```
**Expected:** `200`
### 6.2 Test Tenant Routing
Open in browser: `https://wedding-photos.exe.xyz?subdomain=test`
Should show the tenant app (may require sign-in).
### 6.3 Clerk Dashboard Checklist
Present this checklist to the user:
> **Clerk Dashboard Settings Checklist**
>
> Verify these settings in your Clerk Dashboard:
>
> **Domains** (Dashboard → Domains):
> - [ ] Add your deployment domain (e.g., `wedding-photos.exe.xyz`)
> - [ ] If using custom domain, add that too
>
> **Webhook** (Dashboard → Configure → Webhooks):
> - [ ] Endpoint URL matches your deployment: `https://wedding-photos.exe.xyz/webhook`
> - [ ] Events selected: `user.created`, `user.deleted`, `subscription.*`
>
> **If using billing** (Dashboard → Billing):
> - [ ] Stripe connected
> - [ ] Plans created with matching names: `pro`, `basic`, `monthly`, `yearly`, `starter`, or `free`
### 6.4 Admin Setup (After First Signup)
Guide the user through admin setup:
> **Set Up Admin Access**
>
> 1. Visit your app and sign up: `https://wedding-photos.exe.xyz`
> 2. Complete the signup flow (email → verify → passkey)
> 3. Go to Clerk Dashboard → Users → click your user → copy User ID
> 4. Re-run assembly with admin access:
>
> ```bash
> node "${CLAUDE_PLUGIN_ROOT}/scripts/assemble-sell.js" app.jsx index.html \
> --clerk-key "pk_test_xxx" \
> --app-name "wedding-photos" \
> --app-title "Wedding Photos" \
> --domain "wedding-photos.exe.xyz" \
> --admin-ids '["user_xxx"]' \
> [... other options ...]
> ```
>
> 5. Re-deploy:
> ```bash
> node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" \
> --name wedding-photos \
> --file index.html \
> --clerk-key "$(cat clerk-jwks-key.pem)" \
> --clerk-webhook-secret "whsec_xxx"
> ```
---
## Key Components
### Client-Side Routing
The unified template uses `getRouteInfo()` to detect subdomain and route:
```javascript
function getRouteInfo() {
const hostname = window.location.hostname;
const parts = hostname.split('.');
const params = new URLSearchParams(window.location.search);
const testSubdomain = params.get('subdomain');
// Handle localhost testing with ?subdomain= param
if (hostname === 'localhost' || hostname === '127.0.0.1') {
if (testSubdomain === 'admin') return { route: 'admin', subdomain: null };
if (testSubdomain) return { route: 'tenant', subdomain: testSubdomain };
return { route: 'landing', subdomain: null };
}
// Handle exe.xyz testing (before custom domain is set up)
if (hostname.endsWith('.exe.xyz')) {
if (testSubdomain === 'admin') return { route: 'admin', subdomain: null };
if (testSubdomain) return { route: 'tenant', subdomain: testSubdomain };
return { route: 'landing', subdomain: null };
}
// Production: detect subdomain from hostname
if (parts.length <= 2 || parts[0] === 'www') {
return { route: 'landing', subdomain: null };
}
if (parts[0] === 'admin') {
return { route: 'admin', subdomain: null };
}
return { route: 'tenant', subdomain: parts[0] };
}
```
### TenantContext
Provides database scoping for tenant apps:
```javascript
const TenantContext = createContext(null);
function TenantProvider({ children, subdomain }) {
const dbName = `${APP_NAME}-${subdomain}`;
return (
{children}
);
}
```
### SubscriptionGate with Billing Mode
The subscription gate respects the billing mode setting:
- **`off`**: Everyone gets free access after signing in
- **`required`**: Users must subscribe via Clerk Billing
Admins always bypass the subscription check.
**SECURITY WARNING**: Do NOT add fallbacks like `|| ADMIN_USER_IDS.length === 0` to admin checks. An empty admin list means NO admin access, not "everyone is admin".
---
## Testing
Test different routes by adding `?subdomain=` parameter:
**Localhost:**
```
http://localhost:5500/index.html → Landing page
http://localhost:5500/index.html?subdomain=test → Tenant app
http://localhost:5500/index.html?subdomain=admin → Admin dashboard
```
**exe.xyz (before custom domain):**
```
https://myapp.exe.xyz → Landing page
https://myapp.exe.xyz?subdomain=test → Tenant app
https://myapp.exe.xyz?subdomain=admin → Admin dashboard
```
---
## Import Map
The unified template uses React 19 with `@necrodome/fireproof-clerk` for Clerk integration:
```json
{
"imports": {
"react": "https://esm.sh/stable/react@19.2.4",
"react/jsx-runtime": "https://esm.sh/stable/react@19.2.4/jsx-runtime",
"react/jsx-dev-runtime": "https://esm.sh/stable/react@19.2.4/jsx-dev-runtime",
"react-dom": "https://esm.sh/stable/react-dom@19.2.4",
"react-dom/client": "https://esm.sh/stable/react-dom@19.2.4/client",
"use-fireproof": "https://esm.sh/stable/@necrodome/fireproof-clerk@0.0.3?external=react,react-dom",
"@fireproof/clerk": "https://esm.sh/stable/@necrodome/fireproof-clerk@0.0.3?external=react,react-dom"
}
}
```
---
## Troubleshooting
### "Unexpected token '<'" in console
- JSX not being transpiled by Babel
- Check that `