sell

Self-contained SaaS automation — invoke directly, do not decompose. Transforms 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.

16 stars

Best use case

sell is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Self-contained SaaS automation — invoke directly, do not decompose. Transforms 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.

Teams using sell should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/sell/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/development/sell/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/sell/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How sell Compares

Feature / AgentsellStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Self-contained SaaS automation — invoke directly, do not decompose. Transforms 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.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

> **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: 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) - 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`, `useFireproofClerk`, `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
node "${CLAUDE_PLUGIN_ROOT}/scripts/assemble-sell.js" ...
node "${CLAUDE_PLUGIN_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
- ❌ Deploy without `--clerk-key`

**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.

---

### Terminal or Editor UI?

Detect whether you're running in a terminal (Claude Code CLI, Codex) 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 Check for Fireproof Connect

```bash
cat .env 2>/dev/null | grep VITE_API_URL || echo "NOT_FOUND"
```

**If `NOT_FOUND`:** Run `/vibes:connect` first.
**STOP HERE** if Connect is not configured.

### 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** — enable email signup with verification, and **set "Require email address" to OFF** (signup fails otherwise)
> 3. **Configure Passkey Settings** — enable sign-in with passkey, autofill, passkey button, and add-passkey-to-account
>
> See [CLERK-SETUP.md](./CLERK-SETUP.md) for the complete settings tables and step-by-step instructions.
>
> **When you're ready, I'll collect your Clerk credentials.**

### 2.2 Collect App Name (Needed for Webhook URL)

The webhook endpoint URL requires the app name. Collect it now so we can give the user the exact URL.

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).

Now resolve the Cloudflare Workers URL so the webhook step has the real domain:

```bash
node "{pluginRoot}/scripts/lib/resolve-workers-url.js" --name "{appName}"
```

The script outputs the full URL, e.g., `wedding-photos.marcus-e.workers.dev`. Store this as `domain`.

**Fallback**: If the script fails (e.g., wrangler not authenticated), ask the user:

```
Question: "What's your Cloudflare Workers subdomain? (Run `npx wrangler whoami` to find your account name)"
Header: "CF subdomain"
Options:
- Label: "Let me check"
  Description: "I'll run wrangler whoami and tell you"
- Label: "I know it"
  Description: "I'll type my subdomain (e.g., marcus-e)"
multiSelect: false
```

Then construct: `{appName}.{subdomain}.workers.dev` and store as `domain`.

The user can configure a custom domain later (see Step 5.2).

### 2.3 Create Webhook

Use AskUserQuestion:
```
Question: "Create a webhook in Clerk: Go to Webhooks > Add Endpoint. Set the URL to: https://{domain}/webhook — Subscribe to these events: user.created, user.deleted, subscription.deleted"
Header: "Webhook"
Options:
- Label: "Webhook created"
  Description: "I've added the endpoint and subscribed to the events"
- Label: "I need help"
  Description: "I'm having trouble finding the webhooks page"
multiSelect: false
```

If "I need help": walk them through navigating Clerk dashboard > Configure > Webhooks > Add Endpoint, making sure to repeat the URL `https://{domain}/webhook`.

### 2.4 Collect Clerk Credentials

Use AskUserQuestion with these 2 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 Webhook Secret?"
Header: "Webhook"
Options: User enters via "Other"
Description: "From the webhook you just created — click the endpoint, copy the Signing Secret. Starts with whsec_"
```

### 2.5 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 |
| 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.6 Clerk Configuration Complete

Confirm to the user:
> "Clerk credentials validated and saved:
> - ✓ Publishable Key: pk_test_... (saved for assembly and 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

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 is configured in Clerk Dashboard → Billing"

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 <br> for line breaks (e.g., 'SHARE YOUR DAY.<br>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` |
| Clerk Publishable Key | `--clerk-key` | `pk_test_xxx` |
| Title | `--app-title` | `Wedding Photos` |
| Tagline | `--tagline` | `SHARE YOUR DAY.<br>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)`

`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 CLERK_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 '["<user_id>"]'`. 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
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 "{domain}" \
  --tagline "SHARE YOUR DAY.<br>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

**Deploy Target: Cloudflare Workers.** SaaS apps always deploy to Cloudflare Workers (not exe.dev). The KV registry and subdomain routing require the CF Worker runtime.

**Registry server credentials are REQUIRED for SaaS apps.**

### 5.1 Deploy to Cloudflare Workers

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-cloudflare.js" \
  --name wedding-photos \
  --file index.html \
  --clerk-key "pk_test_xxx" \
  --webhook-secret "whsec_xxx"
