smallbox

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.

CompanyGraph
Websitecompanygraph-website
Admincompanygraph-admin
Smallbox Labs
Websitesmallboxlabs-website
Adminsmallboxlabs-admin
Backendcompanygraph-backend
Backendsmallboxlabs-backend
CompanyGraph only
Stocksstock-service
Observation enginestock-observation-engine
Contentcontent-management-system
Shared by both products
Imagesimage-service
Emailemail-service
Usersuser-service
Infrastructure — callable from anywhere
Logslogger-serviceevery box on this map reports here — no arrows drawn, all of them real
Translationtranslation-service

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 ↓

How complexity becomes manageable

Layered responsibility, not extra ceremony.

Layers are used where they reduce confusion, risk, or maintenance cost — and skipped where they only add ceremony. Click a layer to see what lives there, and what doesn't.

ClientFrontend / Admin / External
ControllerHTTP
FacadeCoordination
Business ServiceRules & meaning
Domainmodel
RepositoryData access
DatabaseStored state

Every problem has coordinates.

The map above shows the boxes from the outside. This shows their inside — and it is nearly the same inside in every one. A bug, a slow page, a misfired rule: each one lives at an intersection of which box, and which layer within it. That intersection is its coordinates.

The columns are the ten boxes from the map that are built this way — every box except the four frontends; a frontend calls in from outside and has no inside of its own. Read down a column and a box stops being a blob: a controller, a facade, a business service, a repository, a database, in that order. Read across a row and the same responsibility sits at the same level in every box — which is why someone who learns one box can find their way around all of them.

The cells are not features — they are addresses: a bug or a rule belongs at one intersection, which box and which layer. Hover to preview, click to pin; the columns are the boxes from the map above, the rows the one shape every box repeats.

Composition seamsCompanyGraph subsystemsShared subsystemsInfrastructure
Inside each box

Layer · communication rule

Facade

What it does

The switchboard. It sequences the work and maps between transport shapes and domain entities — and it is the only layer that calls another box, through a typed SDK client, by id.

The same in every box

When one box needs another, the call leaves from here — never from a controller, never sideways into another box's internals.

The rule it carries

Cross-service calls leave here, through a typed SDK client, by id — nowhere else.

The rules the shape enforces

  • Requests enter at the Controller — the one way in.
  • Cross-box calls leave through the Facade — a typed SDK or public contract.
  • Rules do not live in controllers.
  • Boxes do not reach into each other’s databases.

The same shape with the product stripped out — the reusable boxes, and the full list of connections the system is built to refuse — is the Foundation Map.

Start from this shape.

A new product does not have to re-earn this map. The reusable boxes — identity, email, images, translation, logging — plus the backend skeleton install as Prototype to Product: a fixed-price package that ends with this shape running under your product, one real workflow wired end to end. Not a parts list, and not a proposal to build it someday — the boxes above are the ones that arrive.