A faster, smarter interface for Google Search Console data. Portfolio monitoring across every property you manage, saved query/page classifications, trend detection against Google core updates, cannibalization and growth- opportunity analysis, team sharing, and public read-only dashboards — without the 1,000-row limit or the CSV exports.
Serql is open core: the application in this repository is licensed under the
AGPL-3.0. The hosted service at serql.io adds a
commercially licensed enterprise layer that lives in src/ee/ —
see Licensing.
Status: self-hosting is new. Expect rough edges and please open an issue when you hit one.
- Portfolio dashboard — every GSC property from every connected Google account on one screen, with tags, sparklines, and period-over-period deltas.
- Performance explorer — queries, pages, countries, devices, and search appearance with five-dimension filtering, saved presets, comparison ranges, and CSV export.
- Classifications — rule-based query/page groups (branded vs. non-branded, product lines, content types) evaluated live over the full dataset.
- Annotations & core updates — your notes and Google's confirmed ranking updates overlaid on every chart.
- Cannibalization & growth opportunities — pages competing for the same queries, and queries with impressions but poor CTR/position.
- Teams & sharing — invite collaborators per property; publish white-label, read-only share links.
- Self-host friendly — any Postgres, Redis optional, Docker Compose included.
You need three external things: a free Clerk application
for sign-in, a Google Cloud OAuth client with the Search Console API enabled,
and Node.js 22.22.1 (see .nvmrc). Everything else runs locally.
git clone https://github.com/snowopsdev/serql.git
cd serql
npm install
cp .env.example .env.local
# Fill in: DATABASE_URL, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY,
# GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, TOKEN_ENCRYPTION_KEY (openssl rand -hex 32)
docker compose up -d postgres # local Postgres on :5432 (DATABASE_URL=postgresql://serql:serql@localhost:5432/serql)
npm run db:migrate
npm run db:seed # Google core-update dates
npm run devOpen http://localhost:3000, sign in, and connect a Google account from the
onboarding screen. In Google Cloud, add
http://localhost:3000/api/integrations/google/callback as an authorized
redirect URI.
cp .env.docker.example .env.docker # fill in the same keys
docker compose --profile app up --build--profile redis adds a Redis instance behind a REST proxy so GSC responses
are cached; without it the app still works, it just re-queries Google more
often. The image is built with output: "standalone" (see Dockerfile).
Serql is a standard Next.js 16 app. It runs on Vercel (the hosted product
does), on any Node.js 22 host, or from the Docker image. The one scheduled job
(/api/cron/sync-core-updates, protected by CRON_SECRET) is declared in
vercel.json; on other platforms call it daily from any scheduler with an
Authorization: Bearer $CRON_SECRET header.
.env.example documents every variable. The important groups:
| Variable | Description |
|---|---|
DATABASE_URL |
Postgres connection string. *.neon.tech hosts use Neon's serverless driver automatically; anything else uses pg. Force with DB_DRIVER=neon|pg. |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY |
Clerk auth (free tier is fine). There is no local auth bypass. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
OAuth client for the Search Console API (webmasters.readonly scope). |
TOKEN_ENCRYPTION_KEY |
32-byte hex key; Google OAuth tokens are AES-256-GCM encrypted at rest and the OAuth state cookie uses it too. |
NEXT_PUBLIC_APP_URL |
Origin the app is served from (http://localhost:3000 locally). |
| Variable | Description |
|---|---|
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN |
Upstash-compatible Redis REST endpoint for GSC response caching and rate limiting. Off when unset. |
ENABLE_GSC_CACHE |
Set "false" to disable caching even when Redis is configured. |
ENABLE_API_RATE_LIMITING |
Set "true" to enforce limits (needs Redis); otherwise limits are computed but not enforced. |
CLERK_WEBHOOK_SECRET |
Lets Clerk push user create/update/delete events to /api/webhooks/clerk. Without it users are provisioned lazily on first sign-in. |
CRON_SECRET |
Protects /api/cron/*. |
NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_APP_DESCRIPTION, NEXT_PUBLIC_SUPPORT_EMAIL |
Rebranding (see BRAND.md). |
NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_COOKIE_DOMAIN |
Split-domain deployments (marketing on www., app on app.). Leave unset for one domain. |
BETA_ACTIVE |
Legacy gate. Unset or anything other than "false" means every signed-in user has full access — the right default when you are not running Stripe. |
Stripe (STRIPE_*, NEXT_PUBLIC_STRIPE_ENABLED), the marketing site
(NEXT_PUBLIC_EE_MARKETING, NEXT_PUBLIC_MARKETING_ASSETS_URL), PostHog,
Kit, and Vercel telemetry. All default to off; the core app never
needs them. See src/ee/README.md.
npm run dev # Turbopack dev server
npm run verify # eslint (zero warnings) + tsc
npm run test:unit # core unit tests — no credentials, no network
npm run test:ee-unit # enterprise-layer unit tests — also credential-free
npm test # Playwright e2e (needs a running app, Clerk, and a GSC property; see tests/README.md)
npm run db:generate # generate a Drizzle migration from schema changes
npm run db:studio # browse the databaseHusky runs lint-staged on commit and npm run verify on push. CI runs lint,
typecheck, unit tests, a core-mode production build, the open-core boundary
check, and npm audit on every PR; the e2e suite runs only where repository
secrets are available.
Read CONTRIBUTING.md before opening a PR and
docs/ARCHITECTURE.md for how the pieces fit together.
src/
├── app/
│ ├── (auth)/ # Clerk sign-in / sign-up / reset
│ ├── (app)/ # Authenticated shell (access gate in layout.tsx)
│ │ ├── dashboard/ # Portfolio view
│ │ ├── property/[id]/ # Performance, cannibalization, growth opportunities, annotations, settings
│ │ ├── settings/ # Team, account & data (DSAR), billing (ee)
│ │ └── onboarding/ # Google OAuth connect flow
│ ├── share/[token]/ # Public read-only dashboards
│ ├── api/ # REST routes (properties, tags, filter presets, team, share, integrations, webhooks, cron, privacy)
│ ├── page.tsx # Core landing page, or the ee marketing site when enabled
│ └── (legal)/, subscribe/, thank-you/ # One-line shims into src/ee
├── components/ # ui/ (shadcn), charts/, dashboard/, property/, filters/, shared/, theme-customizer/
├── config/ # site.ts (URLs, names), csp.ts
├── ee/ # Enterprise layer — billing/, marketing/, telemetry/ (separate license)
├── hooks/ # TanStack Query hooks
├── lib/
│ ├── access/ # Entitlement seam: unlimited provider, or ee billing when Stripe is configured
│ ├── auth/ # requireAppUser, property authorization
│ ├── db/ # Drizzle client (Neon or pg) + schema
│ ├── gsc/ # Search Console client, caching, sync
│ ├── google-oauth/ # OAuth flow, encrypted tokens, refresh/revoke
│ ├── classifications/, annotations, share-performance …
│ └── cache/, consent/, privacy/, utils/
└── proxy.ts # Clerk middleware + optional split-domain routing
- Core (everything outside
src/ee/): GNU AGPL-3.0. If you run a modified version as a network service you must make your modified source available to its users. - Enterprise layer (
src/ee/): source-available under the Serql Commercial License. It is disabled unless you set its environment variables, and you may delete the directory entirely. - Brand: the Serql name and logos are trademarks and are not covered by
either license — see
BRAND.md. - Third-party code redistributed here is listed in
THIRD_PARTY_NOTICES.md.
Need a license for src/ee/ or a hosted plan? Visit serql.io.