VibesOS sell

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.

install
source · Clone the upstream repo
git clone https://github.com/popmechanic/VibesOS
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/popmechanic/VibesOS "$T" && mkdir -p ~/.claude/skills && cp -r "$T/vibes-desktop/build/stable-macos-arm64/VibesOS.app/Contents/Resources/vibes-plugin/skills/sell" ~/.claude/skills/popmechanic-vibesos-sell && rm -rf "$T"
manifest: vibes-desktop/build/stable-macos-arm64/VibesOS.app/Contents/Resources/vibes-plugin/skills/sell/SKILL.md
source content

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


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:

StepScriptWhat it does
Assembly
assemble-sell.js
Generates unified index.html
Deploy
deploy-cloudflare.js
Deploys to Cloudflare Workers with registry

Script location:

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

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

ConfigScript FlagExample
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.<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 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.

// 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:

CategoryGlobals
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 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:

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

: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

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 → SettingsDomains & 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

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:

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

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:
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 ...]
  1. Re-deploy:
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:

grep -q ADMIN_USER_ID .env 2>/dev/null && \
  sed -i '' 's/^ADMIN_USER_ID=.*/ADMIN_USER_ID=<new>/' .env || \
  echo "ADMIN_USER_ID=<new>" >> .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}"