A page, including a journal day, can be moved into the unlocked vault with its id kept. Links to it elsewhere become [[locked:id]], shown as Locked Page until the vault is unlocked, so its title is kept only in the vault. Move out reverses it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
229 lines
12 KiB
Markdown
229 lines
12 KiB
Markdown
# Mako
|
|
|
|
A private notebook in the spirit of Obsidian and Notion, for people who would rather run their own.
|
|
Pages that nest inside each other, `[[links]]` between them with backlinks, a journal page for each
|
|
day, tags, and quick search, all kept as encrypted markdown files. One small binary, no database, no
|
|
dependencies. You sign in with **Authentik** and nothing else: there are no passwords in Mako.
|
|
|
|
Named for the mako shark, the fastest in the sea, because a notebook should keep up with you.
|
|
|
|
## What it does
|
|
|
|
- **Pages in a tree.** Give a page a title and it appears in the sidebar. Add a subpage with the `+`
|
|
next to it, or move a page with Move. Delete moves a page to the trash with an Undo, and any
|
|
subpages move up a level so nothing is lost with it.
|
|
- **Link pages with `[[`.** Type `[[` and pick a page, or type a new title to create it. A dashed link
|
|
is a page that does not exist yet; click it and it is made. Each page lists what links to it.
|
|
Rename a page and every `[[link]]` to it is updated.
|
|
- **Type `/` for commands**: headings, to-do lists, quotes, code blocks, dividers, today's date, the
|
|
time. Lists continue when you press Enter and indent with Tab. Ctrl+B and Ctrl+I work as you expect.
|
|
- **Live, Edit or View.** Live is the default: every line shows rendered except the one you are on,
|
|
which shows its markdown, as in Obsidian's live preview. Edit shows the whole page as plain
|
|
markdown, and View shows the whole page rendered, with tasks you can tick and links you can follow.
|
|
Click a page in View to start writing. Ctrl+E switches between editing and View.
|
|
- **Quick find with Ctrl+K** searches titles and text as you type, and makes a new page if there is no
|
|
match.
|
|
- **A journal.** Today opens the page for the day, with an Insert time button. Days link to each
|
|
other: `[[2026-10-31]]` goes to that day.
|
|
- **Quick notes.** The box on Home takes a thought in two keystrokes (`n`, then Ctrl+Enter). They stay
|
|
out of the sidebar until you give them a title.
|
|
- **Tags by typing.** Write `#errands` or `#projects/mako` anywhere. Numbers such as `#12` and
|
|
anything in code are left alone.
|
|
- **Autosave that does not lose things.** Changes save as you type. If the connection drops, Mako
|
|
keeps your text and retries. If your Authentik session ends mid-sentence, it keeps the text, offers
|
|
to sign in again in another tab, and saves when you are back. If a page changed on another device,
|
|
it asks which version to keep instead of overwriting.
|
|
- **A vault for what needs more.** Pages in the Vault open only with a passphrase of their own, on top of
|
|
signing in. The server cannot read them until you unlock it, and they are in no list, search or download
|
|
of your ordinary notebook. It locks itself after 15 minutes idle, when you press Lock, and when you
|
|
sign out. See Encryption below. Press To vault on any page, journal days included, and Move out on a
|
|
vault page to bring it back. A page that links to a vaulted page shows Locked Page in its place until
|
|
you unlock the vault, and the vaulted page's title is kept only in the vault.
|
|
- **Take it with you.** Download everything as a zip that opens as a vault in Obsidian: file names are
|
|
titles, so the links keep working, and journal days sit in a Journal folder.
|
|
|
|
## Signing in with Authentik
|
|
|
|
Mako never sees a password. Who may open it is decided in Authentik, and each person gets a
|
|
notebook that only they can read.
|
|
|
|
1. In Authentik, go to **Applications → Providers → Create** and choose **OAuth2/OpenID Provider**.
|
|
Set the client type to **Confidential**, add the redirect URI (strict)
|
|
`https://notes.example.com/auth/callback` (use the address you will open Mako at), and keep the
|
|
default scopes (openid, profile, email).
|
|
2. **Applications → Create**: name it Mako and pick that provider. To limit who can open it, bind a
|
|
user or group to the application.
|
|
3. Copy the provider's **Client ID** and **Client Secret**, and note its **OpenID Configuration
|
|
Issuer**, which looks like `https://auth.example.com/application/o/mako/`.
|
|
4. Give Mako those four things (see below), then run `mako check`. It tells you whether
|
|
Authentik answers and what redirect URI to allow.
|
|
|
|
Mako asks for the `openid profile email` scopes and uses the authorization code flow with PKCE, a
|
|
`state` and a `nonce`. Whoever Authentik lets into the application can sign in; to be stricter, list
|
|
people under `allowed` (emails or usernames), and Mako turns anyone else away even if Authentik
|
|
lets them through.
|
|
|
|
## Installing on Linux (systemd)
|
|
|
|
```sh
|
|
curl -fsSL https://git.veillax.com/Veillax/mako/raw/branch/main/deploy/install.sh | sudo sh
|
|
```
|
|
|
|
This downloads the latest release (checking its SHA-256), installs `/usr/local/bin/mako`, creates a
|
|
`mako` user, `/etc/mako/` (settings) and `/var/lib/mako/` (your notes), and writes the
|
|
systemd service and `/etc/mako/mako.env`. Then:
|
|
|
|
```sh
|
|
sudoedit /etc/mako/mako.env # fill in the four Authentik lines at the top
|
|
sudo systemctl start mako
|
|
journalctl -u mako -f # it says at once if a setting is missing or wrong
|
|
```
|
|
|
|
Mako listens on `127.0.0.1:8490`. Put an HTTPS reverse proxy (or
|
|
[Hammerhead](https://git.veillax.com/Veillax/hammerhead)) in front of it and open the address you gave
|
|
as `MAKO_PUBLIC_URL`. Authentik will only send people back to an address you allowed, and cookies
|
|
are marked Secure when that address is https.
|
|
|
|
Run the installer again to upgrade; your settings and notes are kept and a running service is
|
|
restarted. Options: `--start`, `--version v0.1.0`, `--listen 127.0.0.1:8490`, `--source` (build with Go
|
|
instead) and `--uninstall` (add `--purge` to delete settings and notes). Read it first if you like:
|
|
[`deploy/install.sh`](deploy/install.sh).
|
|
|
|
## Running it by hand or in Docker
|
|
|
|
```sh
|
|
export MAKO_PUBLIC_URL=https://notes.example.com
|
|
export MAKO_OIDC_ISSUER=https://auth.example.com/application/o/mako/
|
|
export MAKO_OIDC_CLIENT_ID=...
|
|
export MAKO_OIDC_CLIENT_SECRET=...
|
|
./mako check # optional: test the settings
|
|
./mako serve
|
|
```
|
|
|
|
Docker: see [`Dockerfile`](Dockerfile) and [`deploy/docker-compose.yml`](deploy/docker-compose.yml).
|
|
|
|
### Commands
|
|
|
|
```
|
|
mako serve [-config mako.json] [-listen addr] [-data dir]
|
|
mako check [-config mako.json] test the Authentik settings without starting
|
|
mako version
|
|
```
|
|
|
|
## Settings
|
|
|
|
Everything can be set with environment variables, which win over the settings file and are never
|
|
written to it:
|
|
|
|
| Setting | Variable | |
|
|
| --- | --- | --- |
|
|
| `public_url` | `MAKO_PUBLIC_URL` | where you open Mako, no path |
|
|
| `oidc.issuer` | `MAKO_OIDC_ISSUER` | the provider's issuer |
|
|
| `oidc.client_id` | `MAKO_OIDC_CLIENT_ID` | |
|
|
| `oidc.client_secret` | `MAKO_OIDC_CLIENT_SECRET` | |
|
|
| `allowed` | `MAKO_ALLOWED` | optional; emails, usernames or Authentik ids, comma separated |
|
|
| `key` | `MAKO_KEY` | encrypts your pages; made for you, see Encryption below |
|
|
| `listen` | `MAKO_LISTEN` | default `127.0.0.1:8490` |
|
|
| `data_dir` | `MAKO_DATA` | default `data` |
|
|
|
|
`mako.json` also holds a `secret` that signs session cookies. It is made on first run and written
|
|
with mode `0600`. Changing it signs everyone out.
|
|
|
|
## Encryption
|
|
|
|
Every page is encrypted on disk with AES-256-GCM. Each person has a key of their own, made from the
|
|
`key` in `mako.json` together with who they are in Authentik (the issuer and their subject id). So one
|
|
person's files cannot be opened with another's key, and a file copied over another page's name stops
|
|
opening. Pages are decrypted in the server's memory while it runs, and your downloads are plain
|
|
markdown.
|
|
|
|
- **The key is made on first run** and written to `mako.json` (mode `0600`), or set `MAKO_KEY` (at least
|
|
32 characters, for example `openssl rand -base64 32`) to keep it out of the file.
|
|
- **Back the key up, apart from the notes.** Without it the pages cannot be read, and Mako refuses to
|
|
start a notebook it cannot decrypt rather than overwrite it. Do not change it. Unlike `secret`,
|
|
changing `key` does not just sign people out.
|
|
- **What it protects:** a stolen disk or backup of `data/`, and anyone who can read the notes folder but
|
|
not the settings. **What it does not:** someone who controls the running server, or who can read both
|
|
`mako.json` and `data/`. Who you are in Authentik is not secret, so it cannot be the key by itself.
|
|
- **The Vault** adds a second lock. Its pages are encrypted with a key made from your passphrase
|
|
(PBKDF2, 600,000 rounds) together with the server's key, so someone with the server's key still cannot
|
|
read them without the passphrase. The server holds the unlocked key in memory only, for as long as the
|
|
vault stays open. **There is no way to reset the passphrase**; forget it and those pages are gone.
|
|
Five wrong guesses in ten minutes lock out further tries for a while.
|
|
- **Upgrading from 0.1.0:** existing plain files are read and saved again encrypted the first time
|
|
that person signs in, and a `key` is added to your settings file.
|
|
|
|
## Where your notes are
|
|
|
|
```
|
|
data/users/<hash of who you are>/
|
|
notes/20261031-143205-a1b2c3 one encrypted file per page (named .md)
|
|
notes/daily-2026-10-31 the journal page for a day
|
|
trash/ deleted pages, until the trash is emptied
|
|
```
|
|
|
|
Inside, a page is markdown with a small header (shown here decrypted, and what the zip export holds):
|
|
|
|
```
|
|
---
|
|
title: "Trip to Lisbon"
|
|
parent: 20261030-091200-ffee11
|
|
created: 2026-10-31T14:32:05.123Z
|
|
updated: 2026-10-31T14:35:00.456Z
|
|
pinned: true
|
|
---
|
|
|
|
Flights and hotels, see [[Packing list]] #travel
|
|
```
|
|
|
|
A plain `.md` file dropped into `notes/` is picked up when that person's notebook next opens, with its
|
|
file time as its date, and is then encrypted (name it with lower-case letters, numbers and dashes).
|
|
|
|
## Security notes
|
|
|
|
- Sign-in is Authentik only: the authorization code flow with PKCE, `state` and `nonce`, over https. The
|
|
ID token comes straight from Authentik's token endpoint, and its issuer, audience, expiry and nonce
|
|
are checked. Authentik and Mako must be reachable over https unless both are on the same machine.
|
|
- Sessions are `HttpOnly`, `SameSite=Lax` cookies that last 14 days, signed with the secret in the
|
|
settings file. Nothing is kept on the server, so a restart does not sign anyone out. The `allowed`
|
|
list is checked on every request.
|
|
- Each person's folder is named from a hash of who they are, never from anything they typed. One
|
|
person's requests can only ever reach their own folder.
|
|
- Every change from the browser needs a header a page on another site can't send. The page has a strict
|
|
Content-Security-Policy, and pages are never rendered as HTML.
|
|
- Pages are encrypted on disk (see Encryption), with a key per person. The server itself, while it
|
|
runs, can read them: this is not end-to-end encryption.
|
|
|
|
## Honest limits
|
|
|
|
- No sharing or real-time collaboration: each notebook has one reader.
|
|
- No attachments or images, and no offline mode; it needs the server to be reachable.
|
|
- No scripts or tokens for posting notes from elsewhere: Authentik is the only way in.
|
|
- Searching holds every page in memory, which is comfortable for tens of thousands of pages and not
|
|
built for millions.
|
|
- Pages are only reloaded from disk when Mako starts.
|
|
- A page in the Vault cannot link to your ordinary pages or use their tags, and the vault is not in the zip
|
|
download. Moving a page in takes its subpages up a level but does not move them with it, and a journal day
|
|
moved in leaves that day empty, so starting it again makes a new page which cannot be moved in while
|
|
the first is there. You cannot change its passphrase yet.
|
|
- Live preview works a line at a time (a code block counts as one), so a table or a quote spread over
|
|
several lines shows rendered line by line. View shows the whole page as one. Selecting across lines
|
|
(Ctrl+A, Shift+Up or Down, Shift+click, or dragging) opens those whole lines as raw markdown, so a
|
|
selection that starts or ends partway through a line takes in the whole line.
|
|
|
|
## Design
|
|
|
|
The interface follows the Lightwell design language, the same one Hammerhead and Megalodon use. See
|
|
[docs/design.md](docs/design.md). `node scripts/lint-design.mjs` checks the rules a machine can check.
|
|
|
|
## Development
|
|
|
|
```sh
|
|
go test ./...
|
|
go run ./cmd/mako serve # with the MAKO_* variables set
|
|
```
|
|
|
|
Releases are built by the Gitea workflow when a `v*` tag is pushed.
|
|
|
|
Licensed under the AGPL-3.0 (see [LICENSE](LICENSE)).
|