# ARCHITECTURE — end-to-end view

Stitches the desktop app (`TECHNICAL.md`) and the license server
(`LICENSE-SERVER.md`) into a single picture. Read this first for "how
does it all fit together"; drill into the per-component docs for
detail.

---

## 1. System diagram

```
┌────────────────────────────────────────────────────────────────────────┐
│                  OPERATOR / DEVELOPER LAPTOP                           │
│                                                                        │
│   git clone / push          ←─── code lives in git.invixiom.com        │
│   datatools-admin CLI       ─── manual mints, list, revoke ─────┐      │
│   ssh -L 8090:127.0.0.1:8090  ───── tunnel for /internal/* ─────┤      │
└────────────────────────────────────────────────────────────────────────┘
                                                                  │
        ┌─────────────────────────────────────────────────────────┘
        │
        │   internal Bearer-auth API (over SSH tunnel only)
        ▼
┌────────────────────────────────────────────────────────────────────────┐
│  LICENSE SERVER — 46.225.166.142                                       │
│  ─────────────────────────────────────────────────────────────────     │
│                                                                        │
│  ┌──────────────────────────────────────────────────────────────────┐ │
│  │  nginx 1.24  (TLS termination, public reverse proxy)             │ │
│  │                                                                  │ │
│  │  datatools.unalogix.com           → static placeholder           │ │
│  │  licenses.datatools.unalogix.com  → 127.0.0.1:8090 (FastAPI)     │ │
│  │  /internal/* on public surface    → blocked (404)                │ │
│  └────────────────────────────┬─────────────────────────────────────┘ │
│                               │                                        │
│  ┌────────────────────────────▼─────────────────────────────────────┐ │
│  │  FastAPI app — datatools-api (Docker container, UID 10001)       │ │
│  │                                                                  │ │
│  │   ┌──────────────────┐  ┌──────────────────┐  ┌───────────────┐ │ │
│  │   │  /webhooks/*     │  │  /internal/*     │  │  /health      │ │ │
│  │   │  (storefronts)   │  │  (Bearer-auth)   │  │  (public)     │ │ │
│  │   └────────┬─────────┘  └────────┬─────────┘  └───────────────┘ │ │
│  │            │                     │                              │ │
│  │            ▼                     ▼                              │ │
│  │   ┌────────────────────────────────────────┐                    │ │
│  │   │  SourceAdapter (Protocol) — normalized │                    │ │
│  │   │   • ManualAdapter   • GumroadAdapter   │                    │ │
│  │   │   • (LemonSqueezy, Stripe — future)    │                    │ │
│  │   └────────────────┬───────────────────────┘                    │ │
│  │                    │ SaleEvent / RefundEvent                    │ │
│  │                    ▼                                            │ │
│  │   ┌────────────────────────────────────────┐                    │ │
│  │   │  mint_from_sale()                      │                    │ │
│  │   │   • Ed25519 sign via PyCA cryptography │                    │ │
│  │   │   • idempotent on (source, order_id)   │                    │ │
│  │   └────────────────┬───────────────────────┘                    │ │
│  └────────────────────┼─────────────────────────────────────────────┘ │
│                       │ SQL                                            │
│  ┌────────────────────▼─────────────────────────────────────────────┐ │
│  │  Postgres 16 — datatools-postgres (container, vol pg_data)       │ │
│  │   • licenses        — authoritative customer record              │ │
│  │   • gumroad_events  — webhook audit log (idempotency, replay)    │ │
│  └──────────────────────────────────────────────────────────────────┘ │
└───────────────────────┬────────────────────────────────┬───────────────┘
                        │                                │
            ┌───────────┘                                └──────────┐
            │ POST /email (httpx)                       Gumroad Ping│
            ▼                                            POST       │
   ┌───────────────────┐                              ┌─────────────▼──┐
   │  Postmark         │                              │   Gumroad      │
   │  (transactional   │                              │   (storefront, │
   │   email)          │                              │    payments)   │
   └───────┬───────────┘                              └────────────────┘
           │ DKIM-signed email with license blob                 ▲
           ▼                                                     │
┌────────────────────────────────────────────────────────────────┴───────┐
│                       BUYER'S MACHINE                                  │
│                                                                        │
│   Receives email ──► copies DTLIC2: blob ──► pastes into desktop app   │
│                                                                        │
│   ┌──────────────────────────────────────────────────────────────────┐ │
│   │  DataTools desktop (Python 3.12 + Streamlit + Typer CLIs)        │ │
│   │                                                                  │ │
│   │   ┌────────────────────────────────────────────────────────────┐ │ │
│   │   │  Activate screen — verifies blob signature                 │ │ │
│   │   │  against EMBEDDED Ed25519 public key                       │ │ │
│   │   │  (NO network call to the license server, ever)             │ │ │
│   │   └─────────────────────────┬──────────────────────────────────┘ │ │
│   │                             ▼                                   │ │
│   │  ~/.datatools/license.json    (signed blob, mode 644, on disk)  │ │
│   └──────────────────────────────────────────────────────────────────┘ │
│                                                                        │
│   Pays via web browser ─────► Gumroad ────► (kicks off the Ping)       │
└────────────────────────────────────────────────────────────────────────┘
```

