a YC-backed documentation platform

Do the task once. Get the step-by-step guide.

A Chrome extension that records real user actions into illustrated how-to guides and clips any web page into clean, importable markdown — built on Manifest V3's most hostile constraint: nothing is allowed to stay alive.

Role: design & implementation, full stack · Surface: Chrome MV3 extension, four execution contexts · Stack: TypeScript · React · Chrome APIs

The brief: the most-requested docs are the most tedious to write

How-to guides are the workhorse of product documentation, and producing one is miserable: perform the task, screenshot every step, crop, annotate, describe what you clicked, repeat. Teams asked the platform for a recorder — do the task once in the browser, get a publishable guide.

The second ask rode along: teams migrating into the platform wanted to grab existing content — a competitor's docs page, an internal wiki article, a selection of a blog post — and pull it in as clean markdown, without copy-paste mangling.

Both features live where the work happens: a browser extension with a side panel, an on-page overlay, and a direct line into the documentation platform. This case study is about what it takes to build a reliable recorder on a runtime designed to kill everything you build.

Manifest V3 is built to stop you

Runtime — the brain gets killed mid-session

MV3 service workers are terminated after ~30 seconds of inactivity — including in the middle of a recording session. All orchestration state can evaporate between two clicks.

Topology — four contexts, zero shared memory

The side panel, the service worker, a content script injected into every page, and an offscreen document each run in their own isolated world. Everything is asynchronous message passing.

Territory — every page is hostile terrain

The recorder runs inside arbitrary third-party pages — any framework, any DOM structure, any CSS. It must observe everything and disturb nothing, including never recording its own UI.

Signal — DOM events aren't steps

A person "types a name into one field"; the DOM fires fifty input events, a click on a label fires twice, and a slow drag is a hundred mousemoves. Raw events must be distilled into human actions.

Four isolated contexts, one typed protocol

┌─────────────────────────────┐   ┌─────────────────────────────┐
│ Side panel                  │   │ Platform web app            │
│ React UI — session controls,│   │ external messaging —        │
│ live step list, review &    │   │ login/logout session sync   │
│ publish · runs the HTML→    │   │ with the extension          │
│ markdown conversion         │   │                             │
└──────────────┬──────────────┘   └──────────────┬──────────────┘
═══════════════╪══ typed message bus ════════════╪═══════════════
   40 message types · 8 sender→receiver routes · route encoded
                     in every message name
═══════╤═══════════════════╤════════════════════╤═══════════════
┌──────┴───────┐  ┌────────┴─────────┐  ┌───────┴──────────────┐
│ Service      │  │ Content script   │  │ Offscreen document   │
│ worker — the │  │ in every page —  │  │ heartbeat every 25s —│
│ orchestrator:│  │ event capture,   │  │ keeps the worker     │
│ session state│  │ element highlight│  │ alive only while a   │
│ tab tracking,│  │ on-page menu,    │  │ session runs         │
│ screenshots, │  │ content extract  │  │                      │
│ API calls    │  │                  │  │                      │
└──────────────┘  └──────────────────┘  └──────────────────────┘

No context trusts another to be alive. Every hop is a message; every message type is shared, typed, and named by its route.

SP_BG_StartCapture           // side panel → service worker
CS_BG_ThrottleSaveStep       // content script → service worker
BG_UI_ShowFinishConfirmation // worker → both UIs at once
APP_EXT_LogoutUser           // platform web app → extension

Sender and receiver are encoded in the constant itself — misrouted messages become visible at the call site, not at runtime.

Decision 01 — Name clicks the way a screen reader would

The difference between a useful guide and a useless one is the step title. "Click on div.btn-primary-3" documents nothing; "Click on Create invoice" documents the task. The recorder derives an accessible name for every event target through a fallback cascade — the same information a screen reader would announce:

  1. The form control's associated <label> — or a label sitting directly before it.
  2. aria-labelledby — resolved to the referenced element, which is then named recursively.
  3. aria-label, aria-haspopup, and recognized roles, paired with the element's visible text.
  4. Per-element rules: link text, button text, input placeholder or name, image alt text — and SVG icons, which walk up to their labeled parent.
  5. The nearest ancestor carrying any ARIA labeling, then the parent element, retried once.
  6. Last resort: "the highlighted element" — paired with the screenshot, still usable.

