/root is mode 0700 and the container runs as uid 1000, so it cannot even
traverse it — the server fails to write no matter who owns the directory
underneath. Both ways out are written down, with the trade each makes:
chmod o+x /root keeps the unprivileged container and still refuses ls,
running as uid 0 avoids the chmod but puts a break-out on root.
Also noted that the path has to start with / or ./, since anything else
is read as a named volume name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It sat in backend/ while Compose reads .env from the repository root, so
the "cp .env.example .env" in the compose header pointed at nothing.
It also documented four settings that Compose never forwarded to the
container: ALLOWED_ORIGINS and the three version-retention knobs would
have been set in .env and silently done nothing. They are passed through
now, unset meaning the server's own defaults. Checked both directions:
every variable Compose reads is documented, and nothing documented is
ignored.
Dropped PORT from the example — the bundled nginx proxies to 4000 by
name, so changing it could only break that. Added the data-directory and
uid/gid settings, which were read but undocumented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three leftovers: a named-volume block that nothing has used since the
move to bind mounts, the obsolete version key, and a comment claiming the
admin variables only apply to an empty database — they also decide whom
to promote when upgrading one that has users but no admin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a private instance, so anyone able to reach the login page could
previously give themselves an account. The registration route is gone
rather than hidden, and the login screen no longer hints that accounts
can be made.
Accounts now come from an admin under Account → Administration: create,
delete, and reset a password. Appointing further admins is deliberately
not offered — with one admin it is not needed, and it is the one power
that turns a compromised account into a lost instance.
The first admin comes from INKNOTE_ADMIN_USER / INKNOTE_ADMIN_PASSWORD,
used only when the database has no users, or to choose whom to promote
when an older database has users but no admin. Once an admin exists both
are ignored, so leaving them in .env cannot reset anything.
Two things that would otherwise have been quietly wrong:
- a deleted user's token stayed valid until it expired, because tokens
are signed rather than stored and there was no session to revoke.
authMiddleware now checks the account still exists, so an open tab
stops working the moment the account is deleted.
- deleting an account takes its notes, notebooks, drawings and version
history with it via ON DELETE CASCADE. Verified: no orphaned rows in
notes, notebooks, sync_log, attachments or note_versions.
Deletion asks for the username to be typed back, since it destroys data
that version history cannot bring back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit moved the images to honey/, which was wrong: the
images have lived under the Docker organisation since February, alongside
learning-tracker and whopays, and are linked to this repository there.
Only the placeholder 'marc' was never real.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The images were tagged gitea.../marc/..., a placeholder that was never
changed and is not a Gitea account, so every push failed with a 404 and
the images had in fact never been published. The registry owner is honey.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app is meant to run in daily private use now, so this pass was mostly
about the ways it could quietly lose data.
Reliability and security:
- the server no longer invents a JWT_SECRET or ENCRYPTION_KEY when one is
missing; it refuses to start. The old fallback silently made every stored
note unreadable after a restart.
- notes are encrypted with AES-256-GCM (v2:); existing CBC bodies still read.
- login is rate limited and compares against a dummy hash when the user does
not exist, so timing does not reveal who is registered.
- a server response issued before a local save is never adopted. This had
already eaten drawing strokes and note links; sync.hasPendingWrites() now
guards every path that takes a body from the server.
Editor and features:
- TipTap replaces the contenteditable editor: tables, task lists, and links
to other notes and notebooks, with a backlinks bar on the target.
- content format v2 ({v:2,doc}); v1 {html,tasks} bodies migrate on open.
- version history: the state before each write is snapshotted, coalesced
into 5-minute windows, 50 versions or 90 days per note.
- full-text search runs in the client, because the server cannot read
encrypted bodies and a server index would defeat the encryption.
- image attachments are stored by SHA-256 and deduplicated; the client
fetches them authenticated and caches the blob, so they work offline.
- drawings are vector strokes in a fixed 1200x1600 page space and now look
the same on every screen (0.0-0.2% deviation across four widths).
- login is by username; existing email accounts migrate automatically.
Operations:
- data lives on host bind mounts, not Docker volumes, so Proxmox Backup
covers it. Back up .env with it — without ENCRYPTION_KEY the dump is
unreadable.
Structure:
- NoteApp.jsx is gone. App.jsx now holds only where the user is; note data,
search, layout, history and drag each live behind a hook, and the four
panes are stateless.
- the whole data layer is TypeScript, with 81 unit tests concentrated on
the places that have actually broken.
- fixed along the way: the browser back button had a dead press, because
restoring a history entry pushed a duplicate one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>