```

**Required Flags for SaaS:**
| Flag | Source | Purpose |
|------|--------|---------|
| `--clerk-key` | Clerk publishable key (pk_test_/pk_live_) | deploy-cloudflare.js auto-fetches PEM from JWKS endpoint |
| `--webhook-secret` | Clerk webhook signing secret | deploy-cloudflare.js sets it as a Wrangler secret |

**Without `--clerk-key`, the Worker won't be able to verify JWTs for subdomain claiming.**

### 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
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-cloudflare.js" \
  --name wedding-photos \
  --file index.html \
  --clerk-key "pk_test_xxx" \
  --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 `npx wrangler tail --name {appName}` for errors
- Verify `--clerk-key` was provided during deploy

---

## 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 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., `{domain}`)
> - [ ] If using custom domain, add that too
>
> **Webhook** (Dashboard → Configure → Webhooks):
> - [ ] Endpoint URL matches your deployment: `https://{domain}/webhook`
> - [ ] Events selected: `user.created`, `user.deleted`, `subscription.deleted`
>
> **If using billing** (Dashboard → Billing):
> - [ ] Stripe connected
> - [ ] Plans created with matching names: `pro`, `basic`, `monthly`, `yearly`, `starter`, or `free`

### 6.4 Billing Verification (if `--billing-mode required`)

If billing mode is "required", verify the billing flow works:

1. **Check landing page pricing**: Open `https://{domain}` and confirm the PricingTable is visible
2. **Test paywall**: Open `https://{domain}?subdomain=test`, sign in, and confirm non-subscribed users see the SubscriptionPaywall
3. **Test checkout**: Click a plan in the paywall, use test card `4242 4242 4242 4242` with any future expiry and any CVC
4. **Verify access**: After subscribing, confirm the user can access the tenant app
5. **Check webhook**: Cancel the subscription in Clerk Dashboard, then verify the subdomain is released in `/registry.json`

If any step fails, check the Troubleshooting section for billing-specific issues.

### 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 → 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 "{appName}" \
>   --app-title "{appTitle}" \
>   --domain "{domain}" \
>   --admin-ids '["user_xxx"]' \
>   [... other options ...]
> ```
>
> 5. Re-deploy:
> ```bash
> node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-cloudflare.js" \
>   --name {appName} \
>   --file index.html \
>   --clerk-key "pk_test_xxx" \
>   --webhook-secret "whsec_xxx"
> ```

After collecting the user ID, save it to the project `.env`:
```bash
grep -q CLERK_ADMIN_USER_ID .env 2>/dev/null && \
  sed -i '' 's/^CLERK_ADMIN_USER_ID=.*/CLERK_ADMIN_USER_ID=<new>/' .env || \
  echo "CLERK_ADMIN_USER_ID=<new>" >> .env
```

---

## 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 workers.dev (use ?subdomain= param, no wildcard SSL)
  if (hostname.endsWith('.workers.dev')) {
    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 (
    <TenantContext.Provider value={{ subdomain, dbName, appName: APP_NAME, domain: APP_DOMAIN }}>
      {children}
    </TenantContext.Provider>
  );
}
```

### SubscriptionGate

Wraps tenant content and enforces billing mode:

- **`off`**: Everyone gets free access after signing in
- **`required`**: Users must subscribe via Clerk Billing before accessing tenant content

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".

### SubscriptionPaywall

Shown to authenticated users who do not have an active subscription (when `billingMode === "required"`). Displays a pricing table via Clerk's `<PricingTable />` component so users can subscribe without leaving the app.

### UpgradePrompt

Optional component shown inside the tenant app when a user's plan has limited features. Use this for soft upsell messaging (e.g., "Upgrade to Pro for unlimited exports"). Not shown when `billingMode === "off"`.

---

## 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
```

**Workers.dev (before custom domain):**
```
https://{domain}              → Landing page
https://{domain}?subdomain=test → Tenant app
https://{domain}?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 `<script type="text/babel" data-type="module">` is present

### "Cannot read properties of null (reading 'useEffect')"
- React version mismatch between packages
- Ensure fireproof-clerk imports have `?external=react,react-dom`

### "Subscription Required" loop
- Check that admin user ID is correct and in the `ADMIN_USER_IDS` array
- Verify Clerk Billing is set up with matching plan names

### Clerk not loading / Passkey fails / "Verification incomplete" error
- See [CLERK-SETUP.md Troubleshooting](./CLERK-SETUP.md#troubleshooting) for detailed Clerk auth fixes
- Most common fix: set "Require email address" to **OFF** in Clerk Dashboard → Email settings

### Admin shows "Access Denied"
- User ID not in --admin-ids array
- Check Clerk Dashboard → Users → click user → copy User ID
- Re-run assembly with correct --admin-ids

### Database not isolated
- Verify `useTenant()` is used in the App component
- Check `useFireproofClerk(dbName)` uses the tenant database name

### Registry returns HTML instead of JSON
- Deploy was run without `--clerk-key` — re-deploy with it
- See also [CLERK-SETUP.md Troubleshooting](./CLERK-SETUP.md#troubleshooting) for registry fetch errors

### Assembly fails with ".env file not found"
- Fireproof Connect is not configured
- Run `/vibes:connect` first to set up your sync backend
- Then return to `/vibes:sell`

### PricingTable not showing on landing page
- Verify `--billing-mode required` was passed during assembly
- Check browser console for Clerk Billing errors
- Ensure at least one plan exists in Clerk Dashboard > Billing > Plans

### Paywall shows but checkout fails
- In dev mode, Clerk auto-connects to Stripe sandbox — no Stripe account needed
- Use test card `4242 4242 4242 4242` with any future expiry date and any 3-digit CVC
- If using production keys, ensure Stripe is connected in Clerk Dashboard > Billing > Stripe

### User subscribed but still sees paywall
- Clerk subscription webhooks may be delayed — wait 10-15 seconds and refresh
- Verify webhook endpoint URL matches: `https://{domain}/webhook`
- Subscription status is checked via JWT claims, not webhooks — verify Clerk Billing plan exists
- Check Worker logs: `npx wrangler tail --name {appName}`

### Subscription canceled but subdomain not released
- Verify webhook events include `subscription.deleted`
- Check webhook secret is set: `npx wrangler secret list --name {appName}`
- View Worker logs for webhook processing errors: `npx wrangler tail --name {appName}`

---

## 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}"
```

Related Skills

amazon-bestseller-launch

16
from diegosouzapw/awesome-omni-skill

Complete Amazon KDP bestseller launch system with proven strategies for achieving

bgo

10
from diegosouzapw/awesome-omni-skill

Automates the complete Blender build-go workflow, from building and packaging your extension/add-on to removing old versions, installing, enabling, and launching Blender for quick testing and iteration.

Coding & Development

seo-meta-tags

16
from diegosouzapw/awesome-omni-skill

Comprehensive SEO and social metadata implementation for HTML, Next.js App Router, and Vite (React & Vue) projects. Use when: (1) Creating, editing, or reviewing meta tags, Open Graph, Twitter Cards, canonical/hreflang links, JSON-LD structured data, theme-color, or favicon/manifest config; (2) Working with html/index.html, nextjs/metadata.ts, nextjs/dynamic-metadata-example.tsx, vite/index.html, vite/SEO.tsx, or vite/SEO.vue; (3) Adding SEO to a new page or route; (4) Migrating from react-helmet, vue-meta, or next-seo; (5) Generating dynamic metadata for content-driven pages; (6) Auditing metadata for completeness or best-practice compliance.

seo-geo

16
from diegosouzapw/awesome-omni-skill

Audit website for Generative Engine Optimization (GEO) - optimization for AI search engines like Perplexity, ChatGPT Search, Gemini, and Claude.

sentry-setup-tracing

16
from diegosouzapw/awesome-omni-skill

Setup Sentry Tracing (Performance Monitoring) in any project. Use this when asked to add performance monitoring, enable tracing, track transactions/spans, or instrument application performance. Supports JavaScript, TypeScript, Python, Ruby, React, Next.js, and Node.js.

sentry-setup-metrics

16
from diegosouzapw/awesome-omni-skill

Setup Sentry Metrics in any project. Use this when asked to add Sentry metrics, track custom metrics, setup counters/gauges/distributions, or instrument application performance metrics. Supports JavaScript, TypeScript, Python, React, Next.js, and Node.js.

sentry-setup-logging

16
from diegosouzapw/awesome-omni-skill

Setup Sentry Logging in any project. Use this when asked to add Sentry logs, enable structured logging, setup console log capture, or integrate logging with Sentry. Supports JavaScript, TypeScript, Python, Ruby, React, Next.js, and other frameworks.

sentry-python-setup

16
from diegosouzapw/awesome-omni-skill

Setup Sentry in Python apps. Use when asked to add Sentry to Python, install sentry-sdk, or configure error monitoring for Python applications, Django, Flask, FastAPI.

sentry-python-sdk

16
from diegosouzapw/awesome-omni-skill

Full Sentry SDK setup for Python. Use when asked to "add Sentry to Python", "install sentry-sdk", "setup Sentry in Python", or configure error monitoring, tracing, profiling, logging, metrics, crons, or AI monitoring for Python applications. Supports Django, Flask, FastAPI, Celery, Starlette, AIOHTTP, Tornado, and more.

sentry-debugger

16
from diegosouzapw/awesome-omni-skill

Debug production issues using Sentry error tracking API. Use when Claude needs to investigate production errors, crashes, or issues by fetching error data from Sentry - including stack traces, user context, breadcrumbs, and error frequency. Triggers on requests like "check Sentry for errors", "debug this production issue", "what's causing crashes", "investigate errors in [project]", or when users share Sentry issue URLs.

senior-software-developer

16
from diegosouzapw/awesome-omni-skill

Provides production-quality patterns, standards, and language-specific best practices for Python, C#, Node.js, and TypeScript. Used as the implementation guideline within test-driven-development workflow, or directly for architecture decisions and complex refactoring. Not invoked directly for new feature implementation - use test-driven-development instead.

senior-qa

16
from diegosouzapw/awesome-omni-skill

Comprehensive QA and testing skill for quality assurance, test automation, and testing strategies for ReactJS, NextJS, NodeJS applications. Includes test suite generation, coverage analysis, E2E testing setup, and quality metrics. Use when designing test strategies, writing test cases, implementing test automation, performing manual testing, or analyzing test coverage.