An Obsidian-compatible notes app in the browser sounds smaller than it is until a real vault opens.

Say it quickly enough and it has the pleasant shape of a weekend project. Render Markdown. Add a file tree. Draw a graph. Put some tabs around it. Maybe call the first version a prototype and allow the README to smell faintly of ambition.

A tidy version lasts until the first real vault arrives. Suddenly the screenshot answers dull questions. Can a note open in another editor and still make sense? Can a wikilink survive a rename? Can frontmatter be parsed without being kidnapped from the user? Can a Canvas file round-trip? Can a plugin extend the app without becoming the app? The graph earns prettiness only after the cache knows what is true.

Granite, which began life in the repo as the cheerfully misspelled obsedian-clone, discovers this the honest way: the surface gets implemented, and then the surface stops being the hard part.

Compatibility is the product

Inside the README, the real assignment is unusually clear: a local-first, Markdown-native knowledge base that opens a folder of plain text files, indexes the relationships between them, renders the Markdown people actually write, and tries very hard not to turn your notes into somebody else’s database.

That last clause changes the assignment because the file is not an implementation detail. A renderer can show Markdown. A notes app has to avoid taking possession of it.

Local-first is often described as a storage decision, but in a notes app it is closer to a moral hazard. The app writes files because files are the contract. Every write is also an opportunity to smuggle in an assumption about ownership. If the app turns the vault into a private runtime artifact, the user has not gained a local-first tool; they have gained a cloud app with worse furniture and no sync button.

Leaving the vault as the authority is the stronger instinct. Markdown files stay Markdown files. Canvas files use the JSON Canvas shape. Bases are text files. The app’s own state lives under .granite/, the right sort of compromise: the application keeps receipts without storing the user’s furniture in there.

By then, the project is less a clone than a compatibility argument. Familiar sidebars are cheap. Existing habits, files, links, and escape routes surviving a new implementation are not.

The cache is a reading habit

Metadata cache is where the pleasant weekend-project version really gives up.

Rendering a single note is the easy demonstration. Understanding a vault requires the app to read across files and remember what it found: headings, aliases, tags, footnotes, wikilinks, embeds, blocks, frontmatter, backlinks, outgoing links, unlinked mentions. Granite’s cache walks the vault, parses Markdown files in chunks, watches for file changes, and refreshes individual entries as the disk shifts underneath it.

Plumbing, yes. Product too.

Backlinks are not stored in a note. They are an interpretation of the whole vault at a moment in time. A quick switcher entry might be an alias hiding in YAML rather than the filename on disk. A tag list is an index gathered from body text and frontmatter, not a CSS decoration. The graph is not an illustration of knowledge, at least not at first. At first, it tests whether the app has been paying attention.

Attention works best here as a local service rather than a remote intelligence. No mystical theory of “linked thinking” is required. Parse files, update the cache, and tell the truth quickly enough that the interface feels like memory.

A serious app shell

Around the editor sits the usual visible machinery: ribbon, left sidebar, right sidebar, workspace, tabs, command palette, quick switcher, settings, notices, tooltips, hover popovers. Screenshots show that machinery, but it is not merely decoration.

Tabs have history. Groups split. Leaves can represent Markdown, graph views, Canvas, Bases, and a web viewer. Workspaces persist, first in browser storage and then back into the vault’s .granite/workspace.json. Dirty-state indicators appear because save semantics are not a matter of taste. Pop-out windows exist because a desktop notes app eventually admits that one rectangle is not enough.

All of this has a pleasingly unromantic quality. Granite is not trying to invent a new philosophy of attention every time the user opens a file. The job is to make the expected operations real: open, split, pin, drag, search, restore, rename, follow, recover.

Eventually, “polish” stops meaning animation and starts meaning that the application remembers where the tools were.

A painterly editorial collage of Markdown pages, graph-node threads, cache slips, plugin fences, and file-boundary cloth.
The vault contract as a material treaty.

Markdown is a treaty

Markdown rendering tells the same story in miniature.

Plain Markdown is never quite plain once people have lived in it for long enough. Notes accumulate callouts, comments, highlights, task lists, tables, footnotes, KaTeX, Mermaid diagrams, syntax highlighting, embedded queries, backlinks blocks, heading anchors, and wikilinks with display text that may or may not correspond to anything real. A renderer that handles paragraphs and headings is not wrong, exactly. Just innocent.

