# SaaS Forge — Build a SaaS in 5 commands

The OpenLife `v1.15→v1.18` arc landed a coherent end-to-end path from
**idea → production SaaS**. Five composable CLI commands. Each one has
safety boundaries. Each one is tested.

This guide walks the full sequence with **real CLI output** captured
from smoke tests on 2026-05-22 against `@openlife/cli@1.18.2`.

> Last verified: 2026-05-22 with `@openlife/cli@1.18.2`,
> Claude Code CLI 2.1.148, Node 22.22.0.

---

## TL;DR — the 5 commands

```bash
# 1. Bootstrap a working SaaS skeleton (v1.16)
openlife scaffold meu-saas --template=nextjs-supabase-clerk-stripe

# 2. Pull a design system from a real brand (v1.15) — optional
openlife design pull stripe

# 3. Implement a story autonomously (v1.17)
openlife story execute 1.1

# 4. Open a PR (v1.18)
openlife ship

# 5. Deploy to production (v1.18)
openlife deploy --provider=vercel
```

Each command can run standalone. The sequence is opinionated but not
forced — skip any step you don't need.

---

## Step 1 — Scaffold

**Command:** `openlife scaffold <name> --template=<id>`

**What it does:** Bootstraps a Next.js 16 + Supabase + Clerk + Stripe
SaaS skeleton in `./<name>/`. Substitutes `{{project_name}}` tokens.
Excludes `node_modules/`, `.next/`, `.vercel/`.

**Real output** (from smoke 2 against `@openlife/cli@1.18.2`):

```
$ openlife scaffold meu-saas --template=nextjs-supabase-clerk-stripe

✓ Scaffolded 24 file(s) into /tmp/meu-saas
  Template: Next.js + Supabase + Clerk + Stripe (SaaS-ready)
  Substituted: 5 file(s)

Next steps:
  cd meu-saas
  Copy .env.example to .env.local and fill in the keys
  npm install
  supabase db push       # apply the migration in supabase/migrations/
  npm run dev            # http://localhost:3000
  Deploy: vercel --prod  # or push to a Vercel-connected git remote
```

**What you get:**

| Layer | Pre-wired |
|---|---|
| Framework | Next.js 16 (App Router) + React 19 + TypeScript strict |
| Auth | Clerk middleware + sign-in/sign-up routes + `UserButton` + server helpers + svix webhook |
| Database | Supabase Postgres dual clients (anon + service-role) + `users` + `subscriptions` migration with RLS policies |
| Billing | Stripe SDK + plan defs + checkout flow + customer portal + subscription webhook |
| Styling | Tailwind v3.4 + globals.css ready for shadcn/ui drop-in |
| Deploy | `vercel.json` + `.env.example` documenting 8 required keys |

**Verify it builds** (5 commands away from a working dev server):

```bash
cd meu-saas
cp .env.example .env.local        # fill in your Clerk + Stripe + Supabase keys
npm install                        # ~1min — installs Clerk 6, Stripe 17, Supabase ssr 0.5
npm run build                      # MUST succeed (was broken before v1.18.2)
npm run dev                        # → http://localhost:3000
```

**Other templates available:** `openlife scaffold list` shows what's
bundled. Add your own under `~/.openlife/templates/<id>/` (any directory
with a `manifest.yaml`).

**Custom templates from git** (v1.18.1+):

```bash
openlife scaffold meu-app --from=https://github.com/myorg/template-saas
openlife scaffold meu-app --from=myorg/template-saas   # github shorthand
```

Allowed hosts: github.com, gitlab.com, bitbucket.org, codeberg.org.
SSH URLs deferred to v1.18.4.

---

## Step 2 — Design (optional)

**Command:** `openlife design pull <brand>`

Bundled brands ship in `vendor/awesome-design-md-core/`: apple, claude,
stripe, vercel, linear. Each one has:

- `DESIGN.md` — the LLM-friendly design system spec (~30KB)
- `tokens.json` — extracted color/spacing/font tokens
- `preview.html` — visual rendered preview
- `render-contract.json` — what components the design assumes
- `style-fingerprint.json` — archetype classification

**Real output:**

```
$ openlife design list

Available design profiles:
  apple        modernist-functional  quality: A
  claude       editorial-warm         quality: A
  stripe       institutional-trust    quality: A
  vercel       brutalist-minimal      quality: A
  linear       editorial-cool         quality: B+
```

```
$ openlife design pull stripe

✓ Pulled stripe (~120KB → ./design/profiles/stripe/)
  Apply tokens.json values to tailwind.config.ts to use the brand colors.
```

**Extract from any URL** (v1.15+):

```bash
openlife design extract https://your-brand.com
```

Runs the bundled `design-md-extractor` skill (8-phase pipeline: fetch
→ collect CSS → static-CSS detect → HTML→md → prompt LLM → normalize
→ log+quality → preview.html). SSRF-guarded — only public URLs allowed.

---

## Step 3 — Story execute (the autonomous loop)

**Command:** `openlife story execute <id>`

This is the most ambitious piece. Reads `docs/stories/<epic>/<id>.story.md`,
spawns a Claude/Gemini/Codex CLI per persona, mutates the story file as
ACs are checked, commits locally per progress, transitions status to
`InReview` when all ACs are done.

**Real output** (from smoke 3 — actual Claude execution against a
synthetic 1-AC story):

```
$ cat docs/stories/epic-test/1.1.story.md
# Story 1.1 — [BUG] Change greeting from Hello to Hola
**Status:** InProgress
## Acceptance Criteria
- [ ] Function `greet()` in src/greet.ts returns "Hola" instead of "Hello"

$ cat src/greet.ts
export function greet(): string { return "Hello"; }

$ openlife story execute 1.1 --max-iters=2 --stop-after=builder

✓ Story 1.1 completed in 1 iteration(s) (71.8s)
  ACs checked this run: 1
  Local commits: 1 (b013f35)
  Branch: feature-rename

  Run `openlife ship` to push (v1.18+) or `git push -u origin feature-rename`

$ cat src/greet.ts
export function greet(): string { return "Hola"; }

$ grep "\[" docs/stories/epic-test/1.1.story.md
- [x] Function `greet()` in src/greet.ts returns "Hola" instead of "Hello"
```

**Flags:**

```bash
openlife story execute 1.1                            # default: 10 max-iters
openlife story execute 1.1 --plan-only                # print plan, no mutation
openlife story execute 1.1 --max-iters=5              # safety cap
openlife story execute 1.1 --persona=builder          # force single persona
openlife story execute 1.1 --stop-after=builder       # skip QA gate
openlife story execute 1.1 --dry-run-tests            # mutate but skip test runs
openlife story execute 1.1 --force                    # allow dirty working tree
openlife story execute 1.1 --adapter=mock             # CI-safe (no real LLM)
openlife story execute 1.1 --json                     # structured output
```

**Persona routing** (from story title prefix):

| Tag | Persona sequence |
|---|---|
| `[BUG]/[FIX]/[HOTFIX]` | builder → sentinel |
| `[FEATURE]/[FEAT]` | genesis → architect → builder → sentinel |
| `[DESIGN]/[UI]/[UX]` | prism → builder → sentinel |
| `[DATA]/[DB]/[MIGRATION]` | mesh → builder → sentinel |
| `[REFACTOR]/[DEBT]` | architect → builder → sentinel |
| `[DOCS]/[POLISH]` | lyra → sentinel |
| (untagged) | builder → sentinel |

**Safety boundaries:**

- Refuses to operate on `main/master/develop/production` — auto-creates
  `story-<id>-<slug>` branch first
- Refuses dirty working tree without `--force`
- Max-iters hard ceiling 50 (env override can only lower)
- No-progress watchdog: 3 consecutive iters without new AC checked → HALT
- Adapter timeout 5min, SIGKILL after 2s grace
- Atomic story-file mutation via `AtomicWriter.writeStringAtomic`
- Test command whitelist (npm/yarn/pnpm/bun/pytest/jest/vitest/mocha/cargo/go/deno)
- Role-gated status transitions (dev cannot transition to Done — qa/po only)
- **NEVER calls `git push` or `gh pr create`** — that's `openlife ship`'s job

---

## Step 4 — Ship (PR + push)

**Command:** `openlife ship`

