{
  "markdown": "# HTMLRadar\n\n### The open-source DocSend alternative for HTML files.\n\nRead tracking for the decks, reports and proposals you now send as HTML.\n\n[![License: AGPL v3](https://img.shields.io/badge/license-AGPLv3-7A1F2E)](./LICENSE)\n[![GitHub stars](https://img.shields.io/github/stars/htmlradar/htmlradar?style=flat&color=7A1F2E)](https://github.com/htmlradar/htmlradar/stargazers)\n[![Self-hostable](https://img.shields.io/badge/self--hostable-yes-7A1F2E)](#quick-start--self-host)\n[![PRs welcome](https://img.shields.io/badge/PRs-welcome-7A1F2E)](https://github.com/htmlradar/htmlradar/issues)\n\n**HTMLRadar is an open-source tool for sharing an HTML deck, brief, or proposal\nas a tracked link, and seeing who opened it, which sections they read, and for\nhow long.** Not just that it was opened — dwell time, section by section. You\nknow who a reader is when the share's email gate collected an address;\notherwise the reader is an anonymous row with the same reading detail.\n\n[htmlradar.com](https://htmlradar.com) · free for 2 tracked links, $15/mo or\n$150/yr for unlimited · or self-host the whole thing.\n\n[Try the public demo, no sign-up](https://htmlradar.com/r/lumenforge-demo) — a live tracked link you can open in a browser.\n\nUsing Claude? Add HTMLRadar as a connector by pasting one address —\n`https://mcp.htmlradar.com/mcp`. Nothing to install and no API key to make first;\n[how it works](#use-it-from-your-agent).\n\n[Issues and PRs](https://github.com/htmlradar/htmlradar/issues) · [roadmap](https://github.com/htmlradar/htmlradar/issues?q=is%3Aissue+label%3Aroadmap) · [changelog](./CHANGELOG.md) documents v1.2; latest published tag is v1.1.2.\n\n![HTMLRadar dashboard walkthrough using synthetic sample data](./docs/assets/htmlradar-dashboard-demo.gif)\n\n<sub>Walkthrough uses synthetic sample data in the real sender dashboard.</sub>\n\n---\n\n## Why this exists\n\nThe documents that matter — decks, client reports, proposals, board updates —\nare becoming HTML, because an HTML page can be interactive, reflows to whatever\nscreen opens it, and can be changed after it has been sent. A PDF you have sent\nis fixed. More of them are written with AI tools now, and ChatGPT, Claude, v0,\nLovable and Anthropic Artifacts all produce HTML. The format is what makes these\ndocuments better; who typed them is beside the point.\n\nThe tracking tooling never followed. When I went looking, I could not find an\nopen-source tool that reported reading at section level for an HTML document.\nThe document-tracking products I did find grew up around uploading a file and\ntracking that file.\n\nHTMLRadar tracks the document people actually send now, and reports reading at\nsection level rather than a single \"opened\" flag.\n\n---\n\n## What this is\n\nSend-side analytics for HTML documents. Upload an HTML file (or paste a URL you already host), send a tracked link `htmlradar.page/r/{slug}`, see who opened it, which sections they dwelled on, and when they bounced. Section-level dwell, not \"opened.\"\n\n## What it does\n\n- **Section-level dwell.** At least half a section must stay visible for one continuous second before its dwell starts qualifying; the read signal fires after three qualified seconds. The tracker auto-detects sections from your HTML: explicit anchored headings → bare `h1/h2/h3` (slugged from text) → slide/page containers (`section`, `.slide`, `.page`) → paragraph buckets on plain prose. Dashboard tells you a recipient spent 2m 41s on §03 The Ask, 12s on Problem, and skipped Market sizing.\n- **Per-viewer dashboard, aggregated across every share.** One row per person who actually opened the doc, with email + country + device + referrer + total time + scroll depth + visits + first/last seen. Updates live every 30 seconds while the tab is in focus.\n- **Per-recipient share links.** One document, many shares. Each share carries its own email gate, password, expiry, revocation, and email-domain or per-email allow-list.\n- **Files alongside the deck.** Attach PDFs, financial models, images, and ZIPs to any share. Recipients see a small corner pill that opens a side drawer; files are always available when present (the per-share \"Lock the deck\" toggle controls deck save/print only, never attachments). Every download is logged per session and per filename, and tied to the viewer record when one exists.\n- **Version history.** Replace the HTML after partner feedback. Every existing share keeps the same link and serves the new version on next open. The `v{n}` chip on the doc page is a popover with every upload's original local filename, byte size, and timestamp.\n- **Retroactive share access.** Change a share's password, expiry, or allow-list without revoking. The proxy re-checks the allow-list on every request — removing an email kicks them out immediately on their next click, not their next browser session.\n- **Edit + preview without leaving the dashboard.** Preview the doc as the recipient sees it before sending (short-lived HMAC token, no gate). Both \"Preview document\" and \"Preview as you\" open in a new tab so your dashboard stays where you left it.\n- **Branded first-open email.** When a recipient creates their first real session, HTMLRadar requests an HTML notification — viewer email + doc title + a single \"See the read →\" CTA back to the dashboard. Tease, not report.\n- **Engaged-time, not tab-open time.** Both per-section dwell and per-session active time apply a 5-second idle watchdog (keydown / scroll / touchstart, mousemove deliberately excluded). That is the same five-second inactivity window engaged-time tools such as Chartbeat and Parse.ly use; those tools also read further interaction signals that HTMLRadar deliberately does not collect. A tab parked while the reader walked away stops counting after 5 seconds.\n- **Bot / accidental-tap filter.** After the document loads, HTMLRadar waits through a 5-second warm-up before creating the session. If the recipient backgrounds the tab or bounces during that wait, there is no session, notification request, or inflated viewer count.\n- **Data collected.** A recipient record holds an email address when a share's gate collects one, otherwise a random identifier (kept in the browser's localStorage where the page can reach it, freshly generated on each load where it cannot — which is the case on the sandboxed proxy-served links); first-seen and last-seen times; visit count; the browser identification string; referrer; country and city; device type; operating system; browser. A session record holds the document version seen, start and last-heartbeat times, active seconds, and maximum scroll depth. A section record holds dwell per section. These records are kept until the owner deletes the document — there is no automatic purge. Not stored in recipient records: raw IP address, cursor positions, keystrokes, page snapshots, session replay. An ungated document shows no tracking notice; the email-gate page shows one sentence (\"Reading activity on this document is shared with the sender\") and a link to the privacy page. Opt-out is a developer-console call, `window.HTMLRadar.optOut()`, followed by a confirmation page — documented, but not something an ordinary recipient will discover. Audit for comparison: [what each of seven tools loads in a recipient's browser, HTMLRadar included](https://htmlradar.com/blog/what-deck-sharing-tools-record).\n\n## What it deliberately is not\n\nA sender-side analytics tool for one document at a time. **Not** a CMS, deck builder, static-site host, PDF viewer, or website analytics platform. You bring the HTML.\n\n---\n\n## Architecture\n\nSix packages, two storage backends. Three of the six are Cloudflare Workers, one is the Next.js app\non Cloudflare Pages, and two are libraries that ship as bundles.\n\n```\nhtmlradar/\n├── packages/\n│   ├── tracker/      # 8.5 KB gzipped browser IIFE — embedded in the recipient's view\n│   ├── proxy/        # Cloudflare Worker at htmlradar.page/r/{slug} — gates + HTML fetch + tracker inject + attachment serving\n│   ├── app/          # Next.js 14 on Cloudflare Pages — sender's dashboard\n│   ├── monitor/      # Cloudflare cron Worker — checks Supabase every 5 min, pages the founder on regressions, and answers the Telegram webhook\n│   ├── connector/    # Cloudflare Worker at mcp.htmlradar.com — the remote MCP server, so Claude can be connected by pasting one address\n│   └── mcp/          # stdio MCP server on npm — lets an agent publish HTML and read back who opened it\n├── schema/           # Ordered idempotent SQL migrations — tables, RLS, SECURITY DEFINER RPCs, triggers\n├── examples/         # Demo HTML for trying it locally\n└── docs/             # Architecture, privacy, quickstart, self-hosting\n```\n\nDocument HTML + attachment bytes live in Cloudflare R2. Everything else (sessions, sections, viewers, shares, attachments metadata, version history) lives in Supabase Postgres.\n\nRecipient links live on a second domain, `htmlradar.page`, while the dashboard and the marketing site stay on `htmlradar.com`. A recipient document is HTML somebody else wrote, and serving it on the application's own domain would put a stranger's markup on the same origin as a signed-in session, and would let anyone who uploaded a convincing fake sign-in page have it served under our certificate and our reputation. A separate registrable domain removes both problems at once: the document's origin carries no application cookies, and if the content domain ever ends up on a phishing blocklist, the application domain does not. Links sent before the split still work — the worker answers `htmlradar.com/r/…` with a permanent redirect. Self-hosters choose their own two hosts, or run both roles on one; see [`docs/self-hosting.md`](./docs/self-hosting.md).\n\nThe architecture decisions — why a Cloudflare Worker proxy, why hand-rolled PostgREST instead of `@supabase/supabase-js`, why per-session bearer tokens instead of HMAC, the engagement-time methodology, the retroactive allow-list — are in [`docs/architecture.md`](./docs/architecture.md).\n\n## Stack\n\n- **Frontend**: Next.js 14 (App Router, Server Components), Tailwind CSS, Newsreader + Geist (self-hosted via `next/font`)\n- **Backend**: Supabase Postgres — RLS + SECURITY DEFINER RPCs + `pg_net` triggers for email\n- **Proxy**: Cloudflare Worker, HTMLRewriter for tracker injection\n- **Remote MCP connector**: Cloudflare Worker on `mcp.htmlradar.com`, OAuth on top of ordinary API keys, one KV namespace for grants and tokens\n- **Storage**: Cloudflare R2 for uploaded HTML\n- **Auth**: Supabase Auth (Google OAuth + magic-link)\n- **Email**: Resend, invoked from Postgres via `pg_net`\n- **Payments**: Polar.sh checkout link (Stripe Connect Express under the hood for Indian indie founders)\n\nCore hosting runs on Cloudflare and Supabase. Resend is optional for notification email, and Polar handles billing for the hosted Pro plan.\n\n---\n\n## Quick start — hosted\n\n1. Sign in at [htmlradar.com](https://htmlradar.com) with Google or magic link.\n2. Upload an HTML file or paste a URL.\n3. Create a per-recipient share. Email gate / password / expiry / allow-list optional per share.\n4. Send the tracked link.\n5. Watch the dashboard. HTMLRadar requests a first-read email when the recipient creates their first real session.\n\nFree tier: 2 tracked links lifetime across unlimited documents, 20 attachments per doc up to 25 MB each and 100 MB total per doc. Pro tier ($15/month, or $150/year — two months free): unlimited tracked links, your own link names (`htmlradar.page/r/acme-proposal` rather than a generated one), no \"Powered by HTMLRadar\" footer on the recipient view, priority support. Coming soon on Pro: custom domain on share URLs, dynamic per-viewer watermark, repeat-open alerts. What's next is on the [public roadmap](https://github.com/htmlradar/htmlradar/issues?q=is%3Aissue+label%3Aroadmap).\n\n## Quick start — self-host\n\nYou'll need:\n\n- A Cloudflare account (Workers + R2 + Pages)\n- A Supabase project (free tier is enough)\n- A domain on Cloudflare DNS\n- Node ≥20, PNPM ≥10\n- A Resend account for outbound email (optional — without it, the first-read trigger writes a `skipped` row to `notifications_log` and the rest of the product still works)\n\nThen:\n\n```bash\ngit clone https://github.com/htmlradar/htmlradar\ncd htmlradar\npnpm install\ncp .env.example .env.local           # then fill it in, see the note below\npnpm typecheck && pnpm test          # sanity check\npnpm build                           # builds app, tracker and mcp — the three packages that have a build script\n```\n\n**`pnpm build` needs two variables filled in before it will finish.** Copying `.env.example` is not\nenough on its own: several marketing pages are pre-rendered at build time and they create a Supabase\nclient while doing it, so `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` have to hold\nreal values in `.env.local` first. Leave them blank and the build stops on `/`, `/pricing`,\n`/privacy`, `/terms` and `/why` with `Your project's URL and Key are required to create a Supabase\nclient!`, which does not say which file it wanted. Everything else in `.env.example` can wait until\nyou deploy.\n\nSchema setup: apply every numbered SQL file directly under `schema/`, in order, via the Supabase SQL\neditor — and nothing in `schema/tests/`. There is no last file to stop at; the folder grows, so apply\nwhatever is in it, from `001` upwards, and add each new one as you pull it. The files in\n`schema/tests/` are destructive test programs for a scratch database (they create auth users and\nsample rows) and must never run against a real install. Each migration is idempotent\n(`CREATE TABLE IF NOT EXISTS`, `CREATE OR REPLACE FUNCTION`, `DO $$ ... IF NOT EXISTS ... $$`), so\nre-running any of them is safe, and so is re-running the whole chain.\n\nTwo Postgres extensions have to be available, and both are on every Supabase tier: `pgcrypto`, for\n`gen_random_uuid` and the hashing the schema does, and `pg_net`, for the asynchronous HTTP call the\nnotification triggers make. `001_init.sql` creates both itself. A third, `pg_cron`, is optional:\nmigrations `044` and `045` use it to schedule the notification reconciler and the expired-handle\nsweep, and where it is missing they log a notice, skip the scheduling and carry on, leaving both\nfunctions callable by hand or by any scheduler you already run.\n\nThe five most recent migrations, as of this commit:\n\n- `043_trust_layer_foundation.sql` — per-customer handles, the permanent registry of claimed names behind them, the per-share hostname the proxy routes on, and the private `share_lookup` view the proxy reads instead of three separate tables.\n- `044_notification_reconciler.sql` — `reconcile_notification_sends()`, which finally moves a notification row off `queued` by joining it against `pg_net`'s own response table; scheduled every ten minutes where `pg_cron` exists.\n- `045_connect_handles.sql` — the short-lived, single-use handoff from the signed-in consent page to the remote MCP connector. The table stores only a hash of the handle. Run it after `040`.\n- `046_connector_grants.sql` — what the application knows about each remote-connector connection and what became of it, so a revocation whose OAuth clean-up failed is a row somebody can find.\n- `047_radar_drafts.sql` — the drafted-reply queue and the reservation ledger that makes \"one comment per thread, five a day\" an enforced fact rather than an intention.\n\nOne migration wants editing before you run it: `032_comped_accounts.sql` carries a placeholder list\nof internal addresses that are never billed. Put your own addresses in it, or none.\n\nResend secrets go in Supabase Vault (works on free tier — no `ALTER DATABASE SET` required):\n\n```sql\nselect vault.create_secret('re_your_resend_api_key', 'resend_api_key');\nselect vault.create_secret('hello@yourdomain.com',  'resend_from');\n```\n\nFull guide with deployment commands in [`docs/self-hosting.md`](./docs/self-hosting.md).\n\n---\n\n## Use it from your agent\n\nHTMLRadar ships an MCP server, so the agent that wrote the HTML can publish it as a tracked link —\nand ask, the next day, whether anyone read it.\n\n**Claude Desktop and claude.ai — one address, no install**\n\nSettings → Connectors → Add custom connector, and paste:\n\n```\nhttps://mcp.htmlradar.com/mcp\n```\n\nNothing to install and no API key to make first. The first time Claude reaches for a tool it shows a\nConnect card; you sign in to HTMLRadar, choose read-only or read-and-publish, and the key is minted\nfor that connection. Revoke it any time under **Connected apps** in\n[Settings](https://htmlradar.com/settings) — access ends on the next tool call.\n\n**Every other client — run the package**\n\nCreate an API key at [htmlradar.com/settings](https://htmlradar.com/settings) under **API keys** —\nthe same key also calls the [HTTP API](https://htmlradar.com/docs/api) directly, if you would\nrather script it than run an agent — then export it, so the key never becomes a command-line\nargument that lands in your shell history:\n\n```bash\nexport HTMLRADAR_API_KEY=hr_live_xxx\n```\n\n**Claude Code**\n\n```\nclaude mcp add htmlradar -e HTMLRADAR_API_KEY=$HTMLRADAR_API_KEY -- npx -y htmlradar-mcp\n```\n\nOr install the plugin, which wires up the same server and adds a skill that knows when to offer a\ntracked link and when to stay quiet:\n\n```\n/plugin marketplace add htmlradar/htmlradar\n/plugin install htmlradar@htmlradar\n```\n\n**Cursor** — put this in `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` to make it\nglobal. Cursor expands `${env:NAME}` inside `env`, which keeps the key out of a file you might\ncommit:\n\n```json\n{\n  \"mcpServers\": {\n    \"htmlradar\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"htmlradar-mcp\"],\n      \"env\": { \"HTMLRADAR_API_KEY\": \"${env:HTMLRADAR_API_KEY}\" }\n    }\n  }\n}\n```\n\nThere is a one-click **Add to Cursor** button on [htmlradar.com/mcp](https://htmlradar.com/mcp). It\ninstalls the server with a placeholder key, which you then replace with your own.\n\n**Codex CLI**\n\n```\ncodex mcp add htmlradar --env HTMLRADAR_API_KEY=$HTMLRADAR_API_KEY -- npx -y htmlradar-mcp\n```\n\nSeven tools: `whoami`, `list_shares` and `get_share_activity` read; `share_html`, `create_share`,\n`replace_document` and `revoke_share` write. Every option, the self-hosting variable and the privacy\nnotes are in [`packages/mcp/README.md`](./packages/mcp/README.md). The connector at\n`mcp.htmlradar.com` serves the same seven, imported from this package rather than copied — see\n[`packages/connector/README.md`](./packages/connector/README.md).\n\nIf you modify the source and run a network service from it, AGPL-3.0 requires you to make your modifications available. See [`LICENSE`](./LICENSE).\n\n---\n\n## Development\n\n```bash\npnpm dev                              # runs app, monitor, proxy, connector, tracker in parallel (mcp has no dev script)\npnpm typecheck                        # tsc --noEmit across all six packages\npnpm lint                             # eslint + prettier\npnpm test                             # vitest across app, mcp, monitor, proxy, connector, tracker\n```\n\nLocal URLs after `pnpm dev`:\n\n- Web app: `http://localhost:3000`\n- Proxy worker: `http://localhost:8787`\n- Tracker bundle: `packages/tracker/dist/tracker.js` (after `pnpm --filter @htmlradar/tracker build`)\n\nTracker bundle size budget: ≤14 KB gzipped. Build will warn if you cross it.\n\n---\n\n## Contributing\n\nPRs welcome. [DCO sign-off](https://developercertificate.org/) is required — just `git commit -s`. No CLA.\n\n- Big features: open an issue first to discuss scope.\n- Bug fixes + small improvements: PR directly.\n- Style is enforced by `pnpm lint`. CI runs the full suite on every push.\n\nSee [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full guide.\n\n## Security\n\nFound a vulnerability? Email `security@htmlradar.com`. Please don't open a public issue. See [`SECURITY.md`](./SECURITY.md) for the disclosure policy.\n\n## License\n\nAGPL-3.0-or-later. See [`LICENSE`](./LICENSE).\n\nWant to run a hosted service from a closed-source modified version, or embed the tracker in a closed-source product? A **commercial license** is available — see [`COMMERCIAL-LICENSE.md`](./COMMERCIAL-LICENSE.md), or email `hello@htmlradar.com`.\n\n---\n\nEngineering deep-dive: [htmlradar.com/blog/how-we-built-htmlradar](https://htmlradar.com/blog/how-we-built-htmlradar)\n",
  "bytes": 20281,
  "sha": "89dc7ba8316c2083bdb461b271ab111b33c41b5fb36348df9b00613a1e7b9b3d",
  "repo_slug": "htmlradar/htmlradar",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_htmlradar_share_f8ab0021/readme"
}