URL: https://mkdocs.justinsforge.com/memory/general/reference_browser_stream/
Forge Browser Stream¶
Headed real Google Chrome on a virtual X display on Console, exposed as a web app via noVNC + Cloudflare tunnel + Cloudflare Access. Runs Playwright-driven or hand-driven. Watchable from Sol, Vector, Astra, anywhere.
Built 2026-05-01.
Architecture¶
Console (192.168.86.50)
├── Xvfb :99 (1920x1080x24 virtual display)
├── fluxbox (window manager on :99)
├── google-chrome 147 (real Chrome from Google's apt repo, NOT snap)
├── x11vnc :5900 (VNC server, localhost-only, password-protected)
├── websockify :6080 (noVNC web frontend, bound to LAN IP)
└── cloudflared (Finn) (media-server tunnel routes browser.justinsforge.com -> 192.168.86.50:6080)
└── Cloudflare Access (Google login, locked to [email protected] + [email protected])
Public URL: https://browser.justinsforge.com/
Key paths¶
| Thing | Path |
|---|---|
| CLI | forge/scripts/forge_browser.py |
| Skill | forge/.claude/skills/browser/SKILL.md (/browser slash command) |
| Driver script (legacy, kept for direct calls) | forge/scripts/forge_browser_drive.py |
| Playwright venv | ~/.forge-venvs/browser/bin/python (has both playwright and patchright) |
| Chrome profiles per display | ~/.forge-browser-profiles/d<N>/ (cookies persist across runs) |
| VNC password | ~/.forge-secrets/forge-browser.env (FORGE_BROWSER_VNC_PASSWORD) |
| VNC password file (x11vnc format) | ~/.x11vnc/passwd |
| systemd units (primary) | /etc/systemd/system/forge-browser-{xvfb,fluxbox,x11vnc,novnc}.service, forge-browser-stream.target |
| systemd units (templated, parallel) | same names with @.service / @.target |
Quick commands¶
| Want | Run |
|---|---|
| Open URL on primary stream | /browser open https://example.com |
| Get the VNC password | /browser pwd |
| List active displays | /browser list |
| Spin up a parallel display | /browser spawn 100 then /browser add-tunnel 100 |
| Tear down a parallel display | /browser kill 100 |
| Direct Playwright run on the streamed browser | DISPLAY=:99 ~/.forge-venvs/browser/bin/python script.py |
Patchright (preferred over vanilla Playwright)¶
Vanilla Playwright leaks CDP Runtime.enable ID — Cloudflare/DataDome catch it. patchright is a drop-in replacement that fixes this. Identical API.
# OLD: from playwright.sync_api import sync_playwright
from patchright.sync_api import sync_playwright # use this
Both are installed in ~/.forge-venvs/browser/. Use patchright by default unless a script needs a vanilla-Playwright-only feature.
Parallel sessions¶
Use [email protected] (templated). Display :N gets:
- VNC port 5900 + (N - 99)
- noVNC port 6080 + (N - 99)
- Tunnel hostname browser{N-98}.justinsforge.com (e.g. :100 -> browser2.justinsforge.com)
/browser spawn 100 # start display :100
/browser add-tunnel 100 # add browser2.justinsforge.com -> 192.168.86.50:6081
/browser open https://shopify.com --display 100
Each display has its own Chrome profile at ~/.forge-browser-profiles/d<N>/ so logins don't collide.
Capacity (Console = 4 vCPU / 11GB RAM): comfortably 5-8 parallel headed-streamed sessions, 8-12 parallel headless. RAM is the bottleneck.
When to use vs. other tools¶
| Need | Tool |
|---|---|
| One-shot screenshot | /screenshot |
| Drive Justin's actual local Chrome on Sol/Vector | claude-in-chrome MCP |
| Bulk stock image download | forge_assets_grab |
| Pure scrape, no login, no eyes | Headless Playwright (no stream needed) |
| Login-walled long-running, or you want to watch | /browser |
| Read/write Gmail/Calendar/Drive/Workspace | OAuth API client (never browser-automate Google sign-in) |
| Captcha/2FA babysitting handoff to a worker | /browser + Justin solves via VNC |
| Visual regression on deploy | Headless Playwright + diff (use /browser only for debugging) |
Don'ts¶
- Don't try to log into a Google account via Playwright. Google blocks it. Use OAuth APIs for Gmail/Calendar/Drive (already in use). For YouTube Studio / GA / Search Console UI, log in by hand via VNC, then have Playwright navigate the existing session.
- Don't share user-data-dir across simultaneous Chromes. They lock-collide and silently route to the first one. Use a different
--user-data-dirper parallel job. - Don't bind noVNC to 0.0.0.0. Bound to
192.168.86.50:6080so only Finn's tunnel and LAN can reach it. CF Access gates everything from the public internet. - Don't expect anti-bot stealth. Use Patchright (already installed) for CDP-detection bypass; for hardcore vendors (DataDome, Akamai, HUMAN), need WebGL renderer spoof + residential proxy beyond Patchright.
Mount-namespace caveat¶
/etc and /usr are mounted ro in Claude Code sessions spawned by certain forge systemd services (ProtectSystem=strict). The primary install of this stack happened after a sudo mount -o remount,rw /etc /usr lift. Installed files persist on disk; the protection re-applies on next service restart, which is correct.
If you need to add packages or units later from a Claude session and hit "read-only filesystem":
sudo mount -o remount,rw /etc
sudo mount -o remount,rw /usr
# do work
# (no need to restore: next systemd respawn restores ro)
Build history¶
- 2026-05-01: Built end-to-end. Justin confirmed live via noVNC. Smiley demo via Playwright proved mouse control works.
- 2026-05-01: Patchright added, Cloudflare Access wired (policy
Justin Secure Only), templated systemd units installed,/browserskill shipped.