Access the project here: https://notion-atlas-extension.onrender.com/
A companion layer that turns a Notion workspace into a living map: it learns which pages matter from how you actually work, discovers connections nobody wrote down, and brings forgotten knowledge back exactly when it becomes useful again.
Notion is excellent at helping people create and organize information. The problem appears after a workspace grows. With hundreds or thousands of pages, you start losing track of information you already wrote: how pages relate, what you actually use, what you were working on last month, which old notes just became relevant again. The workspace becomes an archive you search instead of a map you navigate.
Atlas answers one question: what does my workspace actually look like as a brain, and which part of it is useful to me right now? It infers structure instead of asking you to maintain it. Your workspace already contains a map of how you think; Atlas reveals it.
A TypeScript monorepo containing a browser extension, a background sync and analysis engine, and a companion web app. Atlas connects over Notion's official OAuth API, indexes pages and their relationships, learns from real reading behavior recorded by the extension, and serves four hero experiences:
Adaptive Home. Opens on what matters right now: current work ranked by a blend of recency, sustained engagement, thread context, personalized graph rank, and upcoming dates, never a plain ORDER BY last_edited. Threads (active regions), recent changes grouped by region, and one resurfaced page complete the start screen.
Map. The workspace as cartography, not a particle simulation. Communities detected in the graph become tinted regions with place names; important pages read as cities; navigation habits, semantic similarity, links, and hierarchy draw as distinguishable roads. Semantic zoom reveals labels progressively; selecting a page fades everything unrelated to its k-hop neighborhood, and every connection can explain itself.
Resurface. Finds pages that are historically important, quiet for weeks, and strongly related to what you are doing today, then says why in plain terms with the actual graph path ("System Design Interview Prep → Distributed Systems Notes, last opened 24 days ago, 85% similar"). Exposure history and feedback keep it from nagging.
Wren, the navigator. A cute agentic chatbot with zero API keys, zero cloud calls, and zero cost to run. Wren classifies your question with the same feature-hashed n-gram embeddings the semantic layer uses, then plans visible tool calls over the product's real machinery — search fusion, weighted-Dijkstra path tracing, the importance and resurface rankers, Louvain regions — and every step it shows carries real numbers. Ask "how is X connected to Y?" and it walks the actual strongest path, hop by hop. An online logistic model trained on your recommendation feedback personalizes its suggestions and says exactly what it learned ("active reading time weighs more for you").
Around them: fused instant search (⌘K), mined navigation Trails with honest confidences, quiet workspace Insights (landmarks, emerging and dormant regions, orphans, probable duplicates), and privacy controls that actually delete things.
| Adaptive Home | Resurface |
|---|---|
![]() |
![]() |
| Focus mode + explanations | Insights |
|---|---|
![]() |
![]() |
| Wren, the navigator — agentic answers from your own graph and notes |
|---|
![]() |
npm install
npm run build
npm run dev # http://127.0.0.1:8792No Docker, no accounts, no API keys. Atlas boots on an embedded PostgreSQL (PGlite + pgvector) stored under ~/.cache/notion-atlas, and the first screen offers a sample workspace: 77 pages and eight weeks of scripted history that exercise the entire production pipeline (embeddings, semantic edges, community detection, layout, ranking). Nothing in the demo is hand-placed; delete the activity history in Settings and watch the product degrade honestly.
To connect a real workspace, create a public integration at notion.so/profile/integrations, set NOTION_CLIENT_ID and NOTION_CLIENT_SECRET in .env (see .env.example; the OAuth redirect URI is derived from the request), restart, and choose "Connect your Notion".
The extension: cd apps/extension && node build.mjs, then load apps/extension/dist/chrome unpacked at chrome://extensions (Firefox: about:debugging → Load Temporary Add-on → any file in dist/firefox), open its options page, and click "Connect to Atlas".
Notion
REST API Webhooks (signed, deduped, treated as hints)
│ │
Browser extension ▼ ▼
(page opens, active ┌──────────────────────┐
reading, paths) ──▶ │ Fastify API │ typed routes, zod at the
│ + durable job queue │ boundaries, TTL caches
└──────────┬───────────┘ keyed by graph version
│ SKIP LOCKED claims, idempotency keys,
│ backoff, dead-letter, heartbeat reaping
┌──────────▼───────────┐
│ Workers │ sync → embed → semantic
│ (inline or separate) │ edges → PageRank+importance
└──────────┬───────────┘ → Louvain regions → cached
│ force layout → trails,
┌──────────▼───────────┐ metrics
│ PostgreSQL + pgvector│
│ (embedded PGlite or │
│ docker-compose) │
└──────────┬───────────┘
▼
React web app (custom Canvas2D map renderer)
One process runs everything by default. Set DATABASE_URL (see docker-compose.yml) to move onto a real PostgreSQL, where workers scale out as separate processes (npm run worker); the CI matrix runs the storage suite against both drivers.
Pages are nodes. Five heterogeneous edge types carry different meaning and different weight:
| Type | Source | Notes |
|---|---|---|
parent |
Notion hierarchy | structural background, weighted lowest |
mention |
page links and mentions in content | a deliberate authorial act |
relation |
database relation properties | typed, bidirectional |
semantic |
embedding similarity | discovered, capped, explains itself with a % |
navigation |
repeated page-to-page moves | behavioral, time-decayed |
Density is controlled deliberately: semantic edges need to clear a calibrated similarity threshold, get a stronger bar when either page is tiny (unstable vectors), skip pairs hierarchy already explains, and respect a per-page degree cap applied strongest-first. Navigation edges require repeated transitions (decayed count, not raw), so an accidental click never becomes structure, and old habits fade with a three-week half-life.
Embeddings are provider-pluggable: hash (default) is a local hashed n-gram embedder (feature hashing with signed projections, sublinear TF, L2 norm) that is deterministic, offline, and free; openai swaps in text-embedding-3-small with one env var and a re-embed job. Both store into the same pgvector column with an HNSW index.
Similarity uses retrieve-then-rerank: candidates come from approximate nearest neighbors on raw vectors, then pairs are re-scored with the workspace centroid removed (anisotropy correction: short notes share a baseline of common vocabulary that inflates every raw cosine; subtracting the corpus mean leaves only distinctive overlap). Search queries, which are keyword-shaped and nearly baseline-free, deliberately use the raw space instead. The thresholds are calibrated against the seeded corpus in the pipeline test.
packages/ranking is pure data-in/data-out, so every scoring decision is unit-tested:
- Importance is a transparent blend of percentile-normalized signals (PageRank, decayed visit frequency, decayed active reading time, connectivity, edit and visit recency). The per-page breakdown is stored and inspectable (
GET /api/dev/importance/:id); no invented coefficients pretending to be machine learning. - Continue ranks current work: visit and edit recency, engagement, thread context, personalized PageRank from the active set, and upcoming dates, so a deadline two days out can outrank something touched an hour ago.
- Resurface multiplies importance × current context relevance × staleness × novelty. It requires real usage history and a quiet period, names its anchor page, and suppresses itself through exposure records and feedback ("not relevant" silences a page for a month; "don't show again" is forever).
- Related blends direct edges, a two-hop expansion, personalized PageRank from the page, cluster co-membership, and engagement, with each contribution kept as a typed reason. The blend function is one small module away from being replaced by learning-to-rank.
- Search is reciprocal rank fusion over four independent lists: token-based title match, tsvector full-text relevance, semantic similarity, and a behavioral prior.
Every recommendation carries its reasons; every surface exposes a "Why?".
An agentic chat over the workspace that costs nothing to run: no API keys, no cloud model, no tokens. Wren classifies questions with the same feature-hashed n-gram embeddings the semantic layer uses (raw-cosine matching against intent exemplars, surface patterns as priors), then plans visible tool calls over the product's own machinery — search fusion, weighted-Dijkstra path tracing, the importance and resurface rankers, Louvain regions — and answers with the numbers to back every claim. A small online learner (logistic regression over the six named importance signals, trained on your recommendation feedback) personalizes its suggestions and says exactly what it learned ("active reading time weighs more for you"). Every step Wren takes is shown in the transcript; nothing it says can't be traced to a query.
- Weighted PageRank via power iteration (dangling mass redistribution, weight-proportional link following)
- Personalized PageRank seeded by recent activity, the connective tissue behind Home, Resurface, Related, and search boosts
- Louvain community detection (two-phase modularity optimization) with stable cluster matching across recomputes, so manual renames survive
- Strongest-path Dijkstra with cost
1/(0.15+w), powering "how are these connected?" with paths a person recognizes - Capped k-hop neighborhoods (strongest edges first) for focus mode
- Barnes-Hut force layout with cluster gravity, seeded and deterministic, computed server-side and cached so the Map only ever draws
Webhook events are signals that state may have changed, never state itself: verified (HMAC over the exact raw body), deduped by event id, they only enqueue a page_sync that re-fetches truth from the API. Sync jobs carry idempotency keys that include last_edited_time, so event bursts collapse; a fetched page older than what is stored is ignored (out-of-order guard); missing pages become tombstones; child pages discovered inside block content are crawled recursively because the search endpoint is not assumed to enumerate the workspace. The queue gives at-least-once execution with exponential backoff, dead-lettering, and heartbeat-based crash recovery, and the db test suite proves the duplicate/out-of-order/retry behaviors.
A custom Canvas2D renderer (evaluated Sigma.js, Cytoscape, and D3 first; the cartographic look needed full pipeline control, and at the target scale a well-culled Canvas2D beats a WebGL dependency):
- draw-on-demand: zero rendering while idle; a ticker that races
requestAnimationFrameagainst a timeout runs only during camera animation (and survives environments where rAF never fires) - positions precomputed by the layout job; the client never runs physics
- viewport culling, batched edge strokes grouped by style, spatial-grid picking
- semantic zoom: labels place greedily by importance into a screen-space occupancy grid, region names set in a serif fade out as page labels take over, emoji icons appear up close
- regions render as convex hulls stroked thick with round joins (a soft padded blob for the price of one path)
npm run seed:large replaces the workspace with 2,000 synthetic pages across 12 topical clusters and runs the same pipeline, which is how the smooth-at-2k claim is validated rather than asserted: on an M-series laptop the full pipeline (embeddings, semantic edges, PageRank, Louvain, layout, trails, metrics) settles in about 43 seconds on embedded PGlite, and the resulting map (2,000 nodes, ~7,500 edges) renders at about 2.9 ms per frame fully zoomed out and under 1 ms zoomed in, several times inside the 60 fps budget.
Atlas handles private knowledge, so the rules are explicit:
- The extension only ever observes
notion.so/notion.site. It records page opens, page-to-page navigation, and active reading time, counted only while the tab is visible, focused, and recently interacted with; an idle tab overnight earns nothing (the tracker is unit-tested on exactly that). - Every control in Settings is enforced server-side, not hidden client-side: ingestion rejects what preferences forbid.
- Deletion is real: activity history, the semantic index, or the whole workspace (including the stored OAuth token) each have a button.
- Notion tokens are AES-256-GCM encrypted at rest; the activity endpoint requires a workspace-scoped ingest token; webhook payloads are signature-verified.
- Everything lives on your machine by default. No telemetry.
Things that turned out to matter:
- Short-text embeddings are a calibration problem. Raw hashed n-gram cosines on note-sized documents are dominated by shared common vocabulary; the fix that survived measurement was centroid removal for document pairs, the raw space for keyword queries, a stronger bar for tiny pages, and thresholds locked in by an end-to-end pipeline test over the seeded corpus.
- The demo is the product path. Demo mode does not mock anything; it seeds data and runs the same jobs. That decision caught real bugs (a workspace-scoped ETag miss, a canvas sizing loop) that mocks would have hidden.
- Behavior needs half-lives. Visits, transitions, and navigation edges all decay (14 to 28 day half-lives) with periodic exact recomputes from the event log, so the product reflects your current life, not your permanent history.
- Absolutely-positioned replaced elements do not stretch. A canvas with
inset: 0keeps its intrinsic size; combined with a ResizeObserver writing the backing store, that is a feedback loop to a 16-million-pixel canvas. Explicit width/height plus a change guard fixed it.
apps/api Fastify server, routes, inline worker, integration tests
apps/web React app, design system, custom map renderer
apps/extension MV3 extension: tracker, queue, side panel, options
packages/graph PageRank, Louvain, paths, layout (tested)
packages/ranking importance, continue, resurface, related, threads (tested)
packages/search embedding providers, chunking, RRF fusion (tested)
packages/notion API client, OAuth, extraction, webhooks (tested)
packages/db schema, migrations, durable queue, activity ledger (tested)
packages/shared types, API contracts, config validation, decay math
workers job handlers, pipeline, demo corpus, scale seeder
npm test runs 110+ tests across every layer, including a full pipeline integration test (seed → embeddings → edges → regions → layout → resurface) and API tests against the real stack via fastify.inject. CI runs typecheck, lint, tests, build, and the storage suite against real PostgreSQL with pgvector.
Learned ranking on the recommendation features (the scorer interface is already shaped for it); collaborative workspace intelligence; richer topic evolution over time; graph history and time-travel for the Map; an LLM pass for region naming and connection summaries where a key is configured, kept strictly optional, the way the rest of the product treats AI: a supporting tool, not the point.