Rendering becomes less innocent once the file survives the round trip. Frontmatter disappears from reading view while source line numbers remain, so task checkboxes can still map back to the original file. Headings become deterministic anchors. Comments stay hidden. Embedded backlink and query blocks resolve through the metadata cache. Markdown-form links and wikilinks get to participate in the same vault world.

Round-tripping is the hinge. The app can render the note, but the note is still text. The app can decorate a task checkbox, but the checkbox still belongs to a line in a file. The app can surface properties, but frontmatter has not stopped being frontmatter.

Careful rendering makes the existing file more usable without changing what kind of object it is.

Extensibility requires a fence

Plugins are where the project becomes slightly more dangerous, which is usually a sign that it has become real.

Plugin loading starts in .granite/plugins/<id>/: read a manifest, evaluate a CommonJS-style main.js, and pass a typed API into onLoad. That API is deliberately small: commands, workspace actions, notices, vault reads and writes, Markdown file listing, and a little app metadata. Sample plugins demonstrate the shape with a word counter and an auto-tagger.

Nobody mistakes that for the full ecosystem-compatible API described in the product specs. First boundary, nothing more.

Plugins are the place where a local-first app can accidentally invite a stranger into the library with a master key. Even a modest plugin API thinks about trust. Granite’s Restricted mode default is the correct posture: third-party code does not run merely because a folder exists. The user enables it.

Plenty of work still hides here. A mature plugin platform has unload discipline, API stability, permissions thinking, broken-plugin recovery, and a public contract that does not change every time the app has a new idea. The current implementation at least names the right problem. Extensibility is not “let JavaScript happen.” Extensibility is a controlled agreement between the core app and the work people want to automate.

Clone stops helping

“Clone” helps at the beginning because it gives the project a target. The word starts to fail once the work turns specific.

Renaming the project to Granite now feels right because the work has moved from imitation into material. A clone asks whether the surface matches. Granite asks what stays true under the surface when the same habits move into a different implementation.

Backlog tells the same story. The unfinished items are not glamorous: full live-preview fidelity, a complete properties editor, graph filters, community plugin and theme browsing, large-vault performance budgets, accessibility audits, mobile breakpoints, compatibility checks. A project gets that list when the first screenshot has stopped being the milestone.

Opening a demo vault proves very little. The honest test is whether a real vault can move between tools without semantic damage: a renamed file leaving links in a sensible state, a renamed tag touching the right YAML arrays and inline tags without becoming a search-and-replace incident, a plugin unloading after registering commands, a graph surviving past toy scale, a workspace restoring without trapping the user inside yesterday’s layout.

Not as photogenic as a graph view. Also where the app becomes trustworthy.

Boring at the edge

Beneath the surface, the work is boring in the correct places.

CodeMirror is there because editing text is a solved problem until you try to solve it yourself. The File System Access API handles real folders when the browser allows it, with OPFS as the fallback rather than the thesis. React carries the shell, Vite the build, markdown-it plus local extensions the rendering, and a service worker the production shell. The choices are not exotic. They are arranged around a conservative product claim: the user owns the files, and the app earns its place by making those files easier to work with.

Risk still lives inside the implementation. TypeScript 7 native preview and Effect 4 beta are not exactly beige-cardigan dependencies. The README is honest about that, which helps. A local-first notes app can experiment internally as long as the experiment does not leak into the vault contract.

Protect the boundary. Inside the application, one can try new runtimes, stores, renderers, workers, and abstractions. At the edge where the app touches the user’s notes, the system becomes almost boringly respectful.

None of this proves that everyone wants another note app, though software appears committed to testing that proposition until the end of time.

Compatibility is a product discipline, not a checkbox. It lives in parsers, file writes, cache invalidation, plugin teardown, link rewriting, keyboard affordances, and every other small place where a user discovers whether “plain text” was a promise or merely a launch slogan.

Still a workshop. Fine. A workshop shows you which tools are sharp, which ones are missing, and which promising idea has just rolled out of reach.

Chris Chabot · May 2026