documentation

Everything you need to run Quarry yourself.

Install it, hit the API, wire up an LLM, and self-host with confidence.

Overview

Quarry is a self-hosted web scraper and crawler built for AI agent context. Scrape pages to clean markdown, crawl entire sites, monitor for changes, and extract structured data. It runs on your own infrastructure, with zero third-party dependency.

EndpointWhat it does
POST /scrapeSingle page → clean markdown
POST /crawlMulti-page crawl, async job, webhook on completion
POST /monitorsRecurring crawl on schedule, webhook only when content changes
POST /extractPage → structured JSON fields via LLM
GET /dashboardOps dashboard: jobs, monitors, queue depth, config
GET /metricsPrometheus metrics

Quick start

Clone the repo, set your Postgres password, and bring the stack up with Docker Compose.

manual setup
git clone https://github.com/samuelorobosa/quarry
cd quarry
cp .env.example .env   # edit .env, set POSTGRES_PASSWORD at minimum
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Install script

Prefer a one-liner? The installer detects your OS, installs Docker if missing, clones the repo to /opt/quarry, generates a .env with random secrets, starts every service (including the Lightpanda browser worker for JS-rendered pages), and confirms the API is healthy before declaring success.

install.sh
curl -fsSL https://raw.githubusercontent.com/samuelorobosa/quarry/main/install.sh | bash

POST /scrape

Fetch a single URL and return clean markdown.

request
curl -X POST http://localhost:3000/scrape \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com" }'
response
{
  "url": "https://example.com",
  "title": "Example Domain",
  "markdown": "# Example Domain\n\nThis domain is for use in...",
  "scraped-at": "2026-06-20T10:00:00Z"
}

POST /crawl

Start an async multi-page crawl. Returns immediately with a job ID; the crawl runs in the background.

request
curl -X POST http://localhost:3000/crawl \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "max-depth": 3,
    "max-pages": 100,
    "include-patterns": ["/docs/*", "/blog/*"],
    "exclude-patterns": ["/legal/*"],
    "webhook-url": "https://your-app.com/webhooks/crawl-done"
  }'

Status lifecycle: queued → running → completed | failed. Page status values: scraped · not_found ·blocked · timeout · error.

check status
curl http://localhost:3000/jobs/job_8f2a1c

POST /monitors

Save a crawl config as a recurring monitor. Re-crawls on schedule and fires a webhook only when content actually changes.

request
curl -X POST http://localhost:3000/monitors \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "frequency": "daily",
    "webhook-url": "https://your-app.com/webhooks/site-changed",
    "goal": "alert me when pricing changes"
  }'

frequency options: hourly · daily · weekly.goal is optional — when set, each diff is evaluated by your configured LLM, and only relevant changes fire the webhook.

manage
GET    /monitors/:id   # status, frequency, last checked, last job
DELETE /monitors/:id   # stop and remove

POST /extract

Scrape a URL and extract structured fields using an LLM. RequiresLLM_PROVIDER and LLM_API_KEY to be configured. Fields not found on the page return null — the model will not hallucinate values.

request
curl -X POST http://localhost:3000/extract \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "schema": {
      "plan-name": "string",
      "monthly-price": "number",
      "features": "array of strings"
    }
  }'

Schema type hints: "string" · "number" · "boolean" · "array of strings".

GET /dashboard

Available at http://localhost:3000/dashboard once running.

TabWhat's there
jobsAll crawl jobs with status and progress, auto-refreshes while running
monitorsActive monitors, last checked time, pause / resume / delete
workersQueue depths for crawl and monitor queues
configRead-only display of all env config

GET /metrics

Prometheus metrics for everything the worker touches. Point your scraper at /metrics and go.

LLM providers

Only required for /extract and monitor goal filtering. Everything else works without it.

OpenAI
LLM_PROVIDER=openai
LLM_API_KEY=sk-...
LLM_MODEL=gpt-4o
Anthropic
LLM_PROVIDER=anthropic
LLM_API_KEY=sk-ant-...
LLM_MODEL=claude-sonnet-4-6
Ollama (local)
LLM_PROVIDER=openai
LLM_BASE_URL=http://localhost:11434/v1
LLM_MODEL=llama3.2
LLM_API_KEY=ollama

Environment variables

VariableDefaultDescription
POSTGRES_PASSWORDRequired. Set in .env.
DATABASE_URLFull Postgres connection string.
REDIS_URLredis://redis:6379Redis connection string.
PORT3000API port exposed to host.
LLM_PROVIDERopenai"openai" or "anthropic".
LLM_API_KEYProvider API key.
LLM_MODELgpt-4oModel for extraction and AI judge.
LLM_BASE_URLOverride for Ollama, Groq, etc.
POLITENESS_MS300Min delay between requests to the same domain (ms).
LOG_RETENTION_DAYS30Days to keep structured logs in Postgres.

Services

All services run in Docker.

servicenotesstatus
postgresJob and monitor statealways on
redisBullMQ queues, crawl frontieralways on
apiNestJS API, default port 3000always on
worker-fetchCrawl engine, monitor runneralways on
lightpandaLightweight headless browser (Zig/V8)optional
worker-browserPlaywright/Chromium fallbackoptional

Enable the browser worker with: docker compose --profile browser up -d

Hardware sizing

SetupCPURAM
Dev / no browser2 vCPU4 GB
Production, Lightpanda4 vCPU8 GB
Production, Chromium used often4–8 vCPU16 GB