spaced repetition over an obsidian vault
Project description
mdsr
Spaced repetition for an Obsidian vault. You write flashcards as callouts inside your notes; mdsr parses them, schedules them with FSRS, and serves a small web UI for reviewing.
mdsr only reads from the vault. Schedule state lives in SQLite under your XDG data dir.
Card syntax
> [!sr] What is ...?
> The ...
> [!sr-bi] Cat
> Katze
[!sr]— one card, front (the question) → back.[!sr-bi]— two cards, front ↔ back, marked as siblings (reviewing one hides the other until the next study day).- Obsidian's fold markers work:
[!sr]-(collapsed) and[!sr]+(expanded). Both are equivalent to[!sr]. - LaTeX, code, images, wikilinks — anything Obsidian renders works here too.
Card identity is sha256(front). Editing the back keeps the schedule.
Editing the front creates a new card (the old one is trashed; you can merge its schedule onto the new card from the sidebar).
Run
uv tool install mdsr
sr --vault /path/to/your/vault
# or, clone-on-first-run into $XDG_DATA_HOME/mdsr/vaults/:
sr --vault git@github.com:you/notes.git
If --vault is a git clone (or a git URL, which gets cloned on first run and reused after), the server runs git pull --ff-only every --poll-seconds (default 60), re-parses changed files, and updates card state.
You can let trash get auto-purged after --trash-purge-days (default 30).
If --vault is a plain directory it skips the pull and detects changes via per-file mtime instead.
Open http://127.0.0.1:8765, sr --help lists all flags.
[!TIP] With the Obsidian web viewer plugin, you can use mdsr directly within Obsidian.
Storage
One SQLite file per vault, under XDG data home:
~/.local/share/mdsr/<vault-basename>-<hash8>.db
The hash is the first 8 chars of sha256(resolved-vault-path), so running against two different vaults gives you two independent DBs automatically.
Override with --db-path if you want a specific location.
Deploy on a VPS
A single Python process. Bind it to 127.0.0.1 and reach it via Tailscale.
Keep it running with systemd
The sr command runs in the foreground and exits when its shell dies, so on a server you want something to (re)start it: daemonize at boot, restart on crash, survive logout.
A user-level systemd unit:
[Unit]
Description=mdsr review server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=%h/.local/bin/sr --vault %h/vault --host 127.0.0.1 --port 8765
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now mdsr
loginctl enable-linger $USER # keep the unit running after logout
Expose it on your tailnet
The simplest setup — one service on one machine — is one port:
tailscale serve --bg --https=443 http://127.0.0.1:8765
You'll then have it at https://<machine>.<tailnet>.ts.net.
If you want several services on the same machine, mount them under different URL paths.
Tailscale forwards the prefix as-is, so the app has to know its own prefix — pass it via --root-path:
sr --vault ~/vault --port 8765 --root-path /sr
tailscale serve --bg --https=443 --set-path=/sr http://127.0.0.1:8765
Reachable at https://<machine>.<tailnet>.ts.net/sr/.
TODO
- Core: Properly render note content,images (parsing and rendering step via https://onyx.md)
- Cosmetic: Make wikilinks resolve to published vault (once vault is published via onyx)?
Develop
make dev # uv sync --group dev
make test
make check # lint + format
make fix # ruff fix + format
make build # uv build → dist/
make release-{patch,minor,major} # bumps pyproject.toml version, commits and git tags
make publish # requires UV_PUBLISH_TOKEN
Inspiration
- The Obsidian Spaced Repetition plugin — the original cards-in-notes Obsidian SRS, and prior art for most of what this does.
- Andy Matuschak's notes on evergreen note-writing — concept-orientation, cards living inside concept notes, and How to write good prompts.
- Michael Nielsen, Using spaced repetition systems to see through a piece of mathematics and Augmenting Long-term Memory.
- FSRS and its Python port
fsrs.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mdsr-0.2.0.tar.gz.
File metadata
- Download URL: mdsr-0.2.0.tar.gz
- Upload date:
- Size: 24.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.4 {"installer":{"name":"uv","version":"0.11.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
775e83e8591a428e2428c873f9af676c8628f6dbe01fbc0ed84043f29e9a2590
|
|
| MD5 |
fc69fd9f7dd59271aaf624ba188691fd
|
|
| BLAKE2b-256 |
cf614a4e38ed52a70e277275a97995aa580649b439ae96ac72f23e8818ff62b2
|
File details
Details for the file mdsr-0.2.0-py3-none-any.whl.
File metadata
- Download URL: mdsr-0.2.0-py3-none-any.whl
- Upload date:
- Size: 29.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.4 {"installer":{"name":"uv","version":"0.11.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9d371f9439d0cdaa32e746068e25cf4213c9ecb0e486a4cdb3c2cb4cf28e60c
|
|
| MD5 |
126cb37b66897a1ea5249f38d56706d7
|
|
| BLAKE2b-256 |
3abe922f635e773687eb40b3a464edcaa4d277161b14c4f1294ffacc170b54fc
|