crouton-run-and-operate
Operate a running crouton app — locally or on deployed staging — from cold boot to evidence capture. ("crouton" = this monorepo's schema-driven CRUD framework: @fyit/crouton-* packages that generated Nuxt apps extend as layers.)
When to use / when NOT to use
| You want to… | Use |
|---|
| Boot, log in, seed, inspect, observe, screenshot a running app | this skill |
| Recreate the environment (install/build traps, typecheck story, versions) | crouton-build-and-env |
| Deploy to staging / wire CI / first-time Cloudflare setup | deploy (apps), poc-deploy (pocs); prod only via deploy-production |
Generate/apply Drizzle migrations, schema.mjs gotcha, infra tables | db-migrations |
| Mirror one D1 env into another (refresh staging from prod) | db-clone |
| Run the Playwright fixture smoke (boot+auth+CRUD proof) | e2e-smoke |
| Deploy a preview for UI sign-off (the gate workflow) | ui-proposal |
| Symptom → root-cause lookup | sibling crouton-diagnostics-index |
| Measure the harness itself (context budget, traces) | loop-station / sibling crouton-harness-observability |
1. Boot any app locally from cold, with auth working
Prerequisites (one line — full story in crouton-build-and-env): pnpm install && pnpm build:packages. Apps consume @fyit/* packages from dist/; skipping build:packages = dev-server error on missing crouton-core/dist.
export BETTER_AUTH_SECRET=$(openssl rand -hex 32) # required (crouton-auth throws if unset); better-auth only WARNS below 32 chars
pnpm --filter velo dev # or: cd apps/velo && pnpm dev
- There is no root
dev script (verified: root package.json scripts). Root CLAUDE.md's "Development Commands" listing pnpm dev only works inside an app dir — a known doc contradiction; the app package.json is ground truth here.
- On Claude-web sessions the SessionStart hook (
.claude/hooks/session-start.sh) already did install + build:packages and exported a cached dev BETTER_AUTH_SECRET (from ~/.crouton-dev-auth-secret) — check echo $BETTER_AUTH_SECRET before generating your own.
- Apps ship
.env.example (copy to .env). Trap: it hardcodes BETTER_AUTH_URL=http://localhost:3000 but each app runs on its own pinned port (table below) — set it to the app's real port or cookies misbehave.
- E2e fixtures (
fixtures/*) commit a dummy .env on purpose (BETTER_AUTH_SECRET=dev-fixture-secret-do-not-use-in-production) — they boot with zero env setup.
- No manual DB step:
hub: { db: 'sqlite' } in each app's nuxt.config.ts (root CLAUDE.md gotcha — never hub: { database: true }) auto-applies all layers' migrations at dev boot; locally it's a sqlite file, at runtime it maps to Cloudflare D1/KV/R2.
Fixed dev ports — pinned in each app's/POC's nuxt.config.ts devServer.port. Snapshot 2026-07-02 — regenerate with the re-verify block before relying on numbers.
| App | Port | | POC | Port |
|---|
| apps/triage | 3005 | | pocs/alexdeforce | 3001 |
| apps/velo | 3006 | | pocs/sintlukas | 3003 |
| apps/kassa | 3007 | | pocs/crouton-builder-demo | 3010 |
| e2e fixtures | 3000 | | pocs/crouton-builder and pocs/kvr | 3011 — collision; run one with --port |
| | | pocs/loop-station | 3021 |
Pocs that set no port (blog, thinkgraph*, three-demo, booking-demo at the snapshot) default to 3000/auto-bump.
First login (auth = better-auth via @fyit/crouton-auth; facts from e2e/CLAUDE.md):
- Login/register is a RouteModal overlay, not a page —
/auth/login redirects to / and opens the modal (useAuthModal). Register a fresh account in the modal on a fresh DB.
- Signup creates no team. Programmatically, create one via the better-auth org API:
POST /api/auth/organization/create then /set-active — both require an Origin header (CSRF). See ensureTeam() in e2e/helpers.ts for the exact calls. Or seed staff accounts instead (§2).
- Generated collections render at
/admin/{teamSlug}/crouton/{collectionKey}.
- Session check:
GET /api/auth/get-session returns the authenticated user (what smoke-deployed.mjs uses as login proof).
Do NOT verify with nuxt preview: crouton collection pages currently 500 under production-preset SSR — an internal data-fetch loses the auth cookie (e2e/CLAUDE.md, open problem). Operate against nuxt dev.
1.5 Reproduce a runtime bug locally — MOBILE-FIRST (never push a fix blind)
The method rule is in AGENTS.md (Bug work → reproduce against the running system): for a runtime bug (reload loop, 500, broken interaction), reproduce + verify the fix locally before pushing. Here's the crouton how-to. This stack is phone-first (#722, atelier-plan) — drive at a phone viewport (390×844) by default; only check desktop when a surface is explicitly wide.
Write a throwaway driver inside the repo tree (playwright resolves from repo node_modules; a /tmp script can't import it) — repro.local.mjs at the repo root; don't commit it.
import { chromium } from 'playwright'
const CHROME = '/opt/pw-browsers/chromium-1194/chrome-linux/chrome' // glob newest if the build bumped
const browser = await chromium.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox'] })
const ctx = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 3 }) // MOBILE
const page = await ctx.newPage()
page.on('console', m => console.log('console', m.type(), m.text()))
page.on('load', () => console.log('PAGE LOAD', page.url())) // count loads → reload vs re-render
page.on('framenavigated', f => f === page.mainFrame() && console.log('nav', f.url()))
page.on('pageerror', e => console.log('PAGEERROR', e.message))
Auth/team bootstrap for a team-scoped page (better-auth's org calls need an origin header, else MISSING_OR_NULL_ORIGIN). Do it via ctx.request so cookies are shared:
POST /api/auth/sign-up/email {email,password,name} → POST /api/auth/organization/create {name,slug} (headers {origin, referer}) → POST /api/auth/organization/set-active {organizationId} → seed rows via the collection API POST /api/teams/<orgId>/<collection> → page.goto('/…').
Instrument, then observe — don't theorise. A full-page reload in Nuxt is often chunk-error auto-reload; counting load events tells a reload from a re-render. (#988 board reload: three blind pushes vs one local repro that found it — autosave→refetch→reload — in minutes.)
2. Seeding
Package demo data — every app has db:seed scripts (verified in the apps' package.json):
pnpm --filter velo db:seed # = crouton-seed --db velo-db --with-staff (local)
pnpm --filter velo db:seed:staging # = crouton-seed --db velo-staging-db --remote --with-staff
crouton-seed (bin: packages/crouton-cli/bin/crouton-seed.mjs) discovers SeedProviders from every extended @fyit/crouton-* package, emits idempotent upsert SQL, and executes it via npx wrangler d1 execute <db> --local|--remote. Flags (verified): --db (required) --remote --dir --team (default test1) --locale (default nl) --with-staff --dry-run. It also seeds the app's default layout: crouton.layout.json → a layout_configs row with id default (#709; lib/seed-app.ts).
⚠️ THE seed-visibility trap (code-derived, not reproduced end-to-end): a local crouton-seed writes via wrangler d1 execute --local into <app>/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlite (miniflare = wrangler's local Cloudflare simulator), but nuxt dev with hub: { db: 'sqlite' } reads <app>/.data/db/sqlite.db. seed-app.ts has no copy step into .data/ (verified: it only shells out to wrangler d1 execute — re-check with the seed grep in the re-verify block). So locally-seeded data may never appear in the dev app. crouton db-pull handles the split explicitly — lib/db-pull.ts copies the imported DB to .data/db/sqlite.db and marks _hub_migrations applied. If seeded data "doesn't show up", this split is why; workaround is copying the sqlite file into .data/db/sqlite.db by hand (stop dev first).
App-specific content seeds (seedData/ convention): velo keeps CSVs/markdown under apps/velo/seedData/{school-velotek,velosolidaire}/, consumed by HTTP endpoints POST /api/seed and POST /api/seed/velosolidaire (apps/velo/server/api/seed/*.post.ts — additive, dedupes users by email, hashes passwords properly). This is the "per-collection seeding" the db-clone skill contrasts itself with.
Pull real remote data local:
cd apps/velo && npx crouton db-pull --env staging --dry-run # then drop --dry-run
Flags (verified in bin/crouton-generate.js dbPullCmd): --env, --config, --keep-sql, --dry-run. Flow: export remote D1 → wipe local wrangler D1 → import → copy to .data/db/sqlite.db.
3. Inspecting the local DB
4. Review overlay and eruda
- Review overlay —
NUXT_PUBLIC_CROUTON_REVIEW=true at build time (read in packages/crouton-devtools/src/module.ts) makes @fyit/crouton-devtools install @fyit/crouton-feedback (epic #960) with the GitHub sink defaulted: a glasses launcher with Console (eruda), Annotate (pin a comment on an element → resolves the source file → lands as a 🎯 Preview feedback PR comment), and Changelog. Generated cf:staging scripts bake the flag in; zero prod footprint. The sign-off workflow around it belongs to the ui-proposal skill.
- Eruda mobile devtools layer — opt-in per app:
extends: ['@fyit/crouton-devtools/eruda'], active in local dev or when NUXT_PUBLIC_CROUTON_ERUDA=true at build (verified in packages/crouton-devtools/eruda/nuxt.config.ts; default off, chunk never fetched in prod). Set the flag in cf:staging only, never cf:deploy.
5. Observing deployed staging
Deploy/migrate mechanics live in the deploy skill — this is the watching side.
| Need | Command | Caveat |
|---|
| Live Worker logs | pnpm --filter triage logs / --filter fanfare logs (= npx wrangler tail triage|fanfare) | Only some apps have a logs script (grep for it — re-verify block), and those tail the production worker. No staging tail script exists anywhere (verified at the snapshot); hand-run npx wrangler tail <app>-staging (unverified against a live worker — needs CF creds). No log persistence: an error nobody was tailing is gone. |
| Prove a deployed preview works | node scripts/smoke-deployed.mjs --url https://<app>.pmcp.dev --email <e> --password <pw> [--app <n>] [--manifest <app>/deploy.config.json] | Login proof via /api/auth/get-session → optional CRUD round-trip (from deploy.config.json smoke.crud) → screenshot screenshots/<app>-smoke.png. CI runs it per deploy, report-only unless smoke.required: true. |
| Seed a loginable review account on a deploy | node scripts/seed-review-login.mjs --url <deployedUrl> --email <e> --password <pw> | Uses the app's own HTTP auth routes (real password hashing); best-effort. --seed-team <slug> attaches it to the seeded demo org — with --db <d1> (remote, the deploy path) or --sqlite <path> (the local .data/db/sqlite.db, #1777). |
| Look at a change locally, in one command | pnpm preview <app> | Boot + seed + disposable review login + a prefilled one-click URL (scripts/app-preview.mjs, #1777). Use this instead of hand-rolling boot→seed→login; it's the only preview a packages-only change gets, since PR deploy previews match only apps/<app>/** by design. --no-seed / --no-login for faster re-runs; landing page comes from reviewLogin.landing in the app's deploy.config.json. |
| What's in the staging DB | node scripts/db-counts.mjs --app <app> --env staging | §3. |
At last verification, no runtime analytics was wired in any deployed app: packages/crouton-analytics exists but no app/poc/fixture depends on it (unverified beyond a grep of package.jsons — re-run that grep before relying on it; from the #1073 discovery sweep, adapters were in-flight, #947).
6. Screenshots
node scripts/app-shots.mjs <baseUrl> <path[:name]> [more paths...] [--out <dir>] [--desktop|--viewport WxH]
# e.g. node scripts/app-shots.mjs http://localhost:3006 /:home /auth/login:login # MOBILE (default)
Shots are MOBILE (390×844) by default — this stack is phone-first (#722); verify the phone layout first and only add --desktop (1280×900) for surfaces that are explicitly wide. (Also pnpm app:shots.) Writes screenshots/<name>.png; exits 1 on any failure. It auto-resolves the pre-installed chromium under /opt/pw-browsers/ (globs the newest build; override with PLAYWRIGHT_CHROMIUM_PATH) — a failing npx playwright install does NOT mean "no browser"; doctrine: crouton-harness-observability §5. All screenshots go in screenshots/ at repo root (HARD GATE, root CLAUDE.md).
7. Where artifacts land
| Artifact | Location | Committed? |
|---|
| Local dev DB (NuxtHub) | <app>/.data/db/sqlite.db (+ applied-migration copies in .data/db/migrations/sqlite/) | No (.data gitignored — verified git check-ignore) |
| Wrangler-local D1 (miniflare) | <app>/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlite | No |
| Build output | <app>/.output/ | No |
| Screenshots | screenshots/<name>.png | No (*.png gitignored) |
| Drizzle migrations (source of truth) | <app>/server/db/migrations/sqlite/ | Yes |
| e2e outputs | e2e/.auth/, playwright-report/, test-results/ | No |
Provenance and maintenance
verified: 2026-07-02
grep -rn devServer apps/*/nuxt.config.ts pocs/*/nuxt.config.ts # ports table
grep -n '"dev"\|"logs"\|db:seed' apps/*/package.json # scripts / tail gap
grep -n 'wrangler\|\.data' packages/crouton-cli/lib/seed-app.ts # seed write target (has a .data copy step appeared?)
grep -rln crouton-analytics apps pocs fixtures --include=package.json # analytics still unconsumed?
head -20 scripts/app-shots.mjs scripts/smoke-deployed.mjs # usage lines
grep -n CROUTON_REVIEW packages/crouton-devtools/src/module.ts # review-overlay gate