**Three primary flows**, distinguishable by where the green arrows
start in the diagram:

1. **Sale → fulfillment** (the automated path)
   Buyer pays at Gumroad → Gumroad fires Ping to
   `licenses.datatools.unalogix.com/webhooks/gumroad?secret=…` → nginx
   → FastAPI → audit-log row → adapter normalizes payload → `mint_from_sale`
   writes the `licenses` row + Ed25519-signs the blob → Postmark emails
   the buyer their blob. End-to-end latency: a few hundred milliseconds.

2. **Manual mint** (operator path — comps, support replacements)
   Operator opens SSH tunnel → `datatools-admin mint` → `/internal/mint`
   (Bearer-authed, never publicly reachable) → same `mint_from_sale`
   path → blob returned in HTTP response. Operator delivers to buyer
   out-of-band.

3. **Activation** (buyer path — fully offline)
   Buyer pastes blob into desktop's Activate screen → desktop verifies
   the Ed25519 signature against the public key **embedded in the
   shipped binary** → license written to `~/.datatools/license.json`.
   The desktop app makes **no network calls** to the license server at
   any point. This preserves the "your data never leaves your computer"
   promise (`DECISIONS.md §9b`).

---

## 2. Tech stack

Layered view of what technology lives where. "External SaaS" entries
are services we depend on but don't operate.

```
┌────────────────────────────────────────────────────────────────────────┐
│                  DESKTOP APP   (shipped binary, runs on buyer's box)   │
├──────────────────┬─────────────────────────────────────────────────────┤
│  GUI             │  Streamlit 1.35 — local web server, browser opens   │
│  CLI             │  Typer 0.12 — per-tool entry points                 │
│  Core logic      │  pandas 2.x, numpy, rapidfuzz, charset-normalizer   │
│  Crypto (verify) │  PyCA cryptography — Ed25519 public-key verify only │
│  Storage         │  ~/.datatools/license.json (file, mode 644)         │
│  Internationalization │ i18n via JSON catalogs in src/i18n/            │
│  Build           │  PyInstaller — one-file binary, per OS              │
│  Runtimes        │  Python 3.12 (bundled into installer)               │
│  Platforms       │  Windows · macOS · Linux                            │
└──────────────────┴─────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│                  LICENSE SERVER   (this box; non-buyer-facing)         │
├──────────────────┬─────────────────────────────────────────────────────┤
│  Edge            │  nginx 1.24 + Let's Encrypt (auto-renew via timer)  │
│  HTTP framework  │  FastAPI 0.119 + Starlette + Pydantic v2            │
│  ASGI server     │  uvicorn 0.39 (+uvloop, +httptools, +watchfiles)    │
│  Form parsing    │  python-multipart (for Gumroad form-encoded Pings)  │
│  ORM             │  SQLAlchemy 2.0                                     │
│  Migrations      │  Alembic 1.18 (one initial migration so far)        │
│  Database        │  Postgres 16-alpine (containerized, single node)    │
│  Database driver │  psycopg 3.3 (with binary wheel)                    │
│  Crypto (sign)   │  PyCA cryptography — Ed25519 private-key sign       │
│  HTTP client     │  httpx 0.28 (Postmark calls, test mocking)          │
│  Config          │  Pydantic Settings + YAML (products.yaml)           │
│  Container       │  Docker + Docker Compose v2 plugin                  │
│  Image base      │  python:3.12-slim                                   │
│  Process user    │  UID 10001 (non-root `app` user defined in image)   │
│  Logging         │  stdlib `logging` to container stdout → docker logs │
│  Host OS         │  Ubuntu 24.04 LTS                                   │
└──────────────────┴─────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│                  OPERATOR / DEVELOPER MACHINE                          │
├──────────────────┬─────────────────────────────────────────────────────┤
│  Source control  │  git → self-hosted Gitea (git.invixiom.com)         │
│  Admin CLI       │  Typer (src/admin_cli.py)                           │
│  Server access   │  SSH tunnel for /internal/* (no public exposure)    │
│  Break-glass     │  scripts/generate_license.py (offline-only mints,   │
│                  │  used when the license server is unreachable)       │
│  Test runner     │  pytest 8.3 + SQLite in-memory (no docker required) │
│  Smoke test      │  bash + docker compose (server/scripts/smoke.sh)    │
└──────────────────┴─────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│                  EXTERNAL SaaS / dependencies                          │
├──────────────────┬─────────────────────────────────────────────────────┤
│  Storefront      │  Gumroad — Ping webhook to /webhooks/gumroad        │
│  Transactional   │  Postmark — HTTP API for license-delivery emails    │
│   email          │   (LoggingEmailService fallback when token unset)   │
│  TLS CA          │  Let's Encrypt — ACME HTTP-01 challenge via certbot │
│  Authoritative   │  supercp / cPanel (your DNS host for unalogix.com)  │
│   DNS            │   — Cloudflare front-door deferred                  │
│  Source hosting  │  Self-hosted Gitea (git.invixiom.com) — not on the  │
│                  │   datatools box; shares the same physical host      │
└──────────────────┴─────────────────────────────────────────────────────┘
```

