Building a Chrome extension from scratch: lessons from Threads DraftCraft
A few months ago I shipped a Chrome extension called Threads DraftCraft. It does one thing: it sorts your Threads drafts by scheduled date so you can see what goes live next. That sounds simple, and the final result is simple for the person using it. But building it taught me more about browser extension architecture, DOM manipulation in hostile environments, and privacy-first design than any tutorial ever could.
This is the technical story behind DraftCraft. I am writing it for intermediate developers who have thought about building a browser extension but have not started yet, or who started and hit a wall when the real-world DOM turned out to be nothing like the examples in the Chrome developer docs.
Why build a browser extension at all
Browser extensions occupy a unique space in software development. They run inside someone else's application. You do not control the HTML. You do not control the JavaScript. You do not control when the page updates, what classes the elements use, or whether the structure you relied on yesterday still exists today. It is the opposite of building on your own platform.
That constraint is also what makes extensions valuable. You can solve problems that the original application chose not to solve. Threads, for instance, shows your drafts in a flat list with no particular order. If you schedule content in advance, that list becomes useless once you have more than a handful of posts. No amount of feature requests to Meta is going to fix that on your timeline. A browser extension can fix it today.
Extensions also have a distribution advantage. The Chrome Web Store handles updates, installation, and discovery. You do not need to convince anyone to download an app or visit a website. The tool lives where the user already works: inside their browser, on the page they are already looking at.
The anatomy of a Chrome extension
Every Chrome extension starts with a manifest.json file. This is the configuration that tells Chrome what your extension does, what permissions it needs, and which scripts to run. DraftCraft uses Manifest V3, which is the current standard and the only version Chrome will accept for new submissions.
The manifest defines three categories of code. First, there is the background script, sometimes called a service worker. This is a script that runs independently of any tab. It handles events like installation, alarms, and messages between different parts of the extension. DraftCraft uses a minimal background script because most of the work happens on the page itself.
Second, there are content scripts. These are JavaScript files that Chrome injects into web pages that match a pattern you specify. In DraftCraft's case, the content script runs on threads.net pages. The content script can read and modify the DOM of the host page, but it runs in an isolated JavaScript context. It cannot access the page's variables or functions directly, and the page cannot access the extension's code. This isolation is a security feature, not a limitation.
Third, there is the popup or options page. This is the small interface that appears when a user clicks the extension icon. DraftCraft keeps this minimal -- a brief description of what the extension does and a link to report issues. The real interface is the sorted draft list on the Threads page itself.
The permissions model in Manifest V3 is deliberately restrictive. You declare exactly which sites your extension can access, which browser APIs it needs, and what data it can read. Chrome shows these permissions to the user during installation. Asking for too many permissions is one of the fastest ways to lose trust, and it is also one of the most common reasons extensions get rejected during review.
Content scripts and the DOM
The content script is where the interesting work happens. When Chrome injects your script into the page, you get full access to the DOM. You can query elements, modify styles, insert new nodes, and listen for events. It feels like writing a regular frontend application, except you are building on top of someone else's frontend application that was not designed to be extended.
The first challenge is finding the elements you care about. Threads, like most modern web applications, does not use semantic or stable class names. The classes are generated by a CSS-in-JS tool and look like random strings. They change between deployments. You cannot write a selector like .draft-list-item because that class does not exist. Instead, you rely on structural patterns: the hierarchy of elements, ARIA attributes, data attributes, and the text content inside nodes.
DraftCraft identifies draft entries by looking for a specific combination of DOM structure and content patterns. It looks for containers that hold a timestamp, a text preview, and are nested inside the drafts view. This approach is more resilient than class names, but it is not bulletproof. Any significant redesign of the Threads drafts interface could break the selectors. That is a risk every content script developer lives with.
Once you find your elements, manipulation is straightforward. The DOM API works the same way it does in any other context. You can reorder elements by removing them and reinserting them in the correct order. You can add visual indicators. You can modify styles. The key is doing all of this without breaking the host page's event listeners or React state.
The challenge of single-page applications
Threads is a single-page application built with React. That means the page does not reload when you navigate between views. When a user clicks on their drafts, Threads does not fetch a new HTML page. It updates the existing DOM by mounting and unmounting React components. From the browser's perspective, it is the same page. A traditional content script that runs once on page load would miss most of the action.
The solution is MutationObserver. This browser API lets you watch a section of the DOM for changes. When elements are added, removed, or modified, the observer calls your callback function with a list of mutations. DraftCraft sets up a MutationObserver on the main content area and watches for the appearance of draft entries.
The tricky part is efficiency. A MutationObserver on a React application fires constantly. Every state change, every re-render, every animation produces mutations. If your callback does expensive work on every mutation, you will destroy the page's performance. DraftCraft handles this by debouncing the observer callback and checking a lightweight condition before doing any real work. The observer fires, the debounce waits for mutations to settle, and only then does the extension check whether the current view is the drafts list and whether sorting is needed.
There is another subtlety with SPAs: the URL changes without a page load. Chrome's content script injection is tied to page loads, so your script is already running when the user navigates to the drafts view. You need to detect the URL change yourself. DraftCraft monitors the URL by periodically checking window.location.href inside the MutationObserver callback. When the URL matches the drafts path, the extension activates. When it does not, the extension stays dormant. This pattern avoids running unnecessary code on every page of the Threads site.
Sorting drafts without an API
Threads does not expose a public API for accessing drafts. There is no endpoint you can call to get a list of scheduled posts with their timestamps. The only source of truth is the rendered DOM. DraftCraft reads the scheduled time from each draft entry, parses it into a comparable format, and sorts the entries accordingly.
The parsing step is where things get interesting. Threads displays dates in a human-friendly format. You might see "Tomorrow at 9:00 AM" or "Mar 15 at 3:30 PM" or just "9:00 AM" for posts scheduled today. Each of these formats needs to be converted into a timestamp that can be compared numerically. DraftCraft includes a date parser that handles the various formats Threads uses and normalizes them into Unix timestamps.
The sorting algorithm itself is simple: extract the timestamp from each draft element, sort the timestamps, and reorder the DOM nodes to match. The complexity is not in the sort. It is in reliably extracting dates from text that was designed for human readability, not machine parsing. Edge cases include drafts with no scheduled time, drafts in different time zones, and the occasional format change when Threads updates their UI.
One decision I made early was to sort in place rather than rebuilding the draft list from scratch. Removing elements and reinserting them can break React's internal state tracking. By using insertBefore to move existing nodes, DraftCraft preserves the event listeners and React fiber references attached to each element. The visual result is the same, but the underlying DOM integrity is maintained.
Privacy by architecture, not by promise
Most browser extensions ask for permissions they do not need. Some collect browsing data, inject tracking scripts, or phone home to analytics servers. Users have learned to be suspicious, and they are right to be. The permissions dialog during installation is the first and often the only trust signal a user gets.
DraftCraft takes a different approach. It collects no data. It makes no network requests. It has no analytics. It does not even have a backend server. The entire extension runs locally in the browser. Your drafts, your scheduled times, your content -- none of it ever leaves your machine.
This is not privacy by policy. It is privacy by architecture. There is no server to be breached. There is no database to be leaked. There is no third-party SDK that might change its data practices in a future update. The extension literally cannot send your data anywhere because it does not have the code or the permissions to do so.
Making this architectural choice also simplified development significantly. No authentication, no server infrastructure, no data storage, no compliance requirements. The extension is a pure client-side script that reads the DOM and rearranges it. That simplicity is a feature, not a compromise. It makes the codebase small enough that anyone can audit it, and since DraftCraft is open source, anyone actually can.
What I would do differently
Hindsight is a luxury. Here is what I would change if I started DraftCraft from scratch today.
I would write end-to-end tests from the beginning. Testing a browser extension is awkward because your code depends on a live web page you do not control. But tools like Puppeteer and Playwright can load extensions and simulate user interactions on real pages. I added tests later, and the effort of retrofitting them was significantly higher than it would have been to write them alongside the features.
I would also invest in a more robust element selection strategy earlier. My initial selectors were too specific and broke when Threads shipped a minor UI update. A more defensive approach -- looking for the most stable structural patterns and failing gracefully when they change -- would have saved me several emergency fixes in the first few weeks after launch.
The Chrome Web Store submission process deserves its own mention. The review process is not difficult, but it is opaque. You submit your extension, and then you wait. The first submission took about five days to review. Subsequent updates were faster, usually one to two days. The rejection reasons can be vague, and the appeal process is slow. My advice: read the Chrome Web Store policies before you write any code, not after. Knowing what will and will not be accepted shapes your architectural decisions early.
Finally, I would plan for the host page changing from the start. Meta updates Threads frequently. Every update has the potential to break DraftCraft's selectors. Building a monitoring system that detects when the extension stops working -- and alerts me before users report it -- is something I should have done on day one.
Ship something small
The biggest lesson from building DraftCraft is not technical. It is about scope. The extension does one thing: sort drafts by date. That narrowness made it possible to ship in weeks instead of months. It made the codebase small enough to reason about completely. It made the privacy story simple. It made the Chrome Web Store description clear.
There is always pressure to add more features. DraftCraft could have a search function, a tagging system, analytics on posting frequency, integration with other scheduling tools. All of those ideas have merit. None of them were necessary to solve the core problem: seeing your drafts in the right order.
If you are thinking about building a browser extension, start with the smallest version that solves a real problem. Get it into the Chrome Web Store. Get feedback from actual users. Then decide what to build next based on what people actually need, not what you imagine they might want. The extension you ship is infinitely more useful than the perfect one you never finish.
Browser extensions are one of the most accessible forms of software distribution. The barrier to entry is low. The tools are well-documented. The impact can be immediate and tangible. If there is an annoyance in a web application you use every day, you have everything you need to fix it. DraftCraft started as exactly that: a personal fix for a personal annoyance. It turned out other people had the same annoyance. That is usually how the best tools begin.
Try DraftCraft
Sort your Threads drafts by scheduled date. Open source, privacy-first, and free.
Learn more about DraftCraft