Co-authored-by: Michael Chihlas <michael@resolutionflow.com> Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
672 lines
30 KiB
Markdown
672 lines
30 KiB
Markdown
# 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 = 5433 # host-side port. 5433 is the recommended default on any shared host to avoid collision with a host-level Postgres. The container's internal port stays 5432.
|
||
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 see `patherly` in a config file, that's drift from an earlier rename and is being swept in a separate commit — use `resolutionflow`. CLAUDE.md tracks the live-code files that still reference `patherly`.
|
||
|
||
---
|
||
|
||
## 5. Setup procedure
|
||
|
||
Run these in order. Stop at the first failure and investigate.
|
||
|
||
### 5.1 Install system dependencies
|
||
|
||
```bash
|
||
# Ubuntu / Debian
|
||
sudo apt update && sudo apt install -y \
|
||
git curl build-essential \
|
||
python3.12 python3.12-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:
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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):**
|
||
|
||
```bash
|
||
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. The local compose defaults use container name `resolutionflow_postgres`, database `resolutionflow`, and host-side port `5433` (mapped to the container's internal `5432`) — see CLAUDE.md Lesson 65. The host-side `5433` is the recommended default on any shared host: it keeps the port free for a host-level Postgres if you ever need one. The compose file also defines explicit `command:` directives on both `backend` and `frontend` to force `--host 0.0.0.0`, and expects the caller to pass `REPO_ROOT` (see 5.4) for bind-mount resolution. Confirm what the compose file actually says on your branch before trusting these values.
|
||
|
||
```bash
|
||
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:**
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
VITE_API_URL=<DEV_HOST_SCHEME>://<DEV_HOST>:<BACKEND_PORT>
|
||
```
|
||
|
||
Optional PostHog (CLAUDE.md Lesson 64 — enables product analytics locally):
|
||
|
||
```bash
|
||
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):
|
||
|
||
```bash
|
||
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>
|
||
# Absolute host-side path to the repo root. REQUIRED whenever docker-compose is
|
||
# invoked from inside a container (e.g. a code-server container with the host
|
||
# Docker socket mounted in). Without it, the bind mounts in
|
||
# docker-compose.dev.yml (`${REPO_ROOT}/backend:/app`, `${REPO_ROOT}/frontend:/app`)
|
||
# resolve against the CLI's CWD — a path the host daemon cannot see — and
|
||
# Docker silently creates empty directories there instead of mounting the code.
|
||
# If you run docker compose directly on the host shell, you can set this to `.`
|
||
# or the absolute path of the repo; being explicit is safer either way.
|
||
REPO_ROOT=/absolute/path/to/resolutionflow
|
||
```
|
||
|
||
> **Never commit any `.env` file.** The `.gitignore` already covers this.
|
||
|
||
### 5.5 Run the backend setup
|
||
|
||
**Option A (native):**
|
||
|
||
```bash
|
||
cd backend
|
||
python3.12 -m venv venv
|
||
source venv/bin/activate
|
||
pip install -r requirements.txt
|
||
|
||
# Migrate the DB to head.
|
||
alembic upgrade head
|
||
```
|
||
|
||
**Option B (Docker):**
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
# 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!`):
|
||
|
||
| Email | 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:**
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:**
|
||
|
||
```bash
|
||
cd frontend
|
||
npm install
|
||
npm run dev -- --host 0.0.0.0 --port 5173
|
||
```
|
||
|
||
**Option B:**
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
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:
|
||
|
||
1. Confirm `AI_PROVIDER=anthropic` and `ANTHROPIC_API_KEY` is set in `backend/.env`.
|
||
2. Start the backend with log level INFO or lower.
|
||
3. In the UI, open `/pilot` and send a chat message. Wait a few seconds for the response.
|
||
4. Send a second chat message in the same session, within 5 minutes of the first.
|
||
5. In backend logs, grep for lines containing `anthropic.cache`:
|
||
|
||
```bash
|
||
# Option A
|
||
grep 'anthropic.cache' <log-path>
|
||
# Option B
|
||
docker compose -f docker-compose.dev.yml logs backend | grep 'anthropic.cache'
|
||
```
|
||
|
||
6. Expected: two `anthropic.cache` log events. First has `cache_creation_input_tokens > 0`. Second has `cache_read_input_tokens > 0`.
|
||
7. 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
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
# 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/`.
|
||
|
||
### Backend/frontend containers start but `/app` is empty (no code mounted)
|
||
|
||
Almost always a `REPO_ROOT` problem. `docker-compose.dev.yml` uses `${REPO_ROOT}/backend:/app` and `${REPO_ROOT}/frontend:/app` bind mounts. If `REPO_ROOT` is unset, or set to a path that doesn't exist *on the Docker host* (not inside the code-server container), Docker silently creates an empty directory at that path and mounts it — the containers come up but have no source code. Symptom: backend returns import errors, or frontend serves a default Vite page. Fix: set `REPO_ROOT` in the repo-root `.env` to the absolute host-side path to the repo, then `docker compose down && docker compose up -d`. See 5.4 for the full note. This matters specifically when `docker compose` is invoked from inside a container (e.g. code-server with the host Docker socket mounted) — the CLI's CWD is container-local but the daemon resolves paths against the host filesystem.
|
||
|
||
### Frontend shows "Blocked request. This host is not allowed" in the browser
|
||
|
||
Vite 5+ ships DNS-rebinding protection that rejects any `Host:` header not in `server.allowedHosts`. The browser's hostname must be in that list. Edit `frontend/vite.config.ts` — the `server.allowedHosts` array should include every hostname you reach the dev server from (e.g. `'docker-01'`, `'localhost'`, `.ts.net` as a wildcard for Tailscale MagicDNS). Restart the Vite dev server (for Option B: `docker compose restart frontend`). This is unrelated to CORS — Vite blocks the request before any app code runs.
|
||
|
||
### `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 `.env` files. The `.gitignore` covers this.
|
||
- `SECRET_KEY` should be generated per-host, not reused across environments.
|
||
- `ANTHROPIC_API_KEY` is 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.1` or 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>:5173` and `<host>:8000` to the right services. **Direct port exposure over a private network** (Tailscale, WireGuard, a VPN, or a LAN behind a firewall) is a fully supported option for dev and is what the homelab reference topology in Section 11 uses — no reverse proxy, no TLS, just `http://<host>:5173` and `http://<host>:8000` reachable only from the private network. That's a perfectly reasonable choice; it's just not the only one.
|
||
- **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.
|
||
|
||
---
|
||
|
||
## 11. Reference topology: homelab Proxmox + code-server (Option B)
|
||
|
||
This section documents the first concrete host instantiation since the April 2026 host-agnostic rewrite. It's a worked example, not the canonical topology — Section 3's Option A/B/C framing still stands. If your setup looks different, follow Sections 1–10 and ignore this appendix.
|
||
|
||
### 11.1 Host
|
||
|
||
- **Hypervisor:** Proxmox (homelab).
|
||
- **VM:** `docker-01`, Debian 13, running Docker Engine + Docker Compose natively.
|
||
- **Tailscale IP:** `100.64.78.44`. MagicDNS hostname: `docker-01` (and the full `.ts.net` FQDN).
|
||
- **code-server:** runs on the same VM in its own container, with the host's Docker socket mounted in so it can drive `docker compose`. Its workspace bind-mounts the repo at `/opt/docker/code-server/workspace/resolutionflow`.
|
||
|
||
This is a concrete instance of Option B from Section 3: Postgres, backend, and frontend all run as containers from `docker-compose.dev.yml`; the editor lives outside that compose network.
|
||
|
||
### 11.2 Access pattern — direct port over Tailscale, no reverse proxy
|
||
|
||
The browser reaches the dev stack directly:
|
||
|
||
- Frontend: `http://docker-01:5173`
|
||
- Backend: `http://docker-01:8000`
|
||
- Backend API docs: `http://docker-01:8000/api/docs`
|
||
|
||
There is **no Caddy, no Traefik, no nginx, no TLS, no basic auth** in front of either service. The tailnet provides the wire encryption and access control — only devices on the tailnet can resolve `docker-01` or reach `100.64.78.44`, and Tailscale ACLs decide which of those devices are allowed to connect.
|
||
|
||
Why this choice:
|
||
|
||
- **Zero routing config to maintain.** There is no proxy rulebook to keep in sync with new services. Add a container, expose a port, you're done.
|
||
- **Backend-to-backend services stay private.** Redis, Celery workers, the planned ConnectWise proxy, the MCP server — none of them need to be reachable from the browser, so none of them need proxy rules. They stay inside the `resolutionflow` Docker network and talk by service name. The proxy would only ever have carried frontend and backend traffic, so the proxy's value was small relative to its maintenance cost.
|
||
- **Debuggability.** `curl http://docker-01:8000/api/docs` from any tailnet device works without auth headers, TLS handshakes, or DNS shenanigans.
|
||
|
||
Tradeoff: **this only works because every client device is on the tailnet.** If someone needed to test from a non-tailnet device, they'd either join the tailnet or we'd need to front the stack with a proxy. For the current single-developer setup, the tailnet-only assumption holds.
|
||
|
||
### 11.3 Per-host config values (as actually configured on `docker-01`)
|
||
|
||
Plugging these into Section 4's template:
|
||
|
||
```
|
||
DEV_HOST = docker-01
|
||
DEV_HOST_SCHEME = http
|
||
FRONTEND_PORT = 5173
|
||
BACKEND_PORT = 8000
|
||
POSTGRES_PORT = 5433 # host-side; container-internal stays 5432
|
||
POSTGRES_DB_NAME = resolutionflow
|
||
POSTGRES_USER = postgres
|
||
POSTGRES_PASSWORD = postgres # local-dev only
|
||
SECRET_KEY = <generated per host; do not reuse>
|
||
ANTHROPIC_API_KEY = <from console.anthropic.com>
|
||
GOOGLE_AI_API_KEY = <unset; Anthropic is sole provider in dev>
|
||
```
|
||
|
||
And the repo-root `.env` that `docker-compose.dev.yml` interpolates from:
|
||
|
||
```bash
|
||
SECRET_KEY=<redacted>
|
||
ANTHROPIC_API_KEY=<redacted>
|
||
POSTGRES_PORT=5433
|
||
REPO_ROOT=/opt/docker/code-server/workspace/resolutionflow
|
||
```
|
||
|
||
### 11.4 Why `REPO_ROOT` is non-optional on this host
|
||
|
||
code-server runs inside a container. When you open a terminal in code-server and run `docker compose -f docker-compose.dev.yml up -d`, the Docker CLI talks to the *host* daemon via the mounted socket — but the CWD it reports (`/config/workspace/resolutionflow`) is a path that only exists inside the code-server container. The host daemon has never heard of it.
|
||
|
||
Relative bind mounts like `./backend:/app` therefore resolve against a path the host can't see, and Docker silently creates empty directories there rather than erroring out. The containers come up, but `/app` is empty.
|
||
|
||
`docker-compose.dev.yml` sidesteps this by using `${REPO_ROOT}/backend:/app` and `${REPO_ROOT}/frontend:/app`. `REPO_ROOT` must be set to the absolute path **on the host** (`/opt/docker/code-server/workspace/resolutionflow`), not the path inside the code-server container. Same contents, different mount point, different name.
|
||
|
||
If you ever run `docker compose` directly from a host shell (SSH'd into `docker-01`), set `REPO_ROOT` to `.` or the absolute host path. Being explicit is always safe; leaving it unset is the failure mode.
|
||
|
||
### 11.5 Vite `server.allowedHosts` — required for `docker-01` to resolve
|
||
|
||
Vite 5+ rejects any `Host:` header not in `server.allowedHosts` (DNS-rebinding protection). `frontend/vite.config.ts` has:
|
||
|
||
```ts
|
||
server: {
|
||
host: '0.0.0.0',
|
||
allowedHosts: ['docker-01', '.ts.net', 'localhost'],
|
||
...
|
||
}
|
||
```
|
||
|
||
- `docker-01` — the MagicDNS short name the browser uses day-to-day.
|
||
- `.ts.net` — wildcard for the full Tailscale MagicDNS FQDN, in case anyone uses it.
|
||
- `localhost` — for the "am I serving anything at all" smoke-test from inside the container.
|
||
|
||
If you move this setup to a different host, add that host's hostname to `allowedHosts` or the browser will see "Blocked request. This host is not allowed." See Section 8's troubleshooting entry for the full symptom/fix.
|
||
|
||
### 11.6 CORS origins on this host
|
||
|
||
The `backend` service's `CORS_ORIGINS` environment variable is pinned in the compose file to:
|
||
|
||
```
|
||
["http://localhost:5173","http://127.0.0.1:5173","http://docker-01:5173","http://100.64.78.44:5173"]
|
||
```
|
||
|
||
The last two are what make browser calls from tailnet clients work — they cover both MagicDNS (`docker-01`) and the raw Tailscale IP. If you add a new hostname to reach the frontend from, also add the matching origin here and restart the backend.
|
||
|
||
### 11.7 Compose file shape (as of this writing)
|
||
|
||
`docker-compose.dev.yml` has been through a round of cleanup for this topology. Specifics worth knowing if you're comparing against older revisions of the file:
|
||
|
||
- **No Traefik labels.** They were removed — nothing in this topology uses Traefik.
|
||
- **No Hostinger-VPS-era origins** in `CORS_ORIGINS`.
|
||
- `Dockerfile.dev` for both `backend` and `frontend` is still the build source — this didn't change.
|
||
- Explicit `command:` directives on both `backend` (`uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload`) and `frontend` (`npm run dev -- --host 0.0.0.0 --port 5173`) — this guarantees `--host 0.0.0.0` regardless of what's baked into the image, so the services listen on all interfaces and are reachable from outside the container.
|
||
- `REPO_ROOT` is interpolated into both service volume mounts (see 11.4).
|
||
|
||
If you're adapting the file for a different host, the things most likely to need editing are `REPO_ROOT` (see 11.4), `CORS_ORIGINS` (see 11.6), `FRONTEND_URL`, `VITE_API_URL`, and `POSTGRES_PORT` if you want something other than `5433`.
|
||
|
||
### 11.8 End-to-end sanity check for this topology
|
||
|
||
From any device on the tailnet:
|
||
|
||
```bash
|
||
# Backend reachable
|
||
curl -sSf http://docker-01:8000/api/docs >/dev/null && echo OK
|
||
|
||
# Frontend reachable
|
||
curl -sSf http://docker-01:5173 >/dev/null && echo OK
|
||
|
||
# Alembic head matches the branch expectation
|
||
docker exec resolutionflow_backend alembic current
|
||
# expect f07010f17b01 on feat/flowpilot-migration, 074 on main
|
||
|
||
# Postgres is alive inside the compose network
|
||
docker exec resolutionflow_postgres psql -U postgres -d resolutionflow -c "SELECT now();"
|
||
```
|
||
|
||
All four passing = the dev environment is live end-to-end.
|