{"id":45343457,"url":"https://github.com/hodlthedoor/screenforge","last_synced_at":"2026-02-21T11:02:50.949Z","repository":{"id":338413536,"uuid":"1157793913","full_name":"hodlthedoor/screenforge","owner":"hodlthedoor","description":"Self-hostable screenshot, PDF and OG image API","archived":false,"fork":false,"pushed_at":"2026-02-14T11:29:15.000Z","size":572,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-14T19:13:24.017Z","etag":null,"topics":["api","docker","nodejs","og-image","open-source","pdf-generator","playwright","screenshot-api","self-hosted","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hodlthedoor.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-14T10:00:03.000Z","updated_at":"2026-02-14T11:29:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hodlthedoor/screenforge","commit_stats":null,"previous_names":["hodlthedoor/screenforge"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/hodlthedoor/screenforge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodlthedoor%2Fscreenforge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodlthedoor%2Fscreenforge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodlthedoor%2Fscreenforge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodlthedoor%2Fscreenforge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hodlthedoor","download_url":"https://codeload.github.com/hodlthedoor/screenforge/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodlthedoor%2Fscreenforge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29679049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T09:33:50.764Z","status":"ssl_error","status_checked_at":"2026-02-21T09:33:19.949Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","docker","nodejs","og-image","open-source","pdf-generator","playwright","screenshot-api","self-hosted","typescript"],"created_at":"2026-02-21T11:02:50.239Z","updated_at":"2026-02-21T11:02:50.943Z","avatar_url":"https://github.com/hodlthedoor.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ScreenForge\n\n[![CI](https://github.com/hodlthedoor/screenforge/actions/workflows/ci.yml/badge.svg)](https://github.com/hodlthedoor/screenforge/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![Docker Hub](https://img.shields.io/docker/v/hodlthedoor/screenforge?logo=docker\u0026label=Docker+Hub)](https://hub.docker.com/r/hodlthedoor/screenforge)\n[![GHCR](https://img.shields.io/badge/GHCR-ghcr.io-2496ED?logo=github)](https://github.com/hodlthedoor/screenforge/pkgs/container/screenforge)\n[![GitHub Stars](https://img.shields.io/github/stars/hodlthedoor/screenforge?style=flat\u0026logo=github)](https://github.com/hodlthedoor/screenforge/stargazers)\n[![npm downloads](https://img.shields.io/npm/dm/@screenforge/sdk?logo=npm)](https://www.npmjs.com/package/@screenforge/sdk)\n\n**Self-hostable screenshot, PDF, and OG image API.** Open-source alternative to ScreenshotOne, Urlbox, and similar services.\n\n---\n\n## Quick Start\n\n```bash\ngit clone https://github.com/hodlthedoor/screenforge.git\ncd screenforge\ncp .env.example .env\n# Generate required secrets:\n#   openssl rand -hex 32   -\u003e API_KEY_SALT\n#   openssl rand -hex 32   -\u003e SESSION_SECRET\ndocker compose up -d\n```\n\nThe API is available at `http://localhost:3100`, Swagger docs at `http://localhost:3100/docs`, and the dashboard at `http://localhost:3100/dashboard`.\n\n---\n\n## Features\n\n- **Screenshots** -- full page, viewport, or element-level capture in PNG/JPEG/WebP with custom dimensions, dark mode, device scale factor, and delay\n- **PDF generation** -- render any URL to PDF with A4/Letter/Legal page formats, margins, landscape mode, and custom header/footer templates\n- **OG card generation** -- auto-generate Open Graph preview images with built-in templates and light/dark themes\n- **Pre-capture actions** *(v1.2.0)* -- automate click, scroll, type, hover, wait, delay actions before capture for interactive pages\n- **Element hiding/removal** *(v1.2.0)* -- hide or remove DOM elements via CSS selectors before rendering\n- **Element blurring** *(v1.2.0)* -- blur sensitive content with configurable radius for privacy-safe screenshots\n- **Content validation** *(v1.2.0)* -- fail renders if specific text is present or missing from the page\n- **Ad blocking** *(v1.2.0)* -- block 40+ ad/tracker domains for cleaner screenshots\n- **Visual diff** *(v1.4.0)* -- compare two screenshots pixel-by-pixel; returns a diff image, mismatch %, and pixel counts\n- **Visual diff baselines** *(v1.4.0)* -- store reference screenshots and detect regressions automatically with webhook notifications\n- **Custom font loading** *(v1.4.0)* -- render pages with custom fonts via Google Fonts, jsDelivr, or unpkg CDN URLs\n- **CLI tool** *(v1.4.0)* -- `screenforge` command-line interface for taking screenshots, PDFs, and OG cards without writing code\n- **Batch rendering** -- submit up to 50 mixed render jobs (screenshot, PDF, OG) in a single request\n- **Async rendering** -- queue any render job and poll for results, ideal for long-running captures\n- **Signed URLs** -- generate HMAC-signed GET URLs for screenshots and PDFs, perfect for embedding in HTML without exposing API keys\n- **Webhooks** -- receive async render notifications via callback URLs with delivery tracking and retry\n- **Content-hash caching** -- deduplicate identical renders with configurable TTL, saving compute and bandwidth\n- **Browser pool** -- managed pool of Playwright Chromium instances with automatic recycling for stability\n- **API key authentication** -- tier-based access control with sliding-window rate limiting per key\n- **Job queue** -- BullMQ-powered async rendering with Redis-backed persistence and automatic retries\n- **Stripe billing** -- integrated checkout, customer portal, and webhook handling for paid tiers\n- **Transactional email** -- SMTP-based email for verification, password reset, billing receipts, and usage alerts\n- **Dashboard** -- web UI to manage API keys, view usage charts, and configure account settings\n- **Prometheus metrics** -- `/metrics` endpoint for Grafana dashboards and alerting\n- **Swagger docs** -- interactive API documentation at `/docs`\n- **SSRF protection** -- private/internal URL blocking enabled by default\n- **Docker-ready** -- single `docker compose up` with PostgreSQL, Redis, and Chromium included\n\n---\n\n## API Reference\n\n### Endpoints\n\n| Method | Path | Auth | Description |\n|--------|------|------|-------------|\n| `POST` | `/v1/screenshot` | API key | Take a screenshot |\n| `POST` | `/v1/pdf` | API key | Generate a PDF |\n| `POST` | `/v1/og` | API key | Generate OG card |\n| `POST` | `/v1/batch` | API key | Batch render (up to 50) |\n| `GET` | `/v1/batch/:id` | API key | Poll batch status |\n| `GET` | `/v1/render/:id` | None | Poll async job |\n| `GET` | `/v1/signed/screenshot` | Signature | Signed URL screenshot |\n| `GET` | `/v1/signed/pdf` | Signature | Signed URL PDF |\n| `POST` | `/v1/keys` | Admin | Create API key |\n| `GET` | `/v1/keys` | Admin | List API keys |\n| `GET` | `/v1/usage` | API key | Get usage stats |\n| `GET` | `/v1/webhooks/deliveries` | API key | List webhook deliveries |\n| `GET` | `/v1/webhooks/deliveries/:id` | API key | Get delivery detail |\n| `POST` | `/v1/webhooks/test` | API key | Send test webhook |\n| `POST` | `/v1/billing/checkout` | Session | Create checkout session |\n| `GET` | `/v1/billing/portal` | Session | Stripe portal redirect |\n| `POST` | `/v1/billing/webhook` | Stripe sig | Stripe webhook |\n| `GET` | `/v1/health` | None | Detailed health check |\n| `GET` | `/v1/analytics` | API key | Render analytics \u0026 history |\n| `POST` | `/v1/diff` | API key | Visual diff — compare two screenshots |\n| `POST` | `/v1/diff/baseline` | API key | Store a baseline screenshot |\n| `GET` | `/v1/diff/baselines` | API key | List baselines |\n| `DELETE` | `/v1/diff/baseline/:name` | API key | Delete a baseline |\n| `GET` | `/v1/errors` | None | Error code reference |\n| `GET` | `/health` | None | Simple health check |\n| `GET` | `/docs` | None | Swagger UI |\n\nAll render endpoints accept `?async=true` to queue the job and return a `jobId` for polling.\n\n### curl Examples\n\n#### Screenshot\n\n```bash\ncurl -X POST http://localhost:3100/v1/screenshot \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"format\": \"png\",\n    \"width\": 1920,\n    \"height\": 1080,\n    \"fullPage\": true,\n    \"darkMode\": true,\n    \"deviceScaleFactor\": 2\n  }' \\\n  --output screenshot.png\n```\n\n#### PDF\n\n```bash\ncurl -X POST http://localhost:3100/v1/pdf \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"format\": \"A4\",\n    \"landscape\": false,\n    \"margin\": { \"top\": \"1cm\", \"bottom\": \"1cm\" }\n  }' \\\n  --output page.pdf\n```\n\n#### OG Card\n\n```bash\ncurl -X POST http://localhost:3100/v1/og \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"title\": \"My Page Title\",\n    \"template\": \"article\",\n    \"theme\": \"dark\"\n  }' \\\n  --output og.png\n```\n\n#### Batch Render\n\n```bash\ncurl -X POST http://localhost:3100/v1/batch \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"items\": [\n      { \"type\": \"screenshot\", \"url\": \"https://example.com\" },\n      { \"type\": \"pdf\", \"url\": \"https://example.com/docs\" },\n      { \"type\": \"og\", \"url\": \"https://example.com/blog/post-1\" }\n    ]\n  }'\n# Returns: { \"batchId\": \"...\", \"items\": [...] }\n\n# Poll batch status\ncurl http://localhost:3100/v1/batch/BATCH_ID \\\n  -H \"Authorization: Bearer sf_live_...\"\n```\n\n#### Async Rendering\n\n```bash\n# Queue a screenshot\ncurl -X POST \"http://localhost:3100/v1/screenshot?async=true\" \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"https://example.com\"}'\n# Returns: { \"jobId\": \"abc123\", \"pollUrl\": \"/v1/render/abc123\" }\n\n# Poll for result\ncurl http://localhost:3100/v1/render/abc123\n# Returns: { \"status\": \"completed\", \"url\": \"/storage/abc123.png\" }\n```\n\n#### Signed URLs\n\nSigned URLs allow GET-based rendering without exposing API keys -- ideal for `\u003cimg\u003e` tags and CDN integration.\n\n```bash\ncurl \"http://localhost:3100/v1/signed/screenshot?url=https://example.com\u0026width=1280\u0026signature=HMAC_SIG\u0026expires=1700000000\" \\\n  --output screenshot.png\n\ncurl \"http://localhost:3100/v1/signed/pdf?url=https://example.com\u0026format=A4\u0026signature=HMAC_SIG\u0026expires=1700000000\" \\\n  --output page.pdf\n```\n\n#### Visual Diff (v1.4.0)\n\nCompare two screenshots pixel-by-pixel and detect regressions:\n\n```bash\n# Compare two URLs\ncurl -X POST http://localhost:3100/v1/diff \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url_a\": \"https://example.com\",\n    \"url_b\": \"https://staging.example.com\",\n    \"threshold\": 0.1\n  }' \\\n  --output diff.png\n# Returns: diff image (PNG) with mismatch overlay\n\n# Store a baseline for regression detection\ncurl -X POST http://localhost:3100/v1/diff/baseline \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"name\": \"Homepage v1.4.0\",\n    \"threshold\": 0.05\n  }'\n# Returns: { \"id\": \"...\", \"name\": \"Homepage v1.4.0\", \"url\": \"...\" }\n\n# List baselines\ncurl http://localhost:3100/v1/diff/baselines \\\n  -H \"Authorization: Bearer sf_live_...\"\n```\n\n#### Advanced Capture Controls (v1.2.0)\n\nScreenForge supports powerful pre-capture interactions, element filtering, content validation, and ad blocking:\n\n**Pre-capture actions** -- automate interactions before screenshot/PDF capture:\n\n```bash\ncurl -X POST http://localhost:3100/v1/screenshot \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"actions\": [\n      {\"type\": \"click\", \"selector\": \"#accept-cookies\"},\n      {\"type\": \"wait\", \"duration\": 500},\n      {\"type\": \"scroll\", \"x\": 0, \"y\": 500},\n      {\"type\": \"type\", \"selector\": \"#search\", \"text\": \"Hello\"}\n    ]\n  }' \\\n  --output interactive.png\n```\n\nActions are executed in order before capture. Supported types: `click`, `scroll`, `type`, `hover`, `wait`, `delay`.\n\n**Element hiding and removal** -- hide or remove elements from the DOM:\n\n```bash\ncurl -X POST http://localhost:3100/v1/screenshot \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"hide_selectors\": [\".sidebar\", \".footer\"],\n    \"remove_selectors\": [\"#popup-banner\"]\n  }' \\\n  --output clean.png\n```\n\n- `hide_selectors`: elements hidden with CSS (`visibility: hidden`)\n- `remove_selectors`: elements removed from DOM entirely\n\n**Element blurring** -- blur sensitive content:\n\n```bash\ncurl -X POST http://localhost:3100/v1/screenshot \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com/dashboard\",\n    \"blur_selectors\": [\".email\", \".phone\", \".ssn\"],\n    \"blur_radius\": 20\n  }' \\\n  --output blurred.png\n```\n\n**Content validation** -- fail capture if conditions aren't met:\n\n```bash\ncurl -X POST http://localhost:3100/v1/screenshot \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"fail_if_contains\": \"Error 404\",\n    \"fail_if_missing\": \"Welcome\"\n  }'\n# Returns 400 if \"Error 404\" is found or \"Welcome\" is missing\n```\n\n**Ad blocking** -- block ads, trackers, and analytics:\n\n```bash\ncurl -X POST http://localhost:3100/v1/screenshot \\\n  -H \"Authorization: Bearer sf_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"block_ads\": true\n  }' \\\n  --output no-ads.png\n```\n\nBlocks 40+ ad domains including Google Ads, Doubleclick, Amazon Ads, and common trackers.\n\n---\n\n## SDKs\n\n### Installation\n\n**JavaScript/TypeScript:**\n\n```bash\nnpm install @screenforge/sdk\n```\n\n**Python:**\n\n```bash\npip install screenforge\n```\n\n### JavaScript SDK\n\nA typed JavaScript/TypeScript SDK is available in [`sdk/js/`](sdk/js/).\n\n```ts\nimport { ScreenForge } from '@screenforge/sdk';\n\nconst client = new ScreenForge({\n  apiKey: 'sf_live_...',\n  baseUrl: 'https://api.screenforge.dev',\n});\n\n// Screenshot\nconst png = await client.screenshot('https://example.com', { fullPage: true });\n\n// PDF\nconst pdf = await client.pdf('https://example.com', { format: 'a4' });\n\n// OG Card\nconst og = await client.og('https://example.com', { theme: 'dark' });\n\n// Async\nconst job = await client.screenshotAsync('https://example.com');\nconst result = await client.pollJob(job.jobId);\n\n// Batch\nconst batch = await client.batchRender([\n  { type: 'screenshot', url: 'https://example.com' },\n  { type: 'pdf', url: 'https://example.com' },\n]);\n\n// Webhooks\nconst deliveries = await client.listWebhookDeliveries({ page: 1, limit: 10 });\n\n// Usage\nconst usage = await client.getUsage();\n```\n\n### Python SDK\n\nA typed Python SDK with both sync and async support is available in [`sdk/python/`](sdk/python/).\n\n```python\nfrom screenforge import ScreenForgeClient\n\nclient = ScreenForgeClient(\n    api_key='sf_live_...',\n    base_url='https://api.screenforge.dev'\n)\n\n# Screenshot\npng_bytes = client.screenshot('https://example.com', {'fullPage': True})\n\n# PDF\npdf_bytes = client.pdf('https://example.com', {'format': 'a4'})\n\n# OG Card\nog_bytes = client.og('https://example.com', {'theme': 'dark'})\n\n# Async\nfrom screenforge import AsyncScreenForgeClient\n\nasync_client = AsyncScreenForgeClient(api_key='sf_live_...', base_url='https://api.screenforge.dev')\nresult = await async_client.screenshot('https://example.com')\n```\n\n---\n\n## Pricing Tiers\n\n| Tier | Renders/month | Rate Limit | Price |\n|----------|--------------|--------------|---------|\n| Free | 100 | 10 req/min | $0 |\n| Starter | 5,000 | 50 req/min | $29/mo |\n| Pro | 25,000 | 200 req/min | $79/mo |\n| Business | Unlimited | 1,000 req/min | $199/mo |\n\nBilling is handled via Stripe. Self-hosted instances can define custom tiers or disable billing entirely.\n\n---\n\n## Configuration\n\nAll configuration is done via environment variables. Copy `.env.example` to `.env` and adjust as needed.\n\n| Variable | Type | Default | Description |\n|----------|------|---------|-------------|\n| `PORT` | number | `3100` | Server port |\n| `BASE_URL` | string | `http://localhost:3100` | Base URL for async poll URLs and redirects |\n| `NODE_ENV` | string | `production` | Environment (`development`, `production`, `test`) |\n| `DATABASE_URL` | string | **required** | PostgreSQL connection string |\n| `REDIS_URL` | string | `redis://redis:6379/0` | Redis connection string |\n| `API_KEY_SALT` | string | **required** | Salt for API key hashing (min 16 chars) |\n| `SESSION_SECRET` | string | **required** | Session cookie secret (min 32 chars) |\n| `ADMIN_API_KEY` | string | -- | Admin API key for `/v1/keys` management |\n| `REQUIRE_AUTH` | boolean | `false` | Require API key for render endpoints |\n| `ALLOW_PRIVATE_URLS` | boolean | `false` | Allow rendering of private/internal URLs |\n| `STORAGE_PATH` | string | `./storage` | Directory for rendered files |\n| `BROWSER_POOL_SIZE` | number | `3` | Playwright browser instances (1-20) |\n| `MAX_RENDERS_PER_CONTEXT` | number | `100` | Recycle browser context after N renders |\n| `CACHE_TTL_SECONDS` | number | `3600` | Cache duration in seconds (0 = disabled) |\n| `NAVIGATION_TIMEOUT_MS` | number | `30000` | Page navigation timeout in ms |\n| `MAX_CONTENT_SIZE_MB` | number | `50` | Max request body size in MB |\n| `METRICS_ENABLED` | boolean | `true` | Enable Prometheus `/metrics` endpoint |\n| `STRIPE_SECRET_KEY` | string | -- | Stripe secret key (`sk_test_...` or `sk_live_...`) |\n| `STRIPE_PUBLISHABLE_KEY` | string | -- | Stripe publishable key (`pk_test_...` or `pk_live_...`) |\n| `STRIPE_WEBHOOK_SECRET` | string | -- | Stripe webhook signing secret (`whsec_...`) |\n| `SMTP_HOST` | string | -- | SMTP server hostname |\n| `SMTP_PORT` | number | `587` | SMTP port (587 for TLS, 465 for SSL) |\n| `SMTP_USER` | string | -- | SMTP username |\n| `SMTP_PASS` | string | -- | SMTP password or API key |\n| `SMTP_FROM` | string | `noreply@screenforge.dev` | From address for outgoing emails |\n| `ANTHROPIC_API_KEY` | string | -- | Anthropic API key for AI content extraction (`/v1/extract`) |\n\nGenerate secrets with:\n\n```bash\nopenssl rand -hex 32\n```\n\n---\n\n## Self-Hosting Guide\n\nFor the full deployment guide (environment variables, SSL, troubleshooting), see [docs/self-hosting.md](docs/self-hosting.md).\n\n### Docker Compose (Recommended)\n\nThe easiest way to run ScreenForge. Docker Compose starts the API server, PostgreSQL, and Redis with persistent volumes.\n\n```bash\n# 1. Clone and enter the project\ngit clone https://github.com/hodlthedoor/screenforge.git\ncd screenforge\n\n# 2. Create and configure .env\ncp .env.example .env\n# Set API_KEY_SALT and SESSION_SECRET (required):\n#   openssl rand -hex 32\n\n# 3. Start all services\ndocker compose up -d\n\n# 4. Verify\ncurl http://localhost:3100/health\n# {\"status\":\"ok\"}\n```\n\nData is persisted in Docker volumes (`pgdata`, `redisdata`, `storage`). To upgrade, pull the latest image and restart:\n\n```bash\ndocker compose pull \u0026\u0026 docker compose up -d\n```\n\n### Bare Metal\n\nPrerequisites:\n\n- Node.js 22+\n- PostgreSQL 16+\n- Redis 7+\n\n```bash\n# 1. Clone and install\ngit clone https://github.com/hodlthedoor/screenforge.git\ncd screenforge\nnpm install --include=dev\n\n# 2. Install Playwright browsers\nnpx playwright install chromium\n\n# 3. Create the database\ncreatedb screenforge\n\n# 4. Run migrations (in order)\nfor f in sql/*.sql; do psql screenforge \u003c \"$f\"; done\n\n# 5. Configure environment\ncp .env.example .env\n# Edit .env: set DATABASE_URL, API_KEY_SALT, SESSION_SECRET\n# For peer auth: DATABASE_URL=postgresql:///screenforge?host=/var/run/postgresql\n\n# 6. Build and start\nnpm run build\nnpm start\n```\n\nUse a process manager like pm2 for production:\n\n```bash\nnpm install -g pm2\npm2 start dist/index.js --name screenforge\npm2 save\n```\n\n### Production Deployment with SSL\n\nScreenForge includes production-ready nginx configuration with SSL, rate limiting, and security headers.\n\n#### Self-Signed Certificates (Internal/Development)\n\nThe repository includes `nginx-screenforge.conf` configured with self-signed certificates:\n\n```bash\n# 1. Deploy nginx configuration\nsudo bash deploy-nginx.sh\n\n# 2. Add domain to /etc/hosts (use your server's IP)\necho \"YOUR_SERVER_IP screenforge.local\" | sudo tee -a /etc/hosts\n\n# 3. Reload nginx\nsudo nginx -t \u0026\u0026 sudo systemctl reload nginx\n\n# 4. Restart pm2 if running\npm2 restart screenforge\n```\n\nAccess ScreenForge at `https://screenforge.local` (browsers will warn about self-signed cert; this is expected).\n\n#### Let's Encrypt SSL (Public Domain)\n\nFor production with a public domain:\n\n```bash\n# 1. Update nginx-screenforge.conf server_name to your domain\nsed -i 's/screenforge.local/yourdomain.com/g' nginx-screenforge.conf\n\n# 2. Deploy nginx config\nsudo bash deploy-nginx.sh\n\n# 3. Install certbot and obtain SSL certificate\nsudo apt install certbot python3-certbot-nginx\nsudo certbot --nginx -d yourdomain.com\n\n# 4. Update BASE_URL in both .env and ecosystem.config.cjs\nsed -i 's|https://screenforge.local|https://yourdomain.com|' .env\nsed -i 's|https://screenforge.local|https://yourdomain.com|' ecosystem.config.cjs\n\n# 5. Reload nginx and restart pm2\nsudo systemctl reload nginx\npm2 restart screenforge\n```\n\nThe nginx config includes:\n\n- HTTP → HTTPS redirect (301)\n- Rate limiting (10 req/s per IP, burst 20)\n- Security headers (HSTS, X-Frame-Options, CSP-ready)\n- Gzip compression\n- 120s timeout for long renders\n\n---\n\n## Monitoring\n\nScreenForge exposes Prometheus metrics at `/metrics` and includes pre-configured Grafana dashboards for comprehensive observability.\n\n### Quick Start\n\nStart ScreenForge with monitoring enabled using Docker Compose profiles:\n\n```bash\ndocker compose --profile monitoring up -d\n```\n\nThis starts:\n\n- **ScreenForge** at `http://localhost:3100`\n- **Prometheus** at `http://localhost:9090`\n- **Grafana** at `http://localhost:3000`\n\n### Access Grafana\n\n1. Open `http://localhost:3000`\n2. Login with default credentials:\n   - Username: `admin`\n   - Password: `admin`\n   - **⚠️ Production:** Change credentials via `GRAFANA_ADMIN_USER` and `GRAFANA_ADMIN_PASSWORD` in `.env`\n3. The ScreenForge dashboard is auto-provisioned and ready to use\n\n### Dashboard Panels\n\nThe pre-built dashboard includes:\n\n- **Request Rate by Endpoint** — requests/sec grouped by API route\n- **Request Rate by Status Code** — HTTP status code distribution\n- **Render Duration (p50/p95/p99)** — screenshot/PDF generation latency percentiles\n- **Browser Pool Utilization** — percentage of browser contexts in use\n- **Queue Depth** — waiting/active/completed/failed job counts\n- **Cache Hit Rate** — percentage of renders served from cache\n- **Storage Disk Usage** — total cached file size\n- **Cache Entries** — number of cached renders\n- **Rate Limit Rejections** — 429 response count\n- **Error Rate by Type** — failed render breakdown\n- **Active HTTP Connections** — Node.js TCP/TLS handle count\n\n### Metrics Endpoint\n\nThe `/metrics` endpoint is enabled by default and can be scraped by any Prometheus-compatible monitoring system:\n\n```bash\ncurl http://localhost:3100/metrics\n```\n\nTo disable metrics, set `METRICS_ENABLED=false` in your `.env` file.\n\n### Custom Prometheus Configuration\n\nEdit `prometheus.yml` to adjust scrape intervals or add additional targets. Restart Prometheus after changes:\n\n```bash\ndocker compose --profile monitoring restart prometheus\n```\n\n---\n\n## Architecture\n\n```mermaid\ngraph TD\n    Client([Client]) --\u003e LB[Fastify Server :3100]\n    LB --\u003e Landing[Landing Page /]\n    LB --\u003e Auth[Auth Routes /login /register]\n    LB --\u003e Dash[Dashboard /dashboard/*]\n    LB --\u003e API[API Routes /v1/*]\n    API --\u003e Pool[Browser Pool]\n    Pool --\u003e PW[Playwright Chromium]\n    API --\u003e Queue[BullMQ Queue]\n    Queue --\u003e Worker[Queue Worker]\n    Worker --\u003e Pool\n    API --\u003e Cache[Redis Cache]\n    API --\u003e RateLimit[Rate Limiter]\n    Auth --\u003e PG[(PostgreSQL)]\n    API --\u003e PG\n    Cache --\u003e Redis[(Redis)]\n    RateLimit --\u003e Redis\n    Queue --\u003e Redis\n```\n\n---\n\n## Development\n\n```bash\nnpm run dev          # Start with hot reload (tsx watch)\nnpm test             # Run test suite (Vitest)\nnpm run lint         # Lint check (ESLint)\nnpm run typecheck    # Type check (tsc --noEmit)\nnpm run build        # Compile TypeScript to dist/\n```\n\nDatabase migrations are in `sql/` and run automatically on first boot in Docker. For bare metal, apply them manually with `psql`.\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. Quick summary:\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feat/my-feature`)\n3. Write tests first, then implement (TDD)\n4. Ensure all checks pass: `npm test \u0026\u0026 npm run lint \u0026\u0026 npm run typecheck`\n5. Commit using [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `refactor:`, etc.)\n6. Open a pull request against `main`\n\n### Code Standards\n\n- TypeScript strict mode, no `any`\n- Single responsibility per file/function\n- Keep files small and focused\n- Zod for all input validation\n- Tests required for new features and bug fixes\n\n---\n\n## Tech Stack\n\n- **Runtime**: Node.js 22 + TypeScript (strict mode)\n- **Framework**: Fastify 5\n- **Browser**: Playwright (Chromium)\n- **Queue**: BullMQ + Redis\n- **Database**: PostgreSQL 16\n- **Validation**: Zod\n- **Auth**: bcrypt + cookie sessions + API key tiers\n- **Billing**: Stripe (Checkout + Customer Portal)\n- **Email**: Nodemailer (SMTP)\n- **Metrics**: prom-client (Prometheus)\n- **Docs**: @fastify/swagger + Swagger UI\n- **Testing**: Vitest\n- **Containerization**: Docker + Docker Compose\n\n---\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhodlthedoor%2Fscreenforge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhodlthedoor%2Fscreenforge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhodlthedoor%2Fscreenforge/lists"}