One Go binary, three subcommands, one Postgres database, one GraphQL endpoint.
┌─────────────────────────┐
│ Vendor pricing APIs │
│ AWS • Azure • GCP │
└────────────┬────────────┘
│ (bulk JSON / OData / REST)
▼
┌──────────────────────────────┐
│ c3x-pricing-api scrape │ one-shot CLI
│ errgroup per vendor │ Postgres advisory lock
└────────────┬─────────────────┘
│ UPSERT + DeleteStaleProducts
▼
┌──────────────────┐
│ PostgreSQL │ products (JSONB + GIN)
│ │ scrape_runs, schema_version
└────────┬─────────┘
│
▼
┌──────────────────────────────┐
│ c3x-pricing-api serve │ GraphQL @ :4000
│ rate limit → auth → CORS │
│ AST-validated queries │
└──────────────────────────────┘
│
▼
GraphQL clients
(C3X CLI, UIs, scripts)
cmd/server/main.go # Cobra CLI: serve / scrape / seed
internal/
config/ config.go # env-driven Config, Validate() (incl. prod sslmode guard)
db/ db.go # pgxpool, versioned migrations, advisory lock, scrape_runs
queries.go # QueryProducts, UpsertProducts, DeleteStaleProducts, regex cache
seed.go # streaming JSON seeder
migrations*.sql
graphql/ schema.go # single `products(filter, limit, offset)` query
scraper/ aws.go # bulk JSON, fan-out via errgroup
azure.go # retail prices OData, per-service errgroup
gcp.go # Cloud Billing Catalog, per-service errgroup
scraper.go # common interface + ProductHash/PriceHash helpers
hash.go / aws_regions.go
server/ server.go # HTTP server, middleware chain, handlers
server_test.go
deploy/ # compose / k8s / github-actions recipes (not wired into code)
Middleware chain applied top-down in internal/server/server.go:
- requestID: honors
X-Request-IDif it matches^[A-Za-z0-9_-]{1,64}$, else generates viacrypto/rand. - recoverMiddleware: catches panics, returns typed 500 JSON, uses a
responseTrackerso it doesn't try to write headers twice. - securityHeaders: X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy.
- CORS: allow-list from
CORS_ALLOWED_ORIGINS, always emitsVary: Origin. - auth: SHA-256 +
subtle.ConstantTimeCompareonAPI_KEY. Skipped whenAPI_KEYis empty (dev mode). - rateLimit: per-IP token bucket (
golang.org/x/time/rate) in an LRU bounded at 10k.X-Forwarded-Foris honored only when the peer is inTRUSTED_PROXIES(CIDR list). - handleGraphQL: parses single or batched POST, enforces body/size/depth/introspection caps, executes each query with a shared timeout, fills remaining batch slots with a typed timeout error on ctx cancellation.
GraphQL validation uses the graphql-go AST:
- Depth is computed by walking
SelectionSetthrough inline + named fragments with a visited-set cycle guard. - Introspection block matches
__schema/__type/__Typeat the field level, not by substring.
The single root field is products(filter, limit, offset). The resolver lives
in internal/graphql/schema.go and calls into
internal/db/queries.go:
p.Context(the per-requestcontext.WithTimeout) flows directly toQueryProducts.QueryProductsbuilds a dynamicWHEREover the JSONB attributes with$N-bound args only. No string interpolation, no SQL injection surface.- Regex filters compile into a 1024-entry LRU cache, bounded at 200 chars.
- Every pooled connection has
SET statement_timeout = '300000'set viapgxpool.AfterConnect, so even a rogue regex cannot pin Postgres.
Each vendor implements Scraper:
type Scraper interface {
Name() string
Scrape(ctx context.Context) ([]db.Product, error)
}runOneScrape in cmd/server/main.go:
- Acquires
pg_try_advisory_lock(hashtext('scrape:<vendor>'))on a dedicated pool connection. If another run holds it, this process skips with a warning. - Inserts a
scrape_runsrow withstatus='running'and the DB'snow()timestamp. - Calls
Scraper.Scrape(ctx). Each scraper fans out across services witherrgroup.WithContext+SetLimit(cfg.ScrapeConcurrency). Per-service errors are logged but do not abort siblings; ctx cancellation (SIGTERM) does abort. UpsertProductswrites in 1000-row batches (8-column batch size stays well under Postgres's 65,535 parameter cap).DeleteStaleProductsremoves rows whoseupdated_at < scrapeStart(the DB's ownnow(), not wall-clock).- Updates
scrape_runswithstatus,products,deleted, optionalerror.
The scrape_runs table is the source of truth for "is our data stale?":
SELECT vendor, MAX(finished_at) AS last_success
FROM scrape_runs
WHERE status = 'success'
GROUP BY vendor;Expose it via /readyz or a future scrapeStatus GraphQL field. This is the
single most useful signal for operators of a pricing service.
- No embedded scheduler.
scrapeis one-shot. Scheduling is delegated to cron / K8s CronJob / GitHub Actions (deploy/). This keeps the binary simple, leader election someone else's problem, and lets users pick cadence per vendor. - No OpenTelemetry / Prometheus / gzip in v1.0. Tracked as deferred observability items; see AUDIT.md.
- No distributed mode. If a single Postgres can't hold the data, you have earned the right to a v2 conversation.