Skip to content

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.

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 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 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.

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.

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.