avallon
A place where notes are kept, and found again.
A personal note site: every page is a Markdown file in a directory tree that
encodes both its URL and its taxonomy. Pages cite each other with [[wikilinks]]
and show their backlinks, group into dossiers, and are found through faceted
browsing or full-text search. Notes live in their own git repository, apart
from this tool, and sync across devices on their own.
The content model, and the reasoning behind it, lives in
docs/model.md. This README is the user guide.
Status. v0.1.0. What comes next, and why, is in
ROADMAP.md.
How it works in one picture
your machine git remote (e.g. GitHub)
------------ ------------------------
avallon serve --> reads/writes the notes repo
|
+--> commit (instant, batched)
|
+--> [detached] pull + push ------------> origin
^
another device: the running server polls <-- pull ----------------- +
- One directory per page, holding
index.mdand its images. - Instant local commit on every save, network sync in the background.
- The tool and the notes are two repositories. Upgrading one never touches the other.
Requirements
- Python 3.11+
gitpandocfor the Word/PDF exportfzffor the interactive pickersripgrep(optional): full-text search falls back to a pure-Python scan without it
pandoc and fzf are not Python packages. The supported install is through
pixi, which brings them along.
Install
pixi global install avallon # brings pandoc and fzf
pip install avallon # Python parts only, see Requirements
Setup a notes repository
The notes live in their own git repository, separate from this tool. Point avallon at it once:
avallon init ~/notes # local path
avallon init git@github.com:you/notes.git # or a clone URL
init creates the layout, makes the repository active, and remembers its path
in ~/.config/avallon/config.toml. The created repository looks like:
notes/
├── taxonomy.toml # the declared domains and types
└── <domain>/<type>/<slug>/
├── index.md
└── figure.png # assets sit next to the page that uses them
init and repo are create-or-validate:
| Target | What happens |
|---|---|
| Path does not exist | mkdir + git init + full scaffold |
| Existing directory, not a git repository | git init + scaffold what is missing |
| Existing repository, already conformant | adopted as is |
| Existing repository with unrelated content | asks for confirmation first |
| A clone URL | cloned into ~/<repo-name>, then validated |
Switch repositories
avallon repo # print the active notes repository
avallon repo ~/other-notes # switch to another one (same rules)
Pages
A page is a directory holding an index.md. Its path is both its URL and its
taxonomy:
content/<domain>/<type>/<slug>/index.md -> /<domain>/<type>/<slug>/
- domain: what the page is about (
informatique,administratif, ...). A subject, never a context: "work" or "personal" is a tag, because a page can be both. - type: what the page is (
fiche,cr,tutoriel,recueil).
Both are declared in taxonomy.toml, and a page cannot be born under an
undeclared pair. Extend the vocabulary with avallon add-domain /
avallon add-type, check the tree with avallon check.
Frontmatter
---
title: Page title
date: 2026-07-01 # creation, set once
updated: 2026-07-08 # last change, stamped on commit
tags: [smartg, flux]
summary: One sentence, shown on the home page.
project: recours-batterie # slug of the page indexing the dossier (optional)
status: en cours # en cours | terminé | abandonné (optional)
visibility: private # local only, never served elsewhere (optional)
---
Dossiers
A dossier is not a new kind of object: it is an ordinary page, the one that
introduces it, and its pages name it in their project:. The site derives the
rest, so nothing has to be kept up to date by hand:
- the dossier's page lists its own pages, grouped by type;
- the sidebar browses the dossier instead of the whole tree while you are in it;
- each page names the dossier it belongs to, and search shows it too.
Titles can therefore stay short ("Mail retour"): the dossier places them.
Belonging is not citing. A page enters a dossier when it will be archived
with it; a durable note the dossier merely cites stays outside and is linked
with [[…]], so it outlives the dossier it was written during.
Writing
Standard Markdown, plus:
| Syntax | What it does |
|---|---|
[[slug]], [[slug|label]] |
link another page, by slug, path or title |
[[figure.pdf]] |
link a file sitting next to the page |
```python |
syntax-highlighted code (Pygments) |
!!! note / !!! warning |
admonition cards |
$…$, $$…$$ |
LaTeX math (MathJax) |
{rouge}(texte) |
inline coloured span |
```gallery, ```plot, ```csv, ```query |
content blocks |
- [ ] |
task lists |
Every page shows what cites it, so a note is never a dead end.
Commands
| Command | What it does |
|---|---|
avallon serve |
Run the site (see Server). |
avallon init <path|url> |
Initialize or adopt a notes repository, make it active. |
avallon repo [path] |
Print or switch the active repository. |
avallon new |
Scaffold a page (domain and type picked from the taxonomy). |
avallon move <page> |
Re-file a page under another domain/type. |
avallon sync |
Pull, commit, push. --local commits without the network. |
avallon export <page> |
Export a page to .docx or .pdf (needs pandoc). |
avallon add-domain <name> |
Extend the taxonomy. |
avallon add-type <name> |
Extend the taxonomy. |
avallon check |
Verify every page sits under a declared domain/type. |
avallon stamp |
Fill missing date: / updated: in the frontmatter. |
avallon setup |
Write the deployment env file (address, port, token). |
avallon install |
Install and start the systemd user unit. |
Server
Quick start (local)
avallon serve # 127.0.0.1:8000
avallon serve --port 8800
The browser is also the editor: Ctrl+E opens the raw Markdown of the page,
saving writes the file and commits it. Ctrl+K opens the command palette,
Ctrl+N creates a page. What you type is re-rendered live.
Access token
Binding anywhere other than loopback requires a token:
AVALLON_TOKEN=$(openssl rand -hex 32) avallon serve --host 10.8.0.2
The guard is a no-op when no token is set, which is only safe on loopback, so the command refuses any other bind without one. The token is the second layer, behind the network boundary: it is what stands between your notes and any other device that can reach the port.
Deploy (systemd + wireguard)
avallon setup # write the env file, generate a token
avallon install # install and start a systemd user unit
The unit binds the address given at setup, and the notes repository is polled on a timer so pages written on another device show up without a restart.
Configuration
| Variable | Default | What it does |
|---|---|---|
AVALLON_CONTENT_DIR |
the configured repository | Override the notes location. |
AVALLON_TOKEN |
(none) | Bearer token required for every request. |
AVALLON_HOST / AVALLON_PORT |
127.0.0.1 / 8000 |
Bind address. |
AVALLON_SHOW_PRIVATE |
on in debug | Serve pages marked visibility: private. |
AVALLON_SYNC_WINDOW |
900 |
Seconds during which consecutive edits fold into one commit. |
AVALLON_POLL_INTERVAL |
120 |
Seconds between two pulls; 0 disables the poller. |
AVALLON_ALLOWED_HOSTS |
loopback | Comma separated names the site answers to. |
AVALLON_SECRET_KEY |
(generated per process) | Django secret key. avallon setup writes a fixed one. |
AVALLON_DEBUG |
0 |
Debug mode. Never on when exposed. |
Sync model
Every write commits immediately and locally, then a detached process pulls and pushes. A save never waits on the network, and being offline only costs a warning.
Commit batching. Consecutive edits fold into a single commit for
AVALLON_SYNC_WINDOW seconds, so a page written in ten passes does not leave
ten commits behind. Only commits the tool made itself are amended, recognized
by their trailer.
Polling. A long-running server is one more git writer among the devices, so it pulls on a timer to reflect what was written elsewhere.
Development
pixi run serve # the site, with live reload
pixi run fmt # ruff format
pixi run lint # ruff check
pixi run type-check # mypy
pixi run test # pytest + coverage
pixi run all # the four above
Releasing
Version numbers live in pyproject.toml. Tagging vX.Y.Z and pushing the tag
triggers the release workflow, which builds the sdist and the wheel and
publishes them to PyPI through Trusted Publishing (OIDC), so no token is
stored anywhere.
git tag v0.1.0 && git push origin v0.1.0
License
Apache License 2.0, see LICENSE.
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 avallon-0.1.0.tar.gz.
File metadata
- Download URL: avallon-0.1.0.tar.gz
- Upload date:
- Size: 1.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3fc6fdc7d84a6705bb35613a4ac6b935ce67f772aac3df81346fa50f9bf83d14
|
|
| MD5 |
e5260ba36751599474ef34fe3a0283ad
|
|
| BLAKE2b-256 |
98b6a074c175f724cff9b19edac71ba29614f86ddf57774117dfc241f4307363
|
Provenance
The following attestation bundles were made for avallon-0.1.0.tar.gz:
Publisher:
release.yml on walcark/avallon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
avallon-0.1.0.tar.gz -
Subject digest:
3fc6fdc7d84a6705bb35613a4ac6b935ce67f772aac3df81346fa50f9bf83d14 - Sigstore transparency entry: 2356277970
- Sigstore integration time:
-
Permalink:
walcark/avallon@acf4b83c6662b5157a6cf30c7d95ae222de0aa84 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/walcark
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@acf4b83c6662b5157a6cf30c7d95ae222de0aa84 -
Trigger Event:
push
-
Statement type:
File details
Details for the file avallon-0.1.0-py3-none-any.whl.
File metadata
- Download URL: avallon-0.1.0-py3-none-any.whl
- Upload date:
- Size: 1.5 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
495050fde752698e008654ae8d6ff7d514961092c7dec41989f51520ef210345
|
|
| MD5 |
94fa2e7e935d32dbf9176698ce4bb517
|
|
| BLAKE2b-256 |
7fdc958ea1e8df1ea44b643592d9d1fc5990bfb928da8a7ee8fe143857c9382e
|
Provenance
The following attestation bundles were made for avallon-0.1.0-py3-none-any.whl:
Publisher:
release.yml on walcark/avallon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
avallon-0.1.0-py3-none-any.whl -
Subject digest:
495050fde752698e008654ae8d6ff7d514961092c7dec41989f51520ef210345 - Sigstore transparency entry: 2356278057
- Sigstore integration time:
-
Permalink:
walcark/avallon@acf4b83c6662b5157a6cf30c7d95ae222de0aa84 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/walcark
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@acf4b83c6662b5157a6cf30c7d95ae222de0aa84 -
Trigger Event:
push
-
Statement type: