---
name: web-app-builder
description: "Build and deploy web apps (Next.js + Tailwind + shadcn/ui). Use when: creating apps, build errors, chunk loading errors, CSS 404, stale processes, QA testing, deployment (dev or prod), Cloud Run, agency-app-starter template. Keywords: create-next-app (DON'T USE), npm run build, pm2, Caddy, deploy.sh, Ralph Wiggum loops."
---

# Web App Builder

Build production-ready web apps with live deployment.

## Stack

- **Framework**: Next.js **latest** (App Router) — ALWAYS use `next@latest`. Never pin to a specific version. CVE-2025-29927 (CVSS 9.1) affects all Next.js < 15.2.3. Outdated versions = auth bypass vulnerability.
- **Styling**: Tailwind CSS v4 + shadcn/ui
- **Language**: TypeScript
- **Auth**: Firebase Auth (Email/Password)
- **Database**: Firebase Firestore
- **Deployment**: Caddy reverse proxy → `*.dev.apps.symph.co`

## ⛔ MANDATORY: Gather Context First — NO EXCEPTIONS

**Before spawning ANY app build:**

```bash
# Step 1: Run gatherer
bash ~/.openclaw/workspace/skills/build-context-gatherer/scripts/gather-context.sh <slug> app

# Step 2: Read the bundle
CONTEXT=$(cat /tmp/build-context-<slug>.md)

# Step 3: Embed in spawn task (NOT just file paths!)
sessions_spawn({
  task: "Build/modify app for <product>.\n\n## CONTEXT BUNDLE\n${CONTEXT}\n\n## YOUR TASK\n...",
  label: "app-<slug>"
})
```

### ❌ NEVER DO THIS
```bash
# WRONG: Telling subagent to read files
sessions_spawn({
  task: "Build the app. Read design-system-prompt.md at /path/to/file"
})
# Subagents skip files, partially read them, or misinterpret them!
```

### ✅ ALWAYS DO THIS
```bash
# CORRECT: Embed the actual content
CONTEXT=$(cat /tmp/build-context-<slug>.md)
sessions_spawn({
  task: "Build the app.\n\n## CONTEXT BUNDLE\n${CONTEXT}\n\n..."
})
# Content is IN the prompt — can't be skipped
```

**Policy set by Ed (Design) — 2026-02-18. Violations = build rejected.**

## CRITICAL: Read Before Building

**ALWAYS check `agency/Learnings/build-learnings.md` first!**

Key rules:
- **NEVER** use `create-next-app` — ALWAYS clone `agency-app-starter`
- **ALWAYS** configure Firebase Auth before features
- **ALWAYS** create landing page at `/` (public, no auth)
- **ALWAYS** create `/privacy` and `/terms` pages (even if not asked)
- **ALWAYS** run QA Agent with browser testing before marking complete
- Build is **NOT complete** until QA Agent passes

## CRITICAL: Deployment Environment Rules

**Know the difference between DEV and PROD deployments:**

| Environment | Method | URL Pattern | Use Case |
|-------------|--------|-------------|----------|
| **Development/Staging** | VPS + Caddy | `*.dev.apps.symph.co` | Testing, demos, internal review |
| **Production** | Cloud Run | Custom domain | Live customer-facing apps |

**Rules:**
- ⚠️ **Cloud Run is for PRODUCTION ONLY** — Do not recommend for dev/staging/testing
- ⚠️ **VPS + Caddy is for NON-PROD** — All development, staging, and demo deployments
- When asked "where should I deploy?", always clarify the environment first
- Default to VPS + Caddy unless explicitly building for production release

## CRITICAL: Worktree Isolation (MANDATORY)

**NEVER modify product code directly in `apps/main/`.** Always use git worktrees for isolation.

This prevents concurrent requests from overwriting each other's work.

### Before ANY Code Modification

```bash
SKILL_DIR="/home/aria/.openclaw/workspace/skills/worktree-manager/scripts"
SLUG="<product-slug>"
TASK_ID="task-$(date +%s)"

# Create isolated worktree
WORK_DIR=$($SKILL_DIR/create-worktree.sh "$SLUG" "$TASK_ID" "<requester>" "<requester-id>" "<description>")

# Work in the worktree, NOT apps/main
cd "$WORK_DIR"
```

### After Completing Changes

