Skip to content

Repository files navigation

Threadglass

A fast, local-first explorer for Facebook Messenger archive exports.
Browse, search and measure your own message history — without any of it leaving your machine.

CI Node 20.12+ TypeScript MIT licence No tracking

A conversation open in Threadglass, with the sidebar of conversations and a message thread

Point it at your export folder and it becomes a real client for your own history: browse every conversation, search all of it in milliseconds with matching that understands Vietnamese accents, measure how the two of you actually talk, and count how often any word gets used.

Nothing leaves your machine. There is no database, no account, and no network call outside localhost — the archive is read from disk into memory at boot and served by a single Node process.

Every screenshot here is taken from examples/sample-archive, which is entirely fictional and generated by a committed script. No real messages appear anywhere in this repository.


Why it is built this way

A Messenger export is small — a hundred thousand messages is roughly ten megabytes of text. That fits comfortably in memory, so Threadglass parses the whole archive at startup and answers every request from RAM.

The consequences are worth stating plainly:

Decision Reason
In-memory index, no SQLite/FTS5 A linear scan over pre-folded strings resolves 40k messages in under 10 ms. A native better-sqlite3 build is a real portability risk on Windows and buys nothing at this scale.
Zero native dependencies npm install works on any platform with Node 20+; no compiler, no Python, no post-install build step.
Diacritics folded once, at ingest The corpus is Vietnamese. chao must find chào, chảo and cháo. Folding per message at boot beats folding per comparison at query time, and folding is length-preserving so match offsets still index into the original for highlighting.
Whole thread sent in one response Even the largest conversation is a few megabytes over loopback. Holding it client-side makes find, jump-to-message and day grouping instant instead of round-trips.
Charts hand-built in SVG A charting library adds ~100 kB and its own visual language. These inherit the app's design tokens, so the dashboard looks like one product.

Startup on a 103-conversation / 40,599-message archive: ~130 ms.


Requirements

  • Node.js 20.12 or newer (developed on 24.x)
  • A message export directory — or nothing, to try the bundled sample

Quick start

git clone https://github.com/<you>/threadglass.git
cd threadglass
npm install
npm run dev

That runs against examples/sample-archive, a small fictional export, so you can see the app working immediately.

To open your own messages, use the app — click the archive icon in the left rail (or "Open yours" on the sample banner), then paste a path or browse to the folder. The choice is remembered, so you only do it once. No environment variable, no restart.

The archive picker, showing the currently open folder and a folder browser that flags likely exports

Setting THREADGLASS_ARCHIVE_DIR still works and is what a container or a scripted run should use; a folder chosen in the app takes precedence on the next launch.

npm run dev starts the API on http://127.0.0.1:8787 and the client on http://127.0.0.1:5173 with /api and /media proxied.

Production (single process)

npm run build
npm start          # http://127.0.0.1:8787 serves API, media and client

Docker

docker build -t threadglass .
docker run --rm -p 8787:8787 \
  -v "/path/to/messages:/archive:ro" \
  -e THREADGLASS_ARCHIVE_DIR=/archive \
  -e HOST=0.0.0.0 \
  threadglass

The archive is mounted read-only; Threadglass never writes to it.


Supported exports

Threadglass detects the layout automatically — there is nothing to configure.

Facebook "Download Your Information"

Facebook's own export, from Settings → Your information → Download your information (choose JSON, then select Messages). Point Threadglass at the messages folder:

your_facebook_activity/messages/     ← THREADGLASS_ARCHIVE_DIR
  inbox/
    alicetran_9f2c/
      message_1.json                 ← long threads split across numbered parts
      message_2.json
      photos/ · videos/ · audio/ · gifs/
  archived_threads/ · message_requests/
  stickers_used/

Facebook's JSON is awkward in ways Threadglass handles for you:

  • Text is mojibake — UTF-8 bytes written back out as Latin-1, so Nguyễn arrives as Nguyá»…n. Every name, message and reaction is repaired on load.
  • A long conversation is split across message_1.json, message_2.json, …, each repeating the header. Parts are merged and re-sorted.
  • Attachment URIs are written relative to the export root, several levels above the folder you point at. They are re-anchored so they resolve.
  • Every message is typed "Generic"; a useful kind is derived from the payload.

Flat exports

The layout produced by several third-party Messenger exporters — one file per conversation, one shared media/ folder:

