linden.md
Documentation

linden.md

Linden is your personal knowledge tree: a private home for your notes, blog, and tasks, plain Markdown you own, on every device, open to the AI you choose.

Why linden.md

linden.md comes out of nearly thirty years of note-keeping and a long line of tools that each got something right and something wrong. The full story, and how the AI age changed what a second brain has to be, is on its own page: Why I built linden.md. For the tour of what it all adds up to, see everything linden.md can do.

What it is

What it's not

How it compares

Each of these is a good tool. linden.md borrows what they do well and drops what got in the way.

ToolWhat it nailsWhat linden.md adds
EvernoteCapture anywhere, always syncedA library you self-host, with built-in semantic and full-text search
ObsidianLocal Markdown, fast and flexibleSync, publishing, native apps, and agent access built in, not add-ons
Markdown + GitTotal control and portabilityThat control with a real editor, mobile apps, and one-tap publish
BearA beautiful writing experienceThe same polish, plus every platform, self-hosting, and a direct line to your AI

Honestly, I encourage you to try all of these, and any others, until you find the one that fits the way you think. linden.md is the one that fits me. Give it a try too.

Good Markdown tools on Mac

A far-from-exhaustive list of editors and second brains worth your time: iA Writer, Ulysses, Bear, Obsidian, Typora, MarkText, Zettlr, Logseq, Joplin, NotePlan, Craft, Drafts, Standard Notes, Evernote, MacDown, Marked 2, and plain Markdown in a Git repo.

Get the apps

Start here. Grab the app for your devices, then install the daemon below to give it a home to sync with. Linden is native on iPhone, iPad, Mac, and Windows. Android is on the way. On Linux, run the daemon and use the web app.

iPhoneTestFlight invite sooniPadTestFlight invite soonMacDirect downloadWindowsDirect download

Prefer the browser? The web app and your public blog work at your own linden.md address once your daemon is paired.

Install the daemon

One command sets up the daemon, starts it at login, and keeps it updated.

macOS and Linux

curl -fsSL https://linden.md/install.sh | sh

Windows

irm https://linden.md/install.ps1 | iex

The installer drops the linden binary in ~/.local/bin (on Windows, %LOCALAPPDATA%\Programs\Linden), adds it to your PATH, and registers a login-start service. Your data lives in ~/.linden.

Connect an app

Once it is running, pair a device:

linden pair

This prints a QR code. In the app, open Settings › Scan to pair and point your camera at it. The app and daemon are now linked.

The daemon

The daemon is the heart of linden.md. It runs quietly on your computer and:

Because it is just a folder of files on your machine, you can back it up, move it, or walk away at any time. Nothing is locked in.

The gateway

Your daemon lives behind your home or office network, so the gateway (which we run at linden.md) gives it a stable public address and a way for your devices to reach it from anywhere.

The gateway providesThe gateway does not
A public address for your blog (you.linden.md) and optional custom domains.

A real-time relay so your phone, laptop, and the web app reach your daemon from anywhere.

Hosting for app updates and this site.
Store your notes, tasks, or files. It is a blind relay.

Read your content. Traffic passes straight through to your daemon.

Get in the way when offline. Your apps keep working locally and sync when the daemon returns.
You can run entirely without the gateway on your local network. The gateway is only for reaching your daemon from anywhere and for publishing.

The apps

One library, the same on every screen. Everything syncs in real time and works offline; edits made anywhere show up everywhere once your devices reconnect.

AI agents

Every Linden account is an MCP server. The connection URL is https://you.linden.md/mcp (your own address; the app shows it under Settings › Connect an agent), served over plain streamable HTTP so it works with any MCP client.

The fastest way to hand Linden to an agent that can fetch a URL: copy a one-time link from the app (Settings › Connect an agent) and paste it into the chat. The page at https://you.linden.md/agent.md tells the agent how to connect. If the link includes a pair code, the agent claims a key itself (one use, one hour). Machine-readable twin: https://you.linden.md/.well-known/mcp.json.

