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:
.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 processesClients 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/degradedon/health→ Postgres unreachable; the API keeps serving cached aggregates until they expire.429for the web app itself → the Next.js server is one IP; give it aninternalkey (API_KEYenv inapps/web) or terminate limits upstream./snapshots/{id}returnstext: null→ the object key is missing fromCA_DATA_DIR/objects(different data dir or pruned by retention); metadata still renders and/snapshots/{a}/diff/{b}falls back to the storedchanges.diffwhen present./askengine is alwaysdeterministic→CA_LLM_BASE_URL/CA_LLM_API_KEYunset or the small model unreachable (the deterministic parser is the designed fallback; nothing is fabricated either way).