Practices/Real SaaS with Claude Code
4 days · 16 units
PradhyaPractice 15 · Real SaaS with Claude CodeFounder/Builder

From git init to first paying user.

Most “build with AI” practices stop at the prototype. This one runs all the way through. Real prompts, the guardrails layer, the test suite, the security pipeline, the deploy. Sixteen units. By Sunday you have a SaaS service running with paying-grade hygiene.

Audience
Indie devs · founders · shipping engineers
Length
4 sessions · 2 hours each
Walk-away
A deployed SaaS service, real hygiene
Prereq
Claude Code installed; comfortable shipping code
What you’ll be able to do by the end
  • Write a CLAUDE.md that captures your team’s actual conventions
  • Wire the GitHub MCP server with a scoped token
  • Run /ultrareview before every merge with 6 specialized review agents
  • Ship a SaaS service with the 5-Friday post-launch ritual running
§ 15.01.01 · Unit 01

The day-zero ritual.

Six commits you make once. Get them right and the rest of the week flows. Get them wrong and you fight your own setup forever.

1. git init & first commit 2. pyproject.toml / package.json with pinned deps 3. .env.example with required vars (never .env) 4. tests/ folder with one passing test 5. CLAUDE.md (most important file) 6. .claude/settings.json with hooks + permission allowlist
Day zero · the six commits that set the stage

Run the day-zero checklist.

You’ll do
Take one repo through the six commits above — or scaffold a fresh one — until all six exist.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder, or git init a fresh one.
  2. Walk the six rows: confirm a first commit exists, deps are pinned (pyproject.toml/package.json), .env.example is committed and .env is git-ignored, tests/ has one passing test, CLAUDE.md exists, and .claude/settings.json exists.
  3. Create whichever rows are missing. Ask Claude to draft the .env.example and the one passing test if you don’t have them.
Verify
All six exist: git log --oneline | head shows the commit, ls .env.example .claude/settings.json CLAUDE.md returns three paths, and your test command exits 0.

Stretch. Add to CI: a step that fails the build if CLAUDE.md is missing or empty.

§ 15.01.02 · Unit 02

CLAUDE.md, written right.

The single most important file in your repo for working with Claude Code. Write it with the discipline of a hiring manager writing a job description.

# CLAUDE.md

## What this repo is
A SaaS that does X for Y customers. Built in [stack]. Production at [domain].

## How to run
- Setup: make setup
- Run: make run
- Test: make test (pytest with coverage gate at 80%)
- Lint: make lint (ruff + mypy)

## Conventions (the ones I actually want enforced)
- Use sqlalchemy 2.0 style. Never db.query() — always db.execute(select()).
- All routes go through app/routes/. No new top-level routers.
- Tests in tests/. Mirror the source tree.
- Type hints required on every function signature.

## Do NOT
- Use type-ignore casually.
- Add a JavaScript framework. We're on vanilla.
- Mock the database in tests. Use the test container.
- Write docstrings longer than 3 lines.

## Security guardrails
- Never log a token, password, or PII.
- All user input is parameterized in SQL.
- All external API calls use the configured retry harness.
- Claude must not run git push or npm publish autonomously.

Write your CLAUDE.md.

You’ll do
Open your project’s CLAUDE.md. If you don’t have one, write one in 10 minutes from the template above.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder.
  2. Copy the template above. Fill the five sections (what / run / conventions / do-NOT / security) with this repo’s real facts.
  3. Keep it under 100 lines.
  4. Start a fresh Claude session. Ask one project-specific question whose answer is only in CLAUDE.md (e.g. “what’s the test command?”).
Verify
The fresh session answers the project-specific question correctly without you pasting any context first.

Stretch. Add to CI: a step that fails the build if CLAUDE.md is missing or empty.

§ 15.01.03 · Unit 03

Dependencies & conventions.

Pick a small, opinionated stack. Pin everything. Let Claude know what you’ve picked.

LayerPickWhy
API framework FastAPI (Python) / Hono (TS)Type-first, fast, small.
DB Postgres (Neon / Supabase) Boring tech, infinite docs.
ORM SQLAlchemy 2.0 / Drizzle Async, type-safe.
Auth Clerk / Auth.js / Stytch Don’t roll your own.
Payments Stripe One option. Stop researching.
Deployment Render / Fly / Vercel Git push to deploy.
AI Anthropic SDK Frontier capability.
Tests pytest + Playwright Unit + e2e.
Lint / format ruff + mypy / biome Fast, opinionated.

Lock your stack and tell Claude.