```bash
# Commit your changes
git add . && git commit -m "feat: description of changes"
git push -u origin "task/$TASK_ID"

# Attempt merge back to main
$SKILL_DIR/merge-worktree.sh "$SLUG" "$TASK_ID"

# If MERGE_CONFLICT: notify requesters, wait for human decision
# If MERGE_SUCCESS: worktree auto-cleaned, deploy from apps/main
```

### Why This Matters

Without worktrees:
- User A requests "change header color"
- User B requests "update footer text"
- Both modify `apps/main/` simultaneously
- One overwrites the other → lost work, build errors, chunk mismatches

With worktrees:
- Each task gets isolated branch
- Changes merge cleanly or conflict is caught
- No lost work

**See `worktree-manager` skill for full documentation.**

## Workflow

### 1. Spawn Subagent (MUST Use Orchestration Pattern)

App building takes 20-40 minutes. **Main session must orchestrate:**

#### For EXISTING Products (Code Modification)

```bash
# Step 1: Main session creates worktree
SKILL_DIR="/home/aria/.openclaw/workspace/skills/worktree-manager/scripts"
SPAWN_CTX=$($SKILL_DIR/spawn-code-task.sh "<slug>" "<requester>" "<requester-id>" "<description>")
TASK_ID=$(echo "$SPAWN_CTX" | jq -r '.taskId')
SPAWN_TASK=$(echo "$SPAWN_CTX" | jq -r '.spawnTask')

# Step 2: Spawn with prepared context
sessions_spawn({
  task: SPAWN_TASK,
  label: "code-<task-id>",
  runTimeoutSeconds: 2400
})

# Step 3: After completion, main session merges
$SKILL_DIR/merge-worktree.sh "<slug>" "$TASK_ID"
# Then rebuild/deploy from apps/main
```

#### For NEW Products (Initial Build)

New products don't need worktrees yet (no concurrent work). But still include critical rules:

```
sessions_spawn({
  task: "Build a [description] app.\n\nCRITICAL RULES:\n- Use agency-app-starter template (NEVER create-next-app)\n- Include Firebase Auth and landing page\n- Run QA agent before completing\n- Commit all changes before finishing\n\nFollow the web-app-builder skill.",
  label: "build-[appname]",
  runTimeoutSeconds: 2400
})
```

Respond to user: "Building your app — I'll ping you when it's ready with a live URL."

### 2. Clone Agency App Starter (NOT create-next-app!)

```bash
cd /home/aria/projects

# ⚠️ NEVER use create-next-app
git clone git@github.com:symphco/agency-app-starter.git [appname]
cd [appname]
rm -rf .git
git init
```

The starter template includes:
- Firebase Auth setup
- Firestore patterns
- Security rules
- QA/Security/Architect subagents in `.claude/agents/`
- Ralph Wiggum development methodology

### 2.5. REQUIRED: Load Design Assets (If Building Symph Agency Product)

**Before writing ANY code**, check if design artifacts exist and consume them:

```bash
# Check for design folder
ls /home/aria/products/[slug]/design/

# Expected structure:
# design/
# ├── assets/
# │   ├── logo.png          ← MUST copy to public/
# │   └── screen-*.png      ← Reference during build
# ├── brand-guidelines.md   ← Extract colors, fonts, rules
# └── screens.md            ← Screen inventory
```

#### Step A: Extract Brand System

Read `design/brand-guidelines.md` and extract:

| Extract | Use For |
|---------|---------|
| Primary color hex | `--color-primary` in Tailwind config |
| Secondary color hex | `--color-secondary` |
| Accent color hex | `--color-accent` |
| Font names | Google Fonts imports + font-family |
| Spacing scale | Tailwind spacing config |
| Border radius | Component styling |

**Update `tailwind.config.ts`** with extracted values:
```typescript
// Example: Don't use defaults — use brand colors!
colors: {
  primary: '#0D7377',      // From brand guidelines
  secondary: '#E8734A',    // Not generic blue
  accent: '#F5A623',
  // ... etc
}
```

#### Step B: Copy Logo and Assets

```bash
# Copy logo to public folder
cp /home/aria/products/[slug]/design/assets/logo.png ./public/logo.png

# If hero images exist, copy those too
cp /home/aria/products/[slug]/design/assets/hero*.png ./public/ 2>/dev/null || true
```

