- Added REPO_INVENTORY.md with all repos, branches, remotes, and staging info - Added .gitignore - Synced all existing docs from local workspace - Centralized documentation hub for GrowQR team
18 KiB
GrowQR Demo Runbook
Use this when restarting the full local demo after everything has been stopped. It should bring back the workflows dashboard, growqr-app frontend session launchers, backend, Rivet workflow actor runner, interview-service, roleplay-service, QScore service, resume-builder, matchmaking, and the per-user Gitea/OpenCode stack.
What Must Be Running
- Docker Desktop
qscore-service: Postgres, Redis, API, workerinterview-service: API, Postgres, MinIO, bucket setuproleplay-service: API, Postgres, MinIO, bucket setupresume-builder: Docker container with Postgres (port 5433), API (port 8002)matchmaking: API on host port 8006growqr-backend: backend Postgres plus local Node backendworkflows--dashboard: Next.js frontendgrowqr-app/frontend: Next.js frontend for the new interview/roleplay session UI- Per-user OpenCode containers, created by backend provisioning (Gitea is central/shared)
The known demo ports are:
- Workflows dashboard:
http://localhost:3000 - GrowQR app frontend session launchers:
http://localhost:3002 - Backend:
http://localhost:4000 - Backend Rivet proxy:
http://localhost:4000/api/rivet - Interview service:
http://localhost:8007 - Roleplay service:
http://localhost:8008 - QScore service:
http://localhost:8000 - Resume builder:
http://localhost:8002 - Matchmaking service:
http://localhost:8006 - Resume builder DB:
localhost:5433 - QScore Redis:
localhost:6379 - Central Gitea:
http://localhost:3001(shared org-wide, changes.md §2A) - Backend Postgres:
localhost:5432 - Per-user OpenCode containers: dynamically allocated from
20000-29999
Required Env Files
Before starting, verify these files exist and contain the local demo values:
growqr-backend/.envgrowqr-backend/.env.localworkflows--dashboard/.env.localgrowqr-app/frontend/.env.localinterview-service/.envroleplay-service/.envgrowqr-app/resume-builder/.env.local(created by setup script)
Do not print secrets into chat or commit them. The important non-secret shape is:
- Backend service URLs should point to
127.0.0.1:INTERVIEW_SERVICE_URL=http://127.0.0.1:8007ROLEPLAY_SERVICE_URL=http://127.0.0.1:8008QSCORE_SERVICE_URL=http://127.0.0.1:8000RESUME_SERVICE_URL=http://127.0.0.1:8002MATCHMAKING_SERVICE_URL=http://127.0.0.1:8006GROWQR_APP_FRONTEND_URL=http://localhost:3002
- Backend per-user containers should use:
OPENCODE_IMAGE=ghcr.io/anomalyco/opencode:latestUSER_CONTAINER_HOST=127.0.0.1USER_DATA_ROOT=./.data/users
- Frontend should use:
NEXT_PUBLIC_RIVET_ENDPOINT=http://127.0.0.1:4000/api/rivetNEXT_PUBLIC_GROWQR_BACKEND_URL=http://127.0.0.1:4000(NOTGROWQR_BACKEND_URL— must have NEXT_PUBLIC_ prefix for client-side)OPENCODE_API_KEY=<from backend .env>(must match backend)LLM_BASE_URL=https://opencode.ai/zen/v1LLM_MODEL=kimi-k2.6
- Resume builder should use:
DATABASE_URL=postgresql+asyncpg://postgres:postgres@host.docker.internal:5433/growqr_resume(host.docker.internal when running in Docker, localhost when running locally)API_PORT=8002OPENCODE_API_KEY=<from backend .env>— CRITICAL: must be the EXACT same key as backend. Both services share the same OpenCode account.OPENCODE_BASE_URL=https://opencode.ai/zen/v1AI_MODEL=kimi-k2.6CLERK_JWKS_URL=https://noted-elephant-23.accounts.dev/.well-known/jwks.jsonCLERK_ISSUER=https://noted-elephant-23.accounts.devCORS_ORIGINS="http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000"A2A_ALLOWED_KEYS=dev-a2a-keyREDIS_URL=redis://host.docker.internal:6379/0DEBUG=true
SERVICE_TOKENandA2A_ALLOWED_KEYmust match across backend and services.- Clerk keys must match between frontend and backend.
- Gemini and OpenCode keys must be present for the audio/session and LLM paths.
OpenCode API Key Sync (IMPORTANT)
The backend and resume-builder share the same OpenCode API key. When copying values, always use growqr-backend/.env as the source of truth. The resume-builder's .env.local was previously out of sync (25-char dummy key vs 67-char real key), causing silent LLM failures.
To verify keys are synced:
diff <(grep OPENCODE_API_KEY growqr-backend/.env | head -1) <(grep OPENCODE_API_KEY growqr-app/resume-builder/.env.local | head -1)
# Should show no differences
Note: interview-service and roleplay-service use Gemini (not OpenCode) — they don't need an OpenCode key.
Start Order
Run all commands from /Users/divyansh/Desktop/growQR unless a command changes directory.
0. Start Central Gitea first (NEW per changes.md §2A)
The central Gitea is now a shared compose service in the backend's docker-compose. Note: Gitea health check uses port 3000 internally (container), mapped to 3001 on host. Port mapping is 3001:3000.
docker compose -f growqr-backend/docker-compose.yml up -d gitea
Wait until Gitea is healthy (~30 seconds on first startup):
curl http://127.0.0.1:3001/api/v1/version
# Expected: {"version":"1.22.6"}
Per-user Gitea containers are NO LONGER spawned dynamically. All users share the central Gitea instance with one repo per user under the growqr org.
1. Start QScore first
QScore owns the Redis instance exposed on localhost:6379. The interview service currently uses that host Redis for workflow task dispatch, so QScore should come up first.
docker compose -f qscore-service/docker-compose.yml up -d --build
Wait until all QScore containers are healthy/running:
docker compose -f qscore-service/docker-compose.yml ps
curl http://127.0.0.1:8000/health
# Expected: {"status":"ok"}
2. Start Interview Service
docker compose -f interview-service/docker-compose.yml up -d --build
Verify:
curl http://127.0.0.1:8007/health
# Expected: {"status":"ok","service":"interview-service","a2a":true,"agent":true}
3. Start Roleplay Service
docker compose -f roleplay-service/docker-compose.yml up -d --build
Verify:
curl http://127.0.0.1:8008/health
# Expected: {"status":"ok","service":"roleplay-service","a2a":true,"agent":true}
4. Start Resume Builder (NEW)
The resume builder runs as a Docker container built from growqr-app/resume-builder/Dockerfile. It uses its own Postgres database on port 5433.
# Start resume-builder Postgres
docker compose -f growqr-app/resume-builder/docker-compose.yml up -d postgres
# Build resume-builder image (first time only)
docker build -t growqr-resume-builder growqr-app/resume-builder
# Run migrations
cd growqr-app/resume-builder
uv run --env-file .env.local alembic upgrade head
# Start the service with synced OpenCode key from backend
cd /Users/divyansh/Desktop/growQR
OPENCODE_KEY=$(grep OPENCODE_API_KEY growqr-backend/.env | head -1 | cut -d= -f2)
docker rm -f growqr-resume-builder 2>/dev/null
docker run -d --name growqr-resume-builder \
-p 8002:8002 \
-e DATABASE_URL=postgresql+asyncpg://postgres:postgres@host.docker.internal:5433/growqr_resume \
-e OPENCODE_API_KEY="${OPENCODE_KEY}" \
-e OPENCODE_BASE_URL=https://opencode.ai/zen/v1 \
-e AI_MODEL=kimi-k2.6 \
-e CLERK_JWKS_URL=https://noted-elephant-23.accounts.dev/.well-known/jwks.json \
-e CLERK_ISSUER=https://noted-elephant-23.accounts.dev \
-e CORS_ORIGINS="http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000" \
-e A2A_ALLOWED_KEYS=dev-a2a-key \
-e REDIS_URL=redis://host.docker.internal:6379/0 \
-e DEBUG=true \
growqr-resume-builder uvicorn app.main:app --host 0.0.0.0 --port 8002
# Verify
curl http://127.0.0.1:8002/health
# Expected: {"status":"healthy","service":"resume-builder","mcp":true,"agent":true,"a2a":true}
5. Start Backend Postgres
For the demo path that avoids Rivet no_capacity, run the backend locally with its embedded Rivet runner and start only the backend Postgres from compose:
docker compose -f growqr-backend/docker-compose.yml up -d postgres
Important: If the backend was previously running with older schema, you may need to add missing columns:
docker exec growqr-postgres psql -U growqr -d growqr -c "
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS gitea_repo_name TEXT;
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS gitea_repo_owner TEXT;
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS image_version TEXT;
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS migration_version TEXT;
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS prompt_version TEXT;
"
Then migrate:
cd /Users/divyansh/Desktop/growQR/growqr-backend
npm run db:migrate
Start the backend:
cd /Users/divyansh/Desktop/growQR/growqr-backend
RIVET_RUN_ENGINE=1 npm run dev
Keep this terminal open. Verify in a separate terminal:
curl http://127.0.0.1:4000/healthz
# Expected: {"ok":true}
The backend will auto-provision OpenCode containers for existing users on boot (reconcileOnBoot).
6. Start Matchmaking
The matchmaking service powers the Scout/job-search handoff. It runs on port 8000 internally and port 8006 on the host.
docker compose -f matchmaking/docker-compose.yml up -d --build
curl http://127.0.0.1:8006/api/v1/health
# Expected: {"status":"healthy", ...}
7. Start Frontends
Start the dashboard on port 3000:
cd /Users/divyansh/Desktop/growQR/workflows--dashboard
npm run build
npm run start
Start the growqr-app frontend on port 3002. This app now owns the product UI for interview and roleplay sessions launched from workflow chat.
cd /Users/divyansh/Desktop/growQR/growqr-app/frontend
npm install --legacy-peer-deps
npm run dev -- -p 3002
Keep both terminals open. Open:
http://localhost:3000/v2/workflows
Provision OpenCode Containers
OpenCode containers are per-user and spawned dynamically by the backend Docker manager. Gitea is NO LONGER per-user — it's a central shared service (changes.md §2A).
After provisioning, docker ps should show containers with names like:
growqr-opencode-user_<userId>
If they are missing, sign into the dashboard and allow the user stack bootstrap to run. The local data is under:
growqr-backend/.data/users
That folder is intentionally gitignored and should stay local.
Demo Path — Interview-to-Offer Accelerator
Use this path for the Interview-to-Offer workflow demo:
- Open
http://localhost:3000/v2/workflows. - Type in the chat:
I have an interview at Google for SWE - The AI responds with a conversational message and the WorkflowDiscovery card (red, 4 agents, 4 steps) appears below.
- Click "Start this workflow" on the card.
- The workflow progresses step-by-step:
- Step 1: AI asks for job description → you respond with details
- Step 2: AI asks for resume → you respond
- Step 3: Resume Agent analyzes and tailors (shows "DONE")
- Step 4: Sara interview session ready → shows "Open" button
- Step 5: Emily roleplay session ready → shows "Open" button
- Step 6: Quinn computes Q-Score → shows results
- Click Open on Sara's card → new tab opens
http://localhost:3002/service-sessions/interview?session_id=<uuid>... - On the new GrowQR interview launcher, click Start interview and allow microphone/camera access.
- Click Open on Emily's card → new tab opens
http://localhost:3002/service-sessions/roleplay?session_id=<uuid>... - On the new GrowQR roleplay launcher, click Start roleplay and allow microphone/camera access.
- Completed steps are marked with green checkmarks in the chat.
Alternative Workflow Trigger Queries
| Workflow | Trigger Query |
|---|---|
| Interview-to-Offer | I have an interview at Google for SWE |
| Career Switch | I want to switch from marketing to product management |
| Resume Boost | My resume isn't getting any responses |
| Job Search | Find me backend engineering jobs |
| Job Preparation | Prepare me for a role at Stripe |
Direct service demo pages are for low-level service debugging only. Product workflow links should use the new growqr-app/frontend launchers:
http://localhost:3002/service-sessions/interview?session_id=<uuid>
http://localhost:3002/service-sessions/roleplay?session_id=<uuid>
Health Checks
Use these after startup:
curl http://127.0.0.1:4000/healthz # Backend
curl http://127.0.0.1:8007/health # Interview
curl http://127.0.0.1:8008/health # Roleplay
curl http://127.0.0.1:8000/health # QScore
curl http://127.0.0.1:8002/health # Resume builder
curl http://127.0.0.1:8006/api/v1/health # Matchmaking
curl -I 'http://localhost:3002/service-sessions/interview?session_id=demo-interview'
curl -I 'http://localhost:3002/service-sessions/roleplay?session_id=demo-roleplay'
curl http://127.0.0.1:3001/api/v1/version # Gitea
docker ps # All containers
If the workflow actor fails with no_capacity, restart the backend with:
cd /Users/divyansh/Desktop/growQR/growqr-backend
RIVET_RUN_ENGINE=1 npm run dev
Shutdown
Stop frontend and backend with Ctrl+C in their terminals.
Also kill any lingering backend Node processes:
pkill -f "tsx watch" 2>/dev/null
pkill -f "tsx.*src/index" 2>/dev/null
Stop compose services:
docker compose -f growqr-backend/docker-compose.yml down
docker compose -f interview-service/docker-compose.yml down
docker compose -f roleplay-service/docker-compose.yml down
docker compose -f qscore-service/docker-compose.yml down
docker compose -f matchmaking/docker-compose.yml down
docker compose -f growqr-app/resume-builder/docker-compose.yml down
docker rm -f growqr-resume-builder
Stop per-user OpenCode containers if still running:
docker ps --filter "name=growqr-opencode-user"
Then stop/remove the shown containers by name:
docker stop <growqr-opencode-container>
docker rm <growqr-opencode-container>
They can be recreated later by the backend provisioning flow. The local persistent data remains in growqr-backend/.data/users.
Build Rule
Whenever code changes are made, run the relevant build before calling the demo ready:
cd /Users/divyansh/Desktop/growQR/growqr-backend && npm run build
cd /Users/divyansh/Desktop/growQR/workflows--dashboard && npm run build
cd /Users/divyansh/Desktop/growQR/growqr-app/frontend && npm run build
For Dockerized Python services, rebuild with their compose commands:
docker compose -f interview-service/docker-compose.yml up -d --build
docker compose -f roleplay-service/docker-compose.yml up -d --build
docker compose -f qscore-service/docker-compose.yml up -d --build
Agent Architecture & Flow
The system has 5 real service-backed agents:
| Agent | Name | Service | Port | Provider | Creates |
|---|---|---|---|---|---|
| Resume Agent | Mira | resume-builder | 8002 | OpenCode (kimi-k2.6) | Resume analysis + tailoring |
| Interview Agent | Sara | interview-service + growqr-app frontend launcher | 8007 + 3002 | Gemini (audio) | Live interview sessions in new UI |
| Roleplay Agent | Emily | roleplay-service + growqr-app frontend launcher | 8008 + 3002 | Gemini (audio) | Roleplay scenario sessions in new UI |
| Q-Score Agent | Quinn | qscore-service | 8000 | Internal formula | Readiness scores |
| Job Search Agent | Scout | matchmaking | 8006 | Internal ranking engine | Ranked opportunity feed |
Chat Flow (Conversational Step-by-Step)
- User types message → Frontend sends to
POST http://127.0.0.1:4000/api/chat(with Clerk auth) - Backend tries Rivet actor first, falls back to direct LLM with tools
- LLM responds conversationally OR calls a tool (
start_interview_session,analyze_resume, etc.) - If tool is called, backend executes it against the real microservice
- Response includes
{ reply, sessions[], workflow }— sessions contain real URLs - Frontend renders agent cards with photos, status badges, and Open buttons
Trigger Queries (what to type)
| For this agent | Type this |
|---|---|
| Resume analysis | analyze my resume for Google SWE |
| Interview session | launch a behavioral interview for Software Engineer at Google |
| Roleplay session | launch a roleplay for negotiating a Google offer |
| Q-Score | compute my Q-Score |
| Full workflow | I have an interview at Google for SWE |
Session URLs
When a session is created, the backend preserves the session_id from the service configure response and returns one of these product URLs:
- Interview:
http://localhost:3002/service-sessions/interview?session_id=<uuid>&role=<goal>&type=<type> - Roleplay:
http://localhost:3002/service-sessions/roleplay?session_id=<uuid>&goal=<goal>&type=<type>
The old direct demo pages still exist for service debugging, but workflow chat should not link to them.
Key Files
- System prompt:
growqr-backend/prompts/system.txt - Agent definitions:
growqr-backend/agents/*.md - Service probes:
growqr-backend/src/services/service-agents.ts - Chat route (with tool dispatch):
growqr-backend/src/routes/chat.ts - Frontend chat component:
workflows--dashboard/src/components/home-v2/GrowChat.tsx - New service launchers:
growqr-app/frontend/src/app/service-sessions/interview/page.tsx,growqr-app/frontend/src/app/service-sessions/roleplay/page.tsx - Product flow requirements:
.growqr_memory.md
Known Quirks
- Roleplay configure is slow (30-60s): The roleplay service calls an LLM to generate scenarios. Be patient.
- Rivet
/actorsreturns 400: Cosmetic console warning from the Rivet frontend SDK. Does not affect chat — chat uses HTTP fallback. - Rivet proxy vs handler conflict: When
RIVET_RUN_ENGINE=1, the backend proxies/api/rivet/*to the engine atlocalhost:6420instead of usingregistry.handler(). This avoids the "Runtime already started as runner" error. - Multiple backend processes:
tsx watchsometimes spawns orphans. Alwayspkill -f "tsx watch"before restarting.