Moneymoor
NAME
App::Moneymoor - YNAB-style envelope budgeting: a derivation engine over an encrypted SQLite file, and a terminal UI that drives it.
SYNOPSIS
Run the app:
moneymoor # or: raku -I lib bin/moneymoor
moneymoor --version
MONEYMOOR_HOME=/tmp/demo moneymoor # a throwaway data home
Or use the engine directly:
use MacOS::NativeLib <sqlcipher>; # macOS only ā see PORTABILITY
use App::Moneymoor;
use App::Moneymoor::DB;
use App::Moneymoor::Service::Workspace;
use App::Moneymoor::Model::Account;
use App::Moneymoor::Model::Category;
use App::Moneymoor::Model::Transaction;
use App::Moneymoor::Model::Split;
say App::Moneymoor.version;
my $db = App::Moneymoor::DB.new(:db-path("$*HOME/.moneymoor/budget.db"));
$db.connect('correct horse battery staple');
my $ws = App::Moneymoor::Service::Workspace.new(:$db);
my $current = $ws.accounts.create(App::Moneymoor::Model::Account.new(
name => 'Current Account', type => 'cash'));
my $food = $ws.categories.create(App::Moneymoor::Model::Category.new(
name => 'Groceries'));
my $rta = $ws.categories.rta-category;
# £1,800 in:
$ws.transactions.create(
App::Moneymoor::Model::Transaction.new(
account-id => $current.id, date => '2026-03-01', amount => 180000),
splits => [App::Moneymoor::Model::Split.new(
category-id => $rta.id, amount => 180000)],
);
# Give it a job. Budgets are keyed by period start; under the default
# calendar-month scheme that is the first of the month.
$ws.set-assigned('2026-03-01', $food.id, 40000);
my $view = $ws.budget(through-period => '2026-03-01');
say $view.rta('2026-03-01'); # 140000
say $view.category('2026-03-01', $food.id).available; # 40000
$db.disconnect;
DESCRIPTION
Moneymoor is an envelope budget: every pound you have gets a job before you spend it. It is the "give every pound a job, budget only money you actually hold, roll the rest forward" school of budgeting ā the same model YNAB popularised ā implemented as a Raku library over an encrypted SQLite (SQLCipher) file.
The engine came first and is still usable on its own ā everything
under Model, Gateway, Service and DB is a library with no
opinion about how you look at it. On top of that sits a Selkie
terminal UI, bin/moneymoor, which is what most people will actually
use ā see #THE TERMINAL UI below.
WHAT IT IS BUILT ON
Facts in, everything else derived. The database stores only what you authored: accounts, categories, transactions with their splits, and per-period assignments. Balances, activity, available, Ready to Assign and credit-card payment moves are recomputed on demand by a pure function. Nothing derived is stored, so nothing derived can drift.
The master invariant. Envelopes partition cash:
Σ available + RTA == Σ cash-account balances. Its general form (with future assignments and uncovered card spending) is checked for every period byBudgetView.invariant-errorsand asserted after every operation by the property test suite.Integer pence. No floats, no
Rats, anywhere. A budget that has to prove an equality cannot afford lossy addition.Encrypted at rest. One SQLCipher file, keyed on connect.
THE PIECES
App::Moneymoor::DBā SQLCipher connection, migrations (additive ones replayed on every connect, transforming ones gated on a schema revision), transactions.App::Moneymoor::Model::*ā attribute-only row classes.App::Moneymoor::Gateway::*ā the SQL, and the invariants that have to hold before a row is written (splits sum to their transaction, transfers come in pairs, every credit account owns a payment envelope).App::Moneymoor::Service::Budgetā the pure derivation. Its Pod is the specification of the budgeting semantics, with worked numbers for each of the four rules.App::Moneymoor::Service::Workspaceā gateways in, derived budget out. Owns the budget's period scheme and is the only layer that reads the clock.App::Moneymoor::Service::Targetā what an envelope's target asks for in the period you are looking at. Pure, and the only module that knows what the three target kinds mean.App::Moneymoor::Util::Periodā what a budget period is: a calendar month, a month anchored on payday, or an every-N-weeks pay window. Pure, and the only module that knows.App::Moneymoor::Util::Moneyā pence ā"Ā£12.34".
WHAT IT DOES NOT DO
Scheduled transactions, CSV or bank imports, multi-currency, incremental rollup caching, and net worth over time (which needs a per-month, per-account balance derivation the engine does not have yet).
Targets do exist ā see the Budget tab below ā but note where they
live: on the category row, read by the view layer, and invisible to
Service::Budget. There are three kinds of them ā refill to a level,
set aside an amount each period, and reach an amount by a period, the
last with a milestone schedule and optional repeat ā and all three are
derived by Service::Target from figures the engine produced for its
own reasons. The engine still derives money from facts alone, and a
target is not a fact about money that has moved. The Budget tab's
envelope editor sets all three kinds.
THE TERMINAL UI
bin/moneymoor launches it; --version and --help are the only
things it will do without a terminal. Everything else ā creating a
budget, assigning money, entering transactions, reconciling a
statement ā happens inside, from the keyboard.
Logging in
Budgets live in the data home, ~/.moneymoor/, one encrypted
*.db file each. Set MONEYMOOR_HOME to put them somewhere else;
the config file, the budget list and the error log all move together,
which is what makes a throwaway run safe.
The first screen lists the budgets it found and asks for a passphrase,
or ā with none to list, or on Ctrl+N ā offers to create one. That
passphrase is the encryption key: there is no reset, no recovery
question and no copy of it anywhere. A wrong one is reported
differently from a wrong file, because they are different mistakes.
Inside
Three tabs, 1 / 2 / 3 (also Ctrl+1 / Ctrl+2 / Ctrl+3
on terminals that can send it):
Budget ā the envelope grid for one period, grouped, with Ready-to-Assign above it and a detail rail beside it that shows exactly how an envelope's available figure was arrived at.
aassigns,mmoves money between envelopes,xexplains a derived number,[and]change period. Give an envelope a target in its editor ā refill, set-aside, or a goal to reach by a date ā and the grid gains a Target column, the rail says how much is still to fund, the assign field takes=450("make it Ā£450") and a bare=("fund this envelope's plan for this period"), andffunds every underfunded envelope at once in a single write.Accounts ā the ledgers on the left, the register on the right.
nadds a transaction (with splits),ta transfer,ccycles uncleared ā cleared ā reconciled,Ctrl+Rstarts a reconciliation against a statement balance.Reports ā the period's cash flow, and where the money went.
Ctrl+H lists the keys for whatever has focus; the bottom line
always shows the important ones. Ctrl+G opens diagnostics ā the
derivation's warnings, invariant errors and a digest fingerprint safe
to paste into a bug report. Ctrl+O opens settings: eleven palettes
and a glyph tier (plain Unicode or Nerd Font), applied live and
remembered in ~/.moneymoor/config.json.
The UI's pieces
App::Moneymoor::UIā the entry point: app construction, the login handshake, the hand-off to the main screen.App::Moneymoor::Screen::*ā Login, the Main shell, and one controller per tab.App::Moneymoor::StoreHandlersā the state shape, and the single effect every mutation in the app funnels through.App::Moneymoor::View::*ā the row and span builders, pure and unit-tested; no widget code decides what a number says.App::Moneymoor::Theme/Themes/Service::Iconsā palette and glyph tiers.
PORTABILITY
The sqlcipher shared library has to be loadable by NativeCall. On
macOS, <use MacOS::NativeLib <sqlcipher>;> before the first
use App::Moneymoor::DB points NativeCall at the Homebrew install.
On Linux the system loader finds a distro sqlcipher by itself only
when its soname is libsqlcipher.so.0; modern Debian and Ubuntu
(24.04+) package soname 1, which DBIish's built-in lookup refuses ā
point DBIISH_SQLCIPHER_LIB at the library
(/usr/lib/x86_64-linux-gnu/libsqlcipher.so.1) and it is loaded
verbatim. On Windows the DLL just has to be on PATH. Nothing else
in the distribution is platform-specific, and the pure engine
(Service::Budget, Util::Money, every Model) needs no native
library at all.
AUTHOR
Matt Doughty <[email protected]>
COPYRIGHT AND LICENSE
Copyright 2026 Matt Doughty
This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.