Vibecode Urlbox
track this build5 phases, 10 steps, beginner friendly0%Playwright renders any page to a PNG or PDF in one call. A queue and a cache around it is a sitting. The hard parts the service sells are blocking cookie banners and ads reliably, scale, and not babysitting a browser fleet.
You are building a lean indie version of Urlbox. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Urlbox · indie build A screenshot API with Playwright: render any URL to PNG or PDF with viewport, full-page and device-scale options, refuse private addresses, sign requests, cache results, queue concurrency, and keep a browser fleet of one healthy. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Browser | Playwright with Chromium | renders anything; the fleet is the cost | | Runtime | Node 22, node:http, sharp | one process with a page pool | | Hosting | A VPS with 2 GB of RAM | Chromium | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A VPS with at least 2 GB of RAM** · about $6 a month - Why: Chromium needs it. - Get it: Hetzner CX22 or similar. - [ ] **Playwright with Chromium and system dependencies** · free, a few hundred MB - Why: The renderer. - Get it: npm install playwright && npx playwright install --with-deps chromium - Verify: npx playwright --version prints - [ ] **An HMAC signing key** · free - Why: Signed requests only. - Get it: openssl rand -hex 32. - [ ] **A domain or subdomain** (optional) · roughly $10 a year, or free on an existing domain - Why: A public address you own, so links you share never break when a provider changes. - Get it: Register at Cloudflare Registrar, Porkbun or Namecheap, or use a subdomain of one you already own. You add one DNS record in the deploy phase. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir shotapi && cd shotapi && git init && npm init -y && npm pkg set type=module && npm install playwright@1 sharp@0.35.3 mkdir -p renders && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Web-scale reliability against every banner and anti-bot wall. - reliable cookie-banner and ad blocking across the web - scale and concurrency without managing browsers - the CDN and retina rendering polish - the SLA If one of those is essential to you, that is the reason to keep paying for Urlbox, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Urlbox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screenshot API like Urlbox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http, Playwright with Chromium, sharp for post-processing. One process with a small browser page pool. Disk cache. ### Phase 1 · Render one URL Build: a render(url, options) function using one persistent browser and a fresh context per render: viewport, full page, device scale factor, delay, PNG or JPEG or PDF. Hard timeout, and the context always closed even on failure. Done when: a public page renders to a crisp PNG, a full-page render captures below the fold, a PDF opens correctly, and a page that never loads fails at the timeout without leaking a context. Do not build yet: HTTP, cache. ### Phase 2 · Safety Build: refuse private and internal addresses (RFC 1918, localhost, link-local, .internal), resolve DNS before connecting and re-check, block file: and data: schemes. This is the vulnerability every screenshot service has shipped once. Done when: http://127.0.0.1, http://169.254.169.254 and a hostname resolving to a private IP are all refused before any request leaves. ### Phase 3 · The endpoint and cache Build: GET /render?url=&width=&full=&format= with HMAC-signed requests, a cache keyed by the option hash with a TTL, and a concurrency limit with a queue so ten requests share the pool rather than spawning ten browsers. Done when: a tampered signature is refused, a repeat request is served from cache, and 20 concurrent requests complete without the process exceeding its memory cap. ### Phase 4 · Cleaner pages Build: options to hide selectors, inject CSS, block ad and tracker hosts from a list, dismiss common cookie banners by selector, and wait for a selector before capture. Done when: a news page renders without its cookie banner and a hidden selector is absent. ### Phase 5 · Operate Build: cache eviction, a browser restart on a leak threshold, /healthz, a systemd unit with a memory limit, and the README. Done when: the service runs 1,000 renders without memory growth and recovers from a browser crash. ### Out of scope (and why) - Web-scale reliability against every cookie banner and anti-bot wall. That is the fee. ### README must contain - The private-address rule and why it exists. - Memory sizing for Chromium. ===== AGENTS.md ===== # Agent instructions · Urlbox indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Playwright with Chromium, Node 22, node:http, sharp, A VPS with 2 GB of RAM. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ===== BUILD_PLAN.md ===== # Build plan · Urlbox A screenshot API with Playwright: render any URL to PNG or PDF with viewport, full-page and device-scale options, refuse private addresses, sign requests, cache results, queue concurrency, and keep a browser fleet of one healthy. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Render one URL PNG, JPEG or PDF with a timeout and no leaked contexts. ### Steps 1. Install Playwright and write render(url, options) with one persistent browser and a fresh context per render ```sh mkdir shotapi && cd shotapi && git init && npm init -y && npm pkg set type=module && npm install playwright@1 sharp@0.35.3 mkdir -p renders && cp .env.example .env ``` 2. Always close the context in finally; enforce RENDER_TIMEOUT_MS ### Done when - [ ] A public page renders crisp - [ ] Full-page captures below the fold - [ ] A page that never loads fails at the timeout without leaking a context ## Phase 2 · Safety Never fetch private or internal addresses. ### Steps 1. Refuse RFC 1918, localhost, link-local, .internal; block file: and data: 2. Resolve DNS before connecting and re-check the resolved address ### Done when - [ ] 127.0.0.1, 169.254.169.254 and a hostname resolving privately are all refused ## Phase 3 · Endpoint and cache Signed, cached, queued. ### Steps 1. GET /render with HMAC, a cache keyed by the option hash and TTL 2. A queue with MAX_CONCURRENCY ### Done when - [ ] A tampered signature is refused - [ ] A repeat is served from cache - [ ] 20 concurrent requests complete under the memory cap ## Phase 4 · Cleaner pages Hide selectors, inject CSS, block trackers, dismiss banners. ### Steps 1. Options for hide, css, block-hosts list, wait-for selector 2. A cookie-banner selector list ### Done when - [ ] A news page renders without its banner - [ ] A hidden selector is absent ## Phase 5 · Operate Eviction, browser restart on leaks, memory limit. ### Steps 1. Restart the browser past a memory threshold; /healthz; systemd with MemoryMax 2. README with the private-address rule and sizing Files: `README.md` ### Done when - [ ] 1,000 renders without memory growth - [ ] Recovers from a browser crash ## Not in this build - Web-scale reliability against every banner and anti-bot wall. ## After v1, if you want it - Webhook delivery of finished renders - Scheduled recurring captures ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required · secret. openssl rand -hex 32. SIGN_KEY=hex # Required. Cache. CACHE_DIR=./renders # Optional. Pages rendering at once. MAX_CONCURRENCY=3 # Optional. Hard timeout per render. RENDER_TIMEOUT_MS=20000
You are building a lean indie version of Urlbox. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Urlbox · indie build A screenshot API with Playwright: render any URL to PNG or PDF with viewport, full-page and device-scale options, refuse private addresses, sign requests, cache results, queue concurrency, and keep a browser fleet of one healthy. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Browser | Playwright with Chromium | renders anything; the fleet is the cost | | Runtime | Node 22, node:http, sharp | one process with a page pool | | Hosting | A VPS with 2 GB of RAM | Chromium | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A VPS with at least 2 GB of RAM** · about $6 a month - Why: Chromium needs it. - Get it: Hetzner CX22 or similar. - [ ] **Playwright with Chromium and system dependencies** · free, a few hundred MB - Why: The renderer. - Get it: npm install playwright && npx playwright install --with-deps chromium - Verify: npx playwright --version prints - [ ] **An HMAC signing key** · free - Why: Signed requests only. - Get it: openssl rand -hex 32. - [ ] **A domain or subdomain** (optional) · roughly $10 a year, or free on an existing domain - Why: A public address you own, so links you share never break when a provider changes. - Get it: Register at Cloudflare Registrar, Porkbun or Namecheap, or use a subdomain of one you already own. You add one DNS record in the deploy phase. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir shotapi && cd shotapi && git init && npm init -y && npm pkg set type=module && npm install playwright@1 sharp@0.35.3 mkdir -p renders && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Web-scale reliability against every banner and anti-bot wall. - reliable cookie-banner and ad blocking across the web - scale and concurrency without managing browsers - the CDN and retina rendering polish - the SLA If one of those is essential to you, that is the reason to keep paying for Urlbox, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Urlbox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screenshot API like Urlbox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http, Playwright with Chromium, sharp for post-processing. One process with a small browser page pool. Disk cache. ### Phase 1 · Render one URL Build: a render(url, options) function using one persistent browser and a fresh context per render: viewport, full page, device scale factor, delay, PNG or JPEG or PDF. Hard timeout, and the context always closed even on failure. Done when: a public page renders to a crisp PNG, a full-page render captures below the fold, a PDF opens correctly, and a page that never loads fails at the timeout without leaking a context. Do not build yet: HTTP, cache. ### Phase 2 · Safety Build: refuse private and internal addresses (RFC 1918, localhost, link-local, .internal), resolve DNS before connecting and re-check, block file: and data: schemes. This is the vulnerability every screenshot service has shipped once. Done when: http://127.0.0.1, http://169.254.169.254 and a hostname resolving to a private IP are all refused before any request leaves. ### Phase 3 · The endpoint and cache Build: GET /render?url=&width=&full=&format= with HMAC-signed requests, a cache keyed by the option hash with a TTL, and a concurrency limit with a queue so ten requests share the pool rather than spawning ten browsers. Done when: a tampered signature is refused, a repeat request is served from cache, and 20 concurrent requests complete without the process exceeding its memory cap. ### Phase 4 · Cleaner pages Build: options to hide selectors, inject CSS, block ad and tracker hosts from a list, dismiss common cookie banners by selector, and wait for a selector before capture. Done when: a news page renders without its cookie banner and a hidden selector is absent. ### Phase 5 · Operate Build: cache eviction, a browser restart on a leak threshold, /healthz, a systemd unit with a memory limit, and the README. Done when: the service runs 1,000 renders without memory growth and recovers from a browser crash. ### Out of scope (and why) - Web-scale reliability against every cookie banner and anti-bot wall. That is the fee. ### README must contain - The private-address rule and why it exists. - Memory sizing for Chromium. ===== AGENTS.md ===== # Agent instructions · Urlbox indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Playwright with Chromium, Node 22, node:http, sharp, A VPS with 2 GB of RAM. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ===== BUILD_PLAN.md ===== # Build plan · Urlbox A screenshot API with Playwright: render any URL to PNG or PDF with viewport, full-page and device-scale options, refuse private addresses, sign requests, cache results, queue concurrency, and keep a browser fleet of one healthy. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Render one URL PNG, JPEG or PDF with a timeout and no leaked contexts. ### Steps 1. Install Playwright and write render(url, options) with one persistent browser and a fresh context per render ```sh mkdir shotapi && cd shotapi && git init && npm init -y && npm pkg set type=module && npm install playwright@1 sharp@0.35.3 mkdir -p renders && cp .env.example .env ``` 2. Always close the context in finally; enforce RENDER_TIMEOUT_MS ### Done when - [ ] A public page renders crisp - [ ] Full-page captures below the fold - [ ] A page that never loads fails at the timeout without leaking a context ## Phase 2 · Safety Never fetch private or internal addresses. ### Steps 1. Refuse RFC 1918, localhost, link-local, .internal; block file: and data: 2. Resolve DNS before connecting and re-check the resolved address ### Done when - [ ] 127.0.0.1, 169.254.169.254 and a hostname resolving privately are all refused ## Phase 3 · Endpoint and cache Signed, cached, queued. ### Steps 1. GET /render with HMAC, a cache keyed by the option hash and TTL 2. A queue with MAX_CONCURRENCY ### Done when - [ ] A tampered signature is refused - [ ] A repeat is served from cache - [ ] 20 concurrent requests complete under the memory cap ## Phase 4 · Cleaner pages Hide selectors, inject CSS, block trackers, dismiss banners. ### Steps 1. Options for hide, css, block-hosts list, wait-for selector 2. A cookie-banner selector list ### Done when - [ ] A news page renders without its banner - [ ] A hidden selector is absent ## Phase 5 · Operate Eviction, browser restart on leaks, memory limit. ### Steps 1. Restart the browser past a memory threshold; /healthz; systemd with MemoryMax 2. README with the private-address rule and sizing Files: `README.md` ### Done when - [ ] 1,000 renders without memory growth - [ ] Recovers from a browser crash ## Not in this build - Web-scale reliability against every banner and anti-bot wall. ## After v1, if you want it - Webhook delivery of finished renders - Scheduled recurring captures ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required · secret. openssl rand -hex 32. SIGN_KEY=hex # Required. Cache. CACHE_DIR=./renders # Optional. Pages rendering at once. MAX_CONCURRENCY=3 # Optional. Hard timeout per render. RENDER_TIMEOUT_MS=20000
You are building a production product version of Urlbox. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Urlbox · product brief ## Problem Playwright renders any page to a PNG or PDF in one call. A queue and a cache around it is a sitting. The hard parts the service sells are blocking cookie banners and ads reliably, scale, and not babysitting a browser fleet. ## Product outcome Screenshots for your own products, safe against the classic SSRF, with a fleet of one you can operate. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a VPS with 2 GB of RAM for Chromium - Node 22 ## Explicit non-goals for v1 - Web-scale reliability against every banner and anti-bot wall. - reliable cookie-banner and ad blocking across the web - scale and concurrency without managing browsers - the CDN and retina rendering polish - the SLA ## Success criteria - SSRF checks verified - Soak test passes ===== BRIEF.md ===== # Build brief · Urlbox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screenshot API like Urlbox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http, Playwright with Chromium, sharp for post-processing. One process with a small browser page pool. Disk cache. ### Phase 1 · Render one URL Build: a render(url, options) function using one persistent browser and a fresh context per render: viewport, full page, device scale factor, delay, PNG or JPEG or PDF. Hard timeout, and the context always closed even on failure. Done when: a public page renders to a crisp PNG, a full-page render captures below the fold, a PDF opens correctly, and a page that never loads fails at the timeout without leaking a context. Do not build yet: HTTP, cache. ### Phase 2 · Safety Build: refuse private and internal addresses (RFC 1918, localhost, link-local, .internal), resolve DNS before connecting and re-check, block file: and data: schemes. This is the vulnerability every screenshot service has shipped once. Done when: http://127.0.0.1, http://169.254.169.254 and a hostname resolving to a private IP are all refused before any request leaves. ### Phase 3 · The endpoint and cache Build: GET /render?url=&width=&full=&format= with HMAC-signed requests, a cache keyed by the option hash with a TTL, and a concurrency limit with a queue so ten requests share the pool rather than spawning ten browsers. Done when: a tampered signature is refused, a repeat request is served from cache, and 20 concurrent requests complete without the process exceeding its memory cap. ### Phase 4 · Cleaner pages Build: options to hide selectors, inject CSS, block ad and tracker hosts from a list, dismiss common cookie banners by selector, and wait for a selector before capture. Done when: a news page renders without its cookie banner and a hidden selector is absent. ### Phase 5 · Operate Build: cache eviction, a browser restart on a leak threshold, /healthz, a systemd unit with a memory limit, and the README. Done when: the service runs 1,000 renders without memory growth and recovers from a browser crash. ### Out of scope (and why) - Web-scale reliability against every cookie banner and anti-bot wall. That is the fee. ### README must contain - The private-address rule and why it exists. - Memory sizing for Chromium. ===== ARCHITECTURE.md ===== # Architecture · Urlbox ## Stack | Part | Choice | Why | | --- | --- | --- | | Browser | Playwright with Chromium | renders anything; the fleet is the cost | | Runtime | Node 22, node:http, sharp | one process with a page pool | | Hosting | A VPS with 2 GB of RAM | Chromium | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Renderer | Playwright and options | Firefox or WebKit | | Guard | address checks | Never remove | | Edge | signing, cache, queue | A CDN | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `SIGN_KEY` · required, secret · openssl rand -hex 32. - `CACHE_DIR` · required · Cache. - `MAX_CONCURRENCY` · optional · Pages rendering at once. - `RENDER_TIMEOUT_MS` · optional · Hard timeout per render. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Urlbox product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Playwright with Chromium, Node 22, node:http, sharp, A VPS with 2 GB of RAM. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ===== MILESTONES.md ===== # Delivery milestones · Urlbox Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Render one URL PNG, JPEG or PDF with a timeout and no leaked contexts. ### Steps 1. Install Playwright and write render(url, options) with one persistent browser and a fresh context per render ```sh mkdir shotapi && cd shotapi && git init && npm init -y && npm pkg set type=module && npm install playwright@1 sharp@0.35.3 mkdir -p renders && cp .env.example .env ``` 2. Always close the context in finally; enforce RENDER_TIMEOUT_MS ### Done when - [ ] A public page renders crisp - [ ] Full-page captures below the fold - [ ] A page that never loads fails at the timeout without leaking a context ## M2 · Safety Never fetch private or internal addresses. ### Steps 1. Refuse RFC 1918, localhost, link-local, .internal; block file: and data: 2. Resolve DNS before connecting and re-check the resolved address ### Done when - [ ] 127.0.0.1, 169.254.169.254 and a hostname resolving privately are all refused ## M3 · Endpoint and cache Signed, cached, queued. ### Steps 1. GET /render with HMAC, a cache keyed by the option hash and TTL 2. A queue with MAX_CONCURRENCY ### Done when - [ ] A tampered signature is refused - [ ] A repeat is served from cache - [ ] 20 concurrent requests complete under the memory cap ## M4 · Cleaner pages Hide selectors, inject CSS, block trackers, dismiss banners. ### Steps 1. Options for hide, css, block-hosts list, wait-for selector 2. A cookie-banner selector list ### Done when - [ ] A news page renders without its banner - [ ] A hidden selector is absent ## M5 · Operate Eviction, browser restart on leaks, memory limit. ### Steps 1. Restart the browser past a memory threshold; /healthz; systemd with MemoryMax 2. README with the private-address rule and sizing Files: `README.md` ### Done when - [ ] 1,000 renders without memory growth - [ ] Recovers from a browser crash ## M6 · Operate it like a product (production only) Only for the product-builder path: know when the renderer is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443 ===== OPERATIONS.md ===== # Operations · Urlbox ## Backup Nothing; cache is disposable. ## Restore Redeploy. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Memory and render latency. ## Incident checklist Rotate SIGN_KEY; review logs for refused addresses. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] SSRF checks verified - [ ] Soak test passes ## Launch constraint Do not market omitted Urlbox capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required · secret. openssl rand -hex 32. SIGN_KEY=hex # Required. Cache. CACHE_DIR=./renders # Optional. Pages rendering at once. MAX_CONCURRENCY=3 # Optional. Hard timeout per render. RENDER_TIMEOUT_MS=20000
# Urlbox · indie build A screenshot API with Playwright: render any URL to PNG or PDF with viewport, full-page and device-scale options, refuse private addresses, sign requests, cache results, queue concurrency, and keep a browser fleet of one healthy. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Browser | Playwright with Chromium | renders anything; the fleet is the cost | | Runtime | Node 22, node:http, sharp | one process with a page pool | | Hosting | A VPS with 2 GB of RAM | Chromium | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A VPS with at least 2 GB of RAM** · about $6 a month - Why: Chromium needs it. - Get it: Hetzner CX22 or similar. - [ ] **Playwright with Chromium and system dependencies** · free, a few hundred MB - Why: The renderer. - Get it: npm install playwright && npx playwright install --with-deps chromium - Verify: npx playwright --version prints - [ ] **An HMAC signing key** · free - Why: Signed requests only. - Get it: openssl rand -hex 32. - [ ] **A domain or subdomain** (optional) · roughly $10 a year, or free on an existing domain - Why: A public address you own, so links you share never break when a provider changes. - Get it: Register at Cloudflare Registrar, Porkbun or Namecheap, or use a subdomain of one you already own. You add one DNS record in the deploy phase. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir shotapi && cd shotapi && git init && npm init -y && npm pkg set type=module && npm install playwright@1 sharp@0.35.3 mkdir -p renders && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Web-scale reliability against every banner and anti-bot wall. - reliable cookie-banner and ad blocking across the web - scale and concurrency without managing browsers - the CDN and retina rendering polish - the SLA If one of those is essential to you, that is the reason to keep paying for Urlbox, and the README should say so rather than pretend.
# Build brief · Urlbox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screenshot API like Urlbox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http, Playwright with Chromium, sharp for post-processing. One process with a small browser page pool. Disk cache. ### Phase 1 · Render one URL Build: a render(url, options) function using one persistent browser and a fresh context per render: viewport, full page, device scale factor, delay, PNG or JPEG or PDF. Hard timeout, and the context always closed even on failure. Done when: a public page renders to a crisp PNG, a full-page render captures below the fold, a PDF opens correctly, and a page that never loads fails at the timeout without leaking a context. Do not build yet: HTTP, cache. ### Phase 2 · Safety Build: refuse private and internal addresses (RFC 1918, localhost, link-local, .internal), resolve DNS before connecting and re-check, block file: and data: schemes. This is the vulnerability every screenshot service has shipped once. Done when: http://127.0.0.1, http://169.254.169.254 and a hostname resolving to a private IP are all refused before any request leaves. ### Phase 3 · The endpoint and cache Build: GET /render?url=&width=&full=&format= with HMAC-signed requests, a cache keyed by the option hash with a TTL, and a concurrency limit with a queue so ten requests share the pool rather than spawning ten browsers. Done when: a tampered signature is refused, a repeat request is served from cache, and 20 concurrent requests complete without the process exceeding its memory cap. ### Phase 4 · Cleaner pages Build: options to hide selectors, inject CSS, block ad and tracker hosts from a list, dismiss common cookie banners by selector, and wait for a selector before capture. Done when: a news page renders without its cookie banner and a hidden selector is absent. ### Phase 5 · Operate Build: cache eviction, a browser restart on a leak threshold, /healthz, a systemd unit with a memory limit, and the README. Done when: the service runs 1,000 renders without memory growth and recovers from a browser crash. ### Out of scope (and why) - Web-scale reliability against every cookie banner and anti-bot wall. That is the fee. ### README must contain - The private-address rule and why it exists. - Memory sizing for Chromium.
# Agent instructions · Urlbox indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Playwright with Chromium, Node 22, node:http, sharp, A VPS with 2 GB of RAM. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
# Build plan · Urlbox A screenshot API with Playwright: render any URL to PNG or PDF with viewport, full-page and device-scale options, refuse private addresses, sign requests, cache results, queue concurrency, and keep a browser fleet of one healthy. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Render one URL PNG, JPEG or PDF with a timeout and no leaked contexts. ### Steps 1. Install Playwright and write render(url, options) with one persistent browser and a fresh context per render ```sh mkdir shotapi && cd shotapi && git init && npm init -y && npm pkg set type=module && npm install playwright@1 sharp@0.35.3 mkdir -p renders && cp .env.example .env ``` 2. Always close the context in finally; enforce RENDER_TIMEOUT_MS ### Done when - [ ] A public page renders crisp - [ ] Full-page captures below the fold - [ ] A page that never loads fails at the timeout without leaking a context ## Phase 2 · Safety Never fetch private or internal addresses. ### Steps 1. Refuse RFC 1918, localhost, link-local, .internal; block file: and data: 2. Resolve DNS before connecting and re-check the resolved address ### Done when - [ ] 127.0.0.1, 169.254.169.254 and a hostname resolving privately are all refused ## Phase 3 · Endpoint and cache Signed, cached, queued. ### Steps 1. GET /render with HMAC, a cache keyed by the option hash and TTL 2. A queue with MAX_CONCURRENCY ### Done when - [ ] A tampered signature is refused - [ ] A repeat is served from cache - [ ] 20 concurrent requests complete under the memory cap ## Phase 4 · Cleaner pages Hide selectors, inject CSS, block trackers, dismiss banners. ### Steps 1. Options for hide, css, block-hosts list, wait-for selector 2. A cookie-banner selector list ### Done when - [ ] A news page renders without its banner - [ ] A hidden selector is absent ## Phase 5 · Operate Eviction, browser restart on leaks, memory limit. ### Steps 1. Restart the browser past a memory threshold; /healthz; systemd with MemoryMax 2. README with the private-address rule and sizing Files: `README.md` ### Done when - [ ] 1,000 renders without memory growth - [ ] Recovers from a browser crash ## Not in this build - Web-scale reliability against every banner and anti-bot wall. ## After v1, if you want it - Webhook delivery of finished renders - Scheduled recurring captures
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required · secret. openssl rand -hex 32. SIGN_KEY=hex # Required. Cache. CACHE_DIR=./renders # Optional. Pages rendering at once. MAX_CONCURRENCY=3 # Optional. Hard timeout per render. RENDER_TIMEOUT_MS=20000
# Urlbox · product brief ## Problem Playwright renders any page to a PNG or PDF in one call. A queue and a cache around it is a sitting. The hard parts the service sells are blocking cookie banners and ads reliably, scale, and not babysitting a browser fleet. ## Product outcome Screenshots for your own products, safe against the classic SSRF, with a fleet of one you can operate. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a VPS with 2 GB of RAM for Chromium - Node 22 ## Explicit non-goals for v1 - Web-scale reliability against every banner and anti-bot wall. - reliable cookie-banner and ad blocking across the web - scale and concurrency without managing browsers - the CDN and retina rendering polish - the SLA ## Success criteria - SSRF checks verified - Soak test passes
# Build brief · Urlbox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screenshot API like Urlbox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http, Playwright with Chromium, sharp for post-processing. One process with a small browser page pool. Disk cache. ### Phase 1 · Render one URL Build: a render(url, options) function using one persistent browser and a fresh context per render: viewport, full page, device scale factor, delay, PNG or JPEG or PDF. Hard timeout, and the context always closed even on failure. Done when: a public page renders to a crisp PNG, a full-page render captures below the fold, a PDF opens correctly, and a page that never loads fails at the timeout without leaking a context. Do not build yet: HTTP, cache. ### Phase 2 · Safety Build: refuse private and internal addresses (RFC 1918, localhost, link-local, .internal), resolve DNS before connecting and re-check, block file: and data: schemes. This is the vulnerability every screenshot service has shipped once. Done when: http://127.0.0.1, http://169.254.169.254 and a hostname resolving to a private IP are all refused before any request leaves. ### Phase 3 · The endpoint and cache Build: GET /render?url=&width=&full=&format= with HMAC-signed requests, a cache keyed by the option hash with a TTL, and a concurrency limit with a queue so ten requests share the pool rather than spawning ten browsers. Done when: a tampered signature is refused, a repeat request is served from cache, and 20 concurrent requests complete without the process exceeding its memory cap. ### Phase 4 · Cleaner pages Build: options to hide selectors, inject CSS, block ad and tracker hosts from a list, dismiss common cookie banners by selector, and wait for a selector before capture. Done when: a news page renders without its cookie banner and a hidden selector is absent. ### Phase 5 · Operate Build: cache eviction, a browser restart on a leak threshold, /healthz, a systemd unit with a memory limit, and the README. Done when: the service runs 1,000 renders without memory growth and recovers from a browser crash. ### Out of scope (and why) - Web-scale reliability against every cookie banner and anti-bot wall. That is the fee. ### README must contain - The private-address rule and why it exists. - Memory sizing for Chromium.
# Architecture · Urlbox ## Stack | Part | Choice | Why | | --- | --- | --- | | Browser | Playwright with Chromium | renders anything; the fleet is the cost | | Runtime | Node 22, node:http, sharp | one process with a page pool | | Hosting | A VPS with 2 GB of RAM | Chromium | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Renderer | Playwright and options | Firefox or WebKit | | Guard | address checks | Never remove | | Edge | signing, cache, queue | A CDN | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `SIGN_KEY` · required, secret · openssl rand -hex 32. - `CACHE_DIR` · required · Cache. - `MAX_CONCURRENCY` · optional · Pages rendering at once. - `RENDER_TIMEOUT_MS` · optional · Hard timeout per render. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Urlbox product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Playwright with Chromium, Node 22, node:http, sharp, A VPS with 2 GB of RAM. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone.
# Delivery milestones · Urlbox Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Render one URL PNG, JPEG or PDF with a timeout and no leaked contexts. ### Steps 1. Install Playwright and write render(url, options) with one persistent browser and a fresh context per render ```sh mkdir shotapi && cd shotapi && git init && npm init -y && npm pkg set type=module && npm install playwright@1 sharp@0.35.3 mkdir -p renders && cp .env.example .env ``` 2. Always close the context in finally; enforce RENDER_TIMEOUT_MS ### Done when - [ ] A public page renders crisp - [ ] Full-page captures below the fold - [ ] A page that never loads fails at the timeout without leaking a context ## M2 · Safety Never fetch private or internal addresses. ### Steps 1. Refuse RFC 1918, localhost, link-local, .internal; block file: and data: 2. Resolve DNS before connecting and re-check the resolved address ### Done when - [ ] 127.0.0.1, 169.254.169.254 and a hostname resolving privately are all refused ## M3 · Endpoint and cache Signed, cached, queued. ### Steps 1. GET /render with HMAC, a cache keyed by the option hash and TTL 2. A queue with MAX_CONCURRENCY ### Done when - [ ] A tampered signature is refused - [ ] A repeat is served from cache - [ ] 20 concurrent requests complete under the memory cap ## M4 · Cleaner pages Hide selectors, inject CSS, block trackers, dismiss banners. ### Steps 1. Options for hide, css, block-hosts list, wait-for selector 2. A cookie-banner selector list ### Done when - [ ] A news page renders without its banner - [ ] A hidden selector is absent ## M5 · Operate Eviction, browser restart on leaks, memory limit. ### Steps 1. Restart the browser past a memory threshold; /healthz; systemd with MemoryMax 2. README with the private-address rule and sizing Files: `README.md` ### Done when - [ ] 1,000 renders without memory growth - [ ] Recovers from a browser crash ## M6 · Operate it like a product (production only) Only for the product-builder path: know when the renderer is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443
# Operations · Urlbox ## Backup Nothing; cache is disposable. ## Restore Redeploy. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Memory and render latency. ## Incident checklist Rotate SIGN_KEY; review logs for refused addresses. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] SSRF checks verified - [ ] Soak test passes ## Launch constraint Do not market omitted Urlbox capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required · secret. openssl rand -hex 32. SIGN_KEY=hex # Required. Cache. CACHE_DIR=./renders # Optional. Pages rendering at once. MAX_CONCURRENCY=3 # Optional. Hard timeout per render. RENDER_TIMEOUT_MS=20000
$ choose a build depth, inspect the files, then open the complete pack in your agent
A headless browser fleet is a pet that bites at 3am. Paying to never own one is rational above a few thousand renders.
xreliable cookie-banner and ad blocking across the web
xscale and concurrency without managing browsers
xthe CDN and retina rendering polish
xthe SLA
Vibecode Urlbox
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Urlbox replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Urlbox cost?
Urlbox costs about $19/month (Lo-Fi, checked 2026-09-04), which is $228 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Urlbox?
Honestly: reliable cookie-banner and ad blocking across the web; scale and concurrency without managing browsers; the CDN and retina rendering polish; the SLA. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Urlbox?
Yes: Playwright (browser automation, renders pages to images and PDFs). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.