Agents also sign in two other ways. With OAuth, the agent asks to connect on its own, a short match code appears, and you approve it in the Linden app (Settings › Connect an agent). With a bearer token, you mint a key under Settings › AI agents (or linden token create <name> write on a self-hosted daemon) and give it to the agent. Either way, each connection is scoped to read-only, edit, or full access, and you can revoke any of them at any time. Per-client steps:

Claude

claude.ai and Claude Desktop: open Settings › Connectors › Add custom connector, paste https://you.linden.md/mcp, and connect. When the match code appears, approve it in the Linden app.

Claude Code:

claude mcp add --transport http linden https://you.linden.md/mcp

Then run /mcp inside Claude Code to authenticate (the same in-app approval). Prefer a plain key? Mint a token and pass it as a header instead:

claude mcp add --transport http linden https://you.linden.md/mcp \
  --header "Authorization: Bearer <token>"

ChatGPT

Turn on developer mode under Settings › Apps & Connectors › Advanced settings, then add an MCP server: name it Linden, use the same https://you.linden.md/mcp URL, and choose OAuth. Approve the match code in the Linden app.

Once connected, regular chat and deep research use Linden's search and fetch tools automatically. One caveat: full tool use in developer mode (creating and editing notes) requires ChatGPT Business or Enterprise; on Plus and Pro, developer-mode connectors are read-only.

Gemini CLI

