Command-line tool
bin/fk <command> [options]
docker compose run --rm cli <command> [options]The same modules and the same rules as the web interface, driven from a terminal. It exists for the jobs a browser is bad at - migrations, seeding, producing a tax bundle from a cron job, importing a bank file - and for seeing what the application thinks without clicking through it.
bin/fk is the program itself, with #!/usr/bin/env -S jennifer run on the first line and the executable bit set. It works out where the checkout is from its own path - fs.realpath(os.ARGS[0]), so a symlink is followed - and reads .env and writes var/receipts and var/exports relative to that, not to wherever you are standing. Link it onto PATH and fk report works from any directory. jennifer run bin/fk does the same thing for anyone who prefers to say so.
--household is only needed when more than one family has members; a normal single-family installation never needs it.
Payments and their proof
bin/fk settle --from martin --to anna --amount 45,00 --method cash
bin/fk receipt 0 auszug.pdf --payment 12 --kind bank_statement --as martin--method is one of transfer, cash, revolut, card, other; without it a payment is recorded as a transfer. --kind is one of receipt, invoice, bank_statement, payment_slip or other, and defaults to receipt; payment_slip is the proof of the one transfer - the banking app's confirmation, the counter receipt - where bank_statement is a whole month of them. The receipt command takes an expense number as its first argument, or 0 together with --payment to hang the file on a settlement instead - one owner or the other, never both.
Which database
APP_DB_DSN decides, and the process environment beats the value in .env. That is worth knowing when a terminal disagrees with the browser: a server started with APP_DB_DSN=...expenses_demo and a bin/fk run in a plain shell are looking at two different databases, and neither is wrong.
bin/fk doctor prints the one it is using, without the password:
Datenbank
Adresse 127.0.0.1:3307/expenses_demo
Verbindung steht
sql_mode STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,NO_ENGINE_SUBSTITUTION
strict ja
offene Migrationen 0Run with no command at all, fk prints its usage and stops.
Consistency
doctor ends with a section that checks what the schema cannot. Foreign keys hold the shape of the data; these hold its arithmetic, and every one of them is a rule that, broken, shows up as a balance nobody can explain rather than as an error:
Consistency
shares add up to the expense ok
shares not settled beyond their value ok
payments not spent twice ok
allocations stay in one family ok
accepted shares have both sides ok
categories belong to their family ok
payment parties are members ok
share parties are members ok
dedicated payments name a category ok
receipt files present ok
nothing to reportA failing check reports how many rows break it and the first few ids — 1 row(s), e.g. #18315 — and stops counting at 50, which it says as 50+. It reads and changes nothing: what to do about a finding depends on how it got there. receipt files present walks the attachment rows and looks for the file on disk; the usual cause of a miss is a restore that brought the database back without APP_RECEIPT_DIR, and the first thing it breaks is the Finanzamt bundle.
Commands
| Command | Does |
|---|---|
doctor | check the installation: database, directories, tools, and the consistency of the books |
migrate up|down|status | schema migrations |
household-add <name> | create a family, with its categories |
household-list | families and their members |
user-add <user> [--admin] | create an account that belongs to no family yet |
demo-seed --yes | wipe everything and install the demonstration families |
member-add <user> --role | add a person, creating the account if new |
expense-add | record an expense |
expense-list | list expenses |
pending | what is waiting for approval |
review <nr> --as <user> | accept, or --reject to reject |
receipt <nr> <file> --as | attach a document |
income-add / income-list | a child's own income |
balance | wallets, and what the adults owe each other |
settle --from --to | record a payment; without --amount it settles everything open, --purpose <code> dedicates it to one category |
report --child --year | a report; --tax restricts it to tax-relevant entries |
export --child --year | build the PDF and ZIP bundle |
backup [--schedule] | one archive of the database and the receipts, or one a day |
mail [send|status|retry|test] | take the queued notification mails to the relay |
receipt-rehash | give every attachment its SHA-512 and store one file once |
trail | read a family's audit trail; --purge N removes entries older than N days |
Examples
# Anna paid for a schoolbook that a parent bears
familienkonto expense-add --child anna --amount 45,20 --merchant Thalia \
--description "Schulbuch Mathematik" --category schule --bearer martin
# something she paid for herself: no approval needed, still counts for tax
familienkonto expense-add --child anna --amount 15,00 --merchant Kino \
--description Kinokarte --category freizeit --private
familienkonto pending
familienkonto review 3 --as martin
familienkonto balance
# settle everything open, with a generated payment reference
familienkonto settle --from martin --to anna
# a lump sum in advance
familienkonto settle --from martin --to anna --amount 200,00
# money that is for the rent and nothing else: it waits for a Wohnen expense
# instead of being spent on the next schoolbook
familienkonto settle --from martin --to anna --amount 890,00 --purpose wohnen
familienkonto report --child anna --year 2026 --tax
familienkonto export --child anna --year 2026The bank-CSV import is switched off
config.IMPORT_ENABLED is false, and both the web routes and the import command are registered behind it. With the flag down /import is a 404 and familienkonto import is an unknown command; nothing dispatches to importCommand, importPage, importPreview or importCommit, and nothing links to them.
The code is not deleted and its tests still run - bankcsv and bankimport are covered as they were. What is unfinished is not the parsing but the question above it: how a family should reconcile a bank export against entries it has already recorded by hand. Turning it back on is one true in src/core/config.j, one line in bin/fk, and one entry in each of that file's two command maps.
Output
Plain text, aligned into columns, and English throughout - the web interface is the family's, this is the operator's, and reading a log line, a stack trace and a command's output in one sitting should not mean changing language halfway. The German error catalogue is deliberately not installed here: the domain modules throw English sentences and that is what an operator sees. Amounts and dates keep the format the data is in.
The report draws the same coverage bar the browser and the PDF show:
[####################|###################] 100,0 % (threshold met)Errors say what is wrong rather than that something went wrong:
error: Martin Maier is not a child of this family
error: cannot read that amount: fünf
error: Anna Maier may not review expensesA mistyped command line is answered the same way, with the usage of the command that was typed rather than the interpreter's own error struct:
$ bin/fk expense-add
error: missing required flag: --child
Usage: expense-add [options]
...Role checks are enforced here too - review --as anna is refused, not merely discouraged.
The first administrator
An empty installation has no families, and member-add needs one. user-add is the way in:
bin/fk user-add root --name "Administration" --adminThat account belongs to no family - which is the point, an administrator runs the installation rather than living in it - and can create the rest through Verwaltung in the web interface.
Demonstration data
demo-seed empties the database and installs two families - the ones the German guides are written around. It refuses unless APP_AUTH_MODE=dev, and --yes is required, because it deletes everything first.
bash scripts/demo-seed.sh # into a database of its ownThe data lives in src/app/demodata.j, so this command, the button on the development login page and the screenshots in docs/images/ all install exactly the same thing.