**Use the actual logo in components** — not a placeholder SVG icon:
```tsx
// ❌ DON'T: Generic placeholder
<div className="w-12 h-12 bg-primary rounded-lg">
  <HouseIcon />
</div>

// ✅ DO: Actual brand logo
<Image src="/logo.png" alt="Product Name" width={48} height={48} />
```

#### Step C: Reference Screen Mockups

Open `design/assets/screen-*.png` files and **match the layout**:

- Header style (color, gradient, shape)
- Card layouts and shadows
- Button styles and colors
- Navigation structure (bottom nav, sidebar, etc.)
- Typography hierarchy

**The build should look like the mockups**, not a generic card-based layout.

#### Step D: Create Design System Prompt

Save extracted design rules to `src/lib/design-system.ts` for consistency:
```typescript
export const designSystem = {
  colors: {
    primary: '#0D7377',
    secondary: '#E8734A',
    // ...
  },
  fonts: {
    heading: 'Plus Jakarta Sans',
    body: 'Inter',
  },
  // Component-specific rules from brand guidelines
};
```

**⚠️ BUILD WILL BE REJECTED** if design assets exist but are ignored.

---

Launch Claude Code:
```bash
claude --dangerously-skip-permissions
```

Then send `j` + `Return` to bypass the permissions dialog.

#### Claude Code Prompting

**Include design context** in your initial prompt:

Give Claude Code a complete spec in one message:
```
Build [app description].

## Design System (FROM design/brand-guidelines.md)
- Primary: [hex] — headers, nav, primary buttons
- Secondary: [hex] — CTAs, alerts
- Accent: [hex] — highlights, success states
- Fonts: [heading font] for headings, [body font] for body
- Logo: Already copied to public/logo.png — USE IT

## Screen Reference
Match the layouts in design/assets/screen-*.png:
- [Describe header style from mockup]
- [Describe card/component styles]
- [Describe navigation structure]

## Requirements
- [Feature 1]
- [Feature 2]
- [API integrations if any]

Use Next.js App Router, Tailwind, and shadcn/ui components.
Keep API keys in .env.local (server-side only).
The app MUST visually match the design mockups.
```

Monitor progress with `process log`. Let it run until it completes or asks questions.

### 3. Deploy with Caddy (DEV/STAGING ONLY)

Use the `caddy-manager` skill:

```bash
~/.openclaw/workspace/skills/caddy-manager/scripts/caddy-manager.sh add [appname]
```

This auto-allocates a port and creates `[appname].dev.apps.symph.co`.

**Note:** This is for development and staging only. For production deployment, use Cloud Run with the appropriate CI/CD pipeline.

### 4. Build and Start Server

**ALWAYS get port from caddy-ports.json (source of truth):**
```bash
SLUG="appname"
cd /home/aria/projects/${SLUG}  # or /home/aria/products/${SLUG}/apps/main
npm run build
/home/aria/bin/app-manager start ${SLUG}
```

`app-manager` handles port allocation, PM2 supervision, and Caddy config automatically.

### 4b. CRITICAL: After Any Code Changes (Copilot/Claude Code)

**Production servers don't hot-reload.** After making code changes, ALWAYS:

```bash
SLUG="appname"
cd /home/aria/products/${SLUG}/apps/main  # or /home/aria/projects/${SLUG}

# Rebuild
npm run build

# Restart via app-manager (handles PM2 + port cleanup automatically)
/home/aria/bin/app-manager restart ${SLUG}
```

**Rule:** Never report "done" without rebuild + restart. Changes aren't visible until server restarts.

### 4c. Trigger Before/After Approval Card (Auto)

After successful deploy, trigger approval card capture for visual comparison:

```bash
# Fire-and-forget async capture
~/.openclaw/workspace/scripts/post-deploy-hook.sh "$SLUG" app "Change 1" "Change 2" "Change 3"
```

This captures before/after screenshots and creates an approval card in the product's chat.
The capture runs async (~15s) and doesn't block the deploy.

### 5. Verify with Browser

```javascript
browser({ action: "open", targetUrl: "https://[appname].dev.apps.symph.co", profile: "openclaw" })
// Wait for load
browser({ action: "screenshot", profile: "openclaw" })
```

Test key flows by clicking through the UI.

### 6. Design Review Checklist

Before reporting back, verify:
- [ ] **Contrast** — Text readable against backgrounds (WCAG AA minimum)
- [ ] **Status indicators** — Clear visual distinction (green=success, red=error, yellow=warning)
- [ ] **Visual hierarchy** — Headers, cards, and sections clearly differentiated
- [ ] **Consistency** — Colors, spacing, and typography consistent throughout
- [ ] **Dark mode** — If dark theme, ensure all elements have proper contrast
- [ ] **Interactive states** — Buttons/links have hover/active states

Fix any design issues before delivering to user.

### 7. Legal Pages (REQUIRED)

**All user-facing apps MUST have:**
- [ ] `/privacy` — Privacy Policy page
- [ ] `/terms` — Terms of Service page
- [ ] Footer links to both pages on all public pages

**Generate based on:**
- Product name and description from `brain.md`
- What data is collected (auth, user content, analytics)
- Contact email (use `support@symph.co` if not specified)

**Template structure:**
```
app/
├── privacy/
│   └── page.tsx    ← Privacy Policy
├── terms/
│   └── page.tsx    ← Terms of Service
└── components/
    └── Footer.tsx  ← Include legal links
```

**Note:** These are baseline legal pages. Production apps should have legal review.

### 8. Report Back

Send to user:
- Screenshot of working app
- Live URL
- Brief summary of what was built

## Environment Variables

**Server-side only** (no `NEXT_PUBLIC_` prefix):
```bash
# .env.local
API_KEY=xxx
DATABASE_URL=xxx
```

Access in API routes only:
```typescript
// app/api/example/route.ts
const key = process.env.API_KEY
```

## Directory Structure

```
/home/aria/projects/
├── [appname]/
│   ├── app/
│   │   ├── page.tsx          # Main page
│   │   ├── layout.tsx        # Root layout
│   │   └── api/              # API routes (server-side)
│   ├── components/           # React components
│   ├── .env.local           # Secrets (gitignored)
│   └── package.json
```

## Common Patterns

### API Route with External Service
```typescript
// app/api/generate/route.ts
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  const { input } = await request.json()
  const apiKey = process.env.SERVICE_API_KEY
  
  const result = await fetch('https://api.service.com/endpoint', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${apiKey}` },
    body: JSON.stringify({ input })
  })
  
  return NextResponse.json(await result.json())
}
```

### Frontend Calling API
```typescript
const response = await fetch('/api/generate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ input: userInput })
})
const data = await response.json()
```

## CRITICAL Build Process Rules

### ALWAYS Clone agency-app-starter

**NEVER use `create-next-app`!**

```bash
git clone git@github.com:symphco/agency-app-starter.git {slug}
cd {slug} && rm -rf .git && git init
```

### Firebase Auth BEFORE Features

**ALWAYS configure Email/Password auth before building any features:**

```bash
source /tmp/firebase-auth-env/bin/activate
# Read .env.local for ACTUAL project ID first!
python skills/firebase/scripts/enable_email_auth.py <project-id>
```

### Landing Page at `/` is REQUIRED

Every app needs a public landing page at `/` — no auth required. This is the entry point.

### QA Agent with E2E Testing

Build is **NOT complete** until QA Agent passes. QA must:
- Test actual functionality (submit forms, verify outcomes)
- Check data persistence (create → refresh → re-login → verify)
- Stop and report blockers if auth fails

### NO MOCK DATA

Every list/dashboard must query real Firestore. Fresh accounts = empty states.

**Mock data = QA failure = build not complete.**

### Ralph Wiggum Loops

```
implement → build → QA → fix → repeat
```

Keep looping until QA passes all tests.

## Production Deployment (Cloud Run)

**For PRODUCTION releases only — not DEV/staging.**

### Standard Configuration

| Aspect | Value |
|--------|-------|
| Platform | GCP Cloud Run (us-central1) |
| Domain | `{slug}.symph.co` |
| DNS | CNAME → `ghs.googlehosted.com` |
| SSL | Auto-provisioned by Google |
| CI/CD | GitHub Actions |

### Slug Shortening

Strip suffixes for cleaner domains:
- `ai-teacher-assistant` → `ai-teacher.symph.co`
- `travel-planning-app` → `travel-planning.symph.co`

### DNS Setup (Cloud DNS)

```bash
gcloud dns record-sets create {slug}.symph.co. \
  --zone=symph-dot-co \
  --type=CNAME \
  --rrdatas="ghs.googlehosted.com." \
  --project=symph-dns