The Gemini CLI (and the Gemini API's MCP support) connects with a bearer token. Mint one under Settings › AI agents, then add Linden to ~/.gemini/settings.json:

{
  "mcpServers": {
    "linden": {
      "httpUrl": "https://you.linden.md/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}

Note: the consumer Gemini app cannot add user MCP servers (Enterprise only), so this is for the CLI and the API.

Grok

On grok.com, open Settings › Connectors, add a connector with https://you.linden.md/mcp, and finish the OAuth flow by approving the match code in the Linden app. (The xAI API takes a plain bearer token instead.)

Cursor & VS Code

In the Linden app, open Settings › Connect an agent and tap Cursor or VS Code: it deep-links into the editor with Linden pre-filled. Or add it by hand as an HTTP MCP server at https://you.linden.md/mcp, with OAuth or a bearer token header.

Any MCP client

Anything that speaks MCP over streamable HTTP can connect to https://you.linden.md/mcp. OAuth-capable clients discover and register on their own (standard RFC 8414/9728 discovery); you just approve the match code in the app. Everything else sends a token you mint:

linden token create my-agent write   # self-hosted CLI; on Linden Cloud, mint it in Settings › AI agents

What a connected agent can do

Over that one connection an agent can operate your library the way you would. It can:

Ask your agent to call agent_guide first: it returns your instructions, a snapshot of your tag taxonomy, and how to file new notes into the tree you already have.

Tasks

A task is just a note with a status, so your to-dos live in the same library as your notes and inherit everything: rich Markdown, attachments, tags, history, and sync. Turn any note into a task (and back) from its actions menu.

Link notes to each other and click to jump between them, on every device. Type [[ in the editor (or pick Link a note from the slash menu) to search your notes by title and drop in a link. The same picker can also create a new note from what you typed, link it, and leave you writing where you are, so you can branch a thought without losing your place.

(@ is reserved for mentioning people, for when shared editing arrives.)

Epics & sub-tasks

Build real structure out of plain notes. Turn any note into an epic: a checklist where each item links to its own sub-task note. The parent reads like a living index of the work, and every sub-task is a full note in its own right, with its own body, tags, due date, and history. To add one, use Sub-task (linked note) from the slash menu (it creates the sub-task and links it in one step), or link an existing note into a checklist item.

Linden searches two ways, and blends them. Keyword search is instant, fully local, and works offline: it matches exact strings, names, and hashtags. Semantic search finds notes by what they are about, even when they do not contain the words you typed, so "that thing about gearboxes" surfaces the right note however you phrased it at the time.

Search reads your attachments too. Linden pulls the text layer out of PDFs and runs OCR over images, then folds that text into both keyword and semantic search, so a note is findable by what is inside the files you attached to it, not just the words you typed.

In the apps, tap the sparkle in the search bar to switch to search by meaning, on phone, tablet, and desktop alike. When you are offline, search falls back to keyword automatically.

Semantic search runs on vector embeddings: each note is turned into a list of numbers that captures its meaning, and a query finds the notes whose vectors sit closest to it. Embeddings are computed by the AI provider of your choice. In Settings › Semantic search, enter a provider, model, and your own API key; the key stays on your daemon and never leaves it. The daemon indexes your library in the background and keeps it current as you write. Each note's title and full tag path are folded into its embeddings, so where a note sits in your tree shapes how it is found. Changing the provider or model re-indexes the corpus, since each model has its own vector space. Your agents are always online, so they always get full semantic recall.

Live embeds

A note can hold more than text. Fence a block as embed and Linden renders the HTML, CSS, and JavaScript inside it as a live, interactive panel, right in the editor and everywhere your note goes. Fence a block as svg for inline vector graphics. It is a built-in canvas for charts, mockups, small tools, and agent-built UI.

```embed
<button onclick="this.textContent='clicked!'">Tap me</button>
```

Publishing & sharing

Your library has a public face when you want one. Set a publish date on any note and it becomes a post on your own site at you.linden.md (or your custom domain), with a timeline and tag pages readers can browse. Writing and publishing are the same act in the same tool; unpublishing is one tap.

Email inbox

Every account gets a private inbox address, <key>@in.linden.md. Email or forward anything to it and it arrives in your library as an Inbox note: the subject becomes the title, the message body becomes the note, and any files on the message come in as attachments.

Automated backups

Point your daemon at your own storage and it keeps a continuous, encrypted-in-transit backup running on its own. Your data, your bucket.

On Linden Cloud, backups are configured and run for you (see below); you never have to touch a bucket.

Import & export

Your library is plain Markdown, and it stays that way. You can move it in and out whenever you like, no lock-in.

Knowledge packs

A pack is a versioned slice of notes you can share without merging whole libraries. Catalog: linden.md/packs.

Guides

Worked examples of the flows everything above exists for, captured from the real app. Narrated video walkthroughs are on the way; each guide will get one.

Have an agent file your meeting notes

The goal: dump raw notes at an AI and have the finished note land in your tree, where you would have filed it, structured the way you like.

A meeting note filed by an agent under work/acme/meetings, with decisions, open questions, and checkbox next actions
The result: a raw meeting dump, filed by Claude under work/acme/meetings with decisions up top and actions as live checkboxes.
Narrated walkthrough coming soon.

Brainstorm a product with live mockups

The goal: go from an idea in prose to a clickable mockup without leaving the note.

A note about a trip-planner idea with a live, clickable Tailwind card mockup rendering inside the editor
A live, clickable mockup rendering inside the note that describes it. The same panel runs on the published page and in PDF export.
Narrated walkthrough coming soon.

Set up continuous off-site backups

The goal: a self-hosted library that survives the machine it lives on, without a backup script to remember.

Narrated walkthrough coming soon.

Linden Cloud

Linden Cloud is managed hosting on our infrastructure, for when you would rather not run your own server. You get the same app and the same library; we run the daemon behind it.

How it works (under the hood)

Linden is local-first. Your library lives in one process, the daemon, and everything else (the apps, the public blog, your AI agents) talks to it. Here is the shape of the system.

The daemon and its database

The daemon keeps your whole library in a single SQLite database (data.db in ~/.linden), running in WAL mode with a single writer, so writes are serialized and consistent and reads never block them. Notes, tasks, tags, version history, attachment metadata, and your settings all live in that one file; attachment bytes live next to it as content-addressed blobs. Because it is a real database on disk, it is fast, transactional, and yours to copy or move at any time. Full-text search (FTS5) and vector search (embeddings) are indexes inside the same database, so recall works offline with no external service.

The tunnel (how your daemon is reachable)

Your daemon sits behind your home or office router, which normally means it cannot be reached from outside. Linden solves this without any port-forwarding or firewall setup: the daemon dials out to the gateway and holds the connection open. Requests for your library then travel back down that same outbound connection. Because the daemon only ever makes an outbound connection on port 443 (the same port your browser uses for HTTPS), it works from behind almost any network, and NAT traversal is automatic. The transport is NATS multiplexed over a secure WebSocket on 443, so a single connection carries every request and reply.

The gateway (a blind relay)

The gateway we run at linden.md is the public edge. It terminates public TLS, gives each account a stable address (you.linden.md), and matches an incoming request to the right daemon: it maps <account>.linden.md to that account's private message subject and hands the request to whichever daemon is currently connected for it. It is a blind relay: your content passes straight through and the gateway never reads it or stores it. To keep each relayed message small, a single request or reply is capped at about 8 MB; large downloads (a video, a full backup) are streamed through in ranged chunks under that cap, so size is never a limit, only latency. If your daemon is offline, the gateway simply has nothing to relay, and your apps keep working locally and sync when it returns.

On your own local network you can skip the gateway entirely and let apps reach the daemon directly. The gateway exists only to reach your daemon from anywhere and to serve your public blog.

Sync (real-time, offline-first)

Every write, from any surface (web, phone, an AI agent), appends to a single change feed inside the database, in the same transaction as the write itself. Connected apps hold open a Server-Sent Events stream (/sync/stream) and see each change within about half a second, so an edit on your laptop lands on your phone almost immediately. When a device is offline it keeps working against its local copy; on reconnect it asks for everything since its last cursor (/sync/changes?since=N), replays the gap, and resumes the live stream. Deletes are first-class in the feed, so removals propagate too.

Managed Cloud vs self-host

It is the same daemon binary either way. When you self-host, you run it and hold your own keys and backup configuration. On Linden Cloud we run that identical daemon on our hardware and inject the operational bits for you (backup credentials, the semantic-search API key) at run time, so they are never written into your library's database. That is why moving between Cloud and self-hosting is lossless in both directions: the database is the same shape, and the parts we manage were never part of your data to begin with.

Backup format

A full backup is a plain .zip with two things inside, and no proprietary format anywhere:

It is ordinary, unencrypted SQLite. You can open data.db with any SQLite tool and read everything directly. Nothing is locked or obfuscated, which also means the backup file is your responsibility to store somewhere safe. (In transit, backups over the gateway and to your bucket are protected by TLS; the file at rest is not encrypted by Linden.)

Everything that gets stored

The backup is lossless: notes, tasks, your full tag tree, complete version history, attachment metadata, and the attachment bytes themselves. For the technically curious, here is every table in data.db:

TableWhat it stores
notesEvery note (a task is a note with a status). Columns: id (ULID), title, slug, body_markdown, body_json (the editor's rich-text document), tags (JSON array of tag ids), metadata (JSON), published_at (set = public post), share_token, pinned, status (NULL = plain note; otherwise inbox/todo/doing/waiting/done/cancelled), due_at, reminder_at, priority, source_id (canonical URL/id a captured note came from, for dedup), created_at, updated_at, deleted_at.
note_versionsThe full linear version history: one snapshot per note write (with a 30-second same-source debounce). Holds a copy of the note's fields at that point (title, slug, body_markdown, body_json, tags, metadata, published_at, share_token) plus a version number and a source label (web, cli, mcp, sync, import, ...). Its auto-incrementing id is monotonic.
tagsYour hierarchical tag tree as first-class rows. Columns: id (ULID, stable across rename/move), parent_id (adjacency list; NULL = root), name (one segment), path (cached full slash-path), sort_order, pinned (Canopy), color, icon, note_count, and timestamps. Renaming or moving a tag touches only tag rows, not your notes.
note_tagsThe note-to-tag membership projection (note_id, tag_id), maintained on every note write so tag queries are fast.
changesThe unified change feed that drives sync. One row per mutation: seq (the monotonic sync cursor), entity and entity_id, op (upsert or delete), a source label, and created_at.
assetsAttachment metadata (the bytes are in blobs/). Columns: id (ULID), note_id, kind (image/video/audio/file), sha256 (content key into blobs/), filename, content_type, size, duration_ms, poster_key (video poster), metadata, and timestamps.
asset_textText extracted from attachments so files are searchable: asset_id, note_id, sha256, the extracted text, a status (pending/done/skipped/error), and the method (pdf text layer or image ocr).
uploadsIn-flight resumable upload sessions (so a large attachment upload survives a restart): id, filename, content_type, kind, expected size, bytes received, optional note_id, and timestamps.
agent_tokensBearer credentials for your paired devices and AI agents. Only the SHA-256 of each token is stored (token_sha256), never the token itself, alongside a label, a scope (read / no-delete / write), a kind (device or agent), and created/last-used/revoked timestamps.
oauth_clientsRegistered OAuth clients for hosted agent connectors (Claude, ChatGPT, Grok, and any other OAuth-capable MCP client) that sign in over MCP-over-HTTP: client_id, allowed redirect_uris, client_name, created_at.
site_configA single row holding your public site's identity and per-account preferences: name, bio, avatar_url, socials, tagline, and pinned_tags (your Canopy).
settingsDaemon-private key/value settings (for example, your semantic-search provider and API key). These stay on the daemon.
note_embeddingsThe vector index for semantic search: per-chunk embeddings (note_id, idx, the chunk text, the model, its dimension, and the vec as a float32 blob).
note_embed_stateWhich notes are embedded and at what model/time, so the background indexer only re-embeds what changed.
notes_ftsThe full-text search index (FTS5) over note titles and bodies, kept in sync automatically.

How a restore works

Uploading a backup is the mirror of downloading one. The .zip is sent up in chunks (each staying under the relay cap), assembled on the daemon, and then validated before anything is touched: the daemon confirms the archive really contains an openable SQLite data.db. Only then does it stage the file and restart, applying the restore on the next boot, before the database is opened, so a live database file is never hot-swapped. If the staged file ever fails validation it is set aside and your existing data is left completely intact, so a bad or corrupt upload can never wipe good data.

Continuous off-site backup

Separately from on-demand backup files, the daemon can stream your library to a bucket you own (S3 or Backblaze B2) as you write, using Litestream embedded directly in the daemon (no extra process to run) for the database, plus a small content-addressed sync for the attachment blobs. On a fresh machine, pointing the daemon at the same bucket pulls your library down before it starts. See Automated backups.

Markdown export (the portable alternative)

If you want your library in the most portable form possible, export to plain Markdown instead. You get a .zip of one <slug>.md file per note, each with a YAML frontmatter header, and an attachments/<id>/<filename> folder with the referenced files. Links in each note body that point at an attachment are rewritten to the bundled relative path, so the export is self-contained and opens cleanly in Obsidian, Bear, or any Markdown tool.

---
id: 01J...              # stable note id
title: "Meeting notes"
tags: ["work/sales", "2026"]
created: 2026-01-04T09:12:00Z
updated: 2026-01-05T14:03:00Z
published: 2026-01-06T00:00:00Z   # only if it is a public post
status: todo                       # only if it is a task
due: 2026-01-10T00:00:00Z          # tasks with a due date
priority: 2                        # tasks with a non-zero priority
---

# Meeting notes
...body markdown...

Export and import round-trip: an export can be re-imported into Linden (or a fresh library), and import also reads a generic Markdown vault or a Bear export. A full backup keeps strictly more (version history, task and asset metadata, embeddings); Markdown export keeps the readable content and its structure in a form nothing can lock away.

On the roadmap