Titles are truncated to their first line, capped in length, and prefixed by the action — click, right-click, type into, select, drag. Combined with a screenshot per step, the output reads like a guide a person wrote:

Recorded session — auto-generated draft

  1. Go to Billing — Acme dashboard
  2. Click on Create invoice
  3. Type into Invoice number

Decision 02 — Distill events into actions

Seventeen DOM listeners feed the recorder while a session runs. Turning that firehose into a clean step list is a signal-processing problem, solved with a small stack of heuristics:

  • Replace, don't append. Each new event decides whether it replaces the previous step or starts a new one — fifty keystrokes in one field collapse into a single "Type into…" step whose screenshot keeps updating to the final state.
  • Debounce or throttle by event class. Bursty events ride a debounce; continuous ones (mouse activity, checkbox toggling) ride a throttle, so a step lands while the action is still in progress.
  • Suppress the echoes. Clicking a label fires the input too — one of them is dropped. Steps arriving within 200 ms of the previous are discarded. A mouse-up only counts as a drag past a 20-pixel threshold.
  • Navigation is a step. Switching tabs or windows injects a "Go to page title" step automatically — the recorder follows the user across the whole browser, not one page.
  • Screenshots are eager. Every step captures the visible tab; uploads start immediately and run in parallel while recording continues. Finishing a session just awaits what's already in flight — and the final payload replaces each screenshot blob with a reference, keeping the publish request small.

Decision 03 — Assume the worker is already dead

The service worker owns the session, and Chrome reserves the right to kill it at any moment. The design treats that as the normal case, not the failure case:

  • Storage is the source of truth. Every state mutation — each step, each status change — is mirrored to extension storage as it happens. A freshly-woken worker, or a reopened side panel, rehydrates the entire session from storage and continues as if nothing happened.
  • A heartbeat with a lease. During an active session, an offscreen document — a sanctioned, invisible extension surface — pings the worker every 25 seconds, just inside Chrome's idle timeout. It is created when a session starts and destroyed when it ends: the extension consumes zero background resources the rest of the time.
  • Disconnection is a signal. The side panel holds an open port to the worker; when the panel closes mid-recording, the disconnect event finishes and saves the session rather than abandoning it.
  • Logout propagates. The platform's web app messages the extension directly on login and logout — signing out of the product tears down any recording or conversion in progress, everywhere, and clears the stored identity.

The recorder was designed backwards from the crash: first make every state recoverable, then make the crash rare.

Decision 04 — Clip any page; import it exactly as the product would

The second feature turns web pages into markdown. Extraction runs three strategies and keeps the best result: if the user has selected text, the selection wins verbatim; otherwise the page goes through a readability parser, and through a custom extractor that scores candidate containers on text density, multimedia presence, and length while pruning navigation, footers, sidebars, and decorative images — whichever recovers more real content is used.

The conversion itself cheats, productively: the extension lives in the platform's monorepo, so the side panel converts HTML with the exact same markdown module the product uses for imports — plus the product's own UI kit, API client, and design tokens. What the extension shows is byte-for-byte what arrives in the workspace; there is no second converter to drift out of sync.

Guardrails keep the batch flow honest: an element-count cap rejects oversized pages with a clear message, duplicate captures of the same page are detected and flagged, restricted browser surfaces get an explanatory screen instead of a broken recorder, and the collected pages import in one shot as a document tree — the tab then opens directly on the result.

By the numbers

4isolated execution contexts
40typed messages in the protocol
8sender→receiver routes
17DOM listeners while recording
1screenshot per captured step
25sheartbeat holding the worker alive
3build systems in one package
~7klines of TypeScript

The shipped extension records a task across tabs and windows into a guide with human-readable steps and per-step screenshots, survives its own runtime being killed underneath it, and imports web content through the product's own conversion pipeline. The interesting work is invisible by design: users see three clean steps, not the fifty events, four processes, and one deliberately immortal heartbeat that produced them.


TypeScript · Chrome Manifest V3 · React side panel · webpack-bundled service worker & content scripts · Tailwind CSS · readability parsing · shared monorepo markdown pipeline. Client and product details anonymized. Architecture and figures are drawn from the shipped implementation. Companion pieces: An AI agent that rewrites your documentation and Git-style branching for documents.