← All articles
Jun 12, 20263 min read

NorthLab Folders: Local-First Architecture for a ChatGPT & Claude Extension

  • Chrome Extensions
  • WXT
  • React
  • local-first
  • IndexedDB
  • NorthLab Folders
NorthLab Folders: Local-First Architecture for a ChatGPT & Claude Extension

Anyone who lives in ChatGPT or Claude knows the sidebar problem: hundreds of conversations, no folders, search that barely works. NorthLab Folders is my extension that fixes it — nested folders, pinning, full-text search, and Markdown/JSON/zip export, injected natively into both chatgpt.com and claude.ai. The interesting part is the architecture constraints: everything local, nothing scraped, two hostile host pages.

Stack: WXT, React 19, Tailwind v4 in shadow DOM

The extension is built on WXT — the Vite-based framework that does for extensions what Next.js does for React apps: entrypoint conventions, HMR that mostly survives content scripts, manifest generation, and multi-browser builds. React 19 renders the UI; Tailwind v4 styles it — inside shadow DOM containers, which is the only sane way to inject styled UI into pages you don't own. The shadow root gives a hard CSS boundary in both directions: ChatGPT's styles can't break the sidebar, and the sidebar's Tailwind reset can't break ChatGPT.

That isolation matters because host pages disagree about everything — as I posted while building it: docking a sidebar without fighting the host layout is messier than expected; sites don't even agree where <main> starts. The injection logic treats each host as a separate adapter rather than pretending one selector strategy fits both.

The load-bearing decision: intercept JSON, don't scrape DOM

The naive way to read conversations is scraping rendered messages from the page. It's also the way that breaks weekly — both platforms are React apps whose DOM is compiler output: obfuscated classes, virtualized lists, streaming partial renders.

NorthLab Folders instead uses a dedicated interceptor content script that reads conversation data from the platforms' own JSON responses as the page fetches them. The payload that feeds the UI is the same payload that feeds us — structured, complete, and far more stable than any DOM shape, because the platforms' own apps would break if those APIs churned like their class names. (The same survival principle as zen-google anchoring on the accessibility tree: bind to contracts the host can't casually break.)

Local-first is the product, not a constraint

Everything lives on-device: folder structure in chrome.storage.local, conversation content in IndexedDB via Dexie — which is what makes instant full-text search across hundreds of chats possible at all. Nothing syncs, nothing transmits; the host permissions cover exactly chatgpt.com and claude.ai and the store listing leads with that.

People paste their contracts, health questions, and source code into AI chats. An organizer that shipped those conversations to my server would deserve zero installs. Local-first isn't a compromise here — for this product, it's the headline. It also forced honest engineering: export exists (Markdown/JSON per chat, zip for bulk via jszip) precisely because there's no cloud copy. If the user's data lives only in their browser, handing it back to them in portable formats is an obligation, not a feature.

Pro tier without a backend

The freemium gate followed the same gravity. I originally started building custom licensing — and killed it after realizing I was building a second product. Lemon Squeezy's native license keys replaced the whole thing: activation, validation, and deactivation against their API, no server of mine in the loop. The deeper lesson was UX, though: gated actions must fail loudly and gracefully — explain, show the upgrade path — because a silently no-op'ing button doesn't read as "premium feature," it reads as "broken extension."

Accessibility got the same from-the-start treatment: focus traps and aria-modal on dialogs, focus-visible rings throughout — the polish layer most extensions skip and users feel without naming.

The shape of it

A browser extension with real persistence is a full product: storage schema, search index, licensing, a11y, two host integrations — minus the luxury of owning the page. The decisions that made it sustainable were all subtractions: no DOM scraping (intercept JSON), no cloud (IndexedDB), no custom licensing (Lemon Squeezy), no style bleed (shadow DOM). How it gets sold — the Chrome Web Store asset pipeline — turned into its own post.

WRITTEN BY

Shahzaib Muhammad Akram

Senior Frontend EngineerCyberjaya, Malaysia