The vortex-delegated escape hatch. Authorizes a single GitOps.push() call
via a try/finally pattern (push is `@vortex` exclusive everywhere else),
builds a PR body from story metadata, opens via `gh pr create`.

**Flow:**

1. Detect story (most-recent InReview, or `--story <id>` override)
2. Refuse if on a protected branch
3. Refuse if working tree is dirty
4. Authorize → push branch (`--set-upstream`) → release
5. Build PR title from story tag (`feat(1.1):`, `fix(1.1):`, etc.)
6. Build PR body from description + AC checkboxes + File List
7. `gh pr create --body-file -` (body via stdin)

**Real output (dry-run):**

```
$ openlife ship --dry-run

✓ Ship complete.
  Story:   1.1
  Branch:  feature-rename
  Remote:  origin
  PR:      (dry-run — gh pr create not invoked)
  ⚠ dry_run: push skipped
```

**Flags:**

```bash
openlife ship                              # default: auto-detect story, push, open PR
openlife ship --story 1.1                  # force a specific story
openlife ship --target main                # PR base branch (default: main)
openlife ship --no-pr                      # push only, no PR
openlife ship --dry-run                    # print what would happen
openlife ship --json                       # structured output
```

---

## Step 5 — Deploy

**Command:** `openlife deploy [--provider=<name>]`

Pluggable adapter pattern. 4 providers ship in v1.18.1+:

| Provider | CLI | Marker | Native rollback |
|---|---|---|---|
| `vercel` | `vercel` | `vercel.json` or `.vercel/` | ✓ `vercel rollback` |
| `railway` | `railway` | `railway.toml` or `railway.json` | ✓ `railway redeploy <id>` |
| `cloudflare` | `wrangler` | `wrangler.toml` or `wrangler.jsonc` | ✓ `wrangler rollback <id>` |
| `fly` | `fly` (or `flyctl`) | `fly.toml` | ✓ `fly releases rollback <ver>` |

**Auto-detection:**

```
$ openlife deploy list
Available deploy providers:
  vercel       ✓ installed
  railway      ✓ installed
  cloudflare   ✗ CLI not on PATH
  fly          ✗ CLI not on PATH
```

```bash
$ openlife deploy
# Auto-detects from filesystem markers + installed CLIs

$ openlife deploy --preview                 # ephemeral URL
$ openlife deploy --provider=cloudflare    # force a specific provider
$ openlife deploy --skip-preflight         # bypass env+config checks
```

**Preflight** checks per provider:
- **Vercel:** `.vercel/project.json` link state, `vercel.json` validity, `VERCEL_TOKEN` in CI
- **Railway:** `railway.toml` presence, build strategy declaration, `RAILWAY_TOKEN` in CI
- **Cloudflare:** `wrangler.toml` validity, `name` field, `CLOUDFLARE_API_TOKEN` in CI
- **Fly:** `fly.toml` validity, `app` field, `FLY_API_TOKEN` in CI

**Rollback:**

```bash
$ openlife deploy rollback                          # auto-detect provider, last good
$ openlife deploy rollback --to dpl_xxx             # specific deployment
$ openlife deploy rollback --provider=fly --to v17  # specific provider + version
```

---

## Composition skills (v1.18+)

Markdown skills the host CLI applies on demand. Each documents the exact
file recipes + env vars + post-install steps + composition notes.

```
$ openlife skills list | grep -E "(clerk|stripe|supabase|vercel)-wire"
clerk-wire              Wire Clerk auth into a Next.js project
stripe-billing-wire     Wire Stripe subscription billing
supabase-rls-wire       Wire Supabase RLS policies for multi-tenant SaaS
vercel-deploy-wire      Configure a project for Vercel deploy
```

When you invoke them via the host CLI (e.g. `/openlife:skills:clerk-wire`),
the host applies the documented file recipes against your repo.

---

## Full worked example — "add a billing tab"

```bash
# 1. Scaffold the project
openlife scaffold meu-saas --template=nextjs-supabase-clerk-stripe
cd meu-saas && npm install

# 2. Verify it builds
npm run build && npm run dev   # → http://localhost:3000 ✓

# 3. Create a story
mkdir -p docs/stories/epic-billing
cat > docs/stories/epic-billing/1.1.story.md <<'EOF'
---
storyId: 1.1
epic: epic-billing
title: '[FEATURE] Add cancel-subscription button to billing page'
status: InProgress
---
# Story 1.1 — Add cancel-subscription button to billing page
**Status:** InProgress
## Acceptance Criteria
- [ ] /dashboard/billing renders a "Cancel" button when subscription is active
- [ ] Clicking it calls Stripe API and updates subscriptions.status='canceled'
- [ ] UI updates immediately without page reload
EOF

git add . && git commit -m "scaffold + initial story"

# 4. Loop runs it autonomously
openlife story execute 1.1 --max-iters=5

# 5. Inspect what the loop did
git log --oneline                 # → 4-5 commits from the loop
git diff main..HEAD --stat        # → files modified

# 6. Ship
openlife ship                     # → pushes branch + opens PR

# 7. Deploy (once PR is merged)
git checkout main && git pull
openlife deploy                   # → auto-detects vercel, deploys to prod
```

---

## Safety summary

| Boundary | Where |
|---|---|
| `git push` is @vortex-exclusive | Anywhere except `openlife ship` |
| `main/master/develop/production` protected | scaffold, story-execute, ship |
| Dirty working tree refused | story-execute, ship |
| Test command whitelist | story-execute (npm/yarn/etc only) |
| Adapter timeouts (5min) | story-execute, deploy |
| Atomic story file mutation | story-execute |
| Role-gated status transitions | story-execute |
| SSRF guard on git URLs | scaffold --from |
| Preflight before deploy | deploy (--skip-preflight to bypass) |
| No automatic webhook config | All SaaS skills document but don't auto-call provider APIs |

---

## Troubleshooting

| Symptom | Likely cause |
|---|---|
| `template_not_found: <id>` | Run `openlife scaffold list`. User templates live in `~/.openlife/templates/`. |
| `target_not_empty: <dir>` | Pass `--force` or scaffold into a new directory. |
| `npm run build` fails on a fresh scaffold | If on `<v1.18.2`, you have the broken template — re-scaffold with v1.18.2+. |
| `story_not_found: <id>` | Verify the story file exists under `docs/stories/<epic>/<id>.story.md` with correct `storyId` in frontmatter. |
| `dirty_working_tree` | Commit your changes or pass `--force` (will not stash). |
| `no_progress_after_3_iters` | The loop couldn't make progress on the open ACs after 3 iterations. Refine the story or run interactively. |
| `gitops_refused: not_authorized` | Something tried to push outside the `openlife ship` flow. By design. |
| `cli_not_installed: <provider>` | Install the deploy provider's CLI: `npm i -g vercel`, etc. |

---

## Release timeline

| Release | What landed | Smoke status |
|---|---|---|
| v1.15.0 (2026-05-21) | Design.md Hub | ✓ |
| v1.16.0 (2026-05-22) | Scaffold engine + Next.js template | ⚠ template had 5 bugs (fixed in v1.18.2) |
| v1.17.0 (2026-05-22) | Autonomous Story Loop | ✓ (smoke 3 verified) |
| v1.18.0 (2026-05-22) | Ship + Deploy + 4 SaaS skills | ✓ |
| v1.18.1 (2026-05-22) | Cloudflare + Fly + Railway rollback + git URL scaffold | ✓ |
| v1.18.2 (2026-05-22) | Template hotfix (5 bugs from smoke 2) | ✓ |
| v1.18.3 (2026-05-22) | This documentation | — |

---

## What's next

- **v1.19.0** — CI step that scaffolds + builds each bundled template (prevents the v1.18.2 class of regression). Multi-environment deploy promote. AI-driven preflight remediation.
- **v1.18.4** — SvelteKit-Postgres-Lucia template + SSH git URLs + branch/ref selection.
- **Issue #9** — Cross-machine bridge (Tailscale + cloud memory + mTLS), independent track.

---

## Where to go from here

- **Full CLI reference:** `openlife --help` and the subcommand-specific
  `--help` flags
- **Architecture deep-dive:** `docs/complete-context.md` (v1.14
  Composer + Graph + RAG)
- **Design Hub:** `docs/design-hub.md`
- **Per-release changelogs:** `docs/v1.X-changelog.md`
- **Manifesto:** `MANIFESTO.md` — broader OpenLife vision