You’ll do
Make one decision per row of the table, pin the versions, and record the picks in CLAUDE.md.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder.
  2. For each layer (framework, DB, ORM, auth, payments, deploy, AI, tests, lint), write down your one pick. Stop researching once a row has an answer.
  3. Pin every dependency to an exact version in pyproject.toml/package.json — no ^ or ~ ranges.
  4. Paste the nine picks into the Conventions section of CLAUDE.md so Claude stops suggesting alternatives.
Verify
Every dependency line shows a pinned version (grep -E '[\^~]' pyproject.toml returns nothing), and CLAUDE.md names all nine picks.

Stretch. Add a lockfile (uv.lock, poetry.lock, or package-lock.json) and commit it.

§ 15.01.04 · Unit 04

GitHub MCP server.

The official GitHub MCP server gives Claude Code direct access to issues, PRs, code search, and CI status — without you copy-pasting.

# .mcp.json at repo root or ~/.claude/.mcp.json
# Option A — local Docker (recommended for self-hosted setups)

{
  "mcpServers": {
    "github": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
        "ghcr.io/github/github-mcp-server"
      ],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_PAT}"
      }
    }
  }
}

# Option B — remote (OAuth, no Docker needed)
# {
#   "mcpServers": {
#     "github": {
#       "url": "https://api.githubcopilot.com/mcp/"
#     }
#   }
# }

# Claude can now: list issues, read PR diffs, search code across repos,
# check CI runs, comment on issues.
# Note: the older `npx @modelcontextprotocol/server-github` package was
# deprecated in April 2025 — use the official Docker image or remote URL.
Scope the token Use a GitHub PAT scoped to only the repos this Claude session should touch. Never paste your full-scope PAT — if the session gets compromised, the blast radius is bounded.

Wire the GitHub MCP server.

You’ll do
Add the GitHub MCP from the config above with a scoped token, then have Claude read real repo data.
Steps
  1. Create a GitHub PAT scoped to one repo (fine-grained token → that repo only).
  2. Add the Option A or Option B config above to .mcp.json at your repo root; put the token in your environment as GITHUB_PAT.
  3. Restart Claude Code. Run /mcp and confirm github is listed and connected.
  4. Ask: “list the open issues on this repo.” Confirm the titles match what you see on GitHub — real data, not invented.
Verify
/mcp shows github connected, and the issue list Claude returns matches the repo’s actual open issues.

Stretch. Add a second MCP (Linear or Notion). Cross-service queries unlock workflows you can’t do with either alone.

§ 15.02.01 · Unit 05

The feature-dev loop.

The shape of every feature you ship. Six steps. Don’t skip any.

1. issue 2. worktree 3. plan 4. build 5. test 6. PR
The feature-dev loop · six steps, no shortcuts

Run the loop through plan mode.

You’ll do
Take one small change through steps 1–6, using plan mode to scope it before any code is written.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder — pick a small refactor (rename a concept, extract a helper).
  2. Make a worktree or branch for it (step 2 of the loop).
  3. Hit Shift+Tab to enter plan mode (step 3). Describe the change. Read the plan; edit it before approving.
  4. Approve, let Claude build and run tests (steps 4–5), then open the PR (step 6).
Verify
The plan listed every file it then changed (no surprise edits), and the branch ends green: your test command exits 0.

Stretch. Use plan mode for anything touching > 3 files. Always.

§ 15.02.02 · Unit 06

Real prompts for real features.

The four prompts that earn their keep on every feature.

1. Plan mode kickoff

[plan mode]
Read the issue at .claude/issues/{n}.md.
Read the relevant code under app/services/.
Read CLAUDE.md.

Produce a plan with:
- Files to modify (list with one-line reason each)
- Files to create
- Tests to add
- Migration changes (if any)
- Open questions you'd want me to answer

Do not touch any files yet.

2. Implement after plan approval

Implement the plan we just agreed on.

Rules:
- Write the failing test first, then the code that makes it pass.
- Commit after each step.
- After the last step, run `make test && make lint` and fix issues.
- Do not push. Do not open the PR.

3. Critique your own diff

Review the diff for this branch as a hostile reviewer.
Push back hard on:
- Tests that don't actually exercise the new logic
- Functions over 40 lines
- Magic numbers / strings
- Missing error handling at boundaries
- Anything that violates CLAUDE.md

Quote specific lines. Don't be polite.

4. PR description

Write the PR description for this branch.

Sections:
- One-line summary
- Why (link the issue)
- What changed (bulleted, code-level)
- How to test
- Risks I should think about

