SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
5.2 KB

# Company Atlas — operations

# API

The API is one FastAPI process (catlas api, uvicorn) bound to loopback; Next.js proxies /api/v1/* to it. It is stateless: every cache is in-process (api/common.TTLCache, cleared with POST /api/v1/admin/cache/clear or on restart), so several workers may run behind PM2 and the rate limiter buckets are per process (effective limits scale with the number of workers — keep --workers 1 unless traffic requires more, or terminate rate limiting at the tunnel).

# Environment variables (read by config.Settings, prefix CA_)

Variable Default Used by the API for
DATABASE_URL postgresql+asyncpg://companyatlas:companyatlas@127.0.0.1:5432/companyatlas every query (pool 8 + 8 overflow per process)
CA_API_HOST / CA_API_PORT 127.0.0.1 / 8371 (prod 8361) bind address of catlas api
CA_ADMIN_TOKEN (empty → every /admin/* call is 401) X-CA-Admin-Token (also bypasses rate limits)
CA_SITE_URL https://www.company-atlas.co CORS allow-list (plus the localhost dev/prod ports)
CA_DATA_DIR ./data object store read by /snapshots/{id} (text/blocks) and /stats.archive
CA_SEO_MIN_EVENTS / CA_SEO_MIN_SENSORS 1 / 3 /sitemap inclusion rule (companies.indexed or ≥ events and ≥ active sensors)
CA_MIN_INTERVAL_S / CA_MAX_INTERVAL_S 900 / 604800 clamp for POST /admin/sensors/{id}/set_interval
CA_LLM_DAILY_BUDGET, CA_LLM_BASE_URL, CA_LLM_ENABLED 1500, (empty), true /admin/overview.llm.budget_left, /ask LLM refinement availability
CA_NOISE_THRESHOLD … CA_CRITICAL_THRESHOLD 0.20 / 0.40 / 0.65 / 0.85 /methodology.significance_bands, /admin/costs meaningful-event denominator
CA_LOG_JSON true JSON logs (service=ca-api)

Run: .venv/bin/catlas api (dev, port 8371) · .venv/bin/catlas api --port 8361 (prod under PM2 company-atlas-api). Health: GET /health → {status, db, version, api_version, uptime_s}; GET /ready → {ready} (DB reachable). Docs: /api/v1/docs, schema /api/v1/openapi.json. The scheduler's heartbeat (settings_kv['scheduler:heartbeat']) surfaces as /system.scheduler_last_tick_at.

# API keys and rate-limit tiers

Anonymous traffic is limited to 120 requests/min per client IP (first hop of X-Forwarded-For — Caddy on the MacLustr Tunnel sets it). Keys raise the tier (authenticated 600/min, paid 3 000/min, internal unlimited). Keys are stored as sha256 hashes in api_keys; the raw key is printed once:

bash
.venv/bin/catlas api-key create "acme research" --tier paid     # prints ca_paid_… once; store it in the customer's vault
.venv/bin/catlas api-key list [--include-revoked]                # id, prefix, tier, last used, request count
.venv/bin/catlas api-key revoke key_01…                          # soft revoke (row kept, `revoked_at` set); cached for ≤ 60 s in running API processes

Clients send X-CA-API-Key: <key>; responses carry X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Tier; a 429 carries Retry-After. Usage counters (last_used_at, request_count) are flushed every ~30 s per process. There is no self-service key issuance — create keys from the M2U64 shell (or any host with DATABASE_URL).

# Caches and freshness

Endpoint TTL Notes
/pulse, /stats 60 s homepage / counters; archive stats refresh every 10 min (settings_kv['archive:stats'] if the ops job writes it, else a directory walk in a thread)
/system 30 s public health
/rankings 120 s per (kind, window, country, industry, limit)
/industries*, /countries*, /index, /map, /trends, /events/types, /events/summary, /stats/history 300 s aggregate producers in api/aggregates.py
/sitemap 600 s 5 000 entries per page
/live*, owner, admin none (no-store)

After bulk backfills or manual corrections: curl -X POST -H "X-CA-Admin-Token: $CA_ADMIN_TOKEN" localhost:8371/api/v1/admin/cache/clear (optionally ?prefix=pulse). Retract/restore of events clears the caches automatically.

# Pending SQL

migrations/pending/api.sql adds two trigram GIN indexes (people.name, products.name) used by /search?types=people,products. Apply with psql -f (uses CREATE INDEX CONCURRENTLY, so outside a transaction) or fold into the next numbered Alembic migration. The endpoints work without them (sequential scans on small tables).

# Troubleshooting

  • 503/degraded on /health → Postgres unreachable; the API keeps serving cached aggregates until they expire.
  • 429 for the web app itself → the Next.js server is one IP; give it an internal key (API_KEY env in apps/web) or terminate limits upstream.
  • /snapshots/{id} returns text: null → the object key is missing from CA_DATA_DIR/objects (different data dir or pruned by retention); metadata still renders and /snapshots/{a}/diff/{b} falls back to the stored changes.diff when present.
  • /ask engine is always deterministic → CA_LLM_BASE_URL/CA_LLM_API_KEY unset or the small model unreachable (the deterministic parser is the designed fallback; nothing is fabricated either way).