Repository navigation
Releases
v1.6.0 — settings in categories
The settings, sorted.
Upgrading from 1.5.x is changing the tag to 1.6.0 and a restart. No migration, no new environment variable.
# docker-compose.yml
image: ghcr.io/jankln/kassenknoten:1.6.0What's new
Settings in categories. The settings screen had grown into one long column of eight unrelated cards. It is now an overview of five categories, each on its own page:
| Category | What is in it |
|---|---|
| General | Language, appearance, installing the app |
| Planning | The default split for shared costs, the cost categories |
| Sign-in | The household password, the identity provider, who is allowed in |
| Data | Automatic backups, downloads, restoring |
| Extensions | Your own code, running on this server |
On a phone you pick a category and come back to the overview; on a wider screen the list of categories stays beside the page, so moving from sign-in to backups is one click. Every category has its own address — /einstellungen/daten, for example — so it can be bookmarked, and each page loads only what it shows.
Nothing about the settings themselves changed: the same controls, in the same words, in fewer places at once.
v1.5.0 — automatic backups
Your household is backed up every day, without anybody remembering to.
Upgrading from 1.4.x is changing the tag to 1.5.0 and a restart. No migration, and your .env keeps working as it is.
# docker-compose.yml
image: ghcr.io/jankln/kassenknoten:1.5.0After the upgrade the server starts writing backups into /data/backups, inside the data volume you already have. To switch that off, add BACKUP_KEEP=0 to .env; the new lines are in env.example.
What's new
Automatic backups. Once a day, when something has changed, the server saves a backup of the household's data and keeps the last fourteen. It is the same file the Download JSON backup button gives you, so restoring one is the path you already know: Settings → Back up your data lists them with a download each — pick one, choose it under Restore a backup, done.
A day on which nothing changed writes nothing, so fourteen kept files are fourteen different states rather than two weeks of the same one. Files are written in one step and readable only by the app's own user.
BACKUP_KEEP sets how many are kept, BACKUP_DIR where they go — a mounted network share, for example.
They share a disk with your data. That protects against a mistake or a bad restore, not against the disk itself. Copy them somewhere else from time to time:
docker cp kassenknoten:/data/backups ./kassenknoten-backupsFixed
- Restoring a backup no longer changes who can sign in. Since 1.4.0 the sign-in settings — which methods are on, who is on the provider allowlist — were stored alongside the household's data and restored with it, as were the switches for extensions. An older backup could reset sign-in, end the session of the person restoring it, or turn a disabled extension back on. Backups now carry household data only, and a restore leaves this instance's sign-in and extension settings alone — also when an older file still contains them. (#15)
Under the hood
The image build now refuses a standalone build that contains source code or data files. While building this release, a single dynamic path made the bundler pull the whole project into the server output — including local .env and database files. It was caught before any commit and never reached a published image; now it cannot.
v1.4.2 — security update
Security update. Upgrade if you run 1.4.1 or earlier.
Next.js moves to 16.3.5 and sharp to 0.35.4, leaving the versions named in the security advisories published on 2026-09-08. (#14)
Upgrading is changing the tag to 1.4.2 and a restart. No migration, no new environment variable.
# docker-compose.yml
image: ghcr.io/jankln/kassenknoten:1.4.2What it fixes
| Package | Advisory | Severity |
|---|---|---|
| Next.js 16.3.2 | GHSA-2xp9-vwfh-vxw4 — image optimization with AVIF | critical |
| Next.js 16.3.2 | GHSA-p293-qw3h-jr36 — Windows-hosted servers only | critical |
| sharp 0.35.3 | GHSA-rgj7-g3m4-5g8c — libheif | high |
The image runs on Linux, so the Windows advisory does not apply to it, and KassenKnoten neither uses Next's image component nor serves AVIF files of its own, which makes the image-optimization one hard to reach. The endpoint exists in every build regardless — which is why this is a release rather than a note.
Also new since 1.4.1
Nothing that changes what the app does. Every commit on main and every tag is now checked on GitHub before an image is built: typecheck, lint, the full test suite, and a browser test that signs in, sets up a household and checks a shared cost's split on the phone layout — against the same server the image runs.
v1.4.1 — the scanner works
The receipt scanner works in the image.
If you tried Scan receipt on 1.3.0 to 1.4.0 and it never came back, that was not your photo: in every published image the recognition worker could not start, and the scan waited forever instead of saying so. This release fixes that, and reads real phone photos considerably better.
Upgrading is changing the tag to 1.4.1 and a restart. No migration, no new environment variable, and your .env stays as it is.
# docker-compose.yml
image: ghcr.io/jankln/kassenknoten:1.4.1Fixed
- Scanning works on deployed instances. Tesseract starts its worker from a file path in a separate thread, which Next.js' build tracing cannot follow, so the image lacked modules that worker needs — and carried the wrong engine build besides. Under
npm run devthe fullnode_moduleshid it. The image build now performs a real recognition before anything is published, so a scanner that cannot start is a build that fails, not a release. (#10) - A scan that cannot be read says so. A worker that failed while starting left the request open, the sheet spinning, and every later scan queued behind it until a restart. Starting and reading now share the 45-second limit, you get the usual "could not be read" message, and the next scan starts fresh. (#11)
- Photos with a shadow keep their total. A receipt photographed on a table is darker at one edge than the other, and Tesseract's default single threshold lost the large bold
SUMMEline there — the draft then proposed an item price as the total. Recognition now thresholds per neighbourhood and reads with both the German and the English model, your language first. (#12) - The shop name arrives clean, without a
|or;picked up from the edge of the paper. (#13)
Worth knowing
Reading with both language models costs about 35 MB of memory while the recognition worker is alive — it is released two minutes after the last scan — and roughly half a second more on a large photo. The two model files are copied into a private temporary directory when the worker starts and removed as soon as it has loaded them; if the temporary directory is not writable, scanning continues with one model as before.
Everything is still read on your own server. Nothing about scanning contacts a network.
v1.4.0 — sign in with Authentik
Sign in with Authentik — or keep the password. And read your history month by month.
mkdir kassenknoten && cd kassenknoten
curl -LO https://github.com/jankln/KassenKnoten/releases/latest/download/docker-compose.yml
curl -L -o .env https://github.com/jankln/KassenKnoten/releases/latest/download/env.example
# a session secret
echo "SESSION_SECRET=$(openssl rand -base64 32)" >> .env
# your household password, hashed — paste the printed line into .env
docker run -it --rm ghcr.io/jankln/kassenknoten:latest \
node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/hash-password.ts
# optional: a second factor. Prints a QR code straight into your terminal
docker run -it --rm ghcr.io/jankln/kassenknoten:latest \
node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/totp-secret.ts
# optional: sign in with an identity provider — fill in the OIDC_* block of .env
docker compose up -dThen open http://127.0.0.1:3000. Put a reverse proxy in front of it for TLS before anyone signs in from outside the machine.
Upgrading from 1.3.x is changing the tag to 1.4.0 and a restart. There is no migration, and an existing .env keeps working exactly as it is — every new variable is optional. If you want the provider sign-in, copy the Identity provider block from the new env.example into your .env; your old one does not have it.
What's new
Sign in with your identity provider. If you run Authentik — or Keycloak, Pocket ID, Zitadel, anything that speaks OpenID Connect — the household can now sign in with it, next to the shared password or instead of it. Nobody who leaves the new lines empty notices anything.
Decided before install, changeable after. Before the first start, .env says how to reach the provider and AUTH_MODE — now local, oidc or both — says what is switched on. After install, Settings → Sign-in switches the password and the provider on or off without a restart, and keeps the list of e-mail addresses allowed in. Having an account at the provider is not enough; only addresses on that list get in. The client secret stays in the environment and never reaches the database.
It will not let you lock yourself out. One way in always stays on. The method you are signed in with cannot be switched off from that session, so the password only goes off after somebody has actually come in through the provider. Your own address cannot be removed from the list. And if the provider breaks while it is the only way in: take it out of .env, set AUTH_MODE=local, restart — the password works again.
Taking someone off takes effect now. Removing an address or switching a method off ends the sessions it issued on their next click, not when the week-long cookie happens to run out.
The flow is the authorization code flow with PKCE, written directly against jose so every check a relying party owes is in one file and tested: signature against the provider's published keys, issuer, audience, expiry, nonce and state — PKCE verified against RFC 7636's own example. It was run end to end against oidc-provider, the certified reference implementation, in both ways a provider can hand out the e-mail address. The README walks through the Authentik setup, including the two things most likely to bite: the issuer's trailing slash, and picking a signing key.
Read the trend month by month. Drag along the chart on the overview, or use the arrow keys, and a guide line and that month's five figures follow — twelve months of history without a page load. The figures carry the series colours, so they replace the legend.
Fixed
- Signing in with the password returns to the page you were headed for, query included, instead of always landing on the overview. (#4)
- An English instance names months and days in English: June 2026, 03/06. Browsers west of UTC also no longer show the previous month. (#5)
- The login tab title and the shared-cost placeholder are translated. (#6, #9)
- Status badges are readable in the dark theme. (#7)
- On an instance without a provider, the sign-in settings no longer title the row single sign-on. (#8)
v1.3.1 — the scanner is findable
A patch for 1.3.0: the receipt scanner is findable before you have anything to scan into.
Upgrading is a pull and a restart. No migration, no new environment variable, nothing changed about what the scanner does once it is on screen.
docker compose pull && docker compose up -dFixed
- The launcher shortcut led nowhere. Installed as an app, a long press on the icon offers "Scan receipt". On a household with no variable-cost items yet, that landed on the ordinary empty state — no scanner, and not a word about why. It now answers the question that was actually asked, and the button it already carried creates the item.
- A new household never learned the feature existed. The empty state taught items and the two modes and stopped there, so the scanner was invisible to exactly the person who has not yet built the habit of typing receipts in by hand. It now names it.
The scan button is still bound to there being an item in the month on screen, and the scan flow still will not create one in passing. A receipt is booked against an item, the item carries the split, and an item created on the way past would be deciding who pays what on the way past.
Note on tags
latest, 1.3 and 1.3.1 are the same image. 1.3.0 still points at the release before this one, so pinning it keeps you where you are — which is the point of pinning, and also the reason a pinned instance never updates itself. If you want an automatic update, follow latest or 1.3.
v1.3.0 — scan a receipt
Photograph a till receipt and the app fills the expense in for you.
mkdir kassenknoten && cd kassenknoten
curl -LO https://github.com/jankln/KassenKnoten/releases/latest/download/docker-compose.yml
curl -L -o .env https://github.com/jankln/KassenKnoten/releases/latest/download/env.example
# a session secret
echo "SESSION_SECRET=$(openssl rand -base64 32)" >> .env
# your household password, hashed — paste the printed line into .env
docker run -it --rm ghcr.io/jankln/kassenknoten:latest \
node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/hash-password.ts
# optional: a second factor. Prints a QR code straight into your terminal
docker run -it --rm ghcr.io/jankln/kassenknoten:latest \
node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/totp-secret.ts
docker compose up -dThen open http://127.0.0.1:3000. Put a reverse proxy in front of it for TLS before anyone signs in from outside the machine.
Upgrading from 1.2.0 is a pull and a restart. There is no migration and no new environment variable.
What's new
Scan a receipt. On Variable costs, "Scan receipt" opens the camera or your photo library. The total, the date and the shop come back filled in, and the only thing left to say is which budget it belongs to — your own, or the household's. That question stays yours on purpose: the budget carries the split, so a receipt that filed itself would be deciding who pays what.
It is read on your own server. A hosted model would read crumpled thermal paper better; this does not use one. A receipt is the shop, the hour and the basket — the most detailed record of somebody's day this app would ever hold — and the promise that your figures stay on your machine is worth more than the accuracy. Tesseract runs inside the container. There is no key to configure, nothing to sign up for, and no request leaves the box. The photo itself is never written anywhere: it is downscaled in your browser, recognised in memory and dropped.
And it says what it is unsure about. A field it could not read stays empty and tells you. A total inferred from the largest amount on the paper, rather than read off a SUMME line, is marked for a second look. Nothing is booked until you press save.
The parsing is a pure module with its own test suite, built from real OCR output — the space Tesseract drops into 50, 00, a Geg. BAR 50,00 line larger than the total, a Zwischensumme printed before a discount, a VAT rate of 19,00 % that would double a small receipt, and 14.03.2026, which a naive amount pattern reads as 14,03 €. When in doubt it returns nothing: an empty field costs you three seconds, a confidently wrong total is a wrong booking nobody re-reads.
Installed as an app, a long press on the icon now offers Scan receipt as a shortcut.
Note on image size
The image grew by roughly 25 MB: the OCR engine and two language models, English and German, are baked in so that scanning works offline and on first run. Only the integerised models and the LSTM cores ship — the standard models and the legacy engine, another 37 MB, are excluded from the build.
v1.2.0 — English, and your own code
The interface now speaks English, and you can install your own code into it.
mkdir kassenknoten && cd kassenknoten
curl -LO https://github.com/jankln/KassenKnoten/releases/latest/download/docker-compose.yml
curl -L -o .env https://github.com/jankln/KassenKnoten/releases/latest/download/env.example
# a session secret
echo "SESSION_SECRET=$(openssl rand -base64 32)" >> .env
# your household password, hashed — paste the printed line into .env
docker run -it --rm ghcr.io/jankln/kassenknoten:latest \
node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/hash-password.ts
# optional: a second factor. Prints a QR code straight into your terminal
docker run -it --rm ghcr.io/jankln/kassenknoten:latest \
node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/totp-secret.ts
docker compose up -dThen open http://127.0.0.1:3000. Put a reverse proxy in front of it for TLS before anyone signs in from outside the machine.
What's new
English alongside German. A fresh instance starts in English and the setup wizard asks which language you would rather have before it asks anything else; the settings screen changes it later. The choice belongs to the household, not to a browser, so the tablet in the kitchen agrees with the laptop. No string is hardcoded in a component: German is typed against English, and a translation that falls behind fails the build rather than shipping half-translated.
Extensions. A single .mjs file, installed under Settings → Extensions, contributes cards to the overview. It gets the database handle, the services, the pure calculation modules and the app's own formatting, so a card you write looks like it belongs and rounds money the same way everything else does. The guide and a working example are in the repository.
Be clear about what that means: an extension runs on the server, inside the application process, with full access to your household's database. There is no sandbox, the upload form says so before it accepts a file, and EXTENSIONS_ENABLED=false loads none of them if one ever breaks the app badly enough that you cannot reach the settings screen.
A landing page in both languages, with screenshots of the interface in each.
Fixed
- The income total counted months that had not happened yet. A raise entered with a start month in the future, or a job that ended in spring, was part of what the household earned today — and, because the income-ratio split reads the same figure, it quietly shifted the shares on fixed costs, variable costs and savings. The household screen now counts only what applies in the current month; every row stays listed with its validity note. (#1)
- The build no longer traces the whole project into the image because the extensions volume is a runtime path.
Upgrading from 1.1.0
Edit the tag in your docker-compose.yml to 1.2.0 — or take the file attached here — and:
docker compose pull && docker compose up -dYour data is untouched: it lives in the volume, and the database migrates itself on first access. Existing instances keep German; the language is a household setting, and an upgrade does not change one you have already made.
Notes
- The environment template is attached as
env.example, without the leading dot: GitHub renames release assets that start with one, and the download would 404. EXTENSIONS_ENABLEDandEXTENSIONS_DIRare documented in the attached template. Extensions live in the data volume, not in the image, so they survive an upgrade.- One manifest covering amd64 and arm64, tagged
1.2.0,1.2andlatest.
v1.1.0 — pull and run
Pull and run. No checkout, no Node, no build.
The image is published to the GitHub Container Registry for amd64 and arm64, so a Raspberry Pi is as much a target as a NUC. What you download is two files.
mkdir kassenknoten && cd kassenknoten
curl -LO https://github.com/jankln/KassenKnoten/releases/latest/download/docker-compose.yml
curl -L -o .env https://github.com/jankln/KassenKnoten/releases/latest/download/env.example
# a session secret
echo "SESSION_SECRET=$(openssl rand -base64 32)" >> .env
# your household password, hashed — paste the printed line into .env
docker run -it --rm ghcr.io/jankln/kassenknoten:latest \
node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/hash-password.ts
# optional: a second factor. Prints a QR code straight into your terminal
docker run -it --rm ghcr.io/jankln/kassenknoten:latest \
node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/totp-secret.ts
docker compose up -dThen open http://127.0.0.1:3000. Put a reverse proxy in front of it for TLS before anyone signs in from outside the machine.
What's new
A published image. ghcr.io/jankln/kassenknoten — tagged 1.1.0, 1.1 and latest, one manifest covering both architectures. Built once per architecture on a native runner rather than once under emulation, so a release does not cost half an hour of QEMU for byte-identical output.
The setup scripts travel inside it. Without a checkout there is otherwise no way to produce an argon2id hash or a TOTP secret, and "just pull the image" would be a promise that breaks at step two. They are the same files the test suite covers, not a copy free to drift.
Compose pulls; building is an override. docker-compose.yml names the image and nothing else. To build from source, layer docker-compose.build.yml on top — it also renames the result to kassenknoten:local, so a local build cannot masquerade as the published version.
Upgrading from 1.0.0
Replace your docker-compose.yml with the one attached here — it now pulls instead of building — keep your existing .env, and:
docker compose pull && docker compose up -dYour data is untouched: it lives in the volume, and the database migrates itself on first access. Later upgrades are editing the tag in docker-compose.yml and running the same two commands.
Notes
- The environment template is attached as
env.example, without the leading dot: GitHub
renames release assets that start with one, and the download would 404. - The image runs as an unprivileged user and holds no secrets; everything comes from
.envat runtime. - The "Source code" archives below are GitHub's own and contain the whole repository, including the tests, the documentation and the landing page.
Verified from an empty directory with no repository checkout: the two files, the two scripts run inside the image, docker compose up -d, sign-in, and the three-step wizard through to the dashboard.
v1.0.0 — the first stable release
Zwei Einkommen, ein Haushalt, ein klarer Plan.
The first stable release. Everything the spreadsheet did, KassenKnoten does — and the parts a spreadsheet fundamentally cannot do are in and in daily use.
There is a landing page now: https://jankln.github.io/KassenKnoten/
What's new since beta.2
Variable costs, planned or itemised. Groceries, fuel, going out — each gets a budget on its own screen, and you choose per budget how it is kept. Plan counts the figure you set and asks nothing further. Detailliert counts what you actually booked, receipt by receipt with a date, and turns the plan into a budget to measure against. Both are split between you exactly like fixed costs, so the per-person figures stay complete, and both reach the dashboard — its own headline figure, its own section, the category bars and the trend line.
Two-factor sign-in. Set TOTP_SECRET and the login also asks for a six-digit code from any authenticator app (RFC 6238, verified against the RFC's own test vectors). A used code is refused, so one read over your shoulder is not one that still works. npm run auth:totp prints a scannable QR code straight into your terminal. The secret lives in the environment like the password does — which means losing the phone is not a lockout, and there are no recovery codes to keep safe.
Installable as an app. Add it to the home screen or the app list from the settings screen: own icon, no address bar, straight to the dashboard. Browsers that cannot install a web app are told where the option lives in their own menu instead of being handed a button that does nothing. Nothing about the household is cached on the device — offline, the app says so rather than showing yesterday's figures.
Fixed
- The Docker image never shipped
public/, so a container built from source could not serve the service worker or the app icons and silently refused to be installable.
Install
tar -xzf kassenknoten-1.0.0.tar.gz && cd kassenknoten-1.0.0
cp .env.example .env
echo "SESSION_SECRET=$(openssl rand -base64 32)" >> .env
npm run auth:hash # your household password, hashed
npm run auth:totp # optional second factor
docker compose up -dkassenknoten-1.0.0.tar.gz is the deployment bundle: everything needed to build the image and run the app, and nothing else — 132 files, 248 KB. No tests, no documentation, no landing page, no CI. Its contents come from git archive of this tag, so only committed files can be in it and what it holds is exactly what the tag says.
The "Source code" archives below are GitHub's own and contain the whole repository.
Upgrading from a beta
git pull && docker compose up -d --build. The database migrates itself on first access; nothing else is needed. Back up first from Einstellungen → Daten sichern if you would like a copy.
Stability
The data model, the backup format (version 3, still reading 1 and 2) and the environment variables are settled. From here they change by migration, not by surprise, and breaking changes wait for a major version.
Planned next: OIDC sign-in against Authentik with an e-mail allowlist.