1. Conduid
  2. Scraping
  3. Crawlseo
MCP server · Scraping

Crawlseo

Open-source SEO monitoring. GSC + Site Crawler + Core Web Vitals in one dashboard.

52Fair

Scored 3 months ago · breakdown

About Crawlseo

Crawlseo is an MCP server published by crawlseo in the Scraping category: open-source SEO monitoring. GSC + Site Crawler + Core Web Vitals in one dashboard.

The repository has 659 stars and 94 forks, with the last commit 3 days ago. Recent commit activity is the strongest signal that reported issues will get attention.

Install

From source
git clone https://github.com/crawlseo/crawlseo

This server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.

Tools (12)

get_bing_keywordsTop Bing queries, summed over the weeks that end in the period (Bing reports by week)

Parameters not published.

get_bing_pagesTop pages in Bing, summed the same way

Parameters not published.

get_bing_trafficBing daily clicks, impressions and pages crawled, with Bing's indexed-page and crawl-error counts per day

Parameters not published.

get_crawl_issuesIssues of a crawl (the latest completed one by default), by severity

Parameters not published.

get_keywordsTop Search Console queries, for the last N days or a date range

Parameters not published.

get_opportunitiesStriking distance, low CTR, content decay and cannibalization

Parameters not published.

get_pagesTop pages, for the last N days or a date range

Parameters not published.

get_site_overviewClicks, impressions, position and CTR over 28 days against the 28 before, the latest crawl and vitals

Parameters not published.

get_trafficDaily clicks and impressions, for the last N days or a date range

Parameters not published.

get_vitalsCore Web Vitals reports: performance score, LCP, CLS, INP, TTFB

Parameters not published.

list_crawlsThe latest crawls with status, pages, issues and health score

Parameters not published.

list_sitesYour sites with their IDs, Search Console and Bing Webmaster properties

Parameters not published.

Ask AI

✦

Ask AI about Crawlseo

Powered by Claude · Grounded in docs

I know everything about Crawlseo. Ask me about installation, configuration, usage, or troubleshooting.

Security checks

Static checks derived from the repository 7 hours ago. Heuristics on the source, not an audit.

  • ✓README presentThe repository ships documentation.
  • ✓License declaredUsable under a stated license.
  • ✓Tests presentA test suite exists in the repository.
  • ✓Dependencies pinnedA lockfile pins dependency versions.
  • ✓No dynamic code executionSource scan found no eval, dynamic Function, or shell execution.

1 check has not been run yet.

Releases