---

## 3. Trust + isolation boundaries

Worth tracing explicitly because the threat model differs at each
boundary:

| Boundary | What crosses it | Trust model |
|---|---|---|
| Buyer ↔ Gumroad | Payment, buyer details | Out of scope — Gumroad's problem |
| Gumroad → license server (webhook) | Signed-by-shared-secret POST | URL secret check; non-matching = 404 (no info leak); audit-log everything regardless |
| License server → Postmark | DKIM-signed transactional mail | Postmark verified-sender domain; HTTP API auth via server token |
| License server → Postgres | SQL over local docker bridge | Same compose project; password from on-disk secret file |
| Operator → license server (`/internal/*`) | Bearer token over SSH tunnel | Token only on disk + in the operator's env; nginx blocks `/internal/*` publicly as defense-in-depth |
| License server → buyer (email) | Plaintext blob in inbox | Buyer's email account hygiene; we deliberately don't encrypt — blob is self-protecting (signature) |
| Buyer → desktop app (activation) | Signed blob pasted in | Verified against pubkey **embedded in the shipped binary**; no network call |

The single most important property to preserve: **the desktop app
never talks to the license server.** All trust in the desktop comes
from the embedded public key + the signed blob. This is what makes
the offline activation guarantee real, and what keeps a license-server
outage from breaking buyers who've already activated.

---

## 4. Where things are stored

| Lives on… | Path / location | Contents |
|---|---|---|
| Buyer's machine | `~/.datatools/license.json` | Activated license blob |
| Buyer's machine | Postmark email | Delivery copy of the blob |
| License server | `licenses` table (Postgres) | Authoritative customer record — name, email, tier, blob, source, order ID, promotion, amount paid |
| License server | `gumroad_events` table | Append-only webhook delivery audit log |
| License server | `/srv/datatools-license/secrets/` | Postgres password, admin Bearer token, (PR 2) Postmark token + Gumroad secret |
| License server | `/etc/letsencrypt/live/datatools.unalogix.com/` | TLS cert + key |
| Operator's laptop | `~/.datatools-creator/issued.jsonl` | Creator-side issuance log (pre-server era, kept as a break-glass backup) |
| Operator's laptop | Git clone of this repo | Source code, including `server/config/products.yaml` |
| Gitea | This repo's commits | Everything except secrets |

---

## 5. Related docs

| Doc | Scope |
|---|---|
| `TECHNICAL.md` | Desktop app internals (core libs, GUI, CLIs) |
| `LICENSE-SERVER.md` | Server architecture rationale + DB schema |
| `SETUP-LICENSE-SERVER.md` | Server install runbook (DNS, packages, nginx, TLS, Postgres) |
| `ADMIN.md` | Day-2 operations (minting, rotation, inspection) |
| `DECISIONS.md` | Architecture decision records — `§9b` = no online activation check |
| `USER-GUIDE.md` | Buyer-facing documentation |
