Dashboard
The dashboard is a private web page that lists every link with its metadata and lets you
act on it by clicking — no need to know a link’s #id. It’s the browser counterpart to the
Telegram commands.
It’s a React + shadcn/ui single-page app (Vite + Tailwind v4) that lives in its own
dashboard-app/ npm project, builds to /app/ in the Worker’s assets, and is served by the
same Worker. The page calls a small JSON API the Worker exposes — GET /dashboard/api/links,
GET /dashboard/api/meta, POST /dashboard/api/mutate, POST /dashboard/api/action — all
behind the same Cloudflare Access gate. The public List is the same SPA
in a read-only mode, served from a public GET /list/api/links.
It lives on a custom domain (links.<yourdomain>/dashboard) because Cloudflare Access can
only path-protect a real zone, not a *.workers.dev route. The Telegram webhook and the public
List stay on the workers.dev URL, untouched.
What you can do
Section titled “What you can do”Each link is a card with inline controls: toggle ⭐ favorite, toggle 🌐 share (adds it to your public List), edit its category and collection in place, ✏️ edit its title and summary by hand (handy when a site blocks automatic enrichment — the edit also resyncs full-text search and the semantic index), refresh it (re-fetch and re-enrich), and 🗑 delete it behind a confirm dialog (the only action that removes data — it also cleans the search index).
Filter the whole library by category or collection, plus a free-text
filter, to narrow down what’s shown. And run library-wide actions from buttons —
Recategorize, Organize, and Find duplicates — the same jobs as the
/recategorize,
/organize, and /dupes commands.
The public List
Section titled “The public List”The same app powers /list: it shows only the links you’ve marked 🌐 shareable, grouped by
collection, with category badges. No Access gate — it’s your hand-picked public page.
Security — two layers
Section titled “Security — two layers”- Cloudflare Access at the edge: only your email can load the page (you log in via Access).
- Worker JWT check: the Worker independently verifies the Access token, so even the
*.workers.dev/dashboardback-door (which Access doesn’t front) is denied.
It fails closed: until the two secrets below are set, /dashboard returns 403 for
everyone.
One-time setup (Cloudflare Access)
Section titled “One-time setup (Cloudflare Access)”-
In Cloudflare Zero Trust → Access → Applications, click Add an application → Self-hosted.
-
Set the application domain to your dashboard host (e.g.
links.yourdomain.com) with path/dashboard. Make sure an identity method is enabled — the built-in One-time PIN (email code) is enough. -
Add a policy: Action: Allow, Include → Emails → your email address. Save.
-
Open the application’s settings and copy two values: the Application Audience (AUD) tag and your team domain (
https://<team>.cloudflareaccess.com). -
Set them as Worker secrets, then they apply live (no redeploy):
Terminal window npx wrangler secret put ACCESS_AUD # the AUD tagnpx wrangler secret put ACCESS_TEAM_DOMAIN # https://<team>.cloudflareaccess.com
Visit https://links.yourdomain.com/dashboard → Access prompts you to log in → the dashboard
loads. Confirm https://<your-worker>.workers.dev/dashboard returns 403 (no Access token),
and that /webhook and /list still work.
Local development
Section titled “Local development”There’s no Access in local dev, so .dev.vars sets DEV_NO_AUTH=1 to bypass the check and let
you open localhost:8787/dashboard. Never set DEV_NO_AUTH in production — it would make
the dashboard public.
The SPA ships as built assets, so it has to be built before the Worker serves it. npm run deploy builds the docs site then the dashboard-app (the build:ui script) before running
wrangler deploy, so a normal deploy keeps /app/ current — no manual step.