The Problem
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.
The Solution
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.
Key Features
| Feature | Detail |
|---|---|
Labels, threads, sanitized HTML bodies, inline images, attachments, .eml export and full-text search with label: / has:attachment filters |
|
| Chat | Spaces and DMs as a proper timeline, date separators, inline image previews, reactions, bot badges, per-conversation and global search |
| Calendar | One toggle per .ics, agenda and event detail, recurrence (RRULE / EXDATE) expanded on demand for the visible range |
| Drive | Folder browser and index-backed name search over the whole tree; images and videos preview, everything else downloads |
| Contacts, Keep, Tasks | Deduplicated contact cards with photos, Keep card grid, task lists |
| Photos, YouTube | Date-grouped gallery with cached thumbnails; uploaded videos joined to their metadata CSVs |
| Groups | Group members and activity tables, with topics.mbox discussions rendered by the same mail viewer |
| Generic viewer | Any product without a dedicated module (My Activity, Chrome, Maps, Pay…) opens as a tree of JSON, CSV and text |
| Private by default | Localhost-only binding, first-run password, Host/Origin allowlist, no telemetry, no CDN assets |
Technical Architecture
Stack: Next.js 16 · React 19 · shadcn/ui · TanStack Table · Zustand · SQLite (better-sqlite3, FTS5) · sharp · Vitest · Playwright · Docker
Data Flow
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.
Key Design Decisions
Offset-indexed mail, not copied mail
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.
Two data paths
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.
Extracted folder only, no zip support in v1
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.
Drive is download-only except images and videos
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.
Security for a no-auth-looking localhost server
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.
Engineering rules enforced mechanically
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.
Development Process
- Intent, spec, plan - Wrote the intent, then a spec grounded in the real export's shapes (Chat date format with U+202F, mbox counts, Photos sidecar naming) before any code
- Feasibility spike (M0) - Measured mbox and Chat scans native vs Docker bind mount, compared MIME parsers under a memory cap, and confirmed Range streaming and long-timeline behavior
- Indexer, auth and shell (M1) - Resumable
(path, size, mtime)indexing, first-run password, file-serving routes and the home dashboard - Core modules (M2-M4) - Mail, Chat, then Calendar agenda and Drive as the release gate
- Calendar overlay and P2 modules (M5-M6) - Multi-calendar view, Contacts, Keep, Tasks, Photos, Groups, YouTube and the generic viewer
- Packaging and hardening (M7) - Multi-arch Docker image, compose with localhost binding, README, MIT license, Playwright smoke test and a security review
Quick Start
# extract your Takeout first, then:
TAKEOUT_DIR=~/Takeout docker compose up
Open 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.
Impact & Takeaway
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.
Your Google data, readable again - a full Takeout browsed like the original products, self-hosted, read-only, and nothing leaves your machine.
Tags: Google Takeout viewer · self-hosted · open source · Next.js · shadcn/ui · SQLite · FTS5 · mbox viewer · Docker · privacy · data export · Google Chat export
