Skrive
Markdown and rich text, the same file.
Why I built it
Skrive started as a personal itch. I wanted one writing surface that did three things at once, and I couldn't find it anywhere. It had to be plain Markdown, so my words stay portable. A .md file is about the closest thing writing has to a universal format: it opens anywhere, it diffs in git, and every AI model already speaks it. It had to be local, so the work lives in my own folders, not someone else's cloud. And it had to treat rich-text editing and raw Markdown as equals, instead of making me choose a side.
Every few months someone ships another Markdown editor and calls it done. Another run at being the next Obsidian or Bear. I didn't want to add a cliche to the pile, so I put my own spin on it. The non-negotiable is round-trip fidelity: your document stays plain Markdown on disk, byte for byte. The spin is that it does that while actually serving both crowds at once, the people who want to see the syntax and the people who never want to touch it.
Most tools make you choose. Rich-text editors hand you a clean surface but bury your words in a format you can't grep or version or hand off to anything else. Source-only Markdown editors keep the portability but assume you always want the markers in your face. Skrive serves both over one file, and lets you decide how much of the machinery to see, document by document.
One file, however you write
Every document in Skrive is a single Markdown file. There are a few ways to work on it, all switched with a keystroke, all editing the same bytes underneath.
The Rich surface is the no-syntax view. Headings, bold, lists, quotes, dividers, links, and tables all render as real elements. You format the way you would in a word processor, and never type a ** or a # unless you want to. The Text surface is the source itself, Markdown right there in front of you, with a dial for how present the syntax is: fully raw, recessed so the markers fade into the margins, or concealed, where they disappear but the text stays editable in place. And if you like the classic split, a live preview renders alongside the source as you type.
All of them work on the exact same bytes on disk. Switch mid-sentence and nothing converts or re-flows or gets lost. The Rich surface isn't a rendered copy of the source. It's just another set of hands on the same file.
The workspace
Skrive opens a folder of Markdown files and treats it as a project, the way an IDE treats a repository. No import step, no vault. You point it at a directory and it comes to your files instead of asking you to move them into it.
From there it adds the things that make a loose folder of notes feel connected. Backlinks show what points at the current document and what it points to. An outline rail maps the heading structure off to one side, so you can scrub and jump without a panel eating your margin. Project search is full-text across the whole folder, with context previews. Rename a file and every reference to it updates, so the link graph doesn't rot. The whole app is keyboard-first, run off one command registry that also feeds the keybindings and the cheat sheet. Version history gives you structural diffs between revisions, from both git and Skrive's own checkpoints. And copy is smart: it carries raw Markdown and rendered HTML at once, so pasting into Gmail or Docs gives you formatted text instead of literal ## markers.
One thing is deliberately missing: there's no AI inside Skrive, and that's on purpose. The bet runs the other way. Keep the format plain and portable, and your writing is never walled off from whatever tools you do want to reach for, an AI model included.
The projection model
The hard part behind “both surfaces, one file” is round-tripping. Let someone edit rich text and then write it back to Markdown, and a naive serializer quietly rewrites the whole document: re-wrapping lines, normalizing list markers, reordering attributes. The diff explodes even though nothing about the meaning changed.
Skrive solves this with a projection architecture. The canonical Markdown file on disk is the single source of truth. A source-mapped parser turns it into a ProseMirror document where every block remembers the exact bytes it came from. When you save, the serializer splices the untouched blocks back verbatim and re-serializes only the blocks you actually changed. The result is a byte-faithful round-trip: edit one paragraph and the diff shows one paragraph.
A diff that reads structure
Comparing two revisions of prose with a line-based diff is maddening: reflow a paragraph and the whole block lights up red and green even though you changed three words. So Skrive ships a native structural diff written in Rust, compiled to a Node addon with napi-rs and called straight from the desktop shell. It compares the documents paragraph by paragraph instead of line by line. A rewrapped block reads as “unchanged,” and a real edit reads as exactly the words that moved.
Linting off the main thread
Skrive lints the whole project as you write: broken internal links, duplicate headings, heading-hierarchy slips, missing frontmatter, orphaned files. Run all of that on the main thread on every keystroke, and one multi-file pass is enough to make typing feel like it's dragging.
So the lint engine lives in a Web Worker. It keeps its own copy of every file's text, the editor sends only the deltas as you type, and a path-keyed cache means unchanged files never get re-parsed. The project-wide checks run the whole time without ever touching the frame budget the cursor needs.
Electron, deliberately
Skrive is an Electron and React 19 desktop app, laid out as a small monorepo: a React renderer for the editor and panels, an Electron shell for the filesystem and project smarts, a shared layer of types and IPC contracts, and the native Rust diff crate. It got here through a full rewrite. The editor was rebuilt from scratch around the projection model, and the shell moved off an earlier Svelte and Tauri stack.
The editor runs two engines to match its two surfaces. CodeMirror 6 drives the Text surface and its syntax decorations; ProseMirror drives the no-syntax Rich surface. A shared remark and rehype pipeline handles the parsing and serialization that ties both back to the one file on disk.
Where it is now
Skrive is at 1.0 and still early. It's a working desktop app I use every day, not a finished product. The builds above are a signed, notarized universal build for macOS and an unsigned build for Windows that clicks past the usual SmartScreen warning. No Linux build, no web version. It's a native writing tool on purpose.
It's source-available under the PolyForm Noncommercial license: free for personal use, commercial rights reserved. No accounts, no telemetry, no network calls. Bug reports are welcome on GitHub.