v0.3.5v0.3.5 · 26 Oct 2026crawlseo 0.3.5 Email alerts Fired alerts can now be emailed through your own SMTP server. Set `SMTP_HOST` and `SMTP_FROM` (plus `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASSWORD` as your server needs), then each user turns on…
v0.3.4v0.3.4 · 21 Oct 2026New AI agents can read your SEO data. A read-only MCP server on your instance lets an agent such as Claude Code (tested end to end) read your own Search Console data, crawl issues, Core Web Vitals and opportunities. Nine tools, and none of…
v0.3.3v0.3.3 · 20 Oct 2026New Crawls and vitals on your own schedule. Settings > Schedule sets how often your sites are crawled and get a vitals check: the server default, off, or every 1, 3, 7, 14 or 28 days. The `SCHEDULE_*` variables stay the default, and a type…
v0.3.2v0.3.2 · 18 Oct 2026New Extension hooks for forks (#98). A fork or a custom deployment now registers its own code from one file, `extensions/index.ts`, instead of editing shared files, so upstream merges stay clean. `registerExtensions()` runs once per…
v0.3.1v0.3.1 · 18 Oct 2026Fixes Worker crash fix (#96). On an uncaught exception the worker now stops claiming jobs first, then puts the jobs it holds back in the queue, then exits. Before, it could claim a job it had just put back right before the exit, and that…

README

Open-source SEO monitoring for founders, not SEO specialists

Google Search Console, a site crawler, Core Web Vitals and an MCP server in one self-hosted dashboard. Free and open source (MIT).

GitHub stars PRs Welcome Docker


Why crawlseo?

crawlseo
Price Free and open source (MIT)
Self-hosted ✅ Runs on your server, with Docker Compose or manually
Database On your machine (your own PostgreSQL)
GSC integration ✅
Site crawler ✅ Up to 2,000 pages per crawl by default (CRAWL_MAX_PAGES)
Core Web Vitals ✅
MCP server ✅ Read-only: no tool writes, changes, queues or triggers anything
Keyword research ✅ (BYOK)
Backlinks ✅ (BYOK)

BYOK = Bring Your Own Key. Keyword research and backlink data use DataForSEO (optional). Google Autocomplete suggestions work as a free fallback.

In Domain overview, enter a competitor's domain or full website URL and choose Compare to query DataForSEO for that domain. Search Console fallback data is only used for your own site; unavailable competitor metrics are shown as unavailable.

Your data stays yours

crawlseo runs on your server. Your sites, keywords, crawl results and Google tokens stay in your database. We never receive them, and the app sends no usage statistics.

The app talks to outside services only in these cases:

Destination When What is sent How to turn it off
Google (sign-in, Search Console, PageSpeed Insights) When you sign in, connect Google, add a site or sync, run a vitals or index status check, or disconnect Google. Also on the schedule: a GSC sync per site each day and a vitals check per site each week, or as often as you set in Settings > Schedule Requests for your own properties and pages Don't run syncs, vitals or index status checks, or disconnect Google in Settings > Connections. For the schedule: vitals checks Off in Settings > Schedule, SCHEDULE_GSC_SYNC_HOURS=0, SCHEDULE_VITALS_DAYS=0 or SCHEDULER_ENABLED=false
Bing Webmaster Tools Only if you add your own Bing API key; then also a sync per site each day Requests for your own Bing sites Remove the key in Settings > Connections, or SCHEDULE_BING_SYNC_HOURS=0
DataForSEO Only if you add your own DataForSEO key The keyword and domain queries you run Remove the key in Settings > Connections
Google autocomplete Keyword suggestions when DataForSEO is not set up The seed keyword you typed Don't use keyword suggestions
Sites you crawl When you start a crawl, and on the schedule a crawl of each site each week, or as often as you set in Settings > Schedule (Docker) Normal HTTP requests from your server, to the site and to the addresses its redirects point to Don't start a crawl; for the schedule: crawls Off in Settings > Schedule, SCHEDULE_CRAWL_DAYS=0 or SCHEDULER_ENABLED=false
Your alert webhook Only if you set a webhook URL in Settings > Alert webhook: when alerts fire, and when you click Send test A signed JSON POST with the alerts that fired (type, message, site id and domain), to your own URL only Remove the webhook in Settings; ALERT_WEBHOOKS_ENABLED=false turns webhooks off for the whole instance
Your SMTP server Only if the operator sets SMTP_HOST and SMTP_FROM and you turn on Settings > Email alerts: when alerts fire, and when you click Send test An email with the alerts that fired (type, message and domain, plain text and simple HTML, no tracking, links only to this instance), through the SMTP server the operator set, to the addresses you entered Turn off Email alerts in Settings; ALERT_EMAILS_ENABLED=false or no SMTP_HOST turns email off for the whole instance
GitHub releases API Once a day A request for the latest crawlseo release. No data about you or your instance. GitHub sees your server's IP address CRAWLSEO_UPDATE_CHECK=false

Nothing else leaves your server. Alerts are shown in the app, sent to your webhook if you set one, and emailed through the operator's own SMTP server if you turn that on. Nothing goes through a crawlseo service.

The hosts behind these rows are accounts.google.com, oauth2.googleapis.com, www.googleapis.com, searchconsole.googleapis.com, ssl.bing.com, api.dataforseo.com, suggestqueries.google.com and api.github.com, plus the sites you crawl, the webhook URL you set yourself and the SMTP server the operator sets in SMTP_HOST. The list the tests check new code against is outbound-hosts.json. Your Google profile picture, if you have one, is loaded by your browser from Google, not by the server.

Features

🔍 GSC Analytics

Keywords, pages, clicks, impressions, position tracking with 28-day comparison and delta indicators.

🕷️ Site Crawler

Crawl up to 2,000 pages by default (CRAWL_MAX_PAGES sets the server's limit) with concurrent fetching. Health score, 16 issue types, content scoring, and remediation guidance.

The crawler (CrawlSEOBot/1.0 (+https://crawlseo.cloud/bot)) follows robots.txt per RFC 9309: each host (the bare domain and www included) is checked against its own robots.txt, redirect targets are checked before they are followed, a robots.txt that answers 429 or 5xx, times out or fails with a network error skips that host, and Crawl-delay is honoured. The crawl page lists what robots.txt kept it from.

Pages marked noindex are crawled but not scored. A page counts as noindex when a <meta name="robots"> or <meta name="googlebot"> tag, or the X-Robots-Tag response header (for all crawlers or googlebot:), says noindex or none; directives for other crawlers are ignored, and when the tag and the header disagree, noindex wins. Its links are still followed (noindex, follow is common), but it has no issues and does not count toward the health score, the issue counts or the content average, in the app, in /api/v1 and in the MCP tools. The crawl page shows how many there were and a noindex filter on the crawled pages, so an accidental noindex is easy to spot. A broken page is still reported as broken, noindex or not. Crawls from before this change keep the score they were given.

🤖 MCP server: AI agent integration

9 read-only tools for AI agents such as Claude Code, over HTTP at /mcp or over stdio, with access keys from Settings. Query your Search Console data, crawl issues, Core Web Vitals and opportunities without leaving the terminal. See MCP.

More features

Feature Description
⚡ Core Web Vitals LCP, CLS, INP, TTFB via PageSpeed Insights with mobile/desktop comparison
🔑 Keyword Research DataForSEO-powered keyword ideas with volume, difficulty, CPC. Free Google Autocomplete fallback
🔗 Backlinks Backlink profile, referring domains, anchor text, dofollow/nofollow analysis
📊 Rank Tracking Historical position snapshots with saved keywords and notes
💡 SEO Opportunities Striking distance keywords, low CTR, content decay, cannibalization detection
🔔 Alerts Traffic drops, position drops, low crawl health, sites that are mostly noindex and degraded vitals. Checked after each sync and crawl and when you press Evaluate now, shown in the app, and sent to your webhook and email if you set them up
📥 CSV Export Export keywords and pages data for offline analysis

GSC sync, crawls and Core Web Vitals checks run when you start them in the app. There is no built-in schedule yet.

Quick Start

git clone https://github.com/crawlseo/crawlseo.git
cd crawlseo
cp .env.example .env.local
# Add your Google OAuth credentials to .env.local
docker compose up -d db
npm install
npx prisma migrate dev --name init
npm run dev

Open http://localhost:3000, sign in with Google, and add your first site.

  1. Go to Google Cloud Console
  2. Create a project (or select existing)
  3. Enable the Google Search Console API
  4. Go to Credentials → Create Credentials → OAuth 2.0 Client ID
  5. Application type: Web application
  6. Authorized redirect URI: http://localhost:3000/api/auth/callback/google
  7. Copy Client ID and Client Secret to .env.local

Required scopes: openid, email, profile, https://www.googleapis.com/auth/webmasters.readonly

MCP

crawlseo has an MCP (Model Context Protocol) server, so an AI agent such as Claude Code can read your SEO data.

The MCP is read-only. Its tools read your data and nothing else: no tool writes, changes, queues or triggers anything. There is no run_crawl and no sync; crawls and syncs start in the app or on the schedule. Every tool reads through your instance's API (/api/v1) with an access key, never from the database, and a key sees the sites of its own account only.

Create a key

  1. Open Settings > MCP in crawlseo and choose Turn on. MCP is off until you do, and while it is off every key is refused.
  2. Give the key a name and choose Create key. Copy the key: it is stored hashed and shown only once.
  3. The list shows when each key was last used. Revoke stops a key at once.

Connect Claude Code

Over HTTP, to your instance's /mcp endpoint:

claude mcp add --transport http crawlseo https://seo.example.com/mcp --header "Authorization: Bearer <key>"

Or over stdio with the @crawlseo/mcp package, against your instance:

claude mcp add crawlseo -e CRAWLSEO_URL=http://localhost:3000 -e CRAWLSEO_API_KEY=<key> -- npx -y @crawlseo/mcp

Replace https://seo.example.com and http://localhost:3000 with your instance's address. /mcp refuses a request without a key, with a wrong key or with a revoked key (401), and every key while MCP is off for that account (403). /health answers without a key. Each key may make API_RATE_LIMIT_PER_MINUTE requests a minute to /mcp and /api/v1 together (default 60); a tool call counts once. The package also has an HTTP mode; see packages/mcp.

Tested end to end with Claude Code.

Tools

Tool What it reads
list_sites Your sites with their IDs, Search Console and Bing Webmaster properties
get_site_overview Clicks, impressions, position and CTR over 28 days against the 28 before, the latest crawl and vitals
get_keywords Top Search Console queries, for the last N days or a date range
get_pages Top pages, for the last N days or a date range
get_traffic Daily clicks and impressions, for the last N days or a date range
list_crawls The latest crawls with status, pages, issues and health score
get_crawl_issues Issues of a crawl (the latest completed one by default), by severity
get_vitals Core Web Vitals reports: performance score, LCP, CLS, INP, TTFB
get_opportunities Striking distance, low CTR, content decay and cannibalization
get_bing_traffic Bing daily clicks, impressions and pages crawled, with Bing's indexed-page and crawl-error counts per day
get_bing_keywords Top Bing queries, summed over the weeks that end in the period (Bing reports by week)
get_bing_pages Top pages in Bing, summed the same way

The MCP talks only to your own instance, so it adds no outside service to Your data stays yours.

Tech Stack

Layer Technology
Framework Next.js 16 (App Router)
Language TypeScript
Database PostgreSQL
ORM Prisma
Auth NextAuth.js v5
UI shadcn/ui + Tailwind CSS v4
Charts Plain React + Tailwind (no chart library)
Icons Lucide React
MCP @modelcontextprotocol/sdk
Deployment Docker Compose

Self-Hosting

Docker Compose (recommended)

git clone https://github.com/crawlseo/crawlseo.git
cd crawlseo
cp .env.example .env
# Edit .env with your credentials
docker compose pull
docker compose up -d

Compose pulls the prebuilt ghcr.io/crawlseo/crawlseo:latest image, so deployment credentials are only needed at runtime. Images support linux/amd64 and linux/arm64, and database migrations run automatically when the container starts.

Compose starts three services from that image: app (the web app), worker and db. The app queues crawls, vitals checks and syncs; the worker runs them, plus the schedule below, so a long crawl never slows the web app down. See Background jobs.

Image tags:

Tag What it is
latest The latest release. Moves only when a version is tagged.
0.3.5, 0.3 A release, and the newest patch of a minor version.
edge The latest build of main, ahead of the last release.
sha-<commit> One exact build, for main pushes and releases.

To pin a release, follow main, or use an image from a fork, set CRAWLSEO_IMAGE in .env:

CRAWLSEO_IMAGE=ghcr.io/crawlseo/crawlseo:0.3.5

To build locally instead, build the same image name before starting Compose:

docker build -t crawlseo:local .
CRAWLSEO_IMAGE=crawlseo:local docker compose up -d

Run on a server

The defaults above are for a local machine. On a server reachable from the internet, change these before you sign in.

1. Publish the app port on loopback only. In docker-compose.yml, change the app's port mapping:

    ports:
      - "127.0.0.1:3000:3000"

Docker writes its own firewall rules for published ports, so they bypass host firewalls such as ufw. With the default 3000:3000 the app answers plain http on the server's public IP even if ufw blocks port 3000. On loopback only the reverse proxy on the same host can reach it.

2. Put the app behind HTTPS with a reverse proxy. A minimal Caddy setup on the host, with your domain's DNS pointing at the server and ports 80 and 443 open (Caddy gets the certificate itself):

# /etc/caddy/Caddyfile
seo.example.com {
	reverse_proxy 127.0.0.1:3000
}

3. Set the public URL and the Google redirect URI. In .env, set NEXTAUTH_URL=https://seo.example.com and keep AUTH_TRUST_HOST=true. In the Google OAuth client, add https://seo.example.com/api/auth/callback/google as an authorized redirect URI.

4. Set your own database password. Set POSTGRES_PASSWORD in .env before the first docker compose up; the default is changeme. Postgres keeps the password it was created with, so changing it later also means changing it inside the database.

5. Close registration. Sign in once with your own Google account, then set DISABLE_REGISTRATION=true in .env and recreate the app container with docker compose up -d --force-recreate app (docker compose restart does not reread .env). From then on only accounts that already signed in can log in. Without it, anyone with a Google account can sign up on your instance and run crawls from your server. A refused account sees "This instance is not accepting new accounts. Ask its owner to sign you up."; set REGISTRATION_CLOSED_MESSAGE to say something else, such as where to ask for access.

6. Keep NEXTAUTH_SECRET stable. It signs sessions and also encrypts stored API keys and Google tokens. If it changes, saved API keys become unreadable and users have to sign in with Google again.

Manual

# Prerequisites: Node.js 20+, PostgreSQL

npm install
cp .env.example .env.local
# Configure .env.local

npx prisma migrate deploy
npm run build
npm start

npm start runs jobs in the app process (WORKER_MODE=inline, the default). To run them in a separate process instead, set WORKER_MODE=off for the app and run npm run build:worker once, then WORKER_MODE=worker node dist/worker.cjs next to it.

Background jobs

Crawls, vitals checks, GSC and Bing syncs and alert evaluation run as jobs in a queue in your Postgres database. A job that fails for a reason a retry can fix (a timeout, a network error) is retried after 1, 5 and 25 minutes. One that cannot be fixed by waiting (an expired Google connection, a missing key, an exhausted PageSpeed quota) fails at once with its reason. The buttons show when a job is queued, running or waiting to retry.

Who runs the jobs is set by WORKER_MODE:

Setup App Worker
Docker Compose from this repository (recommended) off worker service, node worker.cjs
One container, or npm start inline (the default): the app runs them none
Vercel inline: see Vercel none

An install upgraded from 0.2.x with its old docker-compose.yml keeps working without changes: the app has no WORKER_MODE, so it runs the jobs itself, as it ran crawls before. To move to the worker, take the worker service and the app's WORKER_MODE: "off" line from the current docker-compose.yml. If the worker is not running, the buttons say that queued jobs are not being picked up.

On docker compose stop (SIGTERM) the worker lets a running sync finish (up to WORKER_SHUTDOWN_GRACE_SECONDS, 25 s by default) and puts a running crawl back in the queue; it starts again when the worker does. A job whose process died without stopping is taken back after JOB_STALE_SECONDS.

The schedule. For every site the app queues a GSC sync and, with a Bing property and key, a Bing sync each day, then an alert evaluation after each sync. Each site also gets a crawl (with the page count of its last crawl) and a vitals check each week. After an upgrade, for a new site, or when crawls or vitals checks are turned on (or a worker is added to a Vercel database), the first scheduled crawl and vitals check are spread over the first week, never sooner than a week after the site's last crawl or check, so none of these crawls every site at once. Set the intervals with the SCHEDULE_* variables (0 turns one off), or turn the schedule off with SCHEDULER_ENABLED=false; the buttons keep working either way.

Each user can change the crawl and vitals intervals of their own sites in Settings > Schedule: the server default, off, or every 1, 3, 7, 14 or 28 days. A change applies from the next scheduling pass (within 10 minutes), measured from the site's last crawl or check. The SCHEDULE_* values are the default for everyone who sets nothing, and a type the server turns off (SCHEDULE_CRAWL_DAYS=0, SCHEDULE_VITALS_DAYS=0 or SCHEDULER_ENABLED=false) stays off whatever a user set. Scheduled crawls and vitals checks need the worker or an app in inline mode, so on Vercel these settings apply only when a worker is connected to the same database.

Alert webhooks

Alerts are checked after each GSC or Bing sync, after each crawl and when you click Evaluate now on a site's Alerts page. To get the ones that fire outside the app, set a webhook in Settings > Alert webhook: a URL of your own (n8n, Zapier, Make, a small script) and a signing secret of at least 16 characters (Generate makes one; copy it before you save, it is not shown again). Send test sends one test event with the values in the form and shows what your endpoint answered.

Each delivery is a JSON POST:

{
  "event": "alert.fired",
  "id": "cmh0c9x2k0001",
  "firedAt": "2026-10-19T05:02:11.000Z",
  "site": { "id": "cmgz...", "domain": "example.com" },
  "alerts": [
    {
      "id": "cmgy...",
      "type": "TRAFFIC_DROP",
      "message": "Traffic drop on example.com: clicks -34.2% vs prior week",
      "previousFiredAt": null
    }
  ],
  "url": "https://seo.example.com/sites/cmgz.../alerts",
  "crawlseo": { "version": "0.3.5" }
}

url is set when NEXTAUTH_URL is. All alerts of one site from one evaluation come in one delivery. An alert that keeps firing (a traffic drop lasts days) is sent again at most once a day.

Headers:

Header Value
X-Crawlseo-Event alert.fired or test
X-Crawlseo-Delivery The delivery id, the same on every retry: use it to drop duplicates
X-Crawlseo-Timestamp Unix time in seconds when this attempt was signed
X-Crawlseo-Signature sha256= and the hex HMAC-SHA256 of <timestamp>.<raw body> with your secret

To verify a delivery, compute the HMAC over the raw body before parsing it, compare in constant time and refuse old timestamps:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, headers, secret) {
  const timestamp = headers["x-crawlseo-timestamp"];
  const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const given = Buffer.from(headers["x-crawlseo-signature"] ?? "");
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  return fresh && given.length === expected.length && timingSafeEqual(given, Buffer.from(expected));
}

Answer with any 2xx status. A network error, a timeout (10 s), or HTTP 408, 425, 429 or 5xx is retried after about 1, 5, 25 and 60 minutes; after the fifth attempt the delivery fails. Any other status fails it at once, and redirects are not followed (set the final URL). Settings shows the last delivery and its error. On Vercel a failed delivery is retried in the same request after 2, 10 and 30 seconds, then at the next cron run.

Where webhooks may go. The URL is checked with the same guard as the crawler, on save and again on every connection, so a name that resolves to a private, loopback, link-local or reserved address is refused, also when its DNS answer changes later. For a receiver on your own network, such as n8n in the same Docker network (http://n8n:5678/webhook/...), the operator sets ALERT_WEBHOOK_ALLOW_PRIVATE=true. That allows 10/8, 172.16/12, 192.168/16, 100.64/10, loopback and fc00::/7; link-local addresses (including the cloud metadata address 169.254.169.254) and reserved ranges stay blocked. It is a server setting, not a per-user one, so on a shared instance users cannot reach the server's network unless you allow it. ALERT_WEBHOOKS_ENABLED=false turns webhooks off for the instance.

Alert emails

The same alerts that reach the webhook can be emailed, through an SMTP server the operator of the instance sets up. crawlseo sends no email through any service of its own: without SMTP settings, nothing is emailed.

The operator sets these in the environment (.env with Docker) and restarts the app:

SMTP_HOST=smtp.example.com
SMTP_PORT=587          # default 587, or 465 with SMTP_SECURE=true
SMTP_SECURE=false      # true: TLS from the start (port 465); false: STARTTLS when offered
SMTP_USER=alerts@example.com
SMTP_PASSWORD=your-smtp-password
SMTP_FROM="crawlseo <alerts@example.com>"

SMTP_HOST and SMTP_FROM are required; leave SMTP_USER out for a server that takes mail without a login. Any SMTP server works: your mail provider's, a transactional email service or a relay on your own network. Many providers want an app password or an API key as SMTP_PASSWORD rather than your account password, and a sender address on a domain they verified; their SMTP documentation names the host, port and TLS mode. The login stays in the environment: it is never stored in the database, never returned by an API, never shown in Settings and never logged (errors are shown without it).

Each user then opens Settings > Email alerts, enters up to 5 addresses, turns on Email fired alerts and saves. Send test sends one test email to the addresses in the form and shows the SMTP server's answer.

Recipients are not verified. On an instance with open sign-up, any user can have your SMTP server send alert emails, and up to 5 test emails per 10 minutes (counted per app process, each to up to 5 addresses), to any address they enter. If you allow open sign-up, know that mail sent this way goes out under your SMTP account and sender address. DISABLE_REGISTRATION=true stops new accounts from doing so (accounts that already signed in keep their access); ALERT_EMAILS_ENABLED=false turns email off for everyone.

All alerts of one site from one evaluation come in one email, plain text plus simple HTML, with no tracking pixel, no remote image and no link except to this instance (when NEXTAUTH_URL is set). An alert that keeps firing is emailed again at most once a day, as with webhooks. Sending is best effort and never holds up a crawl, a sync or an alert evaluation: a network error, a timeout (10 s for each step: DNS, connect, greeting, each answer) or a temporary SMTP answer (4xx) is retried after about 1 and 5 minutes (on Vercel within the request, then at the next cron run), any other failure ends it at once, and Settings shows the last email and its error. ALERT_EMAILS_ENABLED=false turns alert emails off for the instance.

Vercel

There is no long-running process on Vercel, so:

  • A button runs its job in the same request after the response, within the route's time limit. A sync whose attempt fails for a reason a retry can fix is tried again in that request while time is left; if it still fails, the button shows when the next attempt is due and offers Retry. A crawl or vitals check that fails or is cut off fails with its reason; run it again with its button. The routes set maxDuration = 300, the most the Hobby plan allows (function duration limits). A crawl stops at that limit and keeps the pages it reached, so large crawls are cut short on Vercel; use Docker for them.
  • vercel.json adds a cron job that calls /api/cron/tick once a day. It queues and runs the daily syncs and alert evaluation. Scheduled crawls and vitals checks need the worker, so they run on Docker only.
  • Set CRON_SECRET (a random string of at least 16 characters) in the project's environment variables. Vercel sends it with every cron request and the route refuses requests without it (securing cron jobs).
  • The Hobby plan runs cron jobs at most once a day, at some point within the scheduled hour (cron limits). On Pro you can change the schedule in vercel.json to run more often, for example */15 * * * *.

Environment Variables

Variable Required Description
DATABASE_URL Yes PostgreSQL connection string
NEXTAUTH_SECRET Yes Session key, also encrypts stored API keys and Google tokens (openssl rand -hex 32). Keep it stable. AUTH_SECRET works as an alias
GOOGLE_CLIENT_ID Yes Google OAuth client ID
GOOGLE_CLIENT_SECRET Yes Google OAuth client secret
NEXTAUTH_URL No Public base URL of the app, e.g. https://seo.example.com. Set it on a server; locally the request host is used
DISABLE_REGISTRATION No Set to true to close sign-up: a Google account that has never signed in on this instance is refused, while accounts that already signed in keep signing in
REGISTRATION_CLOSED_MESSAGE No What the login page tells an account that registration refused. Plain text, at most 200 characters, shown as text (no HTML). Unset or blank keeps the default line
CRAWLSEO_HIDE_CLOUD_PROMO No Set to true to remove the crawlseo.cloud card from the sidebar
CRAWLSEO_UPDATE_CHECK No Set to false to turn off the daily update check: no request to GitHub is made and the sidebar shows no update notice
CRAWL_MAX_MINUTES No Time limit of one crawl in minutes (default 30). A host whose Crawl-delay does not fit in it is stopped early, with a message on the crawl page
CRAWL_MAX_PAGES No Most pages one crawl may fetch (default 2000). The server applies it to every crawl, from the web app and from the schedule, whatever page count the request asks for
API_RATE_LIMIT_PER_MINUTE No Requests per minute one access key may make to /mcp and the read-only API (/api/v1) together (default 60). Counted per process
WORKER_MODE No Who runs queued jobs: inline (default, the app process), off (this process only queues), worker (the worker process). See Background jobs
WORKER_LONG_CONCURRENCY No Crawls and vitals checks one process runs at once (default 1)
WORKER_SHORT_CONCURRENCY No Syncs and alert evaluations one process runs at once (default 2)
WORKER_POLL_SECONDS No How often an idle process looks for queued jobs (default 5)
WORKER_SHUTDOWN_GRACE_SECONDS No How long the worker lets running syncs finish on SIGTERM (default 25). Keep it below the container's stop timeout
JOB_STALE_SECONDS No A running job whose process sent no heartbeat for this long is taken back (default 180)
JOB_RETENTION_DAYS No Finished jobs are deleted after this many days (default 30)
SCHEDULER_ENABLED No Set to false to stop all scheduled jobs. Buttons keep working
SCHEDULE_GSC_SYNC_HOURS No Hours between GSC syncs of a site (default 24, 0 turns them off)
SCHEDULE_BING_SYNC_HOURS No Hours between Bing syncs of a site (default 24, 0 turns them off)
SCHEDULE_CRAWL_DAYS No Days between scheduled crawls of a site (default 7, 0 turns them off). Each user can set their own interval in Settings > Schedule; 0 here keeps crawls off for everyone. Docker only
SCHEDULE_VITALS_DAYS No Days between scheduled vitals checks of a site (default 7, 0 turns them off). Each user can set their own interval in Settings > Schedule; 0 here keeps them off for everyone. Docker only
ALERT_WEBHOOKS_ENABLED No Set to false to turn alert webhooks off for the instance: nothing is sent and Settings shows no webhook form. See Alert webhooks
ALERT_WEBHOOK_ALLOW_PRIVATE No Set to true to let webhooks reach private networks (10/8, 172.16/12, 192.168/16, 100.64/10, loopback, fc00::/7), for a receiver on your own network. Link-local and reserved addresses stay blocked. Default: public addresses only. See Alert webhooks
SMTP_HOST No Your SMTP server. With SMTP_FROM, turns on alert emails. See Alert emails
SMTP_PORT No SMTP port (default 587, or 465 when SMTP_SECURE=true)
SMTP_SECURE No true: TLS from the start (port 465). false: plain, then STARTTLS when the server offers it. Default: true on port 465, otherwise false
SMTP_USER No SMTP login. Leave unset for a server that takes mail without one
SMTP_PASSWORD No SMTP password or app password. Kept in the environment only: never stored, shown or logged
SMTP_FROM No Sender of alert emails, e.g. crawlseo <alerts@example.com>. Required for email
ALERT_EMAILS_ENABLED No Set to false to turn alert emails off for the instance: nothing is sent and Settings shows no email form. See Alert emails
CRON_SECRET Vercel Protects /api/cron/tick. Without it the route refuses every request

The crawlseo.cloud card

The sidebar shows one card about crawlseo.cloud, the hosted AI visibility product from the same team. It is a plain link to the crawlseo.cloud home page with utm_source=oss in the URL (plus utm_medium=app and utm_campaign=sidebar). The app makes no request for it, loads no image and reports nothing about it. Each user can hide it with the × button (remembered in that browser), and CRAWLSEO_HIDE_CLOUD_PROMO=true removes it for everyone on the instance.

Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

# Fork the repo, then:
git checkout -b feature/your-feature
# Make your changes
git commit -m "feat: add your feature"
git push origin feature/your-feature
# Open a Pull Request

License

MIT License. See LICENSE for details.


Built by Brandson Digital · Created by Mike

Self-hosted SEO tools should be free and open source. Your data stays on your server.

README fetched from the repository 7 hours ago. The original is authoritative.

Questions

About Crawlseo

How do I install Crawlseo?

No package has been published for this server, so install it from source: clone the repository and follow its README.

Is Crawlseo safe to use with an AI agent?

Its trust score is 52 out of 100 (fair). It passes 5 of the 5 static security checks run so far. It has no ConduID identity yet, so agent calls to it are not receipted.

Is Crawlseo still maintained?

Yes — the latest release is v0.3.5 (26 Oct 2026), and the last commit was 3 days ago. The repository has 659 stars and 1 open issues.