---
title: "Moving leads.jsonl Off One PC Into a Durable Object"
canonical: https://dxdev.com/blog/lead-ledger-off-the-desktop/
datePublished: 2026-10-10
---
The lead ledger was one file on one PC, and a missing file read as zero leads. The file was `.runs/blog-leads/leads.jsonl`, and if it wasn't there, `read_leads` returned `[]`. The module's docstring said its sources were verified on 2026-09-30. Ten days later a dependency audit flagged the ledger as row D6, and we moved it off that PC.

## What the ledger holds

Each row is `{slug, kind, at}`, with no personal data in it. The site stamps `post_slug` on every intake form. The form provider delivers submissions to email and exposes no API, so rows are recorded with `blog-funnel --record-lead SLUG --kind review|discovery|contact`. The metrics report reads the file to count leads per post.

The old `read_leads` opened with `if not ledger.exists(): return []`. The existing test for missing sources asserts the text "no leads recorded" for that case. A missing ledger and a ledger with no leads came back as the same `[]`.

## Moving it to a Worker

The ledger now lives in a Cloudflare Worker, `dxdev-leads`, backed by a SQLite Durable Object on our own Cloudflare account. The commit touched 3 files, with 106 insertions and 6 deletions, 49 of those insertions in tests.

In `funnel.py`, the ledger constant changed:

```python
LEADS_LEDGER = lead_metrics.RemoteLedger()
```

`RemoteLedger` in `lead_metrics.py` is a thin client over `urllib.request`. `read_rows()` is a GET and `append(row)` is a POST. Each request carries an `Authorization: Bearer` header, and `urlopen` is called with `timeout=20`. The token comes from `secrets.get("DXDEV_LEADS_TOKEN")` unless one is passed in. A runbook for the Worker is referenced in the module docstring.

`read_leads` and `record_lead` now take either kind of ledger. Given a `RemoteLedger`, they go to the Worker. Given a plain `Path`, they behave as before, which keeps the local jsonl ledger usable for tests and offline runs. `read_leads` filters the remote rows to ones that have a slug and a kind in `LEAD_KINDS`. `record_lead` returns only `slug`, `kind` and `at` from what the Worker saved.

## Failures raise

The class docstring states the rule: "Failures raise: a lead read that cannot reach the ledger must never look like 'no leads' (the callers already surface exceptions as an error, not a zero)."

I kept the local path as it was, so the `[]` on a missing file still exists there. The remote path has no such branch. The docstring also says the PC is only a client of the ledger now.

A missing `leads.jsonl` still returns `[]`, and a `RemoteLedger` call that fails raises instead.
