Google Takeout hands you a complete copy of your data - and then leaves you with a pile of files. A huge .mbox mail archive, .vcf contacts, per-conversation Chat JSON with thousands of attachments, .ics calendars, Keep notes, and a sprawling Drive folder. The data is legally yours and practically unreadable.
Even a single account's export is huge and mostly media. Existing viewers only cover one product each (an mbox reader, a Chat reader, a Photos gallery), and the commercial ones are closed and Windows-oriented. Nothing covered mail, chat, calendar and Drive together, self-hosted, without uploading personal data to someone else's server.
TakeoutLens is a Next.js + shadcn/ui web app shipped as a Docker image. Extract your Takeout, mount it read-only into the container, open localhost, and browse it like the products it came from: a mail client, a chat timeline, a calendar, a file browser.
The result: a full mailbox and years of chat history become searchable in the browser without loading a large file into memory, without modifying the export, and without a single outbound network call.
9 things it does well
.eml export and full-text search with label: / has:attachment filters.ics, agenda and event detail, recurrence (RRULE / EXDATE) expanded on demand for the visible rangetopics.mbox discussions rendered by the same mail viewerHow it fits together
A separate indexer process streams the Takeout tree and writes a SQLite database into a writable /data volume; the Takeout itself is mounted read-only at /takeout. The web server only reads that index. Pages are Server Components, interactive reads (search, pagination) are Server Functions, and media is served through three Range-capable route handlers addressed by opaque IDs - the browser never sends a filesystem path.
The indexer makes one streaming pass over the mbox and stores each message's byte offset and length plus headers, labels, thread ID and a snippet. Full bodies and attachments are parsed on demand with a ranged read at view time. /data stays small and the mbox remains the source of truth.
Mail, Chat, Calendar and Contacts go through the SQLite index. Drive, Photos and YouTube are browsed lazily from the filesystem - a tree that large can't be indexed usefully and doesn't need to be beyond file names and paths.
Mbox needs random access, and reading huge parts through a .tgz means re-decompressing to seek. Supporting archives would mean either doubling disk use by extracting to /data or building a fragile virtual filesystem, so v1 documents a one-line extract command instead.
No docx, xlsx, pdf or pptx conversion libraries are shipped. Downloads carry Content-Disposition: attachment and nosniff, so a hostile file can never execute in the app's origin - and the image gets smaller and the attack surface shrinks.
A local server can still be read by any web page via DNS rebinding. Every request's Host and Origin is validated in proxy.ts, a scrypt-hashed first-run password gates every route (re-checked inside each handler, since the proxy alone isn't the boundary), file paths are realpath-checked against the mount root, and email HTML renders in a sandboxed iframe with a strict CSP and remote content blocked.
No client-side fetch, one shared TanStack Table component, all formatters in lib/helper.ts, all constants in lib/constant.ts, and a designed skeleton for every route. A check:conventions script fails the build if any rule drifts.
6 phases
(path, size, mtime) indexing, first-run password, file-serving routes and the home dashboardMail, Chat, then Calendar agenda and Drive as the release gateMulti-calendar view, Contacts, Keep, Tasks, Photos, Groups, YouTube and the generic viewerMulti-arch Docker image, compose with localhost binding, README, MIT license, Playwright smoke test and a security reviewQuick Start
# extract your Takeout first, then:TAKEOUT_DIR=~/Takeout docker compose upOpen http://localhost:3000, set a password on first run, and start browsing. The Takeout folder is mounted read-only; the index lives in a separate volume.
Your Google data, readable again - a full Takeout browsed like the original products, self-hosted, read-only, and nothing leaves your machine.
A full Takeout goes from unreadable archive to something you can actually search and read, on your own machine. The spike measurements shaped the design more than the plan did: indexer memory came out above the original target, and the long chat timeline needed windowing rather than one giant scroller - both were caught by measuring real data early instead of assuming.
