CASE STUDY 04 · Chrome Extension
Anywhere
A social annotation layer over the entire web.

Chrome extension that pins comments, reactions, and annotations to any website. Add friends, create groups, share private notes — on pages you don't own.
any page
no site cooperation required
private-first
friends & groups ACL on every annotation
multi-signal
anchor scoring (structure + geometry + text)
THE PROBLEM
Pulse lets site owners add collaboration to their own pages. The obvious next question: why should the site owner get to decide? Annotating the web itself — leaving a note for a friend on any article, product page, or doc — requires no cooperation from the page at all.
The graveyard of dead annotation startups shows the hard part: anchoring a note to a page that can change or vanish, and doing it precisely enough that the note still points at the right sentence a month later. The W3C Web Annotation group standardized selectors for this and robust re-anchoring remains an acknowledged open problem.
WHAT I BUILT
A Chrome extension (Manifest V3) that overlays a social layer on every page: pin a comment to any element or text selection, react, and share — visible to your friends and groups, private by default.
A social backend in Go: friendships, groups, notification fan-out, and an anchor-resolution service that stores enough context about each annotation target to re-find it when the page shifts.
ARCHITECTURE
Chrome extension (MV3)
├─ content script ── anchor capture + re-resolution
│ └─ barycentric anchoring (element + neighborhood
│ geometry + text context, scored on re-visit)
└─ service worker ── auth, sync
│ REST/WS
▼
Go (Fiber) API
├─ PostgreSQL ── annotations, friends, groups
└─ Redis ── sessions, notification fan-out- Anchoring is the core IP: each annotation stores its target element plus a geometric-and-textual neighborhood — a barycentric description of where the target sits relative to stable landmarks. On re-visit, candidates are scored against this description instead of trusting any single selector.
- MV3's service-worker lifecycle means nothing in the extension can rely on living long — all state syncs through the API, and the content script rebuilds from the anchor store on every navigation.
- Go + Fiber keeps the API's per-request overhead tiny; annotation lookups happen on every page load for every user, so the hot path has to be cheap.
STACK — AND WHY
Go + Fiber
The annotation-lookup hot path runs on every page visit; Go keeps p99 latency flat under fan-out.
Chrome MV3
The only way to be present on every page the user visits.
PostgreSQL
Annotations, social graph, and group ACLs are relational to the bone.
Redis
Session cache and notification pub/sub.
THE HARD PARTS
Re-finding an annotation on a changed page
CSS selectors rot, XPaths shatter, and text quotes duplicate. The barycentric approach — describing a target by its position relative to multiple stable landmarks, then scoring candidates on re-visit — degrades gracefully where single-selector strategies fail completely. This is the same problem class the W3C annotation community documents as unsolved in the general case; treating it as a scoring problem rather than a lookup problem is what makes it workable.
Privacy model for annotating other people's pages
Public annotations on arbitrary websites are a harassment machine. Everything is private-by-default and shared explicitly to friends or groups — the ACL check happens server-side on every fetch, and page owners never become an audience you can target.
Surviving Manifest V3
MV3 kills background pages: the service worker dies whenever Chrome feels like it. Auth, sync state, and pending writes all had to become resumable, with the content script able to cold-start from server state alone.
WHAT IT TAUGHT ME
- Robust anchoring is a ranking problem, not a lookup problem — the moment you accept candidates and score them, the 'impossible' cases become merely hard.
- Design the abuse story before the feature: private-by-default wasn't a limitation, it was the product decision that made the whole idea defensible.
- MV3's constraints, taken seriously, force a cleaner architecture: server state as truth, everything client-side reconstructible.