messages/                            ← THREADGLASS_ARCHIVE_DIR
  Alice Trần_1.json
  Bob Nguyễn_2.json
  media/
    3f2b91c4-....jpeg
{
  "participants": ["You", "Alice Trần"],
  "threadName": "Alice Trần_1",
  "messages": [
    {
      "senderName": "Alice Trần",
      "text": "chào bạn",
      "timestamp": 1777960838385,     // epoch milliseconds
      "type": "text",                  // text | media | link | placeholder
      "isUnsent": false,
      "media": [{ "uri": "./media/3f2b91c4-....jpeg" }],
      "reactions": [{ "actor": "You", "reaction": "" }]
    }
  ]
}

Common to both

  • Messages are re-sorted by timestamp; no export is reliably ordered.
  • "uri": "Failed to download media" is the sentinel Facebook writes when an attachment could not be fetched. Those render as an explicit note, not a broken image.
  • Files that fail to parse are skipped and reported in GET /api/meta, never fatal.
  • The archive owner is detected as the one participant present in every conversation.
  • WebP attachments are treated as stickers and rendered without bubble chrome, which matches how Messenger stores them. Override by editing STICKER_EXT if your export differs.

GET /api/meta reports which layout was detected (facebook-dyi or normalized), which is the first thing to check if something looks wrong.

Configuration

Every setting is an environment variable. Copy .env.example to .env — both npm run dev and npm start load it if present.

Variable Default Purpose
THREADGLASS_ARCHIVE_DIR examples/sample-archive Path to your export. Usually unnecessary — pick the folder in the app instead.
THREADGLASS_STATE_FILE ~/.threadglass/settings.json Where the chosen archive and recent list are remembered.
THREADGLASS_OWNER auto-detected Override the detected archive owner.
HOST 127.0.0.1 Bind address. Use 0.0.0.0 in a container.
PORT 8787 HTTP port.
THREADGLASS_SERVE_CLIENT on when NODE_ENV=production Serve web/dist from the API process.
THREADGLASS_CORS_ORIGIN same-origin only Allowed API origin. Leave unset unless you know you need it.
LOG_LEVEL info Pino level, or silent.

Features

Conversations. Virtualized message list that stays smooth in a thread of 35,000+ messages — only the visible rows exist in the DOM. Day dividers, message bursts grouped by sender, reactions, inline photo/video/voice attachments with a lightbox, and timestamps that appear on hover instead of cluttering every bubble.

Search. Every message in the archive, with highlighted excerpts. Filter by conversation, message type, and date order. Results group by conversation and click through to the exact message in context.

Three matching modes, shared by search, the conversation find bar and the Word Lab:

Mode Rule
Anywhere Matches anywhere inside a message.
Whole word Matches the word on its own, and tolerates stretched spellings: yêu finds yêu, anh yêu em and yêuuu, but not yêuthương.
Regex Your pattern, applied verbatim.

Both non-regex modes use smart diacritics: type without accents and they are ignored (yeu finds both yêu and yếu); type the accents and they are respected (yêu no longer matches yếu). This mirrors the smart-case convention, and matters here because folding collapses distinct Vietnamese words onto the same letters.

The Word Lab comparing the Vietnamese words yeu and yeu-with-a-different-accent, each broken down by speaker, by month and by day

The Word Lab, showing why the rules exist: two different Vietnamese words that plain accent-folding would merge into one number.

Word Lab. The feature search cannot provide: pick words or phrases and get total occurrences, how many messages contain them, who says them and in what proportion, usage per month and per day, and — at archive scope — which conversations use them most. Compare up to eight terms at once.

Conversation insights. A head-to-head read on any thread: message and word counts, average message length, median reply time, who starts conversations after a lull, who double-texts, reactions given versus received, hour-of-day and weekday rhythm, monthly volume, most-used words and two-word phrases, top emoji and reactions, plus milestones (busiest day, longest daily streak, longest silence).

Archive overview. Totals across all conversations, sent-versus-received balance, volume by month and by day, attachment breakdown by kind, busiest conversations, and the words and emoji you use most anywhere.

The conversation insights drawer, comparing two people across messages, words, reply time and more

The archive overview dashboard in light appearance, with totals, monthly volume and an hour-of-day heatmap

Command palette. ⌘K / Ctrl K jumps to a conversation by name or straight to a message anywhere in the archive.

Export. Any conversation as JSON, CSV (BOM-prefixed so Excel reads UTF-8), Markdown or plain text.

Keyboard

Shortcut Action
⌘K / Ctrl K Command palette
⌘F / Ctrl F Find in the open conversation
/ ⇧↵ Next / previous match
⌘↵ in the palette Open full search results
Esc Close palette, find bar or lightbox

HTTP API

All responses are JSON. The server is read-only; there are no mutating endpoints.

