Your daily notes, from everywhere. Blackwood is a local-first daily notes app that captures text, voice memos, photos, and handwritten notes — then makes all of it searchable through AI.
Write from the web, send a WhatsApp message, import from Obsidian, or drop in a Viwoods handwritten note. Everything lands in a single markdown document per day. A semantic index built on top lets you chat with your notes using RAG.
Blackwood runs entirely on your machine. Your data stays local.
curl -fsSL https://raw.githubusercontent.com/csweichel/blackwood/main/extras/install.sh | shThe installer downloads the latest Linux release, verifies the release checksum, installs Blackwood into ~/.local/bin, creates a minimal ~/.blackwood/config.yaml if needed, and enables the user blackwood.service plus automatic update timer.
- Markdown daily notes — one document per day (
notes/YYYY/MM/DD/index.md), editable in the browser with auto-save - Voice memos — record audio in the web UI or send via WhatsApp; transcribed via Whisper and kept as playable audio files
- Photo capture — upload or snap photos; described via gpt-5.2 vision and rendered inline in the daily note
- Semantic search — find anything across your notes with AI-powered semantic search (
Cmd+K) - RAG chat — ask questions about your notes in natural language; get answers with source citations
- Weekly & monthly views — see notes aggregated by week or month with AI-generated range summaries
- Daily digest — automatic nightly summaries; on-demand summarize for any note
- Location tagging — tag daily notes with your location; reverse-geocoded to show address names
- Web clipping — clip any web page into today's note via bookmarklet; fetches Open Graph metadata
- iOS app — native iOS client for capturing notes, voice memos, and photos on the go
- Desktop app — Electron wrapper for macOS; runs Blackwood as a native desktop application
- Raycast extension — quick capture daily notes from Raycast
- Telegram bot — send text, voice, and photos from Telegram; uses long polling so no public URL is needed
- WhatsApp integration — text, voice messages, and photos sent to your bot appear in today's note
- Granola meeting notes — automatically imports meeting notes from Granola via MCP every hour, including summaries, attendees, and transcripts
- Viwoods handwriting — import
.notefiles from Viwoods AIPaper; pages are OCR'd and added to your daily note - Obsidian import — bulk import your existing daily notes from Obsidian
- TOTP authentication — authenticator-based login to protect your notes
- Themes & preferences — dark, light, or system theme; timezone-aware dates; configurable per user
- Calendar view — monthly grid showing which days have content; click to navigate
- Collapsible sections — headings and nested list items are collapsible (expanded by default) for easier scanning of long notes
- PDF export — download any daily note as a PDF from the note header
- Offline support — service worker caches the app shell; entries created offline are queued and synced when the server is reachable
- Bookmarkable URLs —
/day/2025-01-15for daily notes,/chat/2025-01-15-my-questionfor conversations; browser back/forward works - Keyboard shortcuts —
Cmd+Djump to today,Cmd+/toggle chat,Cmd+Ksearch,Cmd+Tinsert timestamp,Cmd+Entersave edit - HTTPS/TLS — optional TLS with configurable cert/key paths; plain HTTP remains the default
- File watcher — optionally watches a directory for new Viwoods
.notefiles and auto-imports them - Local-first — runs on your machine, no cloud dependency
curl,tar, andsystemctl --userfor the one-line Linux install- Go 1.25+ and Node.js 18+ for source builds
- An OpenAI API key for transcription, vision, embeddings, and chat
# Build the single binary
go build -o blackwood ./cmd/blackwood
# Build the web UI
cd web && npm ci && npm run build && cd ..The easiest way to get started is the interactive setup command:
./blackwood setupThis walks you through creating directories, storing your API key, and generating a config file.
Alternatively, configure manually:
cp blackwood.example.yaml ~/.blackwood/config.yaml
mkdir -p ~/.blackwood/secrets
echo "sk-..." > ~/.blackwood/secrets/openai-api-keySee blackwood.example.yaml for all options.
./blackwood --config ~/.blackwood/config.yamlOpen http://localhost:8080 in your browser.
CLI flags --addr and --data-dir override the corresponding config file values. Environment variables (OPENAI_API_KEY, etc.) work as a fallback when no config file is provided:
export OPENAI_API_KEY=sk-...
./blackwood --addr :8080 --data-dir ~/.blackwoodmake build # Build the blackwood binary
make build-server # Build with protobuf regeneration (requires buf)
make test # Run all tests
make web-build # Build the web UI
make generate # Regenerate protobuf/Connect code
make install-user-service # Install user systemd service + release updaterRelease archives include user-scoped systemd units for running Blackwood and checking GitHub releases for updates:
extras/blackwood-user.serviceruns~/.local/bin/blackwood --config ~/.blackwood/config.yamland should be installed as~/.config/systemd/user/blackwood.service.extras/blackwood-update.serviceruns~/.local/bin/blackwood-update.extras/blackwood-update.timerchecks on boot and then every six hours with a randomized delay.
The updater resolves the latest GitHub release, downloads the matching blackwood_linux_amd64.tar.gz or blackwood_linux_arm64.tar.gz asset, verifies it against checksums.txt, installs the binary to ~/.local/bin/blackwood, records the installed tag in ~/.local/state/blackwood-updater/version, and restarts blackwood.service with systemctl --user.
From a source checkout, install the user service and updater with:
make install-user-service
systemctl --user enable --now blackwood.service blackwood-update.timerThe one-line installer does the same work from the latest release archive. To install from an already-downloaded release archive, copy the files manually:
install -Dm755 blackwood ~/.local/bin/blackwood
install -Dm755 extras/blackwood-update ~/.local/bin/blackwood-update
install -Dm644 extras/blackwood-user.service ~/.config/systemd/user/blackwood.service
install -Dm644 extras/blackwood-update.service ~/.config/systemd/user/blackwood-update.service
install -Dm644 extras/blackwood-update.timer ~/.config/systemd/user/blackwood-update.timer
systemctl --user daemon-reload
systemctl --user enable --now blackwood.service blackwood-update.timerBlackwood is a single Go binary (blackwood) serving a React frontend over a single port.
┌─────────────────────────────────────────────────┐
│ Clients │
│ Web UI (React) · Electron · iOS · Raycast │
│ Calendar · Editor · Search · Chat · Week/Month │
└──────────────────────┬──────────────────────────┘
│ Connect-RPC
┌──────────────────────┴──────────────────────────┐
│ Go API Server │
│ │
│ DailyNotesService · ChatService · ImportService│
│ SearchService · DigestService · AuthService │
│ WhatsApp · Telegram · Granola Sync · Clipper │
├─────────────────────────────────────────────────┤
│ AI Pipelines │
│ Whisper (audio) · gpt-5.2 (vision/chat/OCR) │
│ text-embedding-3-small (semantic index) │
├─────────────────────────────────────────────────┤
│ Storage │
│ Markdown files notes/YYYY/MM/DD/index.md │
│ Attachments notes/YYYY/MM/DD/<file> │
│ SQLite (entries, conversations, embeddings) │
└─────────────────────────────────────────────────┘
| Package | Purpose |
|---|---|
cmd/blackwood |
Entry point — API server + optional file watcher |
internal/config |
YAML config loading with secret file and env var resolution |
internal/storage |
SQLite + filesystem storage (daily notes as markdown, attachments on disk) |
internal/api |
Connect-RPC service handlers |
internal/rag |
RAG engine (search + LLM) |
internal/index |
Semantic index (embeddings + vector search) |
internal/transcribe |
Whisper audio transcription |
internal/describe |
gpt-5.2 photo description |
internal/ocr |
gpt-5.2 handwriting OCR |
internal/noteparser |
Viwoods .note file parser |
internal/watcher |
Viwoods file watcher (polls a directory for new .note files) |
internal/whatsapp |
WhatsApp Business API webhook |
internal/telegram |
Telegram bot (long polling) |
internal/granola |
Granola meeting notes sync (periodic polling) |
web/ |
React + TypeScript + Vite frontend |
The API uses Connect-RPC (gRPC-compatible over HTTP/JSON). Proto definitions are in proto/blackwood/v1/.
| Service | RPCs |
|---|---|
DailyNotesService |
GetDailyNote, ListDailyNotes, CreateEntry, UpdateEntry, DeleteEntry, UpdateDailyNoteContent, ListDatesWithContent |
ChatService |
Chat (streaming), ListConversations, GetConversation |
ImportService |
ImportViwoods, ImportObsidian |
HealthService |
Check |
Blackwood is configured via a YAML config file passed with --config. Secrets are stored in separate files referenced by path. Environment variables are used as fallback when no config file is provided.
Priority: config file > environment variable > default.
server:
addr: ":8080"
data_dir: ~/.blackwood
# tls:
# cert_file: /path/to/cert.pem
# key_file: /path/to/key.pem
openai:
api_key_file: ~/.blackwood/secrets/openai-api-key
model: gpt-5.2
chat_model: gpt-5.2
embedding_model: text-embedding-3-small
# WhatsApp integration (optional)
# whatsapp:
# verify_token: your-verify-token
# app_secret_file: ~/.blackwood/secrets/whatsapp-app-secret
# access_token_file: ~/.blackwood/secrets/whatsapp-access-token
# phone_number_id: "123456789"
# Telegram bot (optional)
# telegram:
# bot_token_file: ~/.blackwood/secrets/telegram-bot-token
# allowed_chat_ids:
# - 123456789
# Granola meeting notes via MCP (optional)
# granola:
# oauth_token_file: ~/.blackwood/secrets/granola-oauth-token
# poll_interval: 1h
# Viwoods file watcher (optional)
# watcher:
# watch_dir: /path/to/viwoods/notes
# poll_interval: 30sSee blackwood.example.yaml for the full reference.
When no config file is used, the following environment variables are recognized:
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
— | OpenAI API key for all AI features |
OPENAI_MODEL |
gpt-5.2 |
Model for vision and OCR |
OPENAI_CHAT_MODEL |
same as OPENAI_MODEL |
Model for RAG chat |
WHATSAPP_VERIFY_TOKEN |
— | Webhook verification token |
WHATSAPP_APP_SECRET |
— | App secret for signature verification |
WHATSAPP_ACCESS_TOKEN |
— | Permanent access token |
WHATSAPP_PHONE_NUMBER_ID |
— | Phone number ID for sending replies |
TELEGRAM_BOT_TOKEN |
— | Telegram bot token from @BotFather |
GRANOLA_OAUTH_TOKEN |
— | Granola OAuth token for MCP meeting notes sync |
To receive messages via WhatsApp, set up a WhatsApp Business App and configure the whatsapp section in your config file (or set the corresponding environment variables).
Set your webhook URL to https://your-domain/api/webhooks/whatsapp.
Send text, voice messages, and photos to a Telegram bot and have them appear in your daily notes. Unlike WhatsApp, the Telegram integration uses long polling — no public URL or webhook setup is needed.
- Open Telegram and search for @BotFather.
- Send
/newbotand follow the prompts to choose a name and username. - BotFather will reply with a bot token like
123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11. Copy it.
echo -n "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" > ~/.blackwood/secrets/telegram-bot-token
chmod 600 ~/.blackwood/secrets/telegram-bot-tokenAdd to your config file:
telegram:
bot_token_file: ~/.blackwood/secrets/telegram-bot-tokenOr use an environment variable instead:
export TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11Start Blackwood. The server logs will show a 6-digit authorization code:
telegram: bot started — send this code to the bot to authorize a chat auth_code=482910
Open your bot in Telegram and send that code as a message. The bot will reply with "✓ Authorized!" and your chat is now connected. The authorization is persisted in the database — you only need to do this once.
The code rotates after each use, so you can authorize multiple devices or group chats by checking the logs each time.
To disconnect a chat, send /revoke to the bot.
You can also pre-authorize chat IDs in the config file (these can't be revoked via /revoke):
telegram:
bot_token_file: ~/.blackwood/secrets/telegram-bot-token
allowed_chat_ids:
- 123456789| You send | Blackwood does |
|---|---|
| Text message | Adds it as a text entry in today's note |
| Voice message | Transcribes via Whisper, adds transcription as entry |
| Photo | Describes via gpt-5.2 vision, adds description as entry |
All messages are appended to the daily note with a timestamp and "Telegram" source label. Attachments (audio files, photos) are stored alongside the note.
Automatically import meeting notes from Granola into your daily notes via the Granola MCP server. The sync runs periodically (default: every hour), fetching new or updated meeting notes and writing them as entries on the day the meeting occurred.
Each imported note includes the meeting title, date, attendees, Granola's AI-enhanced notes, private notes, and transcript (paid Granola tiers).
blackwood granola-loginThis opens your browser for OAuth authentication with Granola and saves the token to ~/.blackwood/secrets/granola-oauth-token.
Add to your config file:
granola:
oauth_token_file: ~/.blackwood/secrets/granola-oauth-token
poll_interval: 1h # optional, default is 1hOr use an environment variable:
export GRANOLA_OAUTH_TOKEN=your-tokenGranola sync auto-enables when an OAuth token is configured.
Clip any web page into today's daily note. The clipper fetches Open Graph metadata (title, description, preview image) and appends a formatted blockquote to the note.
Drag this to your bookmarks bar (replace the URL with your Blackwood instance):
javascript:void(window.open('https://your-blackwood/clip#'+encodeURIComponent(location.href)))
When clicked on any page, it opens Blackwood's /clip route which calls POST /api/clip, saves the card, and redirects to today's note.
Daily notes are stored as markdown files on disk:
~/.blackwood/
├── blackwood.db # SQLite: entries, conversations, embeddings index
├── notes/
│ └── 2025/
│ └── 01/
│ └── 15/
│ ├── index.md # The daily note markdown
│ ├── voice-memo-a1b2.webm
│ └── photo-c3d4.jpg
└── secrets/
└── openai-api-key
Attachments (photos, audio recordings) are stored alongside the daily note in the same per-day folder and embedded in the rendered markdown.
| Shortcut | Action |
|---|---|
Cmd+D |
Jump to today's note |
Cmd+/ |
Toggle between notes and chat |
Cmd+K |
Open search |
Cmd+T |
Insert current time (in edit mode) |
Cmd+Enter |
Save and exit edit mode |
Esc |
Exit edit mode without saving |
On Windows/Linux, use Ctrl instead of Cmd.
- Telegram bot integration
- Offline sync (service worker + IndexedDB)
- PDF export
- HTTPS/TLS support
- Client-side routing with bookmarkable URLs
- Collapsible sections
- Keyboard shortcuts
- Web clipper (bookmarklet)
- Semantic search
- Weekly & monthly views with range summaries
- Daily digest (nightly generation)
- Location tagging with reverse geocoding
- TOTP authentication
- User preferences (timezone, color theme)
- Granola meeting notes via MCP
- Raycast extension
- iOS app
- Mac app (Electron desktop wrapper)