No emoji. Write as if I'll read it on the train.

Run the hostile-reviewer prompt.

You’ll do
Paste prompt #3 (“Critique your own diff”) above at Claude on a real diff. Note what gets caught.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder — make one small edit so there’s a diff to review.
  2. Copy prompt #3 above (the “hostile reviewer” one) and run it against your current branch’s diff.
  3. Read every finding. Label each: real issue / false positive / style.
  4. Fix the real issues before you push.
Verify
The review returns at least one finding with a quoted file:line, and you can name one real issue from it you’d have missed on your own read.

Stretch. Save the prompt as a reusable skill or slash command so every branch starts from it.

§ 15.02.03 · Unit 07

The guardrails layer.

Six rules a SaaS-shipping Claude session should never violate. Encode them as hooks; trust them.

  • No git push without explicit approval. A PreToolUse hook on git push requires the magic word.
  • No rm -rf outside the repo root. Pattern-match on Bash; reject anything that escapes.
  • No secrets in commits. Pre-commit hook scans for high-entropy strings.
  • No new top-level dependencies. See check-package.sh from Practice 02.
  • Tests must pass before commit. A pre-commit runs make test; aborts on failure.
  • No type-ignore without a comment. Lint catches it.

Encode one guardrail as a hook.

You’ll do
Pick one rule from the six above and wire a PreToolUse hook that actually blocks it.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder.
  2. Pick the git push guardrail (rule 1). In .claude/settings.json, add a PreToolUse hook that matches git push and exits non-zero unless an approval phrase is present. (Reuse check-package.sh for the “no new top-level deps” rule if you prefer.)
  3. Restart Claude Code so the hook loads.
  4. Ask Claude to run git push. Watch the hook intercept it.
Verify
The push is blocked: Claude reports the hook denied the command, and git log origin/<branch> shows the remote did not advance.

Stretch. Encode a second rule (block rm -rf that escapes the repo root). Two hooks, two fewer ways to wreck a Friday.

§ 15.02.04 · Unit 08

Pre-commit hooks.

The line of defense between “Claude wrote good code” and “the commit on main is good code.” Run on every commit, no exceptions.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.7.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.13.0
    hooks:
      - id: mypy
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.20.0
    hooks:
      - id: gitleaks      # blocks secrets in commits

  - repo: local
    hooks:
      - id: pytest-fast
        name: pytest (fast subset)
        entry: pytest -m 'not slow' -q
        language: system
        pass_filenames: false
        stages: [commit]

Install pre-commit.

You’ll do
Install pre-commit with the config above on a real project. Run it on your current dirty tree.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder.
  2. Install: pip install pre-commit (or brew install pre-commit).
  3. Save the .pre-commit-config.yaml above to your repo root, then run pre-commit install.
  4. Run pre-commit run --all-files. Fix what it flags.
Verify
pre-commit run --all-files ends with every hook reporting Passed (or Skipped), and a deliberately bad commit — e.g. one with an unused import — is rejected by the hook.

Stretch. Add a custom local hook for a team rule (no console.log / console.warn in prod code).

§ 15.03.01 · Unit 09

Setting up testing.

Three layers. Each catches a different class of bug. Skip any one and the bugs come at the wrong time.

unit pure functions ~1k tests · <5s integration real DB · real API mocks ~100 tests · ~30s e2e Playwright · real browser ~10 tests · ~2 min
Three layers · fast tests on every commit · e2e in CI

Stand up the three layers.

You’ll do
Get one passing test in each layer — unit, integration, e2e — even if it’s trivial.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder.
  2. Unit: write one pytest over a pure function. Ask Claude to add a second covering an edge case.
  3. Integration: one test that hits a real (test) DB or a mocked external API.
  4. e2e: one Playwright test that loads a page and asserts on visible text. (If headless, run with --headed once to watch it.)
Verify
All three commands exit 0: the unit run, the integration run, and playwright test each report at least 1 passing test.

Stretch. Mark the slow tests (@pytest.mark.slow) so pre-commit can run only the fast subset.

§ 15.03.02 · Unit 10

The PR review pipeline.

Six specialized reviewers run in parallel against every PR. Each looks for one class of bug. The orchestrator aggregates.