Endpoint Description
GET /api/meta Owner, counts, date range, ingest time, detected export format, skipped files.
GET /api/health Liveness plus message counts.
GET /api/threads?q=&sort= Conversation summaries. sort: recent | messages | name.
GET /api/threads/:id One conversation summary.
GET /api/threads/:id/messages?from=&to=&around=&limit= Message window by index.
GET /api/threads/:id/analytics Full statistics for one conversation.
GET /api/threads/:id/export?format=json|csv|md|txt Download a conversation.
GET /api/analytics/overview Archive-wide dashboard.
GET /api/search?q=&mode=&thread=&sender=&kinds=&from=&to=&order=&limit=&offset= Full-archive search.
GET /api/count?terms=a,b&thread=&mode=&sender= Term counts, per sender and per month.
GET /media/* Attachments, resolved against the open archive. Supports range requests.
GET /api/archive Current archive: path, detected format, counts, recents.
POST /api/archive Open a different archive. Body: { "path": "..." }.
GET /api/archive/browse?path= List sub-folders, flagging likely exports. Loopback binds only.

mode is substring (default), word, or regex. Invalid regular expressions are reported in the response body rather than raised as a 500.

curl 'http://127.0.0.1:8787/api/search?q=chao&limit=5'
curl 'http://127.0.0.1:8787/api/count?terms=hihi,sorry'

Project layout

server/
  src/
    main.ts                    boot: config → ingest → listen
    app.ts                     Fastify instance, static mounts, SPA fallback
    app-context.ts             shared state and derived-analytics caches
    config.ts                  environment configuration
    ingest/archive-loader.ts   read and normalize the export
    domain/
      types.ts                 raw export shapes and the in-memory model
      text-utils.ts            diacritic folding, tokenizing, matching
      search-engine.ts         archive-wide search
      analytics.ts             per-thread and archive statistics
      term-counter.ts          Word Lab counting
      thread-exporter.ts       JSON / CSV / Markdown / text output
    routes/                    HTTP surface
  test/                        vitest suites (75 tests)
web/
  src/
    app.tsx                    shell, sections, keyboard shortcuts
    components/                views, charts, message rendering
    lib/                       API client, formatters, hooks
    styles/                    design tokens, glass material, layout

Design

The interface follows Apple's Liquid Glass language. In practice that means four things, defined once in web/src/styles/glass.css and reused everywhere:

  1. Translucent fills in three tiers — thin for controls, regular for panels, thick for modals and the rail — light enough that the backdrop still tints each surface. An opaque veil stops reading as glass no matter how much blur sits behind it.
  2. Backdrop blur with a saturation boost, which is what makes the background read as refracted rather than merely faded.
  3. A specular edge — a 1px gradient border, bright along the lit side and dark along the shaded one — that gives each slab thickness.
  4. Layered ambient shadows for depth against the moving colour field behind everything.

Supporting choices: continuous large corner radii, Apple's cubic-bezier(0.32, 0.72, 0, 1) spring easing, the system font stack with optical tracking that tightens as type grows, and a light/dark/system appearance toggle. prefers-reduced-motion disables the drifting backdrop and every transition, and there is a @supports fallback that raises fill opacity where backdrop-filter is unavailable so text keeps its contrast.

Development

npm run dev         # API with watch + Vite dev server
npm test            # vitest, 75 tests
npm run typecheck   # tsc --noEmit across both packages
npm run build       # web bundle then server compile

Tests build a temporary archive on disk — including an unsorted thread, a failed attachment, an unsent placeholder and a deliberately malformed file — and run the real loader against it, so schema assumptions are exercised end to end rather than mocked.

Privacy

Threadglass reads your archive and serves it to your own browser. It makes no outbound requests of any kind — no telemetry, no analytics, no update check, no CDN or web font — and it never writes to the archive directory.

The only things it stores are the archive path and a short recents list in ~/.threadglass/settings.json, and your light/dark preference in localStorage. No message content is written anywhere.

It binds to 127.0.0.1 by default. If you change HOST, anyone who can reach that address can read every message in the archive, with no credentials — see SECURITY.md.

Contributing

Issues and pull requests are welcome — see CONTRIBUTING.md.

Please never attach real message data to an issue or PR. The bundled sample archive exists so there is always a safe way to demonstrate a problem.

Security

Threadglass binds to loopback and has no authentication, because it assumes the only person who can reach it is you. Before exposing it to a network, read SECURITY.md — it describes the threat model and how to report a vulnerability privately.

Licence

MIT. See LICENSE.


Phu Nguyen — HCMC, VN

About

A fast, local-first explorer for Facebook Messenger archive exports. Browse, search and measure your own message history — without any of it leaving your machine.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages