feat(api): move change tracking bookkeeping to Bigtable behind a backend flag by mogery · Pull Request #4475 · firecrawl/firecrawl · GitHub
Skip to content

feat(api): move change tracking bookkeeping to Bigtable behind a backend flag - #4475

Draft
mogery wants to merge 4 commits into
mainfrom
change-tracking-bigtable
Draft

feat(api): move change tracking bookkeeping to Bigtable behind a backend flag#4475
mogery wants to merge 4 commits into
mainfrom
change-tracking-bigtable

Conversation

@mogery

@mogery mogery commented Sep 1, 2026

Copy link
Copy Markdown
Member

Summary

Rebuilds the change tracking pointer bookkeeping (change_tracking_insert_scrape / diff_get_last_scrape_v7) on GCP Bigtable instead of Postgres. The PG table is pure "latest value per key" bookkeeping — content lives in GCS — so one row per scrape is overkill; the insert path is ~30% of database CPU time. The PG path is removed: Bigtable is the store, no flag.

Design:

  • Row key: team_id ‖ sha256(url) ‖ sha256(tag), raw bytes, full 32-byte digests
    • team_id first → team-scoped prefix operations (bulk delete on cleanup) stay a single range
    • url before tag → the dominant write mix (every plain-markdown scrape) carries tag=null, so the high-entropy component occupies the first hashed position
    • injective null-tag encoding (0x00 vs 0x01‖tag) preserves PG's null-vs-empty-tag distinction
  • Single cell m:job = job_id, cell timestamp = date_added (µs), column family GC max_versions=1
  • Semantics: max-date-wins independent of arrival order — an out-of-order (regressed) write is accepted, invisible to latest-version reads, and physically collected at compaction
  • Unconfigured BIGTABLE_* vars make the store throw, landing in the existing non-fatal paths: the insert warns and continues, deriveDiff sets the "Comparing failed" warning — the same degraded posture self-host had before (change tracking remains unavailable in the self-hosting path)

Out-of-band table creation

The app never provisions infrastructure. Before deploying:

gcloud bigtable instances create firecrawl-prod \
  --cluster=firecrawl-prod-c1 --cluster-location=us-east1 \
  --nodes=3 --storage=ssd

gcloud bigtable instances tables create change_tracking \
  --instance=firecrawl-prod \
  --column-families="m:maxversions=1"

(Equivalently: cbt -instance=firecrawl-prod createtable change_tracking families=m:maxversions=1families is a positional argument and the GC knob is maxversions, no underscore.)

Env: BIGTABLE_PROJECT_ID, BIGTABLE_INSTANCE_ID, BIGTABLE_APP_PROFILE_ID (optional), BIGTABLE_CHANGE_TRACKING_TABLE (optional, default change_tracking).

Data migration from the PG table is a one-off ops task run alongside the deploy (distinct on (team_id, url, change_tracking_tag) ... order by date_added desc → bulk mutate with cell timestamp = date_added; idempotent, latest-wins makes re-runs safe). After cutover, drop change_tracking_scrapes and its RPCs server-side — that's the 30% CPU recovery.

Tests

  • Store snips (TEST_PRODUCTION-gated like the other production snips): write/read round-trip incl. date_added fidelity, newest-wins, regressed-write-shadowed, null/empty/named tag partitioning, team isolation, missing row → null, key layout invariants
  • E2E snip: first scrape of a fresh url+tag reports new/previousScrapeAt: null; second scrape reports previousScrapeAt + changed|same — verified locally through the harness (server write → read → GCS fetch all exercised, 8/8)

Dependency notes

  • GHSA-8988-4f7v-96qf (@opentelemetry/core < 2.8.0) ships through the bigtable client's OTel 1.x metrics chain; the advisory is only patched on 2.x. The overrides move the tree's OTel trio + Google's monitoring exporter to 2.x, and the client constructs with metricsEnabled: false since its metrics handler is written against OTel 1.x APIs we don't consume. Audit is clean of any new findings (the remaining ip-address/decode-uri-component advisories pre-date this PR and ip-address is allowlisted in audit-ci.jsonc).
  • Cell timestamps come back as strings (Long-as-string), so conversions go through Number(), never typeof === "number".

Comment thread apps/api/src/__tests__/snips/v2/change-tracking.test.ts Outdated
Comment thread .github/workflows/test-server.yml Outdated
Comment thread apps/api/src/__tests__/snips/bigtable-test-env.ts Outdated
Comment thread apps/api/src/lib/change-tracking-store.ts Outdated
Comment thread apps/api/src/scraper/scrapeURL/transformers/diff.ts Outdated
Comment thread apps/api/src/services/logging/log_job.ts
Comment thread apps/api/src/services/logging/log_job.ts
Comment thread apps/api/src/lib/change-tracking-store.ts
Comment thread docker-compose.yaml
Comment thread SELF_HOST.md Outdated
Comment thread apps/api/package.json
…end flag

Replaces the append-only change_tracking_scrapes insert (30% of DB CPU)
with a latest-wins Bigtable store keyed by team_id || sha256(url) ||
sha256(tag), one row per unique (team, url, tag). Cell timestamp =
date_added with max_versions=1 GC, so reads return the newest scrape and
out-of-order writes are shadowed. Content still lives in GCS.

Rollout via CHANGE_TRACKING_BACKEND=postgres (default, unchanged) |
dual (write both, read PG) | bigtable (write+read Bigtable). Includes a
batched, resumable PG->Bigtable backfill script and emulator-backed
store snips run in CI.
Change tracking bookkeeping now writes to and reads from Bigtable
unconditionally -- no rollout flag, no dual-write, no Postgres fallback.
Unconfigured BIGTABLE_* vars make the store throw, which lands in the
existing non-fatal warn/warning paths (log_job insert warn, deriveDiff
"Comparing failed" warning -- the same degraded posture as an
unconfigured Postgres before). Removes the now-unused
change_tracking_insert_scrape / diff_get_last_scrape_v7 RPC wrappers
and fixes the emulator image reference (the dedicated emulator image
does not exist; use cloud-sdk:slim + the small emulator package).
- Drop the Bigtable suffix from the store functions; it's the change
  tracking store, backend is an implementation detail
- Remove table/family auto-provisioning from production code; the
  table is created out-of-band (instructions in the PR description)
- Gate the store snips with TEST_PRODUCTION like the other production
  snips; drop the custom emulator detection and the env-defaulting
  helper file
- Remove the Bigtable emulator from the self-host CI job, docker
  compose, and SELF_HOST.md -- change tracking stays unavailable in
  the self-hosting path
- Restore the original insert log/warn strings
- Drop the pointless `undefined | null` union; the read returns
  `ChangeTrackingLastScrape | null` and downstream uses it directly
- Remove the backfill script
- Fix GHSA-8988-4f7v-96qf (@opentelemetry/core <2.8.0, pulled via
  the bigtable client's OTel 1.x metrics chain): override the OTel
  trio + monitoring exporter to the 2.x line and disable the client's
  metrics handler, which is written against OTel 1.x APIs we don't use
@mogery
mogery force-pushed the change-tracking-bigtable branch from 05ab23d to 9d11aa3 Compare September 1, 2026 13:30
The rebase resolved the pnpm-lock.yaml conflict against main's side and
the regeneration run that followed never landed (amend executed from the
wrong worktree), leaving the lockfile's overrides section stale relative
to pnpm-workspace.yaml -- frozen installs failed with
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant