It started with a phrase that sounds smaller than it is: an Obsidian-compatible notes app in the browser.

That has the pleasant shape of a weekend project if one says it quickly enough. 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.

The problem, of course, is that a note app is not a Markdown renderer with furniture. It is a long list of quiet promises made to a folder of ordinary files. A note can be opened in another editor and still make sense. A wikilink can survive a rename. Frontmatter can be parsed without being kidnapped from the user. A Canvas file can round-trip. A plugin can extend the app without becoming the app. The graph is allowed to be pretty only after the cache knows what is true.

Granite, which began life in the repo as the cheerfully misspelled obsedian-clone, is interesting because it discovers this the honest way: by implementing enough of the surface that the surface stops being the hard part.

Compatibility is the product

The README is unusually clear about the real assignment. Granite is 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 is doing most of the work.

Local-first is often described as a storage decision, but in a notes app it is closer to a moral hazard. The app must write files because that is the point, but 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.

Granite’s better instinct is that the vault remains the authority. Markdown files stay Markdown files. Canvas files use the JSON Canvas shape. Bases are text files. The app’s own state lives under .granite/, which is the right sort of compromise: the application needs a drawer for its receipts, but it should not start storing the user’s furniture in there.

This is why the project is less a clone than a compatibility argument. The valuable part is not that the sidebar resembles a familiar shape. The valuable part is that the user’s existing habits, files, links, and escape routes keep working.

The cache is a reading habit

The metadata cache is the first place where the app becomes more than a renderer.

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.

That sounds like plumbing, because it is plumbing. It is also the product.

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

The useful trick is that Granite treats this attention as a local service rather than a remote intelligence. It does not need to become mystical about “linked thinking.” It needs to parse files, update the cache, and tell the truth quickly enough that the interface feels like memory.

A serious app shell

The outer shell has the usual visible machinery: ribbon, left sidebar, right sidebar, workspace, tabs, command palette, quick switcher, settings, notices, tooltips, hover popovers. This is the part that makes the app legible in screenshots, 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 vibe. Pop-out windows exist because a desktop notes app eventually has to admit that one rectangle is not enough.

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

At a certain point, “polish” stops meaning animation and starts meaning that the workbench remembers where the tools were.

Hand-drawn systems plate showing ordinary Markdown files, a metadata index, renderer, workspace panes, and plugins connected by freehand arrows.
FIG. 02 — THE VAULT CONTRACT, DRAWN AS A SMALL MACHINE.

Markdown is a treaty

The Markdown renderer 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. It is just innocent.

Granite’s renderer has become less innocent. It strips frontmatter from reading view while preserving source line numbers so task checkboxes can still map back to the original file. It rewrites headings into deterministic anchors. It hides comments. It resolves embedded backlink and query blocks through the metadata cache. It lets Markdown-form links and wikilinks participate in the same vault world.

The interesting detail is not any single extension. It is the care around round-tripping. 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.

In other words, the renderer’s job is not to improve Markdown into a private format. Its job is to make the existing file more usable without changing what kind of object it is.

Extensibility needs a fence

The plugin system is where the project becomes slightly more dangerous, which is usually a sign that it has become real.

Granite loads community-style plugins from .granite/plugins/<id>/, reads a manifest, evaluates a CommonJS-style main.js, and passes 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.

This is not the full ecosystem-compatible API described in the product specs, and it should not pretend to be. It is a first useful boundary.

The boundary matters because 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 has to think about trust. Granite’s Restricted mode default is the correct posture: third-party code should not run merely because a folder exists. The user has to enable it.

There is still a lot of work hiding here. A mature plugin platform needs 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. But the current implementation already 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.

The clone disappears

The word “clone” is useful at the beginning because it gives the project a target. It is less useful once the work becomes specific.

The README now calls the app Granite, which is the better name because the project has moved from imitation into material. A clone asks whether the surface matches. Granite asks what has to be true under the surface for the same habits to survive in a different implementation.

That is why the backlog reads the way it does. 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. This is the list a project gets when the first screenshot has stopped being the milestone.

The honest test is not whether a demo vault opens. It is whether a real vault can move between tools without semantic damage. It is whether a file renamed in the explorer leaves the links in a sensible state. It is whether a tag renamed across the vault touches the right YAML arrays and the right inline tags without becoming a search-and-replace incident. It is whether a plugin can unload after registering commands, whether a graph can stay useful past toy scale, whether a workspace can be restored without trapping the user inside yesterday’s layout.

That is not as photogenic as a graph view. It is also where the app becomes trustworthy.

The useful kind of boring

The best thing about Granite is that its deeper work is boring in the correct places.

It uses CodeMirror because editing text is a solved problem until you try to solve it yourself. It uses the File System Access API when the browser can offer real folders, with OPFS as the fallback rather than the thesis. It uses React for the shell, Vite for the build, markdown-it plus local extensions for rendering, and a service worker for the production shell. The interesting 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 more useful.

There are risky bets too. 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.

That is the line worth protecting. 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 should become almost boringly respectful.

So the lesson from Granite is not that everyone needs another note app, though software appears committed to testing that proposition until the end of time.

The more precise lesson is that 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.

Granite is still a workbench. That is fine. The useful thing about a workbench is that it shows you which tools are sharp, which ones are missing, and which promising idea has just rolled under the cabinet.

— C.C.