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 - Read this first
- Step 1: Pre-Flight Checks - Verify prerequisites
- Step 2: Clerk Configuration - Set up authentication (REQUIRED)
- Step 3: App Configuration - Collect app settings
- Step 4: Assembly - Build the unified app
- Step 5: Deployment - Deploy to Cloudflare Workers
- Step 6: Post-Deploy Verification - Confirm everything works
- Key Components - Routing, TenantContext, SubscriptionGate
- Troubleshooting - Common issues and fixes
Assembly: transform (strip) —
assemble-sell.jsreceives a vibes-generated app.jsx and adapts it for the sell template. It stripsimportstatements,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:
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.jsto 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
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
ls -la app.jsx 2>/dev/null || echo "NOT_FOUND"
If output shows NOT_FOUND:
Check for riff directories:
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 toapp.jsx - Found nothing → Tell user to run
/vibes:vibesfirst
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:
- Create a Clerk Application at clerk.com — choose "Email + Passkey" authentication
- Configure Email Settings — enable email signup with verification, and set "Require email address" to OFF (signup fails otherwise)
- Configure Passkey Settings — enable sign-in with passkey, autofill, passkey button, and add-passkey-to-account
See 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:
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
- Domain is
{domain}(resolved in Step 2.2). Custom domains can be added post-deploy (Step 5.2). - Admin User IDs default to empty (configured after first deploy - see Step 6)
- 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:
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:
// 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:
- Find the
useFireproofClerk("...")call - Add
const { dbName } = useTenant();before it - 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:
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:
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:
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 setVITE_API_URL- must be setVITE_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:
: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
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:
- In the Cloudflare dashboard, go to Workers & Pages → your worker → Settings → Domains & Routes
- Add a custom domain (e.g.,
cosmicgarden.app) - 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
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:
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-keywas provided during deploy
Step 6: Post-Deploy Verification
6.1 Test Landing Page
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.deletedIf using billing (Dashboard → Billing):
- [ ] Stripe connected
- [ ] Plans created with matching names:
pro,basic,monthly,yearly,starter, orfree
6.4 Billing Verification (if --billing-mode required)
If billing mode is "required", verify the billing flow works:
- Check landing page pricing: Open
https://{domain}and confirm the PricingTable is visible - Test paywall: Open
https://{domain}?subdomain=test, sign in, and confirm non-subscribed users see the SubscriptionPaywall - Test checkout: Click a plan in the paywall, use test card
4242 4242 4242 4242with any future expiry and any CVC - Verify access: After subscribing, confirm the user can access the tenant app
- 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
- Visit your app and sign up:
https://{domain}- Complete the signup flow (email → verify → passkey)
- Go to Clerk Dashboard → Users → click your user → copy User ID
- Re-run assembly with admin access:
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 ...]
- Re-deploy:
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:
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:
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:
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 inrequired: 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:
{
"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_IDSarray - Verify Clerk Billing is set up with matching plan names
Clerk not loading / Passkey fails / "Verification incomplete" error
- See 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 for registry fetch errors
Assembly fails with ".env file not found"
- Fireproof Connect is not configured
- Run
/vibes:connectfirst to set up your sync backend - Then return to
/vibes:sell
PricingTable not showing on landing page
- Verify
--billing-mode requiredwas 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 4242with 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}"