Convert Markdown/Obsidian notes to Nostr events with backlink support using NIP-54 wiki articles.
Zettlkastr publishes your zettelkasten notes to the Nostr protocol, preserving the critical feature of bidirectional linking. Each markdown file becomes a NIP-54 wiki article event (kind:30818) with wikilinks converted to Nostr a tags, enabling backlink discovery via relay queries.
- Parse any directory of
.mdfiles (Obsidian vaults or plain markdown) - Extract wikilinks:
[[Note Name]],[[Note|Display]],[[Note#Heading]] - Generate NIP-54 events with proper d-tag normalization
- Publish to local or remote Nostr relays
- Query notes and backlinks via CLI
- Interactive web UI with markdown rendering and network graph
- Preserve frontmatter (title, description)
python --version # Should be 3.10 or higherInstall strfry for local testing:
# Clone and build
git clone https://github.com/hoytech/strfry.git
cd strfry
git submodule update --init
make setup-golpe
make -j4
# Run relay
./strfry relaystrfry will start on ws://localhost:7777 by default.
Install nak for Nostr key management:
# With Go
go install github.com/fiatjaf/nak@latest
# Or download binary from releasesGenerate a keypair:
nak key generate
# Outputs:
# privkey: <64-char-hex>
# pubkey: <64-char-hex># Clone repository
git clone https://github.com/yourusername/zettlkastr.git
cd zettlkastr
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Install in development mode
pip install -e .Create a .env file in the project root:
cp .env.example .envEdit .env:
# Your Nostr private key (from nak key generate)
NOSTR_PRIVATE_KEY=your_64_char_hex_private_key
# Relay URL (default: local strfry)
RELAY_URL=ws://localhost:7777
# Optional: default vault path
VAULT_PATH=/path/to/your/notesImportant: Never commit .env with real keys! It's in .gitignore by default.
zettlkastr infoThis verifies:
.envfile exists- Private key is valid
- Relay is accessible
# Using .env configuration
zettlkastr import-vault /path/to/notes
# Or specify key and relay explicitly
zettlkastr import-vault /path/to/notes --key <privkey> --relay ws://localhost:7777
# Dry run (parse and generate events without publishing)
zettlkastr import-vault /path/to/notes --dry-runOutput:
Using public key: 79dff8f82963424e0bb02708a22e44b4980893e3a4be0fa3cb60a43b946764e3
Scanning vault: /path/to/notes
Found 47 notes
Extracting links and generating events...
Processing [####################################] 100%
Found 156 total links
Generated 47 events
Connecting to relay: ws://localhost:7777
Publishing events...
Publishing [####################################] 100%
==================================================
Successfully imported: 47/47 notes
Your notes are now on Nostr!
Public key: 79dff8f82963424e0bb02708a22e44b4980893e3a4be0fa3cb60a43b946764e3
# Query by d-tag (normalized filename)
zettlkastr get zettelkasten-method
# With explicit key/relay
zettlkastr get zettelkasten-method --key <privkey> --relay ws://localhost:7777Output:
============================================================
Zettelkasten Method
============================================================
The Zettelkasten method is a personal knowledge management system.
# Zettelkasten Method
The Zettelkasten method is a personal knowledge management system.
It emphasizes [[Atomic Notes]] and [[Linking Your Thinking]].
Key principles:
- One idea per note
- Link liberally
- Use unique identifiers
────────────────────────────────────────────────────────────
Links to (4):
• atomic-notes
• linking-your-thinking
• note-taking
• knowledge-management
# Show all notes that link to a specific note
zettlkastr backlinks zettelkasten-methodOutput:
Finding backlinks to: zettelkasten-method
============================================================
Backlinks to 'zettelkasten-method' (3)
============================================================
1. Personal Knowledge Management (pkm)
2. Note-Taking Systems (note-taking-systems)
3. How I Organize My Thoughts (my-organization)
# Display all notes with their connections
zettlkastr graph
# Export as JSON
zettlkastr graph --format jsonLaunch the interactive web interface:
zettlkastr webThis starts a web server at http://localhost:8000 with:
Features:
- Notes View: Browse and read notes with clickable wikilinks
- List View: See all notes with forward links and backlinks
- Network View: Interactive force-directed graph (Obsidian-style)
- Drag nodes to rearrange
- Click nodes to navigate
- Double-click to focus
- Toggle physics simulation
- Zoom and pan
Rendered Markdown:
- Full GitHub Flavored Markdown support
- Styled headings, lists, code blocks, tables
- Wikilinks converted to clickable links:
[[Note]]→ clickable - Broken links shown in red
Interactive Graph:
- Nodes sized by number of connections
- Arrows show link direction
- Dark theme for better visibility
- Controls: toggle physics, fit to screen, reset zoom
Options:
zettlkastr web --port 8080 # Custom port
zettlkastr web --host 0.0.0.0 # Bind to all interfaces
zettlkastr web --reload # Enable auto-reload (dev mode)The web UI requires FastAPI and uvicorn:
pip install fastapi uvicorn[standard]# Alternative way to run
python -m src import-vault /path/to/notes
python -m src get zettelkasten-method
python -m src info- Recursively scans directory for
.mdfiles - Skips
.obsidian,.trash,.git, and hidden directories - Parses YAML frontmatter if present
- Extracts file modification time for event timestamps
Supports Obsidian wikilink syntax:
- Basic:
[[Note Name]] - Display text:
[[Note|Custom Display]] - Headings:
[[Note#Section]] - Blocks:
[[Note#^block-id]]
Ignores embeds: ![[Image.png]]
Filenames are normalized per NIP-54:
| Filename | d-tag |
|---|---|
Zettelkasten Method.md |
zettelkasten-method |
2024-01-01.md |
2024-01-01 |
C++ Programming.md |
c-programming |
Note (Draft).md |
note-draft |
Each note becomes a NIP-54 event:
{
"id": "<sha256-hash>",
"pubkey": "<your-pubkey>",
"created_at": 1733097600,
"kind": 30818,
"tags": [
["d", "zettelkasten-method"],
["title", "Zettelkasten Method"],
["a", "30818:<pubkey>:atomic-notes", "ws://localhost:7777"],
["a", "30818:<pubkey>:linking-your-thinking", "ws://localhost:7777"]
],
"content": "<full-markdown-content>",
"sig": "<schnorr-signature>"
}To find notes linking to "zettelkasten-method":
{
"kinds": [30818],
"#a": ["30818:<pubkey>:zettelkasten-method"]
}The relay returns all events containing that a tag.
zettlkastr/
├── README.md
├── SPECIFICATION.md
├── requirements.txt
├── setup.py
├── .env.example
├── .gitignore
├── src/
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli.py # CLI interface
│ ├── parser.py # Vault parsing and link extraction
│ ├── events.py # Event generation and signing
│ ├── relay.py # Relay client (WebSocket)
│ └── utils.py # Utilities (normalize_d_tag)
├── tests/
│ ├── fixtures/
│ │ └── sample_vault/
│ │ ├── Note A.md
│ │ ├── Note B.md
│ │ └── Note C.md
│ └── (test files)
└── scripts/
└── (helper scripts)
Try the sample vault:
# Start strfry in one terminal
cd /path/to/strfry
./strfry relay
# In another terminal, import the sample vault
cd /path/to/zettlkastr
zettlkastr import-vault tests/fixtures/sample_vault
# Query a note
zettlkastr get note-aError: Failed to connect to relay at ws://localhost:7777
Solution: Start strfry:
cd /path/to/strfry
./strfry relayError: Private key must be 64 hex characters (got 66)
Solution: Ensure you're using the private key (not public key) from nak key generate. It should be exactly 64 hexadecimal characters.
No markdown files found in vault.
Solution: Check that:
- Path is correct:
ls /path/to/vault - Directory contains
.mdfiles - Files aren't in excluded directories (
.obsidian,.trash)
If events publish successfully but don't appear in queries:
- Verify event was accepted: look for "Successfully imported" message
- Check d-tag normalization:
zettlkastr get <d-tag> - Query strfry directly:
echo '["REQ", "test", {"kinds": [30818], "limit": 10}]' | websocat ws://localhost:7777
pip install -r requirements.txt
pip install -e ".[dev]"pytest
pytest --cov=src # With coverage- parser.py: File scanning, frontmatter, wikilink regex
- events.py: NIP-01 event structure, SHA-256 ID, Schnorr signatures
- relay.py: WebSocket protocol, EVENT/OK/REQ/EOSE messages
- cli.py: Click commands, progress bars, error handling
See SPECIFICATION.md for the full roadmap:
- ctrl-p like fuzzy search by note names
- Multi-relay support: Publish to multiple public relays
- wss:// support: Connect to secure WebSocket relays
- Incremental updates: Only publish changed notes
- Sync: Bidirectional sync with Obsidian
- nostrdb: Embedded database for offline-first operation
- Encryption: NIP-44 for private notes
- Search: Full-text search across all notes
- Tags: Support for Obsidian tags and filtering
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
MIT License - see LICENSE file for details
- GitHub Issues: Report bugs and request features
- Nostr: Find the maintainers on Nostr!
Built with love for the zettelkasten and Nostr communities.