Configuration
Every Worker Secret Searchkeep reads, the SITES JSON format, brand regex, write access, sync budgets, how to set secrets with wrangler or the dashboard, and local development.
Where configuration lives
All configuration is stored as Worker Secrets on your searchkeep Worker. None of it is
in the repository, which is what lets your fork stay identical to upstream and upgrade with “Sync fork”.
- Secrets are encrypted, never shown again after you set them, and never printed in logs by Searchkeep.
setup_statusreports only whether each one is present. - The repository’s
wrangler.jsoncsetskeep_vars: true, so if you prefer to keep a non-sensitive value such asBRAND_REGEXas a plain variable in the dashboard, deployments will not overwrite it. - Setting or changing a secret deploys a new version of the Worker within seconds. No build is needed.
- Problems in the values (invalid JSON in
SITES, a bad regex) never crash the Worker; they are listed bysetup_statusand the dashboard’s status page.
Secrets reference
| Name | Required | Description |
|---|---|---|
ACCESS_TEAM_DOMAIN | yes | Your Cloudflare Zero Trust team domain, hostname only. Used to fetch the signing keys for Access JWTs. e.g. myteam.cloudflareaccess.com |
ACCESS_AUD | yes | The Application Audience (AUD) tag of the Access application that protects Searchkeep. e.g. 4714c1358e65fe4b408ad6d432a5f878f08194bdb4752441fd56faefa9b2b6f2 |
CLOUDFLARE_ACCOUNT_ID | yes | The Cloudflare account that owns the Worker, the Access application, the zones and the Web Analytics sites. e.g. 023e105f4ecef8ad9ca31a8372d0c353 |
CLOUDFLARE_API_TOKEN | yes | Read-only API token with Account Analytics Read, Zone Analytics Read and Access: Apps and Policies Read. Required even without Cloudflare data: Searchkeep uses it to check your Access setup. |
SITES | yes | JSON array describing each site and the identifiers it has in every source. |
GSC_CLIENT_EMAIL | for GSC | The client_email field of a Google Cloud service-account key. e.g. searchkeep@my-project.iam.gserviceaccount.com |
GSC_PRIVATE_KEY | for GSC | The private_key field of the same key, including the BEGIN/END lines. Real line breaks and literal \n both work. |
BING_API_KEY | for Bing | Bing Webmaster Tools API key. It can also submit URLs and remove sites, so treat it like a password. |
BRAND_REGEX | optional | Default brand regex (JavaScript regex source, matched case-insensitively). A site’s brandRegex overrides it. e.g. acme|acme\s?corp |
ENABLE_WRITES | optional | Set to true to register submit_sitemap. Requires Full permission in GSC. e.g. true |
GSC_ROWS_PER_DAY | optional | Maximum query × page rows stored per day from Search Console (100–100,000). Defaults to 2500. Higher values keep more of the long tail but cost more D1 writes and Worker CPU per sync. e.g. 2500 |
GSC_BACKFILL_MONTHS | optional | How many months of Search Console history to backfill (0–16). Defaults to 16. e.g. 16 |
SYNC_DAILY_WRITE_BUDGET | optional | Maximum rows the history backfill may write per UTC day; daily increments always run. Defaults to 80000, below the D1 Free limit of 100,000 rows written per day. e.g. 80000 |
SYNC_MAX_CALLS_PER_RUN | optional | Maximum outbound API calls per sync pass (5–900). Defaults to 20, well within the Workers Free subrequest limit. e.g. 20 |
SITES
SITES is a JSON array with one object per site. It maps the site to its identifier in each data source.
Only domain is required; every source-specific field is optional, and a source is simply skipped for a
site that does not set it.
[
{
"id": "acme",
"domain": "acme.com",
"gscProperty": "sc-domain:acme.com",
"bingSiteUrl": "https://acme.com/",
"cloudflareZoneId": "023e105f4ecef8ad9ca31a8372d0c353",
"webAnalyticsSiteTag": "abc123",
"brandRegex": "acme"
}
]| Field | Required | Description |
|---|---|---|
id | no | Short stable identifier used in tool calls and API URLs: lowercase letters, digits and dashes. Derived from the domain when omitted (www.acme.com becomes acme-com). |
domain | yes | Bare hostname, e.g. acme.com (no scheme, no path). |
gscProperty | no | Search Console property: sc-domain:acme.com for a domain property, or https://acme.com/ for a URL-prefix property. |
bingSiteUrl | no | Site URL exactly as registered in Bing Webmaster Tools, e.g. https://acme.com/. |
cloudflareZoneId | no | Zone ID of the proxied zone. Enables AI crawler data. |
webAnalyticsSiteTag | no | Cloudflare Web Analytics site tag (not the snippet token). Enables AI referral visits and Web Vitals. |
brandRegex | no | Brand regex for this site; overrides BRAND_REGEX. |
Several sites, with different sources each, look like this:
[
{ "id": "acme", "domain": "acme.com", "gscProperty": "sc-domain:acme.com",
"bingSiteUrl": "https://acme.com/", "cloudflareZoneId": "023e105f4ecef8ad9ca31a8372d0c353",
"webAnalyticsSiteTag": "abc123", "brandRegex": "acme" },
{ "id": "acme-docs", "domain": "docs.acme.com", "gscProperty": "https://docs.acme.com/",
"webAnalyticsSiteTag": "def456" }
]Where to find each identifier
- gscProperty: the property selector in Search Console. Domain properties are written
sc-domain:example.com; URL-prefix properties are the full URL with a trailing slash. - bingSiteUrl: the site URL exactly as listed in Bing Webmaster Tools.
- cloudflareZoneId: the zone’s Overview page in the Cloudflare dashboard, under API.
- webAnalyticsSiteTag: Analytics & Logs → Web Analytics, then select the site; the
site tag appears in the address bar as
siteTag~in=<tag>. It is not thetokenvalue in the JavaScript snippet. Web Analytics also needsCLOUDFLARE_ACCOUNT_ID.
Brand and non-brand
Every query-level tool and dashboard view accepts brand: "all" | "brand" | "non_brand". A query counts
as brand when it matches the site’s brandRegex, or BRAND_REGEX if the site has none. Both
are JavaScript regular expression sources, matched case-insensitively anywhere in the query text (anchor with ^ and $ if you need to). Changing the regex re-classifies stored queries on the next sync or
query.
acme|acme\s?corp|acmeapp- Do not wrap the pattern in slashes and do not add flags.
- Include common misspellings and the product names people search for.
- If neither is set, every query counts as non-brand, and results say so in their notes.
Enabling writes
Searchkeep is read-only by default. The only tool that changes anything outside your account, submit_sitemap, is not even registered with MCP clients until you opt in:
- Set
ENABLE_WRITEStotrue. - In Search Console, change the service account’s permission on the property from Restricted to Full. Submitting sitemaps requires it.
The tool is annotated as a write, so well-behaved MCP clients ask before running it even when you have auto-approved the read-only tools. Remove the secret to turn writes off again.
Sync budgets
GSC_ROWS_PER_DAY, GSC_BACKFILL_MONTHS, SYNC_DAILY_WRITE_BUDGET and SYNC_MAX_CALLS_PER_RUN control how much each sync pass does. The defaults keep a typical site within the
free plans: D1 Free allows 100,000 rows written per day, and Workers Free allows 10 ms of CPU per invocation. If the
sync keeps skipping units after interrupted attempts, lower GSC_ROWS_PER_DAY or move to Workers Paid.
Values outside a setting’s range are clamped to it. See How syncing works.
Setting and rotating secrets
Use npx wrangler from any directory (it does not need a clone of the repository), or the dashboard: Workers & Pages → searchkeep → Settings → Variables and Secrets → Add, with type Secret.
# Overwrite a secret with a new value; a new version deploys within seconds.
npx wrangler secret put BING_API_KEY --name searchkeep
# List which secrets exist (names only, never values).
npx wrangler secret list --name searchkeep
# Remove one.
npx wrangler secret delete ENABLE_WRITES --name searchkeepTo rotate a credential, create the new one at the source, overwrite the secret, confirm setup_status is
still green, then revoke the old credential at the source.
Local development
For working on Searchkeep itself, pnpm dev at the repository root builds the dashboard and starts the
Worker locally with wrangler dev in dev mode. Local values go in apps/cloudflare/.dev.vars,
which is git-ignored; start from the example file. The local D1 is migrated automatically on the first request.
cp apps/cloudflare/.dev.vars.example apps/cloudflare/.dev.vars
pnpm dev # builds the dashboard, then wrangler dev on :8787
# In another terminal: fill the local D1 with demo data for the site in SITES.
curl -X POST localhost:8787/api/dev/seed# apps/cloudflare/.dev.vars — git-ignored, never commit it
SITES=[{"id":"demo","domain":"demo.example.com","gscProperty":"sc-domain:demo.example.com","cloudflareZoneId":"demo-zone","webAnalyticsSiteTag":"demo-tag"}]
BRAND_REGEX=acmeDev mode skips Cloudflare Access and treats the deployment as ready, but only for requests to localhost, 127.0.0.1 or [::1]; the seed endpoint works only in dev mode.
Without real credentials the sync has nothing to pull, which is what the demo data is for. The repository’s CONTRIBUTING.md and AGENTS.md describe the full workflow, including tests.