ReviewerLooks for
security Injection, secret exposure, auth gaps, SSRF, path traversal.
performance N+1 queries, work inside loops, unbounded allocations, slow I/O.
tests Coverage gaps for new behavior, missing edge and error cases.
types Types that fail to enforce invariants; illegal states left representable.
comments Comments drifted from code, stale TODOs, comments that lie.
simplify Dead code, duplication, needless complexity, missed reuse.
The pack ships, ready to install The six reviewers and the command that fans them out are in code-examples/ultrareview/: the slash command commands/ultrareview.md and the six agents security, performance, tests, types, comments, simplify. The lab below installs them.

Install /ultrareview and run it.

You’ll do
Copy the command + six agents into .claude/, then run /ultrareview on a real diff.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder — make one small edit so there’s a diff.
  2. Right-click → Save As to download ultrareview.md and the six agent files (security / performance / tests / types / comments / simplify).
  3. Place them: mkdir -p .claude/commands .claude/agents, drop ultrareview.md into .claude/commands/ and the six agent files into .claude/agents/. (Full instructions in the README.)
  4. Restart Claude Code. With your edit unstaged, run /ultrareview.
Verify
The report contains all six reviewer sections — the headings ## Security, ## Performance, ## Tests, ## Types, ## Comments, ## Simplify — plus a ranked “Top issues” list at the top.

Stretch. Copy the pack into ~/.claude/commands/ and ~/.claude/agents/ so /ultrareview works in every repo.

§ 15.03.03 · Unit 11

The security review agent.

The reviewer that pays for itself the first time it catches an injection. Run it on every PR; run it again before every deploy.

---
name: security-review
description: Review the diff for security issues. Run before every merge.
tools: [Bash, Read, Grep]
---

You are a senior application-security engineer reviewing a pull request
for a SaaS service. Check for:

1. SQL injection — raw SQL with f-strings or string concatenation
2. Command injection — subprocess calls with user input
3. Path traversal — user input in file paths without normalization
4. Secret exposure — tokens / passwords / keys logged or in errors
5. Auth gaps — endpoints without auth, or auth checked AFTER work
6. SSRF — user-supplied URLs fetched without validation
7. Unsafe deserialization (use only safe loaders)
8. CORS gaps — wildcards in production
9. Dependency vulnerabilities — recent CVEs in pinned versions
10. Logging hygiene — PII or secrets in log lines

For each finding:
- Severity: BLOCKER / HIGH / MEDIUM / LOW
- File:line
- Quote the problem line
- Suggested fix

No preamble. Just the table.

Run the security-review agent.

You’ll do
Install the agent above and run it on a real diff. Triage every finding.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder — or reuse the security agent you installed in §15.03.02.
  2. Save the prompt above to .claude/agents/security-review.md (or use the installed one). Restart Claude Code.
  3. On a branch with recent changes, ask Claude to run the security review over the diff.
  4. Read every finding. Tag each: BLOCKER / HIGH / MEDIUM / LOW. Fix BLOCKERs before merging.
Verify
The agent returns a severity-tagged table where every row has a file:line and a quoted line — not prose — covering the diff you gave it.

Stretch. Wire it into CI as an automated review comment on every PR.

§ 15.03.04 · Unit 12

/ultrareview workflow.

One slash command dispatches the six reviewers in parallel against the current diff. Returns a ranked report. Use before every merge.

/ultrareview security performance tests types comments simplify aggregated · ranked report
/ultrareview · six agents fan out · one report comes back

Gate a merge on /ultrareview.

You’ll do
Run /ultrareview before a real merge and act on the ranked report — the way the CI gate will.
Steps
  1. Use the pack you installed in §15.03.02 (code-examples/ultrareview/). No repo of your own? Run against any project folder, e.g. the capstone agents/ folder.
  2. On a branch you’re about to merge, run /ultrareview main (review everything since main).
  3. Read the “Top issues” list. Apply the merge rule: any BLOCKER or HIGH — fix before you merge.
  4. Re-run /ultrareview main after the fix to confirm it cleared.
Verify
The second run’s “Top issues” list shows zero BLOCKER and zero HIGH rows — the gate is green.

Stretch. Add /ultrareview to your PR checklist so the gate runs before every merge, not just this one.

§ 15.04.01 · Unit 13

Deploy pipeline.

Three environments. Two gates. One easy rollback. Boring on purpose.

# main branch → preview env → staging → production
#                  (auto)      (manual)   (manual after staging soak)

# CI gates (must pass before each promotion):
#   1. pytest + mypy + ruff (all tests pass)
#   2. /ultrareview (no BLOCKER or HIGH security findings)
#   3. integration tests against staging DB
#   4. Playwright e2e against preview URL

# Rollback: git revert + redeploy. 90 seconds.

Write your deploy.md and wire previews.

