How `core` fits together
The whole system in one page. Anything service-specific lives in that service's own docs folder — see the index.
The shape of it
┌─────────────────────────────────────────┐
customers ──────► │ api (Hono, TypeScript, :2626) │
x-api-key │ /v1 /v2 metered data APIs │
│ /auth /hub Toolzer Hub dashboard │
browsers ───────► │ /app Vantage dashboard │
session cookie │ /admin staff, cross-tenant │
│ /internal unauthenticated tooling │
└───┬──────────────┬──────────────┬───────┘
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼──────────┐
│ worker │ │ Postgres │ │ ClickHouse │
│ FastAPI │ │ core_data │ │ core_analytics │
│ Python │ │ identity │ │ request + event│
│ + Redis │ │ billing │ │ analytics │
└─────┬─────┘ └───────────┘ └────────────────┘
│
the public webWeb surfaces are separate Cloudflare Workers and talk to the API over HTTPS like any other client. They are not part of the same deployment.
The two halves of the request path
The API owns the contract. It authenticates, meters, validates, calls out, normalizes the answer, and shapes the response. It never returns an upstream body or an upstream status.
The worker owns the fetch. It is the only thing that touches the public
web: proxy rotation, browser-grade TLS via curl_cffi, session minting and
reuse, and the per-site quirks that make a request succeed. It speaks a raw
internal contract, not the public one, and is not exposed to the internet.
The split exists because those two jobs fail differently. A site changing its anti-bot posture is a worker problem and changes weekly. A response shape a customer depends on is an API problem and must not change at all.
Not every route goes through the worker. Some sites are served from paid
third-party providers, and /v2 is entirely external-sourced. The customer
cannot tell, which is the point.
Where state lives
| Store | Database | Holds |
|---|---|---|
| Postgres | core_data, schema platform | users, workspaces, sessions, API keys, subscriptions, credit balances and ledger |
| Postgres | core_data, schema rankings | Vantage's rank-tracking data |
| ClickHouse | core_analytics | one row per API request, plus the Apify event pipeline |
| Redis | — | worker fetch sessions, ~20 minute TTL |
Postgres is the system of record. ClickHouse is analytical only — losing it loses reporting, not accounts or money. Redis is a cache; it starts cold without consequence.
The API applies its own Postgres migrations at boot under an advisory lock, so a deploy migrates and a bad migration fails the boot rather than serving on a half-built schema.
Identity and money
One identity layer serves both products. A workspace is the tenant; a
subscription is (workspace, capability), where capability is api
(Toolzer Hub), rankings (Vantage), or influencers. One credit wallet per
workspace is shared across every capability it holds.
/v1 and /v2 authenticate with a per-workspace API key and spend credits.
/hub and /app authenticate with a session and spend nothing. That is the
whole distinction between the machine surface and the human one.
Details, including the credit reservation lifecycle and what is and is not
charged, are in apps/api/docs/auth-and-credits.md.
Shared code
packages/ is the TypeScript code the API and any future Node service share:
errors (the error class hierarchy the response contract is built on),
fetch (the HTTP client, including the worker client), and logger. It is
published to npm as @thenetaji/packages; see
operations.md.
The API↔worker route contract is checked in CI (pnpm contracts:check), so a
route renamed on one side fails the build rather than 404ing in production.
What ships where
Five deployables. The API and worker are Docker images on a Swarm; the three web apps are Cloudflare Workers. deployment.md has the table and the mechanics.