```

### DEV vs PROD Isolation

| Aspect | DEV | PROD |
|--------|-----|------|
| URL | `*.dev.apps.symph.co` | `*.symph.co` |
| Firestore | `dev_*` collections | `prod_*` collections |
| Payments | Test/sandbox mode | Live mode |
| Env file | `.env.local` | `.env.production` |

**Never share credentials between environments!**

## Troubleshooting

### STALE PROCESSES FIRST

When debugging "works locally but broken in prod" or chunk loading errors:

```bash
# 1. Check what's running on the port
lsof -i :PORT

# 2. Check process age
ps aux | grep next-server

# 3. Kill ALL stale processes before rebuilding
lsof -ti :PORT | xargs kill -TERM
```

Learned this after 45min wasted in a rebuild loop.

### Common Issues

| Issue | Solution |
|-------|----------|
| Port in use | Run `caddy-manager.sh list` and pick unused port |
| Caddy not routing | Check `systemctl status caddy`, reload with `sudo systemctl reload caddy` |
| Claude Code stuck | Send `Ctrl+C` via process write, restart |
| Build errors | Check `npm run build` output, fix TypeScript errors |
| Chunk loading errors | Kill stale processes, rebuild, restart |
| CSS 404 | Old server serving stale assets — kill and restart |
| Permission denied | Check Firestore rules match namespace (see firebase skill) |

### Git Push Large Repos

SSH pushes timeout on large repos (>50MB assets). Use HTTPS:

```bash
# For new repos (SymphonyAgents org)
git remote set-url origin https://github.com/SymphonyAgents/repo.git
# For legacy repos (symphco org)
# git remote set-url origin https://github.com/symphco/repo.git
gh auth setup-git
git push

# ALWAYS add symphdevtools as collaborator after creating repo
gh api -X PUT "/repos/SymphonyAgents/repo/collaborators/symphdevtools" -f permission=push
```

## Symph Agency Integration

When building as part of the Symph Agency pipeline (Build phase):

### Product Context

Load before building:
```
[product]/agency/prd.md         # Requirements
[product]/agency/context.md     # Brand voice
[product]/agency/artifacts/design/figma.md  # Design link
Learnings/builder-learnings.md  # Build rules
```

### Project Location

For Symph Agency products:
```
/home/aria/projects/[product-name]/
├── app/                    # Next.js code (this skill creates this)
└── agency/                 # Agency files (already exists)
```

### Build Phase Outputs

Update after building:
- `[product]/agency/brain.md` — Update progress, status
- `[product]/agency/artifacts/build/` — Store build artifacts
  - `api-docs.md` — API documentation
  - `releases.md` — Release notes
  - `dev-url.md` — DEV URL reference

### Quality Gate (REQUIRED)

Before delivering ANY build, run the **frontend-design-excellence** checklist:

1. Load skill: `skills/frontend-design-excellence/SKILL.md`
2. Run through anti-pattern checklist — any hits = must fix
3. Generate hero images if missing (use `nano-banana-pro` skill)
4. Verify visual depth, typography, and motion exist
5. Take screenshots and do the "squint test"
6. **⛔ TRIGGER POST-DEPLOY HOOK** (MANDATORY):
   ```bash
   bash ~/.openclaw/workspace/scripts/post-deploy-hook.sh "$SLUG" app "Change 1" "Change 2"
   ```

**Build is NOT complete until it passes the excellence checklist AND triggers the approval card.**

Common fixes needed:
- Replace emoji heroes with generated illustrations
- Add hover states to all interactive elements
- Pick a distinctive display font (not Inter)
- Add at least one "signature element"

### Human Checkpoint

Before advancing to Market phase:
1. Present DEV URL for testing
2. Show GitHub repo link
3. List completed modules
4. Get human approval

### Client Customizations (Deliver Phase)

For Flex/Fluid customers:
```
/home/aria/projects/[product]/clients/[client]/
├── app/                    # Forked code
└── specs.md               # Customization specs
```

Use same build process but deploy to client-specific URL.

## References

- [shadcn/ui Components](references/shadcn-components.md) — Component usage examples