Back to Projects

GovChime Analytics Platform

Federal Contract Data Platform — ETL, CDC & CQRS

Overview

Role: Main developer in a team of four senior engineers — designed and built the core platform end to end: the SmartSync ETL and its move to Temporal, the CDC projection into the ClickHouse read model and the PostgreSQL mirror, the Express + Sequelize API, the Next.js frontend on Cloudflare Workers, the Python AI content service, and CI/CD.

AI content service. A Python AI content service generates blog posts, social posts and SEO metadata from contract data and scores RSS articles weekly. Every draft goes through a human review queue, and a guard strips any AI-stated figure not present in the source data. It started on the Anthropic API and moved to Cloudflare Workers AI (Llama 4 Scout) in September 2026.

Data it stands on. The main work at GovChime is the data platform underneath: a Temporal-orchestrated SAM.gov ETL whose completeness is checked against SAM.gov's own files (0 of 80,922 active notices missing), and a ClickHouse read model kept in checksum parity with PostgreSQL.

Dev workflow. An agentic Claude Code workflow shared with the team through the repo: 13 MCP servers, 23 agents, 26 skills, 43 path-scoped rules, and hooks that enforce a read-only prod database and block force-push.

Engineering: Data Platform, AI & Frontend

Main developer on GovChime: the data platform, the AI features, and the frontend performance work, one view at a time.

A Temporal-orchestrated ETL from SAM.gov into PostgreSQL, projected by CDC into a ClickHouse read model and mirrored to a second PostgreSQL — every hop is checked against a second copy of the data.

SAM.gov APIs + files · USAspending.gov
0

Orchestrate — Temporal

Temporal is the control plane: Schedules start Workflows, rows never pass through it. Production cut over on 2026-09-25 and the legacy scheduler was deleted.

32 Temporal Schedules6 task queues2 production workers (pipelines · mirror)replay tests of production histories in CI
1

Extract

Every SAM.gov call runs on one task queue with a queue-wide rate limit and leased API keys.

13 SAM.gov API keysKeyProvider + usage ledger~8,050 calls/day poolraw JSON into staging tables
2

Transform + Load

One canonical transform per source, then change-aware idempotent upserts into PostgreSQL — a failed window just reruns.

staging → transform → loadchange-aware upsert (IS DISTINCT FROM)natural-key idempotencyopportunities · entities · contract awards
3

Verify

A green run proves execution. Completeness is proven against a second copy of the source.

verify gates on every rundaily key reconciliationcensuses against SAM.gov files
verify gatesevery rundistinct rows = the API's declared total
reconciliationdailystaged keys of each closed day vs the table
censusdaily · weeklySAM.gov's own notice files, no API key
a gap starts a repair run of the pipeline for the missing days
Completeness over error-catching

A green run proves execution, not completeness — every dataset is checked against a second copy of the source.

0 of 80,922 active notices and 0 of 733,627 FY2025–26 archive notices missing against SAM.gov's own files; PostgreSQL and ClickHouse checksums match on 94.4M award transactions.

4

Enrich

Follow-up pipelines fill what the source APIs leave out, started by each source run and backed by their own Schedules.

opportunity descriptionsattachmentsSBA certificationsentity stub fill
Write model · PostgreSQL 16

~7.06M contracts · ~1.48M entities · every pipeline writes here

Read model · ClickHouse

~94.4M award transactions · fed by CDC · analytics reads here

5

CQRS read model — ClickHouse

PostgreSQL stays the write model; one Temporal coordinator per table projects new rows into ClickHouse, and a daily reconciliation proves parity.

query-based CDC (watermark)checksum parity: 94.4M award transactions7.06M contracts · 1.48M entities14 ClickHouse MVs, health-checked hourly
6

PostgreSQL mirror

A logical-replication mirror of production into a second PostgreSQL, run as Temporal Workflows on its own worker.

snapshot + streaming + guardsMIRROR_HEALTH (hourly)MIRROR_RECONCILE (daily count + hash)pgoutput · 4 mirrored tables
7

Serve

Express + Sequelize API reading the ClickHouse read model, with a ClickHouse → Postgres fallback chain that was later simplified.

headline query 13.6 s → ~1 sClickHouse 2.2 s → 0.27 ssingle-flight guard in the API behind the HTTP cache32 Postgres MVs dropped · ClickHouse MVs 31 → 1478 filters · ~242 endpoints
8

Present

Next.js 16 frontend on Cloudflare Workers via OpenNext, with cache freshness matched to the sync cadence.

R2 page cache + D1 tag cacheon-demand revalidationReact Query / ISR / API TTLs aligned at 10 minedge-cached sitemaps (Cache API)
A few hundred daily active users
Cross-cutting concerns

9. Guards & alerts

cross-cutting · spans every layer

Scheduled guards watch the pipelines themselves; alerts are stored and shown in the admin UI.

Temporal health (every 10 min)freshness · tombstones · SLO burn rateAPI key-pool healthalert log in the SLED admin

10. Delivery

cross-cutting · spans every layer

Tests gate every deploy; each push to main deploys SmartSync, then the app.

unit + integration tests gate the deploycontainer health asserted after deployGitHub Actions (self-hosted runner)

11. Infrastructure

cross-cutting · spans every layer

Self-hosted compute on OVHcloud, fronted by Cloudflare.

OVHcloud (Docker, Komodo)self-hosted Temporal Serviceself-managed PostgreSQL 16Cloudflare Workers

