Architecture
LinkBrain is one Cloudflare Worker (src/index.ts) on a Telegram webhook. There
is no long-running process: the Worker wakes per request, does its work, and
sleeps.
Request flow
Section titled “Request flow”Telegram ──webhook──▶ Cloudflare Worker (src/index.ts) │ ┌─────────────┼─────────────────┐ ▼ ▼ ▼ GitHub API HTMLRewriter Workers AI ──▶ D1 (SQLite + FTS5) (repos) (OG/title scrape) (summary + tags)The Worker validates the request, enriches the link, summarizes it with a Cloudflare-hosted model, stores it in D1, and replies over the Telegram Bot API.
The spine
Section titled “The spine”The file is divided into banner-comment sections, and the section order is the dependency direction — a lower section never calls a higher one.
| Section | Job |
|---------|-----|
| ENV / TYPES | The typed Env (bindings linkbrain (D1), AI, plus secrets) and minimal Telegram/row interfaces. |
| CONFIG | The model constant and secrets read from env. |
| DATABASE | All D1 access (saveLink, searchLinks, getRecent, countLinks). The only section that runs SQL. |
| ENRICHMENT | The outside world: GitHub API fetch, OG/HTML scraping via HTMLRewriter, and env.AI.run summaries. Catches its own failures and degrades — never throws into a handler. |
| HELPERS | Pure transforms: URL/note extraction, reply formatting, Markdown escaping, tag normalization. |
| HANDLERS | Orchestration: parse → enrich → store → reply. No SQL, no direct network. |
| DASHBOARD | The Access-gated JSON API the web UI calls (/dashboard/api/links, /dashboard/api/meta, /dashboard/api/mutate, /dashboard/api/action) plus the public /list/api/links. |
| ENTRY — fetch() | The webhook. Validates the secret header, acks immediately, processes in ctx.waitUntil(...), and routes commands vs. plain messages. Also routes the dashboard/list paths and serves the SPA + docs assets. |
The web UI is a separate SPA
Section titled “The web UI is a separate SPA”The private /dashboard and the public /list are a React + shadcn/ui single-page app
(Vite + Tailwind v4) in its own dashboard-app/ npm project — separate from the docs site in
docs-site/. It builds to /app/ in the Worker’s assets and is served by the same Worker. The
SPA never touches D1 directly: it calls the Worker’s JSON API (GET /dashboard/api/links, GET /dashboard/api/meta, POST /dashboard/api/mutate, POST /dashboard/api/action, all behind
Cloudflare Access; public GET /list/api/links), which composes the same data-access and
enrichment functions the Telegram handlers use. npm run deploy builds the docs then the
dashboard-app before wrangler deploy.
Why a webhook (not polling)
Section titled “Why a webhook (not polling)”The original LinkBrain was a local Python bot that long-polled Telegram, so it
only worked while a process was running on a machine that was awake and online.
On Cloudflare there’s no process to babysit — Telegram delivers each update to
the Worker URL, which is always reachable. The Worker acks the webhook with
200 immediately and does the enrichment + save in the background
(ctx.waitUntil) so Telegram never retries or duplicates an update.
Enrichment degrades, never crashes
Section titled “Enrichment degrades, never crashes”Every enrichment function returns a safe fallback on failure: a failed GitHub
fetch returns null, a failed scrape returns the URL’s hostname, and a failed
or non-JSON AI response yields an empty summary. A link always saves, even if
the network or the model is having a bad day. The model used for summaries is
@cf/google/gemma-4-26b-a4b-it (Workers AI, JSON mode) — the same Gemma family
the original local build ran via Ollama.
Categorization and auto-collections on save
Section titled “Categorization and auto-collections on save”The AI category is reuse-or-coin: the model is shown the categories already
in your library and reuses one when it fits, only coining a new short lowercase
category when nothing does — so the taxonomy grows richer without fragmenting.
/recategorize re-derives every
link’s category from its stored text (no re-fetching) to backfill this across the
whole library.
Saving also auto-files the link into a collection by semantic
nearest-neighbour similarity — the same Vectorize vectors and threshold
/organize uses — but only when
the link isn’t already in a collection, so it never overrides one you set by hand.