Skip to content

mufasa159/notes-web

Notes

End-to-end encrypted notes app. Local-first, zero-knowledge, hybrid post-quantum (RSA-4096 + ML-KEM-768). The server stores ciphertext only; plaintext never leaves the browser. See SECURITY.md for the threat model and CLAUDE.md for the architecture.

UI Demo

notes_v2.0.0.mp4

Installation (Linux)

Download install.sh and its sha256 sidecar from the latest release page, verify the script against the posted hash, and run it. The installer writes a hardened systemd unit, optionally configures nginx + a self-signed cert, and preserves data across upgrades. Every operator-facing detail (modes, env vars, data layout, security posture, troubleshooting) is documented in the structured header comment at the top of install.sh itself; read it before running.

curl -fsSLO https://github.com/mufasa159/notes-web/releases/latest/download/install.sh
curl -fsSLO https://github.com/mufasa159/notes-web/releases/latest/download/install.sh.sha256
sha256sum -c install.sh.sha256
sudo bash install.sh --system

Dev Quickstart

Requires Python 3.12+.

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python app.py                  # http://127.0.0.1:8000
NOTES_DEBUG=1 python app.py    # uvicorn --reload + ./.notes/ for state

The first user to register becomes admin.

Where things live

app.py, config.py, storage.py, migrate.py     entry points
auth/        login, register, sessions, tokens, lockout
notes/       /api/notes + /api/folders
audit/       encrypted-at-rest, hash-chained log
admin/       /admin/users, /admin/audit, /admin/settings
crypto/      server-side rotation payload validation
db/          shared async aiosqlite helpers
jobs/        background tasks (backups, chain verify, retention)
middleware/  access, csrf, headers, ratelimit, ip allowlist, validation
migrations/  numbered .sql + .py files
templates/, static/, tests/, docs/

See CLAUDE.md for module-level detail and docs/ for design.

Environment variables

Var Default Effect
NOTES_HOME ~/.notes Where state (DB, keys, backups, logs) lives.
NOTES_DEBUG unset When 1: Starlette debug + uvicorn --reload, pins state to ./.notes/, drops the Secure cookie attribute.
NOTES_INSECURE_COOKIES unset When 1: drops the Secure cookie attribute. For HTTP deployments without NOTES_DEBUG.
NOTES_AUDIT_KEY unset When file: forces audit-log key onto disk (~/.notes/keys/audit.key, mode 0600) instead of OS keychain.
NOTES_DISABLE_BACKGROUND_JOBS unset When 1: skips the backup, chain-verify, and retention tasks.

Crypto

  • KDF in browser: Argon2id over the password, split via HKDF-SHA256 into auth_hash (sent to server) and wrap_key (stays local).
  • Each user holds two keypairs generated in the browser: RSA-OAEP-4096 and ML-KEM-768.
  • Each note has a per-note AES-256-GCM key, wrapped under both public keys.
  • Keys auto-rotate every 90 days at login in a Web Worker (configurable).
  • Audit log is AES-256-GCM encrypted, hash-chained, immutable for 60 days.

See docs/diagrams/ for sequence + activity diagrams covering each flow.

Migrations

Schema lives in migrations/, numbered NNN_name.{sql,py}. The runner applies pending files at startup, refuses to run if a checksum drifted or a gap is inserted. Never edit a migration after it ships; write a new one.

python migrate.py --status   # applied vs pending
python migrate.py            # apply pending

Tests

pytest                # full suite
pytest tests/auth     # one domain

Build (production assets)

Source files under static/{js,css} are served as-is in dev. For production, run the bundler to emit content-hashed copies under static/dist/ plus a manifest.json:

python build.py            # write static/dist/
python build.py --clean    # wipe and rebuild

CSS is minified; JS modules are copied with hashed filenames and their relative imports rewritten. Vendor (static/vendor/*) and Monaco assets are never bundled. Templates resolve assets via the Jinja static_path() helper, which picks /static/dist/... when a manifest is present and NOTES_DEBUG is not set, otherwise falls back to source paths.

Supply chain

Three layers of integrity, smallest to largest:

1. Hash-pinned Python dependencies. requirements.lock pins every direct + transitive dependency to an exact version and a sha256 of the wheel/sdist. Production installs use:

pip install --require-hashes -r requirements.lock

--require-hashes makes pip refuse any wheel whose digest doesn't match — a tampered PyPI mirror or compromised maintainer account cannot silently swap a release. requirements.txt stays the human-readable source of truth (direct deps with version pins). To regenerate the lock after editing requirements.txt, see CONTRIBUTING.md.

2. Content-hashed dist filenames. python build.py rewrites static/js/*.js and static/css/*.css to static/dist/<name>.<sha256-prefix>.<ext>. The hash in the URL is the file's content fingerprint, so any byte change forces a new URL — clients cannot serve a cached, stale copy of an old payload after a security fix ships.

3. Subresource Integrity (SRI) on vendored scripts. build.py also writes static/dist/sri.json with sha384 hashes for every directly-loaded vendor entry point (argon2-browser, mlkem, the monaco loader). Templates render <script integrity="sha384-..." crossorigin="anonymous"> so the browser refuses to execute a vendor file whose body has been altered after build time. Note: Monaco's loader bootstraps further chunks at runtime — those lazily-loaded chunks fall outside the SRI list (best effort; documented in templates/notes.html).

About

e2ee note-taking web-app built with starlette, jinja2, sqlite3 and monaco editor. a privacy-friendly self-hosted note-taking solution.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors