ClientsFlow Studio · Developer Handoff

Studio v3 — your starting point

The n8n-style Lab node redesign + reviewer rebuild, shipped on an isolated link. Clone, check out v3, run the tests, and pick up the two open fronts. v1 and v2 stay frozen — never deploy over them.

For Dani (dani-clientsflow) · from Matyás · 2026-06-16

1What it is

A Modal-hosted sales-prep tool. Its Lab turns a prospect's URL into a scrape (logo, brand palette, images, copy), SEO keyword research + Google-Ads volumes + current rankings, a brand-new website (one-shot Gemini 3.1 Pro from a proven template), competitor/copy research, a multi-page sitemap, a Figma asset bundle, and a Notion CRM write-up.

A second surface — the Reviewer — serves the generated site to the client via a tokenized link, collects pinned comments, and lets the rep apply AI section-edits and ship new versions. Hungarian UI, English code.

You own ClientsFlow Studio only. The CRM/funnel pipeline is a separate system, not part of your work for now — the two connect later.

2The three isolated versions & all the links

Studio runs as three fully isolated deployments that share nothing but the R2 bucket (namespaced by key prefix). This is a hard invariant — a startup guard aborts the app if the URL and the R2 prefix disagree.

VersionBranchLive linkModal dashboardStatus
v1main studio-web app ↗ frozen
v2v2 studio-v2-web app ↗ frozen
v3v3 studio-v3-web app ↗ active — your work

Each app redirects //login and serves the dash after the ADMIN_PASSWORD login (Matyás gives you the password + env + Modal access separately). The v1/v2 links let you A/B against frozen behaviour while iterating on v3.

3Architecture in one screen

Runtime

  • FastAPI on Modal (@modal.asgi_app()), server-rendered Jinja + vanilla JS — no Node build step.
  • SQLite + SQLAlchemy on a Modal Volume. Single DB writer: only the web function (max_containers=1) touches it.
  • Cloudflare R2 for files/images (boto3 S3, presigned PUT).
  • Gemini for all AI — model IDs only via app/config.py.

Rules baked into the design

  • Zero Modal crons — event-driven via .spawn() chains + an HMAC callback.
  • Playwright, never Puppeteer — browser work in browser_image workers only.
  • Workers never import app/db.py; they report to /internal/jobs/{id}/complete.
  • app/core/ is pure, offline-testable logic; app/workers/ wraps it in the job harness.

4What the v3 build delivered

Built from one live test session (Dani × Matyás, 2026-06-15) plus Matyás's explicit asks, executed end-to-end in 10 phases. Full test suite: 124 passed / 1 skipped; deployed; all three apps healthy & isolated; the two-zone node UI verified responsive at 1280 / 768 / 390.

Two-zone Lab nodes

Every step is a node with an editable inputs zone (URL/mode + the prompt itself, save once/default/ named, + overrides/uploads) and an outputs zone (every artifact, download + copy). Horizontal/vertical layout; single-node re-run.

Robust render engine

Per-card error boundary (a throw can't wipe sibling cards), no-op skip, focus/scroll/<details> preserved across the poll, responsive grid.

Scraper

Logo cascade (JSON-LD → apple-touch-icon → <svg><img>), color-probe palette, consent-dismiss + lazy-force, manual override layer, sandboxed AI scraper.

Keywords

Named AdsTokenExpiredError (no silent empties), 1 QPS spacing, soft DataForSEO 404, Ads volume-screenshot upload edge.

Sitemap & GENERATE

Intent clustering + a hard ≥3-page floor; SERP API competitors; GENERATE feeds Ads screenshots (multimodal), thinking_level=low, finish-reason handling, traceability manifest.

Reviewer

Preview-version gate + accept (no version explosion), durable comment re-anchoring + forward-carry, per-version client links, global comments, and the realtime-editor backend.

6Rollback — if v3 gets messed up

Because v3 is a separate app + volume + R2 prefix + branch, the nuclear rollback is trivial: just stop routing to v3 — v1 and v2 stay live and untouched no matter what you do on v3.

# A) redeploy the last-known-good v3 commit (e.g. one phase back)
git checkout v3 && git reset --hard d0c0f09   # = end of Phase 8
python3 -m modal deploy modal_app.py

# B) drop ONE phase, keep the rest (keeps history)
git revert 5b39b86                            # undo Phase 6, then redeploy

# C) back to the pre-v3 baseline (v2's code on the v3 app)
git checkout v3 && git reset --hard 3cf097b
python3 -m modal deploy modal_app.py
There's also a code backup tag backup-2026-06-11 on GitHub + local DB/R2 snapshots under backups/ (see CLAUDE.md). Tip: git tag v3-good-YYYYMMDD before risky work.

7Run · deploy · test

git clone https://github.com/matt-clientsflow/clientsflow-studio.git
cd clientsflow-studio
git checkout v3

python3 -m pytest -q              # expect: 124 passed, 1 skipped
python3 -m modal serve modal_app.py     # local hot-reload (needs Modal auth + env)
python3 -m modal deploy modal_app.py    # deploy v3 — ONLY from the v3 branch
Secrets are built at deploy from ~/.claude/.env (Gemini, R2, ADMIN_PASSWORD, DataForSEO, Google Ads, …). Never print, commit or paste secret values. Matyás hands you the env + Modal access directly.

8The two open fronts — where to start

A · CodeMirror editor + inspect overlay frontend

The backend is shipped & tested: POST /projects/{id}/pages/{slug}/preview-edit takes edited HTML, runs the same rewrite_for_viewer the client sees, stamps nested data-section-ids, and returns a sanitized srcdoc. To build: the CodeMirror 6 pane (debounced POST → iframe) + a Sanity-style inspect overlay (click a section → jump to its source). Rep editor iframe same-origin; client viewer SAMEORIGIN.

B · Live real-domain QA testing

Deploy v3, log in, run two real audits end-to-end (e.g. nyitrakert.hu, beridoor.hu) — once continuously, then re-run single nodes. Screenshot inputs+outputs at 1280/768/390, hunt visual defects, then exercise the reviewer (comment → apply → re-anchor → accept → per-version client link). Iterate until clean. Chrome DevTools MCP only, never headless. Clean up test data under v3/.

9Invariants & gotchas

Don't break these

  • Human gate — nothing reaches a client without an explicit action.
  • Never deploy over v1 (main) or v2. Work on v3.
  • Single DB writer (web container); workers use the HMAC callback.
  • Zero Modal crons; Playwright not Puppeteer.
  • Model IDs via app/config.py; light theme only.
  • Hungarian client copy, English code; tests green before any deploy.

Gotchas worth knowing early

  • SQLAlchemy JSON: mutating a nested dict in place makes old==new → the UPDATE is skipped. Build a fresh dict (helpers already do).
  • db._migrate() does idempotent ALTER … ADD COLUMN; v3 added the preview/anchor/share-token columns.
  • request.form() yields starlette UploadFile, not FastAPI's.
  • Local store (.tmp/blobs) ≠ prod store (R2); tests use LocalStorage.
  • Google-Ads test-mode refresh tokens expire after 7 days → surfaced as AdsTokenExpiredError.
Read next in the repo: PROJECT_STATE.md, the 2026-06-16 CHANGELOG.md entry, HANDOFF.md (the full text behind this page) and CLAUDE.md.