The previous version was tightly coupled to the Hostinger VPS at 46.202.92.250 — hardcoded IP, Traefik/Let's-Encrypt assumption, specific Docker-volume paths. Rewriting ahead of the Proxmox migration so a fresh clone on any Linux host (LXC, VM, bare metal, VPS) can stand up a working dev environment without pre-baked assumptions about topology. Structural changes: - Introduces Option A (all-in-one host) / Option B (Docker Compose) / Option C (split services) topology choice up front, so readers commit to one shape before touching commands. - Adds a "per-host configuration" template the reader fills in once (DEV_HOST, POSTGRES_PORT, SECRET_KEY, API keys), referenced by name throughout the rest of the doc. No more hardcoded IPs. - Adds an explicit verification section (Section 6) with concrete expected outcomes: alembic head, reversibility, prompt-cache hit, frontend build, /assistant→/pilot redirect, dispatcher routing, CORS. - References the Phase 0 TODO(phase0-verify) in ai_provider.py and the expected alembic head (f07010f17b01) as of the current branch. - Adds a troubleshooting section pulling in CLAUDE.md lessons that bite people repeatedly: stale Vite env vars, RLS policy violations, EACCES on dist/, multi-head alembic state, invisible cache misses. - Documents the structured log events the backend emits (anthropic.cache, mcp.turn, mcp.fallback) so readers know what to grep for during verification. Deliberately excluded: - Production deployment (lives in CLAUDE.md Deployment section). - Reverse-proxy configuration (whatever the reader prefers). - code-server install specifics (Docker vs LXC vs native is reader's choice; once running, this doc applies). - Proxmox-specific instructions — the doc is host-agnostic so it survives the next migration as well. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
ResolutionFlow — Dev Environment Setup & Operations Guide
Scope: Stand up a working ResolutionFlow dev environment from scratch on any Linux host (VPS, on-prem Proxmox LXC/VM, bare metal). Self-contained — do not read another doc to get the dev stack running. Last rewritten: April 2026, post-Hostinger-VPS deprecation, ahead of Proxmox migration. Audience: You (returning to the project), a teammate, or a fresh Claude Code session.
If you're picking up mid-migration and need to know what code state is on the current branch, read docs/FlowAssist_Migration/MIGRATION-HANDOFF.md first.
1. What this project needs, regardless of host
These are non-negotiable. If your host can't provide them, fix that before anything else.
| Component | Required version | Notes |
|---|---|---|
| Linux | any mainstream distro | Ubuntu 22.04+ / Debian 12+ tested; Alpine fine for containers |
| Python | 3.11+ | Backend and migrations |
| Node.js | 20.19+ | Vite 7 fails on older versions — CLAUDE.md Lesson 63 |
| PostgreSQL | 16 | gen_random_uuid() + jsonb + RLS are all leaned on |
| Docker + Docker Compose | recent | Only if you are running Postgres and/or backend as containers |
| Git | recent |
Optional but recommended:
| Tool | Why |
|---|---|
| code-server | Browser-based VS Code; how this project has historically been edited |
gh CLI |
Mirror repo is on GitHub via Gitea; gh reads issues and PRs |
| bun | Required for the gstack /browse + /qa skills (CLAUDE.md Lesson 82) |
npx gitnexus analyze |
Code-graph for Phase 2+ work that touches unified_chat_service |
| Claude Code CLI | If you want to run Claude Code locally on the host |
2. Architectural shape
The project is three services plus your editor. Keep these facts in mind regardless of topology:
Your browser
├─► code-server (editor, optional — usually port 8080 or behind TLS)
├─► frontend (Vite) (dev server, port 5173)
└─► backend (FastAPI) (dev server, port 8000)
│
└─► PostgreSQL (port 5432)
The frontend calls the backend by URL at runtime. The frontend does not proxy through the backend. Whatever URL your browser uses to reach the backend is what VITE_API_URL must be set to, baked in at build time. Changing VITE_API_URL requires rebuilding the frontend.
The backend calls the database by URL at runtime. The URL depends on where Postgres is relative to the backend — Docker service name if both are in the same compose network, localhost if Postgres is native on the same host, or a DNS name if they're in separate containers/VMs.
CORS is configured explicitly. The backend's CORS_ORIGINS list must include every origin your browser will use to reach the frontend. A missing origin shows up as failed preflight requests.
3. Topology choices — pick one before you start
The project is agnostic to topology, but each shape has different setup steps.
Option A — all-in-one LXC/VM/host (simplest)
Postgres, backend, and frontend all run on one Linux host. code-server runs on the same host or a sibling. No Docker required. Best for a single-developer Proxmox LXC.
Option B — Docker Compose on one host
Postgres, backend, and frontend run as Docker containers on one host. code-server runs outside the compose network (on the host or in another container). This is how the old Hostinger VPS was configured. Best if you want reproducible container images.
Option C — split services across containers/VMs
Postgres in one container/VM, backend and frontend in another, code-server in a third. Most complex; requires explicit networking between them. Use only if you have a specific reason.
Pick one and stick with it for the entire setup. Mixing Options A and B halfway through is where setup runs off the rails.
4. Per-host configuration
These values are specific to your host. Fill them in once and reference them by name throughout the rest of the doc.
DEV_HOST = <hostname or IP your browser uses, e.g. dev.internal, 10.0.0.42>
DEV_HOST_SCHEME = <http or https; http is fine for internal dev, https if behind a TLS proxy>
FRONTEND_PORT = 5173
BACKEND_PORT = 8000
POSTGRES_PORT = 5432 # or 5433 if you're avoiding conflict with a host Postgres
POSTGRES_DB_NAME = resolutionflow
POSTGRES_USER = postgres
POSTGRES_PASSWORD = <local-dev-password; anything, this is not prod>
SECRET_KEY = <openssl rand -hex 32 — generate fresh per host, do not reuse>
ANTHROPIC_API_KEY = <from https://console.anthropic.com>
GOOGLE_AI_API_KEY = <optional, only if using Gemini as a fallback>
Store these somewhere you can copy from during setup. Do not commit them.
Naming note: the canonical database name is
resolutionflow. If you seepatherlyin a config file, that's drift from an earlier rename and is being swept in a separate commit — useresolutionflow. CLAUDE.md tracks the live-code files that still referencepatherly.
5. Setup procedure
Run these in order. Stop at the first failure and investigate.
5.1 Install system dependencies
# Ubuntu / Debian
sudo apt update && sudo apt install -y \
git curl build-essential \
python3.11 python3.11-venv python3-pip \
postgresql-client # not the server — only if running Postgres natively
# Node 20 via nvm (survives container rebuilds if stored in a volume)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh"
nvm install 20
nvm alias default 20
For Option B (Docker Compose), also:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # log out and back in for this to take effect
5.2 Clone the repo
git clone https://gitea.resolutionflow.com/chihlasm/resolutionflow.git
# or the GitHub mirror:
# git clone https://github.com/chihlasm/resolutionflow.git
cd resolutionflow
# Check out the working branch if you're continuing mid-migration.
git fetch origin
git checkout feat/flowpilot-migration
5.3 Start PostgreSQL
Option A (native Postgres on the host):
sudo apt install -y postgresql-16
sudo -u postgres psql -c "CREATE DATABASE resolutionflow;"
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
# Adjust pg_hba.conf if you need non-local connections.
Option B (Postgres via Docker Compose): The repo has a docker-compose.dev.yml at the root. Check its Postgres service for the container name, port mapping, and volume. CLAUDE.md Lesson 65 notes the local compose defaults use container name resolutionflow_postgres, database resolutionflow, port 5433 mapped to the host. Confirm what the compose file actually says on your branch before trusting those values.
docker compose -f docker-compose.dev.yml up -d db
docker compose -f docker-compose.dev.yml logs db # wait for "ready to accept connections"
Verify:
# From the host (Option A) or the backend container/LXC (Option B):
psql -h <db-host> -p <POSTGRES_PORT> -U postgres -d resolutionflow -c "SELECT now();"
5.4 Write the .env files
The repo expects three env files. Create each one:
backend/.env — backend source of truth:
APP_NAME=ResolutionFlow
DEBUG=true
# DB URLs — `<db-host>` is `localhost` for Option A, the Docker service name
# (e.g. `db`) for Option B, or the DB container/VM hostname for Option C.
DATABASE_URL=postgresql+asyncpg://postgres:postgres@<db-host>:<POSTGRES_PORT>/resolutionflow
DATABASE_URL_SYNC=postgresql://postgres:postgres@<db-host>:<POSTGRES_PORT>/resolutionflow
# Auth
SECRET_KEY=<SECRET_KEY>
ACCESS_TOKEN_EXPIRE_MINUTES=5
REFRESH_TOKEN_EXPIRE_DAYS=7
REQUIRE_INVITE_CODE=true
# AI providers
AI_PROVIDER=anthropic
ANTHROPIC_API_KEY=<ANTHROPIC_API_KEY>
GOOGLE_AI_API_KEY=<GOOGLE_AI_API_KEY or leave unset>
# FlowPilot MCP telemetry — leave on so the Phase 0.5 baseline data keeps accruing
ENABLE_MCP_MICROSOFT_LEARN=true
# CORS + frontend URL
FRONTEND_URL=<DEV_HOST_SCHEME>://<DEV_HOST>:<FRONTEND_PORT>
CORS_ORIGINS=["http://localhost:5173","http://127.0.0.1:5173","<DEV_HOST_SCHEME>://<DEV_HOST>:<FRONTEND_PORT>"]
frontend/.env.local — frontend build-time config:
VITE_API_URL=<DEV_HOST_SCHEME>://<DEV_HOST>:<BACKEND_PORT>
Optional PostHog (CLAUDE.md Lesson 64 — enables product analytics locally):
VITE_PUBLIC_POSTHOG_KEY=<from PostHog project settings>
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
Repo root .env — only needed for Option B (Docker Compose interpolation):
SECRET_KEY=<SECRET_KEY>
ANTHROPIC_API_KEY=<ANTHROPIC_API_KEY>
GOOGLE_AI_API_KEY=<GOOGLE_AI_API_KEY or leave unset>
POSTGRES_PORT=<POSTGRES_PORT>
Never commit any
.envfile. The.gitignorealready covers this.
5.5 Run the backend setup
Option A (native):
cd backend
python3.11 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Migrate the DB to head.
alembic upgrade head
Option B (Docker):
docker compose -f docker-compose.dev.yml up -d backend
docker compose -f docker-compose.dev.yml run --rm backend alembic upgrade head
Expected alembic head (as of feat/flowpilot-migration): f07010f17b01. If alembic current shows anything else after upgrade head, something has gone wrong — stop and investigate.
5.6 Seed test users
# Option A
cd backend && source venv/bin/activate
python -m scripts.seed_test_users
# Option B
docker exec resolutionflow_backend python -m scripts.seed_test_users
Test users (all share password TestPass123!):
| Role | |
|---|---|
admin@resolutionflow.example.com |
super admin |
teamadmin@resolutionflow.example.com |
team admin |
engineer@resolutionflow.example.com |
engineer |
pro@resolutionflow.example.com |
solo pro |
5.7 Run the backend
Option A:
cd backend && source venv/bin/activate
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
Option B: Already running from docker compose up -d backend. Tail logs:
docker compose -f docker-compose.dev.yml logs -f backend
Verify: curl <DEV_HOST_SCHEME>://<DEV_HOST>:<BACKEND_PORT>/api/docs — OpenAPI docs page loads.
5.8 Run the frontend
Option A:
cd frontend
npm install
npm run dev -- --host 0.0.0.0 --port 5173
Option B:
docker compose -f docker-compose.dev.yml up -d --build frontend
Verify: Open <DEV_HOST_SCHEME>://<DEV_HOST>:<FRONTEND_PORT> in your browser. Log in with one of the test users. Navigate to /pilot — the FlowPilot session page should render.
6. Verification — proof the env actually works
Run these after setup. Every item has a concrete expected outcome.
6.1 Database schema is at the right version
# Option A
cd backend && source venv/bin/activate && alembic current
# Option B
docker compose -f docker-compose.dev.yml run --rm backend alembic current
Expected: f07010f17b01 (head) on the feat/flowpilot-migration branch. On main, expected: 074 (head).
6.2 Alembic reversibility
alembic downgrade -1 # should complete cleanly
alembic upgrade head # should return to f07010f17b01
If either step fails, the migration has a bug and Phase 2 cannot start.
6.3 Prompt-cache hit verification (the deferred Phase 0 TODO)
backend/app/core/ai_provider.py module docstring has a TODO(phase0-verify) note describing this. Procedure:
-
Confirm
AI_PROVIDER=anthropicandANTHROPIC_API_KEYis set inbackend/.env. -
Start the backend with log level INFO or lower.
-
In the UI, open
/pilotand send a chat message. Wait a few seconds for the response. -
Send a second chat message in the same session, within 5 minutes of the first.
-
In backend logs, grep for lines containing
anthropic.cache:# Option A grep 'anthropic.cache' <log-path> # Option B docker compose -f docker-compose.dev.yml logs backend | grep 'anthropic.cache' -
Expected: two
anthropic.cachelog events. First hascache_creation_input_tokens > 0. Second hascache_read_input_tokens > 0. -
If the second shows zero reads, inspect the prompt prefix for silent invalidators (timestamps, unsorted JSON keys, varying tool list ordering). Fix before proceeding with any Phase 2 work.
6.4 Frontend build is TypeScript-clean
cd frontend
npx tsc -b # no errors
npm run build # no errors
CLAUDE.md Lesson 105 notes that npm run build may fail with an EACCES on dist/ inside code-server — that is a Docker filesystem permission issue, not a real build error. Use npx tsc -b to verify TypeScript cleanliness in that case.
6.5 /assistant → /pilot redirect
Open <DEV_HOST_SCHEME>://<DEV_HOST>:<FRONTEND_PORT>/assistant/<some-real-session-id> in the browser. Expected: URL changes to /pilot/<that-id>; the FlowPilot session page renders. Bare /assistant redirects to bare /pilot.
6.6 Dispatcher de-branching
Navigate to the dashboard. Click a session in ActiveFlowPilotSessions or RecentFlowPilotSessions. Expected: routes to /pilot/:id regardless of the session's session_type value. (Check the browser URL bar.)
6.7 CORS
Open the browser DevTools Network tab, navigate to any backend-hitting page. Expected: no CORS errors. If you see "blocked by CORS policy," the missing origin needs adding to backend/.env's CORS_ORIGINS.
7. Runbook
Day-to-day commands after setup is complete.
Restart services
# Option A
# backend — Ctrl-C and re-run uvicorn
# frontend — Ctrl-C and re-run npm run dev
# Option B
docker compose -f docker-compose.dev.yml restart backend
docker compose -f docker-compose.dev.yml up -d --build frontend # rebuild required if VITE_* changed
docker compose -f docker-compose.dev.yml down && docker compose -f docker-compose.dev.yml up -d # full restart
Apply a new migration
# Option A
cd backend && source venv/bin/activate && alembic upgrade head
# Option B
docker compose -f docker-compose.dev.yml run --rm backend alembic upgrade head
Create a new migration
# Option A
cd backend && source venv/bin/activate
alembic revision -m "short description" # manual, preferred per CLAUDE.md Lesson 77
# OR
alembic revision --autogenerate -m "description" # pulls in drift; review carefully
Never pass --rev-id — let Alembic generate the hex hash.
Inspect the database
# Option A (native Postgres)
psql -h localhost -p 5432 -U postgres -d resolutionflow
# Option B (Docker)
docker exec -it resolutionflow_postgres psql -U postgres -d resolutionflow
Run tests
# Option A
cd backend && source venv/bin/activate
pytest --override-ini="addopts="
# Option B
docker compose -f docker-compose.dev.yml run --rm backend pytest --override-ini="addopts="
First time only, create the test database:
# Option A
sudo -u postgres psql -c "CREATE DATABASE resolutionflow_test;"
# Option B
docker exec -it resolutionflow_postgres psql -U postgres -c "CREATE DATABASE resolutionflow_test;"
View backend logs
# Option A: wherever you ran uvicorn
# Option B
docker compose -f docker-compose.dev.yml logs -f --tail=100 backend
Structured events to grep for:
anthropic.cache— prompt-cache hit/creation telemetry (Phase 0.1)mcp.turn— per-turn MCP availability/invocation (Phase 0.5)mcp.fallback— MCP silent-retry fallback fired (Phase 0.5)
8. Troubleshooting
CORS errors in the browser
The backend did not accept the origin your browser used. Check backend/.env's CORS_ORIGINS — it must include the exact scheme + host + port the browser sent. Restart the backend after editing.
VITE_API_URL points at the wrong place
The frontend was built with a stale value. Rebuild the frontend. Option B: docker compose up -d --build frontend. Option A: restart npm run dev.
alembic upgrade head fails with "target database is not up to date"
Your DB migration chain is out of sync with the code. On a dev box, the safe recovery is to drop the DB and re-migrate from scratch:
# Option A
sudo -u postgres psql -c "DROP DATABASE resolutionflow;" -c "CREATE DATABASE resolutionflow;"
cd backend && source venv/bin/activate && alembic upgrade head
# Option B
docker exec resolutionflow_postgres psql -U postgres -c "DROP DATABASE resolutionflow;" -c "CREATE DATABASE resolutionflow;"
docker compose -f docker-compose.dev.yml run --rm backend alembic upgrade head
Only do this on a dev box — it destroys all local data.
alembic heads shows more than one head
Only on a local branch that has diverged from origin/main. Production main has a single head. If this happens on a fresh clone, one of your local migration files has the wrong down_revision. Inspect each file's down_revision and reconnect the chain.
Frontend build fails with "EACCES: permission denied" on dist/
Filesystem permission issue inside the code-server container (CLAUDE.md Lesson 105). TypeScript compilation itself completes — use npx tsc -b to verify cleanliness without needing to write to dist/.
docker command not found inside code-server
If your code-server is itself inside a container, Docker is probably not exposed to it. CLAUDE.md Lesson 103 was written for this case on the old VPS. On Proxmox, the fix depends on topology — either SSH to the host to run Docker commands, or mount the host's Docker socket into the code-server container.
Backend returns 500 with InsufficientPrivilegeError: new row violates row-level security policy
RLS is enabled on a table your code wrote to without the right account_id. CLAUDE.md Lessons 107, 108, 110 cover this family of bugs. The fix is always at the service layer: make sure every model creation passes account_id= explicitly, and that startup routines that touch tenant-isolated tables use _admin_session_factory() rather than get_db().
Anthropic cache reads are zero on the second turn
Something in the cached prefix is changing between turns. Inspect the system-block list and the first N history messages for timestamps, datetime.now(), unsorted dict keys in JSON prompts, or varying tool-list order. The anthropic.cache telemetry shows exactly how many tokens were read vs created — use it to narrow down the invalidator.
9. Security posture for dev environments
This doc is about dev, not production. But:
- Never commit
.envfiles. The.gitignorecovers this. SECRET_KEYshould be generated per-host, not reused across environments.ANTHROPIC_API_KEYis billable — rotate if leaked into logs or chat.- Postgres on a dev host should not be exposed to the internet. Bind it to
127.0.0.1or to a private network interface only. - If you expose the frontend or backend publicly (for teammates to test against), put it behind TLS with a real certificate. Do not let dev credentials travel over plain HTTP on the public internet.
10. What's not in this doc
- Production deployment. This is a dev-env doc. Production lives on Railway — see
CLAUDE.md's Deployment section. - How to set up Traefik or any particular reverse proxy. Whichever proxy you use is your choice; the dev stack just needs something that routes
<host>:5173and<host>:8000to the right services. - How to configure code-server itself. Install it however you prefer (native, Docker, LXC); point it at the repo, and the rest of this doc applies.
- Where to host the Proxmox instance. Up to you.
If something in this doc turns out to be wrong on your host, fix the doc. This is a living document — the whole point of rewriting it from the Hostinger-specific version was to make it survive host changes.