Key Features

  • Python AI content service generating blog posts, social posts and SEO metadata from contract data
  • Human review queue on every draft before anything is published
  • Figure guard: strips any AI-stated number not present in the source data
  • Weekly scoring of RSS articles
  • Anthropic API at launch; Cloudflare Workers AI (Llama 4 Scout) since September 2026
  • Agentic Claude Code workflow shared with the team: 13 MCP servers, 23 agents, 26 skills, 43 path-scoped rules, read-only prod DB hooks

Tech Stack

AI & Dev Tools

PythonCloudflare Workers AIAnthropic APIClaude CodeMCPPlaywright

Data Platform / ETL

TemporalSAM.gov APIUSAspending.govCDC (watermark)CQRSLogical ReplicationIdempotent UpsertsChecksum ParityCount Audits

Database & OLAP

PostgreSQL 16ClickHouseMaterialized ViewsOLAP

Backend

Express 4SequelizeNode.jsTypeScriptZodREST APInode-cacheiron-session + JWTStripePostHog

Infrastructure

DockerKomodoOVHcloudGitHub ActionsSelf-Hosted Runner

Frontend & Edge

Next.js 16React 19Cloudflare WorkersOpenNextR2D1TanStack Query 5TanStack Table 8Radix UIRechartsTailwind CSS

Challenges & Solutions

AI Content Without Invented Figures

Problem

AI-generated blog, social and SEO copy about federal contracts must never state numbers the data doesn't support.

Solution

A Python AI content service generates the copy from contract data, a guard strips any AI-stated figure not present in the source data, and every piece goes through a human review queue. It moved from the Anthropic API to Cloudflare Workers AI (Llama 4 Scout) in September 2026.

An ETL That Must Prove It Is Complete

Problem

The SAM.gov pipelines ran on a hand-rolled scheduler, and a green run only proved that code executed. The opportunities sync once reported success while holding only 17.5% of notices — API-key quota starvation, plus a completeness check that passed when nothing was proven.

Solution

Rebuilt SmartSync on Temporal: 32 schedules on 6 task queues, per-window extract → transform → load → verify, a 13-key SAM.gov pool with a usage ledger, and replay tests of production histories in CI. Completeness is checked against SAM.gov's own files — 0 of 80,922 active notices and 0 of 733,627 FY2025–26 archive notices missing. Production cut over on 2026-09-25 and the legacy scheduler was deleted.

Silent Data Loss in Ingestion

Problem

For 10 days the SAM.gov sync reported success while silently dropping ~95–99% of records: SAM.gov changed `totalRecords` from a number to a string, the pipeline read it as null, `null ?? 0` coerced it to 0, and the completeness check `0 >= floor(0 × 0.95)` always passed — no exception thrown, no alert.

Solution

A `parseTotalRecords()` helper that accepts both string and number shapes and treats any NaN / 0 / missing value as an API error (triggering key rotation) rather than coercing to 0. The deeper fix was completeness verification comparing expected vs actual counts — `no exception thrown` does not mean the data is correct.

Keeping ClickHouse and a Mirror in Exact Sync

Problem

Analytics read from ClickHouse, and a second PostgreSQL needs a live copy of production. Both drift silently if nothing proves they match.

Solution

Rewrote the teammate's first PostgreSQL → ClickHouse copy scripts with checkpoints and a parity check and moved them onto Temporal. PostgreSQL is the CQRS write model; a coordinator per table projects new rows into ClickHouse by watermark (query-based CDC), and a daily reconciliation compares counts, distinct ids and checksums — contracts, entities and 94.4M award transactions match. The PostgreSQL mirror uses logical replication (pgoutput; snapshot, then streaming) as Temporal Workflows, with hourly slot-lag checks and a daily count-and-hash reconciliation.

Slow Queries and Stampedes on ~94M Rows

Problem

A headline Postgres query took 13.6 s because it applied a slug function to every row of an 89M-row table, which forces a full scan. Separately, concurrent requests for the same heavy ClickHouse count could pile up into several multi-GiB scans at once.

Solution

Filtered on a trigger-maintained indexed slug column with a 2-year window instead (13.6 s → ~1 s) and on an agency-code lookup in ClickHouse (2.2 s → 0.27 s). Added a single-flight guard to the API's in-memory response cache, so concurrent callers share one query and a failed refresh serves the last good value. Built the ClickHouse → Postgres fallback chain on the team's ClickHouse read model, then simplified it — 32 Postgres MVs dropped, ClickHouse MVs 31 → 14.

Next.js Caching on Cloudflare Workers

Problem

The Next.js 16 frontend runs on Cloudflare Workers via OpenNext with an R2 page cache. With a long-lived regional cache and no tag cache, a published page kept serving the stale copy and on-demand revalidation did nothing; in the worst case feed pages lagged the database by ~3–5 hours.

Solution

Switched the regional cache to short-lived mode and added a D1 tag cache, so revalidatePath and revalidateTag take effect after a publish; aligned the React Query, ISR and API cache TTLs at 10 minutes (worst-case feed lag ~10 min by design); and edge-cached the sitemaps with the Cloudflare Cache API.

Key Achievements

AI Content Service
Blog, social and SEO copy from contract data, human-reviewed
Figure Guard
Strips any AI-stated number not in the source data
Workers AI
Moved from the Anthropic API to Cloudflare Workers AI (Llama 4 Scout)
0 of 80,922
Active notices missing — the data the AI writes from
GovChime Analytics Platform - Project | Oleksandr Yusypenko