The fleet
A fleet of small boxes.
Two products — CompanyGraph and the studio site you are reading — run as fourteen small boxes, each owning one thing, each line between them a typed seam. The payoff: a change stays inside one box, a bug has one place to live, and the next product starts from boxes that already run. This is not an architecture slide; it is the live map of what is deployed right now. Click around.
Hover previews, click pins — the panel under the map explains every box and every line between them.
Composition seam
Backend
companygraph-backend
.NET · PostgreSQL
What it does
Assembles CompanyGraph out of the boxes below it: company identity from one, observations from another, content, images, email — composed into one response per page.
What it owns
Only what makes CompanyGraph a product: portfolios, watchlists, subscriptions, the links between companies and observations, page composition and SEO state.
Why a separate box
Composition needs exactly one home. Because it lives here, every box below stays product-blind and reusable.
Every box, and what it owns.
Ownership is the organizing idea: each piece of data lives in exactly one box, and every other box that needs it holds an id and asks. The map again, in words — the same content the panel above shows.
The surfaces
Website (companygraph-website) — Serves the public CompanyGraph product — company pages, observations, interpretations, screeners — server-rendered in every supported language. Routes, SEO tags and translated URL segments. No business data lives here. A surface that owns no data can be redesigned or replaced without the product losing anything.
Admin (companygraph-admin) — The operator console for CompanyGraph: content editing, observation audits, translation runs, logs, system health. Nothing — every screen works through the same backend API the product runs on. Operators get a full cockpit with no private side-door into any database.
Website (smallboxlabs-website) — Serves the Smallbox studio site — including the page you are reading right now. Pages and articles as files in its repo. Form submissions leave immediately for the backend. Same rule as every surface: if this box vanished, no data would vanish with it.
Admin (smallboxlabs-admin) — The studio's operator console: orientation-report submissions, users, emails, images. Nothing — it is a viewer over the studio backend's API. The second product's console repeated the first one's pattern, which is why it was cheap.
The composition seams
Backend (companygraph-backend) — Assembles CompanyGraph out of the boxes below it: company identity from one, observations from another, content, images, email — composed into one response per page. Only what makes CompanyGraph a product: portfolios, watchlists, subscriptions, the links between companies and observations, page composition and SEO state. Composition needs exactly one home. Because it lives here, every box below stays product-blind and reusable.
Backend (smallboxlabs-backend) — Runs the studio's one live workflow — orientation-report requests in, processed reports out — and serves the admin console. Report submissions and per-image ids. Identity, email and image bytes it asks the shared boxes for. The proof of the model: the second product needed one new box, not a second platform.
The subsystems
Stocks (stock-service) — Knows what a company is: identity, classification, financial statements, the industry structural framework. The truth about every stock. Other boxes that mention a company hold an id that points here. Company truth changes for data-vendor and modelling reasons — never for product reasons. Different clock, separate box.
Observation engine (stock-observation-engine) — Defines and computes structural observations — formula-driven measurements of company behaviour — and the interpretations that emerge when several align. It describes what is; it never predicts. Observation and interpretation definitions, authored as files in its repo, and their computed narratives per language. Measurement is a discipline of its own. This box has no idea users, pages or products exist.
Content (content-management-system) — Serves editorial content: articles, glossary entries, legal pages, frontpage copy. Written content and its translations. English originals are files in the repo; the database is a runtime cache. Prose changes on an editorial clock, not a release clock.
Images (image-service) — Stores and serves images: upload, metadata, categories, a picker for admin surfaces. The bytes themselves — irreplaceable uploads, backed up daily. Binary storage has its own backup, size and caching pressures. Mixing it into a product database makes both worse.
Email (email-service) — Templates, queues, sends and records every email either product sends. Templates per product and language, the outbound queue, and the full send history. Email is operationally noisy — retries, SMTP, audits. Solved once, shared by every product.
Users (user-service) — Identity for every product: credentials, roles, tokens, magic links — each product registered as its own application. The only copy of emails, usernames and password hashes anywhere in the fleet. Products hold an anonymous user id. Identity is the most sensitive thing in the system. One box owns it; everything else points at it.
The infrastructure
Logs (logger-service) — Receives exceptions, log entries and startup heartbeats from every other box on this map — both products, frontends included. The operational record: what failed, what started, where and when. When something breaks across boxes there must be one place to look. This is it.
Translation (translation-service) — Translates missing text on demand when an operator asks — never on a schedule — escalating to stronger models only for strings that fail validation. No product data. It is a capability: text in, translated text out, every attempt logged. Which languages exist is a product decision; how text gets translated is infrastructure. That sentence boundary is this box's boundary.
Every line, and what crosses it.
Every line is the same mechanism: server-side HTTP behind a typed client owned by the box being called, authenticated with a bearer token, referencing data by id only. No shared database, no foreign key crosses a line, and product subsystems never call each other — composition happens in the backends, and only infrastructure is callable from anywhere.
companygraph-website companygraph-backend
Whole pages' worth of composed data: a company page, a screener result, an article — one request, one assembled response. The website never talks to a subsystem directly.
companygraph-admin companygraph-backend
Every operator action — edits, audits, translation runs — through the same API the product uses. The admin's TypeScript types are generated from this backend's OpenAPI contract, so drift fails the build.
smallboxlabs-website smallboxlabs-backend
Orientation-report submissions and their uploads — the studio's one live workflow.
smallboxlabs-admin smallboxlabs-backend
Admin reads and actions over studio state: submissions, users, emails — behind an admin sign-in checked against the shared identity box.
companygraph-backend stock-service
Company identity, classification and financials, fetched by id. The backend keeps a thin link row per company; the truth stays on the far side of this line.
companygraph-backend stock-observation-engine
Observation definitions and computed results. The backend links them to companies and pages; the engine never learns what a product is.
companygraph-backend content-management-system
Articles, glossary entries, legal pages and frontpage copy, composed into the product.
companygraph-backend image-service
Image references resolved by id for page composition, and admin uploads passing through.
companygraph-backend email-service
Send requests and template administration. Verification links and notifications queue and get recorded on the far side.
companygraph-backend user-service
Credential checks, tokens, identity hydration. CompanyGraph stores an anonymous user id; this line turns it back into a person when needed.
smallboxlabs-backend email-service
The studio's outbound mail — the same email box CompanyGraph uses, under its own tenant key. Neither product can see the other's mail.
smallboxlabs-backend user-service
Sign-in and roles for the studio, registered as its own application in the same identity box.
smallboxlabs-backend image-service
Report images — submitter screenshots and operator-added figures. The bytes live in the shared Images box under the studio's own app pool; the studio backend keeps an id per image and re-serves the bytes through its own access-gated endpoints, because report uploads are sensitive material.
companygraph-backend translation-service
Operator-triggered translation fan-out: translate what is missing in each subsystem, never overwrite what an editor already shaped.
email-service translation-service
When an operator asks for a template in a new language, the email box calls the translator for its own missing rows — the one drawn line between two non-backend boxes. It is allowed because the translator is infrastructure: it owns no product data and knows no product nouns.
And the line the map refuses to draw fourteen times: every box reports exceptions, log entries and a startup heartbeat to logger-service — both products, frontends included — so “why did this break” is one query against one box.
The restraint that keeps it legible.
What the map refuses to do is most of why it stays readable. The standing rules:
- A frontend talks only to its product’s backend — one door per product.
- Every piece of data has exactly one owner; everyone else keeps an id.
- Product subsystems never call each other. Composition happens in the backends, nowhere else.
- Infrastructure — logging, translation — is the one tier callable from anywhere, because it owns no product data.
- Inside every box, the same layered shape — controller, facade, business services, repositories — so moving between boxes costs nothing. The same shape across every box, below ↓