| name | exe |
| description | Deploy a Vibes app to exe.dev VM hosting. Uses nginx on persistent VMs with SSH automation. Supports client-side multi-tenancy via subdomain-based Fireproof database isolation. |
Deploy to exe.dev
Deploy your Vibes app to exe.dev, a VM hosting platform with persistent storage and HTTPS by default.
Prerequisites
- SSH key in
~/.ssh/(id_ed25519, id_rsa, or id_ecdsa) - exe.dev account - run
ssh exe.devonce to create your account and verify email - Generated Vibes app - an
index.htmlfile ready to deploy
Gather Config Upfront
Use AskUserQuestion to collect deployment config before running the deploy script.
Use the AskUserQuestion tool with these questions:
Question 1: "What VM name should we use? (becomes yourname.exe.xyz)"
Header: "VM Name"
Options: Suggest based on app name from context + user enters via "Other"
Question 2: "Which file should we deploy?"
Header: "File"
Options: ["index.html (default)", "Other path"]
Question 3: "Does this app need AI features?"
Header: "AI"
Options: ["No", "Yes - I have an OpenRouter key"]
Question 4: "Is this a SaaS app with subdomain claiming?"
Header: "Registry"
Options: ["No - simple static deploy", "Yes - need Clerk keys for registry"]
After Receiving Answers
- If AI enabled, ask for the OpenRouter API key
- If Registry enabled, ask for Clerk PEM public key and webhook secret
- Proceed immediately to deploy - no more questions
Quick Deploy
cd "${CLAUDE_PLUGIN_ROOT}/scripts" && [ -d node_modules ] || npm install
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --file index.html
What It Does
- Creates VM on exe.dev via SSH CLI
- Starts nginx (pre-installed on exeuntu image)
- Uploads your index.html to
/var/www/html/ - Generates HANDOFF.md - context document for remote Claude
- Makes VM public via
ssh exe.dev share set-public <vmname> - Verifies public access at
https://myapp.exe.xyz
AI-Enabled Apps
For apps using the useAI hook, deploy with the --ai-key flag:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --file index.html --ai-key "sk-or-v1-..."
This sets up a secure AI proxy:
- Installs Bun runtime
- Creates
/home/exedev/proxy.js- proxies/api/ai/*to OpenRouter - Configures systemd service for the proxy
- Adds nginx reverse proxy from port 80/443 to Bun (port 3001)
IMPORTANT: Do not manually set up AI proxying. Manual nginx config changes can overwrite SSL settings and miss the Bun/proxy.js service. Always use the deploy script with --ai-key.
Multi-Tenant AI Apps
For SaaS apps with per-tenant AI:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --file index.html --ai-key "sk-or-v1-..." --multi-tenant
Registry Server
For SaaS apps using subdomain claiming (from /vibes:sell), deploy with Clerk credentials:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --file index.html \
--clerk-key "$(cat clerk-public-key.pem)" \
--clerk-webhook-secret "whsec_xxx" \
--reserved "admin,api,billing"
This sets up a subdomain registry server:
- Installs Bun runtime to
/usr/local/bin/bun - Creates
/var/www/registry-server.tswith Clerk JWT verification - Configures systemd service (port 3002)
- Adds nginx proxy for
/registry.json,/check/*,/claim,/webhook
Registry Endpoints
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/registry.json |
GET | None | Public read of all claims |
/check/{subdomain} |
GET | None | Check availability |
/claim |
POST | Bearer JWT | Claim subdomain for user |
/webhook |
POST | Svix sig | Clerk subscription events |
Getting the Clerk Public Key
The registry server needs Clerk's PEM public key to verify JWTs for the /claim endpoint.
Option 1: From Clerk Dashboard
- Go to Clerk Dashboard → API Keys
- Scroll to "PEM Public Key" or click "Show JWT Public Key"
- Copy the full key (starts with
-----BEGIN PUBLIC KEY-----)
Option 2: From JWKS endpoint
# Get your Clerk frontend API domain from dashboard
curl https://YOUR_CLERK_DOMAIN/.well-known/jwks.json
Then convert the JWK to PEM format using an online tool or jose CLI.
Passing to deploy script:
# From a file
node deploy-exe.js --clerk-key "$(cat clerk-public-key.pem)" ...
# Inline (escape newlines)
node deploy-exe.js --clerk-key "-----BEGIN PUBLIC KEY-----\nMIIB...\n-----END PUBLIC KEY-----" ...
Manual configuration on server:
ssh myapp.exe.dev
sudo nano /etc/registry.env
# Add: CLERK_PEM_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
sudo systemctl restart vibes-registry
Continue Development on the VM
Claude is pre-installed on exe.dev VMs. After deployment, you can continue development remotely:
ssh myapp.exe.dev -t "cd /var/www/html && claude"
The HANDOFF.md file provides context about what was built, so Claude can continue meaningfully.
Manual Public Access
If the deploy script doesn't make the VM public automatically, run:
ssh exe.dev share set-public myapp
Multi-Tenant Apps
For apps that need tenant isolation (e.g., alice.myapp.com, bob.myapp.com):
Client-Side Isolation
The same index.html serves all subdomains. JavaScript reads the hostname and uses the subdomain as a Fireproof database prefix:
// In your app:
const hostname = window.location.hostname;
const subdomain = hostname.split('.')[0];
const dbName = `myapp-${subdomain}`;
// Each subdomain gets its own Fireproof database
const { database } = useFireproof(dbName);
Custom Domain Setup
Add
--domainflag:node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --domain myapp.comConfigure wildcard DNS at your DNS provider:
*.myapp.com CNAME myapp.exe.xyz myapp.com ALIAS exe.xyzSet up wildcard SSL on the VM:
ssh myapp.exe.dev sudo apt install certbot sudo certbot certonly --manual --preferred-challenges dns \ -d "myapp.com" -d "*.myapp.com"
CLI Options
| Option | Description |
|---|---|
--name <vm> |
VM name (required) |
--file <path> |
HTML file to deploy (default: index.html) |
--domain <domain> |
Custom domain for wildcard setup |
--ai-key <key> |
OpenRouter API key for AI features |
--multi-tenant |
Enable subdomain-based multi-tenancy |
--tenant-limit <$> |
Credit limit per tenant in dollars (default: 5) |
--clerk-key <pem> |
Clerk PEM public key for JWT verification |
--clerk-webhook-secret <secret> |
Clerk webhook signing secret |
--reserved <list> |
Comma-separated reserved subdomain names |
--preallocated <list> |
Pre-claimed subdomains (format: sub:user_id) |
--dry-run |
Show commands without executing |
--skip-verify |
Skip deployment verification |
Redeployment
After making changes, redeploy with:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp
SSH Access
Access your VM directly:
ssh myapp.exe.dev
Architecture
exe.dev VM (exeuntu image)
├── nginx (serves all subdomains via server_name _)
├── claude (pre-installed CLI)
├── /usr/local/bin/bun ← Bun runtime (system-wide)
├── /var/www/html/
│ ├── index.html ← Your Vibes app
│ └── HANDOFF.md ← Context for remote Claude
├── (with --ai-key)
│ ├── /opt/vibes/proxy.js ← AI proxy service (port 3001)
│ └── vibes-proxy.service ← systemd unit
└── (with --clerk-key)
├── /var/www/registry-server.ts ← Registry service (port 3002)
├── /var/www/html/registry.json ← Subdomain claims data
└── vibes-registry.service ← systemd unit
Port Assignments
| Service | Port | Purpose |
|---|---|---|
| AI Proxy | 3001 | OpenRouter proxy for useAI hook |
| Registry | 3002 | Subdomain claim/check API |
- No server-side logic - pure static hosting (unless using AI proxy)
- Persistent disk - survives restarts
- HTTPS by default - exe.dev handles SSL for *.exe.xyz
- Claude pre-installed - continue development on the VM
Post-Deploy Debugging
After deployment, always work with local files - they are the source of truth. SSHing to read deployed files is slow and wastes tokens.
| Task | Use Local | Use SSH |
|---|---|---|
| Editing/debugging code | ✅ Always | ❌ Never |
| Checking console errors | ✅ Local file | ❌ No need |
| Verifying deploy | ❌ | ✅ curl https://vm.exe.xyz |
| Server-specific issues | ❌ | ✅ Only if local works but remote doesn't |
To redeploy after local fixes:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name <vmname> --file index.html
SSL Configuration
The deploy script preserves existing SSL by using include files for AI proxy config. When manually editing nginx:
- Never replace the entire config - only add/modify specific blocks
- Check for SSL first: Look for
listen 443 sslin the config - Use includes: Put new configs in
/etc/nginx/conf.d/or separate files - Test before reload:
sudo nginx -t
exe.dev Home Directory
The home directory on exe.dev VMs is /home/exedev (not ~ expansion). For manual file operations:
# Use explicit path or /tmp for staging
scp file.html vmname.exe.xyz:/home/exedev/
# or
scp file.html vmname.exe.xyz:/tmp/
What's Next?
After successful deployment, present these options using AskUserQuestion:
Question: "Your app is live at https://${name}.exe.xyz! What's next?"
Header: "Next"
Options:
- Label: "Share my URL"
Description: "Get the shareable link for your app. I'll confirm the public URL and you can send it to anyone - they'll see your app immediately with full functionality."
- Label: "Make changes and redeploy"
Description: "Continue iterating locally. Edit your files here, then run deploy again to push updates. The VM keeps running so there's zero downtime during updates."
- Label: "Continue development on VM"
Description: "Work directly on the server. SSH in and use the pre-installed Claude to make changes live. Great for server-specific debugging or when you want changes to persist immediately."
- Label: "I'm done for now"
Description: "Wrap up this session. Your app stays live at the URL - it runs 24/7 on exe.dev's persistent VMs. Come back anytime to make updates."
After user responds:
- "Share URL" → Confirm "Your app is live at https://${name}.exe.xyz - share this link!"
- "Make changes" → Acknowledge, stay ready for local edits
- "Continue on VM" → Provide:
ssh ${name}.exe.dev -t "cd /var/www/html && claude" - "I'm done" → Confirm app stays live, wish them well