Digital Garden: Designing the trust layer between a writer and an AI editor

Type: Personal project (learning AI product design by building)
Role: Sole designer + builder
Stack: FastAPI, Next.js/React, Claude (Anthropic SDK), CodeMirror, Framer Motion
Timeframe: July 5-10 (~6 days), vision to a dogfooded macOS app

The problem

Note tools split two jobs that used to live in separate apps: linked thinking (Obsidian) and AI-assisted editing (Cursor). I wanted one owned surface: files stay on disk as a local .md vault, notes link the way you naturally write them, and AI can help edit without taking the pen.


Most AI writing tools forced a bad tradeoff: a chatbot bolted onto a text box that talks about your notes but never really touches them, or a passive notebook that cannot reason across what you have already written. Digital Garden was scoped as “Cursor for Writing” on an Obsidian-like vault: linked notes plus a Cursor-style AI layer that proposes real edits, and only writes when you approve.


I threw out an early feature-grid plan and rebuilt the MVP around one loop: open → write → link → ask AI → apply. Anything that did not serve that loop did not make the cut.

Obsidian's linked thinking

Cursors auto-fill

The key decision: AI proposes, the human approves

The central rule is a constraint, not a capability: the AI never writes to your vault directly. Chat can suggest rewrites, new notes, and linked references, but every change surfaces as a diff you accept or reject (per block or approve/deny all) before it touches real files. Edit history lives beside the notes in .garden/edits/, so undo does not require git fluency.

I considered auto-apply with undo (the default in many AI writing tools) and rejected it. Once a tool silently rewrites your notes, you stop trusting it unsupervised. The same trust model covers credentials: bring-your-own-key in backend/.env only, health flags for whether Anthropic/Tavily are configured, a chat banner when the key is missing, and an agent rule that the AI must never ask for a key in chat. Setup problems and infrastructure failures had to read as different states, not one scary generic outage.

The experience: network + AI editing on one surface

Onboarding and ownership.
You pick a folder on disk. There is no account for the MVP. The vault is the product.


Layout.
A docs-style tree on the left (folder → notes, indent guides, chevron on the right, no .md suffixes, shallow auto-expand) so the vault reads as a map of where ideas live, not a flat inventory. Center: CodeMirror markdown editor with a shared light editor surface across editor, tab bar, and backlinks. Graph view for the network. AI chat as a sidebar that pushes the editor when open, so writing and review stay one canvas. Cmd+K is a command palette for jumping the vault; agentic search uses ?query in that same surface.


How the network works.
Links are authored in prose: [[Other Note]]. Typing [[ triggers autocomplete. Backlinks show reverse connections as soon as the text exists. The graph mirrors writing: real wikilinks become edges; unresolved links do not spawn ghost nodes. Visualisation and authoring stay separate jobs on purpose.

How AI editing works.
In chat: sessions, stop, diff previews against the live file, approve/deny all, and undo after apply. The human stays the author; AI is a collaborator with a review gate.


Dogfooding as UX proof.
I moved the build journal into the vault as Building digital garden (hub note + dated update nodes), so sidebar, wikilinks, backlinks, and graph had to work on real project notes, not repo-only files.
A build-chronicler agent logs each session into that graph. That forced product metaphors (nodes, links, garden) onto how the product itself was documented.


Dogfood polish that changed daily use.
Chronological sidebar sort for updates/ (hub pinned first) instead of A→Z, so a journal reads as a timeline. Wikilink colors moved onto CSS theme variables so dark mode stays readable. A later scroll bug (duplicate className dropped h-full) made long notes unscrollable with the mouse wheel; fixing the height constraint restored the editor as the scroll container.

The wrong turn: what “the graph is broken” actually meant

Dogfooding the graph, I tried to drag between nodes to connect notes. Nothing happened. It felt broken even when the product worked as designed: links are written in markdown; the graph only mirrors them.


I could have added drag-to-connect to match the instinct. I held the line instead. Drag-to-connect would duplicate authoring and allow edges with no matching note text, which breaks “files are the source of truth.” Obsidian-trained linking in prose was the model to teach once.

Ghost nodes and updates not connecting properly.

What I fixed underneath: a real backend bug (name_lookup threw 500 when wikilinks were present, so some graphs failed after save), plus teaching in place ([[ autocomplete, backlinks panel) and later removing unresolved ghost nodes so the canvas only showed real notes.

The challenge: recovery paths, not just features

About 80% of the MVP existed as features before the product felt finishable without a terminal. The gap was trust and recovery.


False “Backend not running on port 8000” messages appeared while the API was up. Causes: CORS, stale processes, and frontend ports drifting (3000 / 3002 / 3003). I shipped a same-origin Next.js proxy (/backend-api → 8000), broader local CORS, clearer onboarding errors when the backend is truly down, a restart script, and launcher checks that detect a broken graph and recover. Separate messages for missing keys vs dead infrastructure.


Packaging was the same lesson: requiring uvicorn + npm run dev is engineer-only UX. Double-click Digital Garden.app is the product for a local-first tool. Finder launch does not inherit Terminal PATH the same way, so I had to test the way a real user opens the app, including port recycling and native binary quarantine fixes.

Craft: motion as product language

Motion communicates hierarchy. Chat push uses a critically damped spring (bounce: 0, duration: 0.4) so editor and sidebar share space structurally. Cmd+K drops from above with a snappier spring and no scale, so it reads as a utility overlay. Graph nodes use OKLCH color shifts on hover/select (no size/glow fake), grab-offset dragging, throw/pan momentum, and double-click to pin.


Accessibility shipped with that layer: MotionProvider honors prefers-reduced-motion and prefers-reduced-transparency (opacity cross-fades instead of springs; solid surfaces instead of blur). Spec lived in motion-physics.md as a shared contract, not one-off CSS.

Sidetab interaction with new motion-physics.md

Outcome

Closed the MVP when full loops worked without living in a terminal:

  • Local vault, wikilinks, backlinks, graph

  • Chat with sessions, stop, diff approve/deny, edit undo

  • Agentic search (Cmd+K ?query)

  • Double-click macOS launcher

  • Trust/health layer (BYOK, health flags, no keys-in-chat)


I was testing daily inside the app, including the linked build journal that produced these specifics.

Kunal Chaudhary

Based in Dublin, Ireland

© 2026 Designed by Kunal Chaudhary

Kunal Chaudhary

Based in Dublin, Ireland

© 2026 Designed

by Kunal Chaudhary