A native desktop note-taking app built on ATProto that lets you write rich documents locally and publish them to Bluesky using the standard.site document lexicon.
This is a demo/prototype exploring what a local-first, decentralized note-taking experience can look like when built on top of the AT Protocol.
Documents are stored locally in SQLite using BlockNote's block-based format. When you're ready to share, you can publish any document to your Bluesky PDS as a site.standard.document record. The published record includes:
- The full document content in the
org.blocknote.document#contentlexicon format (preserving all rich formatting) - A plaintext
textContentrepresentation for indexers and search - Standard metadata (
title,publishedAt,updatedAt)
The app tracks which documents have been published and uses a content hash to detect local edits, so you can see at a glance which documents have unpublished changes.
- Electrobun desktop shell (Bun main process + WebKit webview)
- React frontend with BlockNote editor, Base UI, and Tailwind CSS
- SQLite for local document storage and Bluesky session persistence
- atcute packages for all ATProto/Bluesky communication
- Typed RPC bridge between the Bun backend and the webview
- Bun (latest)
- Electrobun CLI (
bun install -g electrobun) - macOS (Electrobun uses WebKit on Mac)
bun install
bun run devThis starts both the Vite dev server (with HMR) and the Electrobun app in parallel.
bun run typecheck # lint + type-check + format-check (oxlint, tsgo, oxfmt)
bun run fmt # auto-format
bun run lint # lint only
bun run build:canary # production build- Open Settings and connect your Bluesky account using an app password
- The app resolves your handle, finds your PDS, and authenticates directly against it
- Click Publish in the editor header or use the context menu in the sidebar
- Published documents show a blue dot in the sidebar and a green "Published" badge in the editor
- After editing, the badge changes to an amber "Update" button; click it to push changes
Session tokens are stored in the local SQLite database and refreshed automatically.
The project defines custom BlockNote lexicons for representing rich documents on ATProto:
| Lexicon | Description |
|---|---|
org.blocknote.schema |
Generic block, inline content, and style shapes |
org.blocknote.defaultBlocks |
All 14 default BlockNote block types (paragraph, heading, table, etc.) |
org.blocknote.document |
ATProto record wrapping BlockNote content with an optional schema declaration |
These live in src/lexicons/ and TypeScript types are generated with bun run generate:lexicons (via @atcute/lex-cli).
When publishing, documents are serialized using createLexiconContent() from src/shared/atproto/serialize.ts, which converts BlockNote's in-memory format to the ATProto lexicon format (handling type vs $type union discrimination, content field splitting, etc.).
src/
bun/ # Bun main process (SQLite, RPC handlers, Bluesky client)
mainview/ # React frontend (editor, sidebar, settings)
shared/ # Types and serialization shared between both sides
lexicons/ # ATProto lexicon JSON schemas
generated/ # Generated TypeScript from lexicons
MIT