Dan Matthew

CH 06 · COLLECTING · PRIVATE

PhilatelyOS

An event-driven platform to digitize a 100,000-stamp collection with AI vision.

A serious, multi-repo, event-driven platform (contracts-first, ADR-driven, IaC) that uses AI vision — detection, OCR, embeddings, VLMs — to catalogue a 100k+ physical stamp collection. A hobby, engineered like a real system: immutable evidence, append-only assertions, reprocess-forever.

Architecture

I have over 100,000 physical stamps, and I want them catalogued — actually digitized, searchable, identified, with a record per stamp I'd trust. By hand, that's a multi-year job. With AI vision, it's a systems problem, and I treat it as one. PhilatelyOS is an event-driven platform, built across several private repos, that turns scans of album pages into catalogue records. The code isn't public, so this is the architecture-level story: the shape, the decisions, and what moves where.

The defining decision: contracts first

PhilatelyOS is polyglot: the platform services are TypeScript, the vision work is Python because that's where the models live. Two languages in an event-driven system have one classic failure mode: a producer changes a message's shape, a consumer in the other language doesn't notice, and the drift surfaces weeks later as a bad catalogue record.

So the domain lives in one place, as JSON Schema, in a dedicated contracts repo: what a stamp is, what an event is, what a detection is, one definition each. From those schemas I codegen types for both TypeScript and Python. Every service and worker compiles against generated types from the same schema, so a producer and its consumers cannot disagree about a field. Changing a contract is deliberate: edit the schema, regenerate, and the compilers on both sides tell you what broke.

The repos are split by concern: architecture (design and Architecture Decision Records), contracts (schemas), platform (services), workers (pipeline stages), and infra (cloud definition). The ADRs earn their keep when a decision gets revisited a year on — I want the reasoning on record, not reconstructed from memory.

Why events

A scan doesn't call a function. It produces an event, and some worker picks it up, does one job, and emits the next event. That's the whole model, chosen for a plain reason: 100,000 items is a batch job, and batch jobs get reprocessed.

When a detection model improves, I re-run detection across the whole collection without touching OCR. When a stage fails on a handful of ugly scans, those get retried, not the whole page. Each stage is independently retryable, scalable, and replaceable, and the contracts guarantee that whatever replaces a stage still speaks the same events.

The vision pipeline

The workers chain through four vision stages:

  1. Detection finds the individual stamps in a scan or photo of an album page. A page is a layout of many stamps; this is where they become separate objects.
  2. OCR reads the text on each one — denominations, country names, inscriptions.
  3. Embeddings vectorize each stamp, which is what makes similarity search and "have I seen this one before?" possible at this scale.
  4. VLMs — vision-language models — produce a description and a candidate identification.

All of this runs on my own hardware, a local GPU box, not a hosted inference API. At a few hundred images that wouldn't matter. At hundreds of thousands of model calls across several stages, re-run as models improve, the economics flip hard — and I want control: which models, which versions, and no rate limits setting my batch schedule.

AI proposes, I confirm

This is the part I care most about getting right. The pipeline proposes catalogue data. It does not write it as fact.

A photograph carries limited evidence. A model can be genuinely good at reading an inscription and still have no basis for a claim about perforation, watermark, paper, or a variety that differs from the common stamp by a detail the scan can't resolve. Assert those anyway and the catalogue is confidently wrong, which is worse than empty. So every proposal passes a human-confirm checkpoint before it becomes catalogue data, and the record keeps observed, proposed, and confirmed distinct. Calibrated, not credulous.

The cloud side

The event backbone, storage, and platform services run on AWS, defined entirely in OpenTofu in the infra repo. The environment is reproducible from code — the only sane way to run a personal system I expect to still be operating years from now.

What it adds up to

One schema, two languages, no drift. Events instead of calls, so the batch can be reprocessed a stage at a time. Inference where I own the economics. And a human at the end of the chain, because the point was never to let a model catalogue my collection. It was to let a model do the tedious ninety percent so I can spend my attention on the part that actually requires judgement.

Signal flow ⤢ Enlarge
AWS · provisioned with OpenTofu Album page scan / photo emits event Event backbone queue / bus — every hop between stages is an event consumes On-prem GPU box · vision workers (Python) Detection find each stamp OCR read the text Embeddings vectorize VLM identify describe · propose emits emits emits proposes AWS · provisioned with OpenTofu Platform services (TypeScript) ingest · review UI · API Catalogue store digitized collection Human confirm Dan reviews proposals Embedding search similarity lookup reads / writes confirms vectors JSON-Schema contracts single source of truth — stamp · event · detection, defined once codegen TypeScript types Python types generates types generates types generates types event flow generates types AI proposes; the human confirms.