Capture a paper from any browser. Annotate the original PDF with Apple Pencil. Ask your own AI grounded questions, then share a revocable, view-only reading.
Your papers, your ink, your conversations, on a stack you control.
The filesystem is the source of truth. Delete the index, keep your library.
Click an arXiv paper directly with the browser extension enabled, then annotate and understand it in Papernook:
How it works · Quick start · Workflow · Highlights · Integrations · Documentation
One server on a machine you control, one folder of files, and whichever AI you already pay for.
flowchart LR
ext["Browser extension<br/>Safari · Chrome"]
ipad["iPad or phone<br/>Pencil ink"]
server["papernook<br/>Next.js, your machine"]
disk[("data/<br/>PDFs · metadata · chats<br/>plain files")]
index[("SQLite FTS5<br/>rebuildable index")]
ai["Your AI<br/>Claude Code · Codex<br/>API · local model"]
ext -->|"PDF navigation"| server
ipad --> server
server <--> disk
server <--> ai
disk --> index
index -->|"search"| server
- Capture. Open a PDF on arXiv or a publisher page. The extension sends the navigation to your server, which downloads the paper, asks your AI for a topic, tags, and a summary, and files it.
- Annotate. Highlight and draw in the browser. The ink is written into the PDF itself, so any reader on any device opens it later, with or without Papernook.
- Ask. The chat sits beside the paper and answers from the paper. Its conversations are JSONL files next to the PDF, not rows in someone's database.
Nothing leaves your machine except the requests you configure to an AI provider. Delete the SQLite index and the library rebuilds from the files.
curl -fsSL https://raw.githubusercontent.com/affromero/papernook/main/scripts/install.sh | bash
# clones the repo, detects Codex/Claude when available, writes .env, docker compose upOpen http://localhost:3000 and create a profile. The two-minute wizard checks the configured agent—or, when none is configured, selects a ready local Codex/Claude CLI—creates personal capture tools, and shows optional WebDAV compatibility when configured. The admin can also add the tldraw production key used by visual paper canvases. An explicit provider that is unavailable is reported, never replaced.
For a reproducible production install, use a release tag:
git clone --branch v0.3.0 --depth 1 https://github.com/affromero/papernook.git
cd papernook
./scripts/install.shThe installer puts a papernook command in ~/.local/bin, so updates are one
word from anywhere:
papernook update # newest release tag: back up, rebuild, check health
papernook update --check # what an update would move to, changing nothing
papernook update --main # follow the development branch instead of releases
papernook status # running version, newer release, healthAn update refuses to run on a clone with uncommitted changes, backs the library
up with scripts/backup.sh first (--no-backup skips it), and deepens a
--depth 1 install so releases stay comparable. In a clone
installed before the command existed, run ./scripts/papernook link once.
| Step | What happens |
|---|---|
| 1. Capture | Save arXiv, PDF, and publisher pages from Safari, Chrome, or Papernook. AI proposes the topic, tags, summary, and links. |
| 2. Annotate | Highlight, add text, or draw in the web reader. Apple Pencil selects drawing automatically, and changes save to the PDF. |
| 3. Understand | Chat with the paper beside the reader or switch between Reading and Chat on a tablet. |
| 4. Practice | Turn an answer into a Pencil-ready exercise PDF, add margins, or append blank pages without shifting existing ink. |
| 5. Share | Send a revocable annotated reading. Conversation snapshots are explicit, immutable, and off by default. |
| 6. Explore | Discover proposes related work grounded in what you already hold, and the graph links papers, authors, topics, and tags. |
Capture, end to end — the browser extension redirects an arXiv PDF into the reader, citations preview on hover, and two clicks file the paper into the library:
| Browser | Install | Status |
|---|---|---|
| Mac App Store | live | |
| Chrome Web Store | live |
Both browsers run the same extension/ source. To run an unreleased build,
npm run build:chrome produces a loadable zip, and every
release ships one.
A paper you actually work through ends up in three places at once: the tablet holding the ink, the reader holding the highlights, and a chat tab holding everything you asked about it. The annotations live in someone else's database, the chat history belongs to whichever model you used that month, and none of it survives leaving the product.
Papernook keeps those three in one place you own. Ink goes into the PDF itself, so any reader on any device still opens it. Metadata, summaries, and conversations are plain files on your disk. The AI is a setting, not a dependency.
| Your workflow | Papernook's approach |
|---|---|
| Files stay portable | PDFs hold their own annotations; metadata, summaries, and chats use plain files. |
| Your AI stays replaceable | Choose Claude Code, Codex, an API provider, or a local OpenAI-compatible server. |
| Reading stays connected | Search, citation previews, grounded chat, and exercises live beside the paper. |
| Readers stay organized | The library is shared; profiles separate chats, capture tokens, and Zotero connections by reader. |
| Sharing stays intentional | Links are revocable and view-only; conversations appear only when explicitly snapshotted. |
| The index stays disposable | SQLite accelerates search, but the filesystem remains the source of truth and rebuilds the index. |
Full module map and data flow
Every node is a real module in this repository.
The PDF is the portable annotation layer. The web reader writes highlights, text, and ink back with version checks so another device cannot be overwritten silently.
| Reading surface | Source of truth | Sync boundary |
|---|---|---|
| Native PDF annotations | data/papers/<topic>/<slug>.pdf |
Autosave from the web reader and flow to shared readings and optional WebDAV. |
| Reference preview | Current PDF | Opens the cited entry beside the citation via embedded links or, when the PDF has none, text-recognized citations (numeric and author-year). |
| Conversations | Per-profile JSONL | Stay out of shares unless selected, but anyone with instance access can switch profiles and read them. |
flowchart LR
subgraph capture["Capture"]
add["/add route<br/>src/app/add"]
capi["/api/v1/capture<br/>session-authed"]
pipe["capture pipeline<br/>src/lib/capture<br/>normalize · download · analyze"]
jobs["jobs/index.ts<br/>background capture · /inbox review"]
discover["discover.ts<br/>related work not yet in the library"]
zotero["zotero.ts<br/>metadata-only refresh · personal/group"]
zcatalog["zotero-catalog.json<br/>per-profile · compact · atomic"]
end
subgraph agent["Agent layer (src/lib/agent)"]
registry["Sidedoor provider registry"]
claude["claude-code.ts"]
codex["codex.ts"]
api["api.ts<br/>anthropic · openai · local"]
attach["attachments.ts<br/>paths · scp · base64"]
end
subgraph library["Library (src/lib/library)"]
papers["papers.ts<br/>data/papers + data/library"]
chats["chats.ts<br/>jsonl per account"]
index["index-db.ts<br/>SQLite FTS5"]
scanner["scanner.ts<br/>chokidar, disk wins"]
expand["expand.ts<br/>pdf-lib growth"]
pdffile["pdf/file.ts<br/>versioned · locked · atomic saves"]
exercises["exercises.ts<br/>md → exercises.pdf"]
shares["shares.ts<br/>revocable reading snapshots"]
graphlib["graph.ts<br/>papers · authors · topics · tags"]
citations["citations/<br/>CSL · RIS · BibTeX · APA · Harvard · Vancouver"]
libctx["context/<br/>related papers + reference→library matching"]
end
subgraph ui["App (src/app + src/components)"]
picker["ProfilePicker"]
libview["LibraryView"]
chatpanel["ChatPanel<br/>markdown + KaTeX · ThreeSandbox iframe"]
pdfreader["PdfReader<br/>pdf.js annotations + reference previews"]
viewer["/viewer + ViewerShell<br/>external PDFs via /api/v1/viewer/pdf proxy"]
wizard["WelcomeFlow"]
shareview["ShareButton + /share<br/>view-only reading"]
citeui["CitationActions + export routes"]
canvasui["CanvasBoard<br/>tldraw canvas per paper"]
graphui["LibraryGraph + /graph<br/>cytoscape-fcose"]
discoverui["DiscoverClient + /discover"]
inboxui["CaptureJobs + ReviewStrip<br/>/inbox, unconfirmed captures"]
end
gate["Sidedoor access (src/lib/auth)<br/>shared password · passkeys · sessions"]
browserext["Safari + Chrome extension<br/>extension/<br/>PDF navigations → /viewer"]
dav["rclone WebDAV sidecar<br/>docker-compose.yml<br/>serves data/papers ONLY"]
authsync["credential-sync sidecar<br/>networkless · auth JSON only"]
hostauth["host CLI homes<br/>read-only"]
ipad["iPad browser<br/>Pencil ink + chat"]
sidedoor["thesidedoor<br/>PWA + QR reach"]
gate --> ui
add --> pipe --> papers
inboxui --> jobs --> pipe
discoverui --> discover --> registry
discover -->|"suggested url"| add
browserext --> viewer
viewer --> pdfreader
viewer --> capi --> pipe
zotero --> zcatalog
zcatalog -->|"explicit one-paper import"| pipe
pipe --> registry
chatpanel --> registry
registry --> claude & codex & api
hostauth --> authsync --> claude & codex
claude & codex & api --> attach
papers --> index
scanner --> index
papers <--> dav <--> ipad
exercises --> papers
shares --> papers
shareview --> shares
citeui --> citations --> papers
citations --> index
canvasui --> papers
graphui --> graphlib --> papers
chatpanel --> libctx --> index
pdfreader --> libctx
expand --> pdffile --> papers
pdfreader -->|"ETag + If-Match"| pdffile
ipad --> pdfreader
libview --> index
picker --> wizard
sidedoor --> ui
Papernook focuses on the self-hosted annotated-paper-to-understanding loop. Zotero remains substantially stronger for live citations, thousands of styles, and mature group reference management.
Compare built-in capabilities
The matrix counts only officially documented, built-in behavior that matches the capability exactly, without third-party plugins or handoffs.
| Capability | Papernook | Zotero | Paperpile | Readwise Reader | NotebookLM |
|---|---|---|---|---|---|
| One-click capture from supported publisher/PDF pages | ✓ | ✓ | ✓ | ✓ | ✗ |
| Organize with collections/folders, tags, and search | ✓ | ✓ | ✓ | ✓ | ✗ |
| Built-in web PDF highlighting and annotation editor | ✓ | ✓ | ✓ | ✓ | ✗ |
| Export reference metadata as RIS or BibTeX | ✓ | ✓ | ✓ | ✗ | ✗ |
| Live citations and bibliographies in writing tools | ✗ | ✓ | ✓ | ✗ | ✗ |
| Dedicated collaborative group libraries with roles | ✗ | ✓ | ✓ | ✗ | ✗ |
| Import one selected Zotero PDF into an AI workspace | ✓ | ✗ | ✗ | ✗ | ✗ |
| Use Zotero annotations as per-profile AI context | ✓ | ✗ | ✗ | ✗ | ✗ |
| Self-host the complete app and data | ✓ | ✗ | ✗ | ✗ | ✗ |
| Annotations are saved directly into the standard PDF | ✓ | ✗ | ✓ | ✗ | ✗ |
| Edit the same PDF from iPad apps over WebDAV | ✓ | ✗ | ✗ | ✗ | ✗ |
| Preview linked references without losing your place | ✓ | ✗ | ✗ | ✗ | ✗ |
| Grounded document chat inside the reading app | ✓ | ✗ | ✗ | ✓ | ✓ |
| Choose a local, SSH, or API AI backend | ✓ | ✗ | ✗ | ✗ | ✗ |
| Generate printable exercise PDFs from a paper | ✓ | ✗ | ✗ | ✗ | ✗ |
| Grow PDF margins or add pages without shifting ink | ✓ | ✗ | ✗ | ✗ | ✗ |
| Whole-library visual relationship graph | ✓ | ✗ | ✗ | ✗ | ✗ |
| Share an uploaded, annotated PDF by link | ✓ | ✗ | ✓ | ✗ | ✗ |
| Share selected owner conversations beside that PDF | ✓ | ✗ | ✗ | ✗ | ✗ |
Sources: Zotero collections, search, and citation styles, PDF reader and annotation editor, word processor integration, standardized formats, and annotation storage; Paperpile exports, embedded annotations, and annotation sharing; Readwise chat, exports, and sharing limits; and NotebookLM study outputs and quizzes.
Choose a provider in browser setup or Settings. Sidedoor stores provider credentials, models, and endpoints in the instance configuration. You can use a local CLI, run a CLI over SSH, provide an API key, or connect a local model server.
SSH execution requires Python 3 and the selected CLI on the remote host. The shared supervisor limits execution time and stops its process group when the connection closes. A network failure can prevent confirmation of remote cleanup.
Providers and configuration
| Mode | Configuration | Keyless |
|---|---|---|
| Claude Code CLI, local | Select in setup or Settings | ✓ |
| Codex CLI, local | Select in setup or Settings | ✓ |
| Claude Code over SSH | Save the SSH connection in setup or Settings | ✓ |
| Codex over SSH | Save the SSH connection in setup or Settings | ✓ |
| Anthropic API | Save the API key in setup or Settings | ✗ |
| OpenAI API | Save the API key in setup or Settings | ✗ |
| Ollama | Save the endpoint and model in Settings | ✓ |
| llama.cpp server | Save the endpoint and model in Settings | ✓ |
| vLLM server | Save the endpoint and model in Settings | ✓ |
Settings detects local endpoints and installed models. Docker reaches local
servers through host.docker.internal; Papernook does not publish model ports.
Images use the native attachment mechanism for each transport. Local image chat
requires a vision-capable model, and paper capture needs enough context for
roughly 60,000 characters. Provider errors surface without silent fallback.
Web access is on by default and can be disabled by an admin in Settings.
Claude Code, Codex, Anthropic, and the OpenAI Responses API use their native
search tools. Ollama, llama.cpp, vLLM, and custom OpenAI-compatible endpoints
use Papernook's bounded web_search / SSRF-guarded web_fetch tool loop.
Docker Compose includes a SearXNG service for those searches, exposed only on
host loopback by default (127.0.0.1:8888). Set WEB_SEARCH_BASE_URL to use a
different SearXNG instance.
Local models must support OpenAI-compatible function calling; llama.cpp and
vLLM also need their tool-call parser/template enabled for the selected model.
The installer prefers an authenticated local Codex CLI, then Claude Code. In
Docker, Compose gives a networkless sidecar read-only access to the selected
CLI credential directory and copies only its auth JSON into a private volume.
Use Reload CLI login in Settings after signing in or out; active chats are
not interrupted. CLI history, sessions, and project configuration stay
unmounted. An installed but logged-out CLI is shown as needing login, not
ready. macOS keeps Claude credentials in Keychain, which containers cannot
read; run claude setup-token and set CLAUDE_CODE_OAUTH_TOKEN in .env when
using Claude Code through Docker.
Configure both CODEX_AUTH_DIR and CLAUDE_AUTH_DIR only when both local CLIs
should be selectable.
Each paper has a shared visual canvas for screenshots, links, videos, diagrams,
and freehand notes. The canvas uses the tldraw SDK, which requires a hobby,
trial, or commercial license key on production domains. The admin can get and
save a key from the setup wizard or Settings → Canvas; no rebuild or restart
is required. TLDRAW_LICENSE_KEY remains available for environment-managed
deployments, and a key saved in Settings takes precedence.
License keys are public and validated by tldraw in the browser. If a key is missing, invalid, expired, or issued for another domain, Papernook shows an actionable setup panel instead of an empty canvas.
Each profile independently connects one personal or accessible group Zotero library. Papernook refreshes a compact, per-profile catalog of metadata and annotations every 30 minutes or on demand. It can catalog the whole library or selected collections; subcollections are included automatically.
Connecting never downloads PDFs and never calls an AI provider. Browse or search the catalog in Settings, then explicitly import one paper when you want it in Papernook. That action downloads only that paper (100 MB maximum), runs the configured AI filing flow once, and adds it to the shared library. Existing Zotero annotations for that paper become per-profile context for the connected profile's conversations. They are treated as untrusted quoted source material and are never added directly to shared links.
Create a dedicated key at zotero.org/settings/keys, enable library read access, then open Settings → Zotero sync. File access is required only to import stored PDFs; write access is never required. Imports join the shared Papernook paper library, while connections, catalog associations, annotation context, and chats remain per-profile. Deduplication scopes Zotero item keys to their source library and also checks DOI and arXiv identity.
The catalog is a disposable cache under
data/users/<username>/zotero-catalog.json; Zotero remains its source of truth.
The size of Zotero file storage—even libraries larger than 1 GB—does not cause
Papernook to copy those files: refresh transfers metadata only. The local
catalog is capped at 200,000 records and 64 MB; if metadata exceeds either
limit, refresh fails visibly without advancing the sync cursor or replacing the
last valid catalog.
Incremental refreshes process metadata updates and deletions without deleting
already imported Papernook papers. Later Zotero edits refresh only
Zotero-owned metadata, leaving the PDF, Pencil ink, topic, summary, chats,
canvas, and exercises untouched. See Zotero's
versioned sync API,
annotation storage model,
and full-text API.
Every paper’s Cite menu copies APA, Harvard, or Vancouver bibliography entries, or downloads CSL JSON, RIS, or BibTeX. Library exports respect the active search, topic, and tag filters.
Other reference managers
| Tool | Sync | How |
|---|---|---|
| Zotero | ✓ built in | Metadata-first catalog; explicitly import individual PDFs for AI |
| Mendeley | possible | Public REST API still up, but OAuth app registration adds friction |
| Readwise Reader | possible | Clean token-authed V3 API exposes saved documents |
| Paperpile | workaround only | No public API; its Google Drive folder sync could feed a watched folder |
| EndNote, ReadCube | ✗ | No usable public API |
Docker Compose runs the Next.js app, a networkless CLI credential-sync sidecar, and an rclone WebDAV sidecar. The web reader is the primary annotation surface on desktop and iPad. WebDAV remains an optional compatibility route for external PDF apps when fully configured.
The WebDAV sidecar serves data/papers only; chats, crops, canvases, and
unconfirmed captures stay private. Connect through
Tailscale or a hardened custom domain. Settings
generates the correct device QR code for the address in use.
The installer creates the first Admin profile and shared password locally.
After entering the password, a reader can save a passkey with Apple
Passwords or another WebAuthn manager before choosing a profile. This step is
optional. A saved passkey opens the picker on later visits. Anyone admitted can
choose Admin and manage settings. Choosing another profile removes Admin access.
Admin can manage passkeys and sessions in settings. If the shared
password is lost, the server owner runs papernook access reset locally.
Share links remain readable without a login because the unguessable share id
is the capability for one paper. For custom domains, Caddy terminates TLS in
front of app and WebDAV ports that bind to 127.0.0.1 by default. See
share household access and
custom-domain setup.
Start with the visual documentation home.
| I want to… | Guide |
|---|---|
| Learn the everyday workflow | User guide |
| Share access by domain or Tailscale | Share household access |
| Capture from Chrome or Safari desktop | Browser extension |
| Capture from an iPhone or iPad | Add to papernook Shortcut |
| Annotate from an iPad | iPad annotation guide |
| Serve the app on a custom domain | Custom domain setup |
| Back up, restore, or watch the server | Operations |
| Understand the security model | Security policy |
npm install # postinstall patches tldraw, copies the sidedoor service worker, vendors three
npm run dev
npm run build:chrome # Web Store-ready zip in build/
npm run test:chrome # packaged extension in real Chromium
npm run ci # lint + typecheck + vitest + build (pre-push runs this too)
npm run test:e2e # Playwright journeys against committed docs screenshots
npm run screenshots # intentionally refresh docs/images
pre-commit install --install-hooks # two-tier local gatePapernook is available under the MIT License. Third-party dependencies and assets remain subject to their respective licenses.
The Dead code workflow runs on pull requests, main-branch pushes, and weekly.
It uses the pinned shared maintenance action
to reject new findings, stale reviewed exceptions, and scanner failures. Reports
are attached to each workflow run. Dependabot updates the shared action and Knip.
Keep runtime entry points in knip.json. Any retained finding in
.maintenance-exceptions.json needs an exact identity and a review reason.