You’ll do
Document the main → preview → staging → production path, and turn on per-PR preview deploys.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder.
  2. Write a one-page deploy.md: for each environment, what triggers a deploy, who can promote, and the CI gates from the block above (tests, /ultrareview, integration, e2e).
  3. Pick a host (Vercel / Netlify / Cloudflare Pages / Fly). Connect the repo; enable preview deployments per branch.
  4. Push a small change. Confirm the preview URL appears on the PR.
Verify
deploy.md exists and names every gate, and the open PR shows a clickable preview URL that loads the change.

Stretch. Add the “90-second rollback” (git revert + redeploy) to deploy.md and test it once on staging.

§ 15.04.02 · Unit 14

Secrets & auth.

Get this right once on day three and forget it. Get it wrong and you’ll find a token in a public repo on day forty.

  • Secrets live in the platform’s secret manager. Render/Fly/Vercel environment variables, not .env files committed.
  • .env is gitignored. .env.example is committed with placeholder values.
  • Gitleaks runs in pre-commit AND in CI. Two layers; the second catches your team.
  • Rotate tokens quarterly. Especially the GitHub PAT used by the MCP server.
  • Use a managed auth service. Clerk, Auth.js, Stytch. Do not write password hashing yourself.

Scan for secrets and move them.

You’ll do
Run gitleaks across history and the working tree; relocate anything it finds.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder.
  2. Run gitleaks detect across history.
  3. For every finding: rotate the secret if it’s real, then move it to the platform’s secret manager or an (un-committed) .env.
  4. Confirm .env is in .gitignore and .env.example holds only placeholders. Add the gitleaks pre-commit hook so this can’t recur.
Verify
gitleaks detect exits 0 (no leaks), and git check-ignore .env prints .env — proof it’s ignored.

Stretch. Move secrets into a managed store (1Password, Vault, AWS Secrets Manager). A local .env should never reach git.

§ 15.04.03 · Unit 15

Observability from day one.

Adding observability after the first incident is twice as much work as adding it before. Three things on day one.

  1. Structured logging. Every log line is JSON with level, request_id, user_id, route. Use structlog (Python) or pino (Node).
  2. Error tracking. Sentry or equivalent. Hooked into your framework’s exception handler.
  3. One dashboard. Requests/sec, p50/p90 latency, error rate, cost per day. Practice 14 covers the agent-specific stack on top.

Add the three day-one signals.

You’ll do
Wire structured logging, error tracking, and three alerts on one service.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder — logging applies to any process.
  2. Structured logging: make every log line JSON with level, request_id, user_id, route (structlog / pino).
  3. Error tracking: hook Sentry (or equivalent) into the framework’s exception handler.
  4. Add 3 alerts — error rate > 1%, p99 latency > 2s, availability < 99.5% — then trigger a fake breach for each.
Verify
A log line parses as JSON with all four fields, a thrown test error shows up in Sentry, and all 3 alerts fire on their fake breaches.

Stretch. Add a 4th alert: daily cost. AI bills spike silently — set a budget threshold. See Practice 14 for the agent-specific stack.

§ 15.04.04 · Unit 16 · Close

The post-launch loop.

Shipping is the start, not the end. The five Friday-afternoon rituals that keep a SaaS healthy.

  1. Read the dashboard. 15 minutes. Note what changed and why.
  2. Read three random support threads. User language is the real spec.
  3. Run /ultrareview against main. Surface what slipped in during the week.
  4. Update CLAUDE.md. Every correction you made twice this week belongs in the file.
  5. Write one paragraph to your team. What shipped, what’s next, what you learned.
The closing The teams that scale SaaS with Claude Code aren’t the ones with the smartest prompts. They’re the ones with the hygiene loop above, running every Friday for a year. The compound is in the consistency.

Run the Friday loop once, now.

You’ll do
Do all five rituals above in one sitting against a real (or the sample) project.
Steps
  1. No repo of your own? Run this against any project folder, e.g. the capstone agents/ folder.
  2. Read the dashboard (or your logs) for 15 min; jot what changed. Skim three support threads (or three issues).
  3. Run /ultrareview main against the main branch. Note anything that slipped in.
  4. Update CLAUDE.md with any correction you made twice this week. Write one paragraph to your team (what shipped, what’s next).
Verify
All five produced an artifact: notes from the dashboard, three threads read, an /ultrareview report, a CLAUDE.md diff (git diff CLAUDE.md is non-empty), and a sent paragraph.

Stretch. Put the loop on your calendar for every Friday. The compound is in the consistency, not the heroics.