Building a Chrome Extension for Netflix — What We Learned
Content scripts, shadow DOM, keyboard trapping, Firestore in restricted contexts — lessons from building CouchCritic.
CouchCritic is a Chrome Extension that injects a comment panel into Netflix and Prime Video. That sentence makes it sound straightforward. It wasn't. Here's what we learned building it.
Netflix fights injected UI
Netflix has one of the most aggressive Content Security Policies on the web. Their pages are heavily locked down, and they actively detect and respond to DOM modifications.
Our first approach — injecting a React component directly into the Netflix DOM — was fragile. Netflix's own React app would occasionally unmount our components during their SPA navigation. Style conflicts were constant.
The solution was Shadow DOM. We create a shadow root, inject our entire UI inside it, and attach our own stylesheet. This gives us complete CSS isolation — Netflix's styles can't bleed into our panel, and our styles can't break Netflix. It also means Netflix's JavaScript can't easily detect or interfere with our components.
Keyboard events are a minefield
When you type in CouchCritic's comment box, you don't want Netflix interpreting those keystrokes as playback controls. Pressing "F" shouldn't toggle fullscreen. Space shouldn't pause the video. Arrow keys shouldn't seek.
We solve this by intercepting keyboard events at the shadow root boundary. Any keydown, keyup, or keypress event originating from our input elements gets its propagation stopped before it reaches Netflix's event listeners.
This sounds simple, but the timing matters. We register our interceptors during the capture phase with high priority, and we have to handle both the shadow DOM boundary and the main document separately. Netflix also listens on the window object, so we trap events there too.
Firestore doesn't love content scripts
Our real-time comments use Firestore's onSnapshot listeners. These work perfectly in normal web apps, but content scripts run in a restricted context that shares the host page's network policies.
Firestore's default transport is WebChannel — a protocol that Netflix's CSP blocks. The fix was switching to long polling by initializing Firestore with experimentalForceLongPolling. It's slightly less efficient but works reliably in every content script context we've tested.
SPA navigation detection
Netflix is a single-page app. When you navigate from the browse page to a show's detail page to the video player, the URL changes but the page never fully reloads. Our extension needs to detect these transitions and update accordingly.
We use a combination of MutationObserver on the body element and URL polling. The MutationObserver catches most transitions instantly. The URL polling (every 2 seconds) is a fallback for edge cases where the DOM mutation happens before the URL update.
Each platform has its own adapter that knows how to extract the content ID, title, season, episode, and other metadata from the current page state. When the URL changes, we ask the adapter "what are we looking at now?" and either inject inline comments or show the overlay panel.
Presence is harder than you'd think
Showing "who's watching right now" requires tracking active users per content ID. We use Firestore documents with a heartbeat pattern — each user writes to a presence document every 30 seconds with their timestamp.
The query filters out stale entries (older than 2 minutes) client-side. When a user navigates away or closes the tab, we fire a delete. But tabs can crash, laptops can sleep, and networks can drop — so the staleness filter is the real source of truth, not the explicit delete.
What we'd do differently
If we started over today:
- Start with Shadow DOM from day one. We wasted time on direct DOM injection before realizing isolation was non-negotiable.
- Build the platform adapter pattern earlier. We initially hardcoded Netflix-specific selectors everywhere. When we added Prime Video support, we had to refactor extensively. The adapter pattern should have been the first abstraction.
- Use long polling from the start. We spent days debugging WebChannel errors before finding the one-line fix.
CouchCritic is open to adding more streaming platforms. If you're a developer interested in contributing an adapter for Disney+, HBO, or another service, reach out — we'd love to collaborate.
Ready to try CouchCritic?
Install the extension and start commenting in 30 seconds.
Add to Chrome — Free