A lightweight, fast, queryable, keyboard-oriented markdown note-taking app with a minimal footprint.
- Self-hosted, WYSIWYG editor, note-taking backed by plain
.mdfiles with frontmatter, no database, no lock-in. - Move your storage folder anywhere, open notes in any editor.
- Clean and minimal UI.
- Minimal footprint: one binary, ~20 MB on disk and just a few dozen megabytes of RAM for 5000 notes.
- Fast search even with thousands of notes.
- Keyboard-first:
/for blocks,Ctrl+Kfor commands. - Write live query blocks directly in your notes:
{area:pro status:active order by priority},{path:Work/ depth:2 order by name and not project print name}— results update in real time. - All the modern features: export notes / copy-paste images / Excalidraw drawings.
- Responsive for smartphone.
- Read-only mode.
- API to query from the CLI (
scripts/an). - OpenAI-compatible endpoint to plug in an LLM.
- Git sync — push notes to any GitHub / Gitea / Forgejo / GitLab repository (HTTPS token).
- OIDC authentication — delegate login to Authelia, Authentik, Keycloak or any OIDC-compliant provider.
- Dashboard system
- Any ideas?
| Layer | Technology |
|---|---|
| Backend | Rust + Axum |
| Frontend | SvelteKit (vanilla CSS) |
| Editor | TipTap (ProseMirror) |
| Storage | Flat .md files (in-memory index) |
The easiest way to get started — no Rust or Node.js required.
Download the latest binary for your platform from the Releases page:
clef-note-linux-x86_64— Linuxclef-note-freebsd-x86_64— FreeBSD
chmod +x clef-note-linux-x86_64
./clef-note-linux-x86_64 --config clef-note.toml- Rust toolchain — for the backend
- Node.js ≥ 20 — only needed to build the frontend
The backend embeds the entire frontend at compile time into a single binary.
# 1. Build the frontend
cd frontend && npm install && npm run build
# 2. Compile the backend (embeds frontend/build/ automatically)
cd ../backend && cargo build --release
# 3. Run — serves UI + API on http://localhost:3000
./target/release/clef-note --config /path/clef-note.tomlNo Node.js needed at runtime — clef-note is self-contained.
Run the backend and frontend in two separate terminals:
# backend (API on http://localhost:3000)
cd backend && cargo run -- --config /path/clef-note.toml --storage /path/storage
# frontend (UI on http://localhost:5173, proxies API to :3000)
cd frontend && npm install && npm run devOpen http://localhost:5173.
Create /etc/systemd/system/clef-note.service:
[Unit]
Description=Clef Note
After=network.target
[Service]
User=clef-note
WorkingDirectory=/opt/clef-note
ExecStart=/opt/clef-note/clef-note
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now clef-note
sudo journalctl -u clef-note -f # follow logsThe clef-note binary serves both the frontend and the API on port 3000 — no nginx, no Node.js required in production.
Create /usr/local/etc/rc.d/clef-note:
#!/bin/sh
# PROVIDE: clef-note
# REQUIRE: NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="clef_note"
rcvar="${name}_enable"
procname="/usr/local/sbin/clef-note/clef-note"
pidfile="/var/run/${name}.pid"
clef_note_config="/usr/local/etc/clef-note/clef-note.toml"
load_rc_config ${name}
command="/usr/sbin/daemon"
command_args="-P ${pidfile} -r -f ${procname} --config ${clef_note_config}"
run_rc_command "$1"chmod +x /usr/local/etc/rc.d/clef-note
echo 'clef_note_enable="YES"' >> /etc/rc.conf
service clef-note startAll configuration lives in clef-note.toml, looked up next to the binary by default. A template is provided at clef-note.toml.example.
# Required unless [oidc] is configured — hash with: ./clef-note --hash-password "yourpassword"
password = "$argon2id$v=19$..."
# Optional
# storage = "/mnt/notes" # default: ../storage relative to the binary
# api_key = "" # CLI/REST access — openssl rand -hex 32
# Git sync — optional
# [sync]
# remote = "https://github.com/you/notes.git"
# branch = "main"
# token = "ghp_xxxx" # GitHub PAT · Gitea/Forgejo token
# interval_minutes = 30 # 0 = manual only (Settings UI button)
# author_name = "clef-note" # optional
# author_email = "sync@local" # optional
# OIDC — optional. When configured, password login is disabled.
# [oidc]
# issuer_url = "https://auth.example.com"
# client_id = "clef-note"
# client_secret = "..."
# redirect_uri = "https://notes.example.com/auth/oidc/callback"
# allowed_email = "user@example.com" # restrict to a single user
# provider_name = "Authelia" # label on the login button (optional)
# disable_password_login = true # hide password form when OIDC is active (optional)CLI flags (override the config file):
--storage <path> Override the notes storage directory
--port <port> Listening port (default: 3000)
--config <path> Path to clef-note.toml
--hash-password <plaintext> Print Argon2 hash and exit
Notes are stored directly in the storage directory. Media directories are hidden to keep the root clean (they are still versioned by git sync).
storage/
Note.md ← Markdown files at the root
Work/
Meeting.md ← sub-directories supported
.assets/ ← uploaded images
.drawings/ ← Excalidraw diagrams (.excalidraw + .svg preview)
settings.json ← app settings (theme, font, …)
A note named Work/Meeting is stored as storage/Work/Meeting.md. The folder can be moved freely — no database, no lock-in.
To import existing notes, copy your .md files into the storage directory — they will appear automatically on next startup.
| Client | Mechanism |
|---|---|
| Web UI — password mode | Password → session token (localStorage, 30-day TTL) |
| Web UI — OIDC mode | Authorization Code + PKCE via external provider |
CLI (scripts/an) |
AN_KEY env var = api_key from config |
The web UI shows a login page on first visit. Sessions expire after 30 days or on sign out.
OIDC mode — add an [oidc] section to clef-note.toml (see above). By default both the OIDC button and the password form are shown, so you can keep a fallback. Set disable_password_login = true to show the OIDC button only.
Any provider that exposes a standard OIDC discovery endpoint (/.well-known/openid-configuration) should work.
Add a client entry to your Authelia configuration:
identity_providers:
oidc:
clients:
- client_id: 'clef-note'
client_name: 'Clef Note'
client_secret: '$pbkdf2-sha512$...' # hash with: authelia crypto hash generate pbkdf2 --variant sha512
public: false
authorization_policy: 'one_factor'
require_pkce: true
pkce_challenge_method: 'S256'
token_endpoint_auth_method: 'client_secret_basic' # required — clef-note uses Basic auth by default
redirect_uris:
- 'https://notes.example.com/auth/oidc/callback'
scopes:
- 'openid'
- 'profile'
- 'email'
grant_types:
- 'authorization_code'
response_types:
- 'code'
consent_mode: implicitThen in clef-note.toml:
[oidc]
issuer_url = "https://auth.example.com"
client_id = "clef-note"
client_secret = "your-plain-text-secret" # the un-hashed value used above
redirect_uri = "https://notes.example.com/auth/oidc/callback"
allowed_email = "user@example.com"
provider_name = "Authelia"
disable_password_login = trueNote:
token_endpoint_auth_method: 'client_secret_basic'is required. Authelia defaults toclient_secret_postfor new clients but clef-note (and most OIDC libraries) useclient_secret_basic, which is the method recommended by RFC 6749.
Clef Note can synchronise your storage directory with any git remote that supports HTTPS token authentication (GitHub, Gitea, Forgejo, GitLab).
- On every sync cycle, all local changes are committed.
- Remote changes are fetched and integrated (fast-forward when possible, merge otherwise).
- Conflicts keep the local version. A
Conflict - <note name>.mdfile is created so you can review both versions. - The result is pushed to the remote.
Sync runs automatically at startup and on the configured interval. It can also be triggered manually from Settings → Git Sync → Sync now.
1. Create a token
- GitHub: Settings → Developer settings → Personal access tokens → Fine-grained → Contents: Read and write
- Gitea / Forgejo: User Settings → Applications → Generate token
2. Add a [sync] section to clef-note.toml
[sync]
remote = "https://github.com/you/notes.git"
branch = "main"
token = "ghp_xxxx"
interval_minutes = 303. Restart clef-note — the initial sync runs on startup.
- The token is read from the config file and kept in memory only — it never appears in
.git/config, commit messages, or logs. clef-note.tomlis automatically added to.gitignoreinside the storage directory on first sync.- Restrict the config file permissions:
chmod 600 clef-note.toml.
All endpoints are served by the Rust backend on port 3000.
| Method | Path | Description |
|---|---|---|
GET |
/notes |
List all notes |
GET |
/notes/{*name} |
Read note ({name, content, frontmatter}) |
PUT |
/notes/{*name} |
Create or overwrite note ({content}) |
PATCH |
/notes/{*name} |
Rename note ({new_name}) |
DELETE |
/notes/{*name} |
Delete note |
GET |
/backlinks/{*name} |
Backlinks for a note |
POST |
/assets |
Upload image (multipart), returns {url} |
GET |
/api/search?q= |
Full-text search |
GET |
/api/query?q= |
DSL metadata query |
GET |
/api/sync/status |
Git sync status {configured, last_sync_at, last_error} |
POST |
/api/sync |
Trigger an immediate sync |
Filters are combined with implicit AND. Tokens are whitespace-separated.
#work status:active → tagged "work" AND status "active"
#work OR #perso → tagged "work" or "perso"
NOT status:archived → all notes except archived
recent:10 → 10 most recently modified notes
oldest:5 → 5 least recently modified notes
date:2025-04 type:meeting → meetings from April 2025
#journal order by date desc → journal notes, newest frontmatter date first
recent:10 order by title → 10 freshest notes, sorted alphabetically
status:active order by due → active tasks sorted by due date
area:pro type:meeting print title date → meetings with only title and date columns
Filters: bare word, #tag, tag:, title:, path:, name:, depth:, status:, type:, area:, author:, rating:, date:, due:, lastModified:, url:, alias:, pinned:, priority:, project:
Limiters: recent:n, oldest:n — by filesystem modification date
Sort: order by <field> [asc|desc] — fields: name, title, date, modified, due, status, rating, area, author, priority, project, lastModified
Columns: print <field> [field2 …] — fields: name, title, tags, date, status, area, author, due, rating, url, priority, project
| Shortcut | Action |
|---|---|
Ctrl+K |
Open command palette |
Ctrl+Shift+H |
Go to home page (set in Settings → General) |
Ctrl+Shift+P |
Navigate back in history |
Ctrl+Shift+N |
Navigate forward in history |
| Shortcut | Action |
|---|---|
/ |
Open slash-command menu |
[[ |
Start a WikiLink |
:shortcode: |
Insert emoji (:smile: → 😊) |
Ctrl+S |
Save (auto-save also active) |
Ctrl+B/I |
Bold / Italic |
Ctrl+Z |
Undo |
MIT