mylana is a minimal, fully local personal finance advisor. It tracks your
liquid money and declared income against your expenses, computes the month's cash flow
deterministically, and asks a local LLM for one short recommendation — from numbers it
can never change.
Fully local & privateOne Go binaryExactly one LLM call
Everything is Go and compiles into a single self-contained binary — no Python runtime,
no Docker, no cloud. A localhost web UI is the daily front end; the same code runs the
analysis and writes one report. This manual is that: one continuous read, from installing
it to how every peso is accounted for.
Every figure is computed by pure, tested Go functions. The LLM
only interprets numbers it cannot alter — a financial advisor that miscalculates is
worse than none.
①
One LLM call, not an agent framework
The flow is fixed, so there is nothing for an "agent" to decide.
(An earlier CrewAI version spent 6–9 inferences per run doing what one does now.)
🔒
Fully local and private
Ollama runs on your machine. The only network request is to an
exchange-rate API, which receives currency codes — never your data.
💬
The UI answers questions instead of raising them
A due day "27" renders as "pay Jul 27 (15d)"; an unknown value
says so explicitly. You should never have to compute or guess.
What it produces
One Monthly Financial Report, in seven sections: position today,
this month's flow, where the money goes, debt motion, a resolved 30-day calendar,
health indicators, and the advisor's take. Read How it
works for the full walk-through.
mylana · a minimal, fully local finance advisor.The Markdown docs in docs/*.md remain the working source.
Clone the repo, seed your data, and run one command. Everything compiles into a
single self-contained Go binary — no Docker, no Python runtime, and a local LLM only if you
want the advisory.
Go 1.26+Runs on localhostNo Docker
1Requirements
Go 1.26 or newer is the only hard requirement — it builds and runs the whole
app. Everything else is optional.
Component
Needed for
Required?
Go 1.26+
Building and running mylana
Yes
Ollama
The advisory section (any chat model; gemma4:12b is the default)
Optional
Python 3.12+ & uv
Running the reference oracle only
Optional
Docker
—
Never used
Without Ollama, the app still produces the full numeric report; the advice section simply notes
that advice is unavailable. Skip the LLM entirely with --no-advice.
2Install & first run
git clone https://github.com/roberjacobo/mylana.git
cd mylana
cp db/expenses.example.json db/expenses.json # then edit with your real data
go run ./cmd/mylana serve
serve starts a localhost-only server at
http://127.0.0.1:8787 and opens your browser. This is the daily front end.
To build a standalone binary once and run it directly:
go build -o mylana.exe ./cmd/mylana
mylana serve
💡
Two entry points, one codebase
mylana serve runs the web UI; plain mylana runs
the pipeline once from the CLI and writes outputs/report.md. See the
Command reference for the full list.
3Your data
All your data lives in one SQLite file, db/mylana.db — every write
is transactional. The expenses.json you copied is a seed: on first run it is
imported into the database and renamed to expenses.json.bak.
Back it up or restore it any time:
mylana export # dump the database to db/*.json
mylana import # load db/*.json back, replacing what's present
📦
Where it lives, and its shape
See Data & storage for the full schema,
how the base directory is resolved, and the document model behind every table.
4Configuration (.env)
An optional .env file in the working directory tunes a few things. Every setting has
a default, so you can skip this entirely.
LLM=ollama/gemma4:12b # Ollama model for the advice ("ollama/" prefix stripped)
BASE_OLLAMA_URL=http://localhost:11434
LANGUAGE=en # UI + report language: en (default), es or fr
5Income
Income is entirely user-declared — there is no AMOUNT setting. The
report's projected income is the sum of the income sources you declare in the app
(Accounts tab → Income).
A source can be a one-time deposit or a perpetual source (fixed or variable, in MXN or USD). USD
sources convert at the day's exchange rate; a variable source counts only what was actually
received that month, never an estimate. With no sources declared, projected income is 0.
6Languages
The UI and the report render in English, Español or Français. Change it live
from the web Settings menu (the gear at the bottom of the sidebar), which
persists your choice to db/settings.json — or set LANGUAGE in
.env.
🌐
English is the fallback
English is the source language. Any untranslated string shows in English
rather than breaking, so switching languages is always safe.
7Demo / privacy mode
A Settings toggle masks every money amount as $•••••• MXN, so you
can show the app without revealing your finances. Names, structure and percentages stay visible,
so the app is still fully demonstrable.
🔒
Server-side, not a blur
The masking happens on the server — real figures never reach the page or
the report. It is not a CSS effect that could be peeled back in the browser.
8Choosing an LLM
The advisory is the one place a model gets involved. Picking one is easy.
🧠
Any Ollama chat model works
No tool-calling is required. The LLM only writes a short advisory from
numbers that are already computed, so a mid-size model is plenty — pick one that fits your
GPU's VRAM (e.g. gemma4:12b for 16 GB). Reasoning models are unnecessary here.
Every command you need, in one place. Each Go command runs from the repo root
with no build step for development — go run compiles and runs in one shot. The
Python commands are only for the reference oracle, and they need uv sync once
before their first use.
Run from repo rootNo build step to developPython = oracle only
📦
One binary, not two
There is no separate frontend and backend process to start. serve
embeds the HTMX templates and runs the pipeline in-process — nothing to
npm install, no Node, no Python runtime to use the app.
1Run (backend + frontend — one binary)
The web UI is the frontend and the pipeline is the backend, but they ship as the same binary
and the same command family.
go run ./cmd/mylana serve # web UI (the frontend) at http://127.0.0.1:8787, opens the browser
go run ./cmd/mylana # run the pipeline (backend) directly, write outputs/report.md
go run ./cmd/mylana --no-advice # numeric report only, skip the LLM entirely (< 1s)
2Build & cross-compile
Set GOOS/GOARCH to build for any platform from any platform.
go build -o mylana.exe ./cmd/mylana # current OS
GOOS=linux GOARCH=amd64 go build -o mylana ./cmd/mylana # Linux
GOOS=darwin GOARCH=arm64 go build -o mylana-macos ./cmd/mylana # macOS (Apple Silicon)
GOOS=windows GOARCH=amd64 go build -o mylana.exe ./cmd/mylana # Windows
The result is a ~12 MB self-contained binary (pure Go, no CGO — that's exactly what keeps
cross-compilation working). Dev and CLI builds must not pass
-H=windowsgui; only the installer build does.
3Data: backup, restore & feed the oracle
Move your data between the SQLite database and the portable JSON shapes.
go run ./cmd/mylana export [dir] # dump db/mylana.db to db/*.json (default: the data dir)
go run ./cmd/mylana import [dir] # load db/*.json back, replacing what's present
export is also how you feed the Python oracle — it reads the JSON files, not the
database.
4Quality gate (run clean before committing)
Both sides must pass before a commit: the canonical Go implementation and the Python oracle
that keeps it honest.
# Go — the canonical side
go vet ./...
go test ./...
gofmt -w assets cmd i18n pipeline store tools web
# Python — the reference oracle
uv run pytest tests/ -q
uv run ruff check src tests
uv run ruff format src tests
5Reference Python pipeline (the oracle)
The Python original produces the same report as the Go pipeline. Sync its isolated venv once,
then run it like the CLI.
uv sync # once: create the isolated venv
uv run mylana # run the oracle (same report as the Go pipeline)
uv run mylana --no-advice # numbers only
6Verify the Go and Python reports stay identical
If you change the math or the report on either side, prove they still match byte for byte
(modulo line endings).
go run ./cmd/mylana export db # write db/*.json next to mylana.db
go run ./cmd/mylana --no-advice # Go report -> outputs/report.md
uv run mylana --no-advice # oracle report -> outputs/report.md (overwrites)
# compare the two runs' outputs/report.md (diff, ignoring line endings)
7Icon & Windows resources (regenerate after changing either)
The app icon is generated from code, never hand-edited; the Windows .syso
resources are rebuilt from it.
go run ./tools/genicon # regenerate assets/icon-256.png, icon.ico, icon.icns
go-winres make --in cmd/mylana/winres/winres.json --out cmd/mylana/rsrc # rebuild the embedded Windows .syso
8Packaging (one installer per platform, all from the same binary)
Each platform gets a per-user installer built from the very same binary.
powershell -File packaging/windows/build.ps1 # Windows: dist/mylana-setup.exe (Inno Setup, per-user, no admin)
sh packaging/macos/build-app.sh # macOS (run on a Mac): dist/mylana.app (unsigned)
sh packaging/linux/build.sh # Linux: tar.gz whose install.sh registers a .desktop launcher
mylana reads your local financial data and produces one output: the Monthly
Financial Report. Every number is computed deterministically; a local LLM only writes the
final advisory — and even that is optional.
Five stages turn your data into the report. The one rule to remember:
the LLM sees the finished numbers; it can never change them.
Load data store
The expenses document, the spending log, and accounts — all from db/mylana.db (SQLite).
↓
Exchange rate rate.go
One HTTP GET with an automatic fallback source, to convert USD income.
↓
Math math.go
Cash flow, debt motion, resolved calendar, health indicators. Pure functions, no I/O, no AI.
↓
Report report.go
Sections 1–6 rendered as Markdown, translated via i18n.
↓
Advice — optional advisor.go
ONE call to the local LLM becomes section 7. Skipped with --no-advice.
2Two ways to run it
go run ./cmd/mylana serve # web UI: opens http://127.0.0.1:8787, runs the analysis on demand
go run ./cmd/mylana # CLI: run the pipeline once, write the report
The web UI runs the pipeline in-process — no subprocess, no Python. Both
entry points execute the same Go code (pipeline.Run).
🔁
The pipeline exists twice
The canonical Go implementation (pipeline/) and the
reference Python original (src/mylana/, run with uv run mylana)
produce the same report, byte for byte. The Python side is an oracle that keeps the math
honest — see Architecture.
3Step by step
Step 0 — Income
Income is entirely user-declared; there is no AMOUNT setting. The projected
income is the sum of the income_sources (a salary, a rent you collect). USD
sources convert at the day's rate; a variable source counts only what was actually
received that month, never an estimate. With no sources, projected income is 0.
Step 1 — Load the data
The expenses document (fixed data + income sources), the spending log, and liquid money
(accounts, or the legacy single cash number). No path is hardcoded — see
Data & storage for where it lives and its schema.
Step 2 — Fetch the exchange rate
One HTTP GET to exchangerate-api.com (free, no API key). If it fails, it
retries against frankfurter.app (European Central Bank). If both fail, the
run stops — better no report than one with an invented rate.
Step 3 — Compute everything
All arithmetic happens here, in pure functions:
Computation
Function
Convert USD → MXN
Convert(3000, 17.5) → $52,500.00
Monthly outflow (the full picture)
MonthlyOutflow(data, entries, ym)
Remaining balance
RemainingBalance(income, outflow)
Debt motion this month
DebtMotion(entries, ym) — paid vs charged
Payment priority
AvalancheOrder(inventory) — highest interest first
Payoff estimate
Payoff(...) — amortized months to zero
Resolved calendar
UpcomingEvents(data, today, 30) — dates resolved
Health metrics
HealthIndicators(...), HeaviestDay(data)
The remaining balance — if positive — becomes the "extra payment" used in payoff estimates.
Step 4 — Render the report
Sections 1–6 are built from those numbers (still zero AI). Every date is resolved
("pay Jul 27 (15d)"), every unknown is explicit. The payment-priority ranking appears
only when interest rates exist; otherwise the report shows a missing-data
warning instead of a meaningless ranking.
Step 5 — The one LLM call
Skipped entirely with --no-advice. Otherwise one HTTP POST to Ollama
(/api/chat, model from LLM in .env, temperature 0.1).
The prompt: here is the exact numeric report — do not alter or recompute any number —
write only the advisory (2–3 number-specific observations plus one concrete action).
🛡️
Degrades gracefully
If Ollama is down, the report is still written; the advice section says
"ADVICE UNAVAILABLE" with the reason. The numbers never depend on the LLM.
4The math, no double counting
The outflow model avoids counting the same peso twice. In short:
total = non-card obligations + cards reserved + variable spending
remaining balance = income − total
Items with charged_to (subscriptions billed to a card) are excluded from
obligations — they already live inside the card's balance.
Each card reserves max(minimum_payment, paid this month); each recurring item
reserves max(amount, payments this month) — a real payment replaces
the floor, it never stacks on top.
The full set of accounting rules — transfers, income, cancellations, domiciled items — lives
in The cash-flow model.
5The report
outputs/report.md is written (the directory is created if missing, gitignored)
and printed — or, in the web UI, rendered in the Report tab with one-click copy. Its seven
sections: position today, this month's flow, where
the money goes, debt motion, the resolved 30-day
calendar, health indicators, and the advisor.
⏱️
Timing
--no-advice: under 1 s (one HTTP request + math + file
write). With advice: adds one LLM inference (~5–15 s with gemma4:12b on a
16 GB GPU).
The rules that keep every peso counted exactly once — where money comes from,
where it goes, and what never counts as spending.
No double countingAccounts are ground truthReplace, don't stack
1Money in, money out
Every figure in the report is one of two things: money that raises what you can spend, or
money that lowers it. The whole discipline of the model is deciding which — and making sure
nothing lands in both columns.
Money IN
Declared income — the projected sum of your income_sources.
Deposits — income entries that credit an account.
Transfers in — the receiving leg of a move between your own accounts.
Money OUT
Non-card obligations — fixed expenses and streaming not billed to a card.
Cards reserved — what each card sets aside this month.
Variable spending — the day-to-day diary.
Card payments — money leaving an account to lower a card's balance.
The rules below say precisely how each item is classified — and, just as importantly, what is
deliberately excluded so it is never counted twice.
2Outflow & remaining
The core of the model is two lines. Everything else is about feeding them the right numbers.
Obligations billed to a card are absent from the first term — they already live inside the
card's balance, which the reserve term accounts for. A positive remaining balance is what
the report can offer up as an extra debt payment.
3Cards & domiciled items
🔗
Domiciled subscriptions — charged_to
An item with charged_to: "<card>" is excluded
from obligation totals: its cost already lives inside that card's balance, and you
pay it by paying the card. It still appears in the views, labeled "On <card>", so
nothing disappears — it just isn't summed a second time.
💳
Card payments
A spending-log entry with category: "card-payment" and a
card. It lowers the paying account and the card's balance. It is
never counted as variable spending — instead it feeds the per-card reserve
for the month.
🛒
Card purchases
An entry with a card and any ordinary category. It raises the
card's balance, never touches your accounts, and does not count as
day-to-day spending — the cash leaves later, when the card gets paid. It stays visible in the
diary so the history is complete.
📐
Cards reserved — replace, don't stack
Per card, the month reserves
max(minimum_payment, payments made this month). Early in the month that's the
minimum; a real payment replaces the floor — it never adds on top of it.
4Accounts & transfers
🏦
Liquid money & accounts
Money lives in accounts (cash / bank). Every
expense and card payment debits its account; an empty account field means the
first account (the default). Accounts are ground truth —
validate them against your bank app; the balance report is the plan, not the ledger.
🔀
Transfers
category: "transfer" with from_account /
to_account (an ATM withdrawal is bank → cash). These are diary entries but
never spending: excluded from variable totals on both sides. The money
just moves — your liquid total is unchanged.
5Recurring payments
🔁
Paying an obligation
category: "recurring-payment" plus recurring
(the item's name). It debits the account and moves the item's paid_through
forward, but is never variable spending — the obligation was already
counted in the outflow. Per item the month reserves max(amount, paid) — the
same replace-don't-stack rule as cards. One payment can cover several months.
6Income & deposits
💰
Deposits — the mirror of an expense
category: "income" plus account: money coming
in. Where an expense debits, this credits the account. It
shows in the diary as a green + credit and is never counted as spending.
⚠️
A deposit does not change projected income
The deposit already lifted the account balance and your net position.
Folding it into projected income as well would count the same money twice.
So the projection stays the forecast (your declared sources); the deposit is the ground-truth
arrival, recorded separately.
7Cancellation
🚫
Cancelling a subscription — ends_on
Cancelling sets ends_on: "YYYY-MM-DD": the last day
of paid service the provider states, not the day you clicked cancel. It never
deletes the item — the charges already made are real, and the final paid cycle still costs.
📅
Strictly before the end date
A charge on day D renews the service past D, so a charge landing
exactly on the end date can never happen. Crunchyroll ending Aug 5 was last charged Jul 5 —
an "on or before" rule would invent a phantom August charge. The final paid cycle still
costs; renewals stop after it.
All your data lives in one transactional SQLite file. The JSON shapes below are
the seed and exchange format — the document model the code reads and mylana export
writes.
One SQLite fileTransactionalGitignored
1One SQLite file
Everything you enter lives in db/mylana.db. Every write is transactional — a
crash can never corrupt it or half-apply money (both legs of a transfer, or a payment's
account debit and card drop, land together or not at all).
The JSON files below are the seed and exchange format, not a second store.
If a legacy db/*.json file sits next to a missing database, it is
imported on first run and renamed *.json.bak (your pre-migration backup). After
that, the database is the single source of truth.
Command
What it does
mylana export
Dumps the DB back to db/*.json — your backup, and what the Python oracle reads.
mylana import
Restores those JSON files back into the database.
🌱
Starting fresh
Copy db/expenses.example.json to
db/expenses.json and run once — it imports as your starting data. All real
data (the database and every seed file) is gitignored.
2Where the data lives
No path is hardcoded. store.BaseDir() resolves the data directory per
call, in this order:
Order
Location
When
1
MYLANA_DATA_DIR env var
Tests and portable runs — an explicit override.
2
./db next to the working directory
Development: the repo root already has a db/.
3
OS user config dir
Installed use: %AppData%\mylana, ~/Library/Application Support/mylana, or ~/.config/mylana — created on first write.
So the same binary runs from the repo checkout or installed anywhere, and always finds its
data.
3The expenses document
Your fixed financial data — historically db/expenses.json. It has five sections.
Keep the field names exactly as shown; they are what the pipeline expects.
A card name. The item is billed to that card, so it is excluded from obligation totals — you pay it by paying the card. Stays visible, labeled "On <card>".
paid_through
string, optional
"YYYY-MM", set by the UI. Counts as paid while >= the current month, so it self-expires; paying ahead keeps it green next month.
ends_on
string, optional
"YYYY-MM-DD" — cancellation. The last day of paid service the provider states, not the day you cancelled.
streaming_services
Streaming subscriptions, tracked separately but with the same shape as
fixed_expenses (name, amount, due_date,
and the optional charged_to, paid_through, ends_on).
The final paid cycle still costs; renewals stop strictly afterends_on, so a charge never lands exactly on it. Because that date is the
renewal anniversary, due_date should be its day of the month. Remove the
field to reactivate the item.
Current balance in MXN (the UI updates it when you register a payment).
minimum_payment
number
Monthly minimum; the card reserves max(minimum, paid this month).
interest_rate
number
Fraction (0.24) or percent (24).
due_date
string
Payment day of the month.
credit_limit
optional
Informational only — the math ignores these. Cut dates are snapshots (some banks move them month to month). paid_month ("YYYY-MM") is the PAID / to-pay status, set by the UI, self-expiring like paid_through.
cut_date
optional
statement_balance
optional
note
optional
bank
optional
⚠️
No rate, no ranking
Without interest_rate the report cannot rank debts by
priority — it shows an explicit missing-data warning instead of a meaningless order.
Fraction or percent; drives payoff order and estimates.
due_date
string
Payment day of the month.
creditor
string, optional
Who you owe.
income_sources
Perpetual monthly income you expect — a salary, a rent you collect. This is
income, never an obligation: it is excluded from every obligation total and
instead drives the report's projected income.
The first account is the default — entries without an explicit
account debit it. Once accounts exist, "cash on hand" everywhere means their
sum.
💵
Legacy cash migrates automatically
Before accounts, liquid money was a single number in
cash.json ({"amount_mxn": 17014.30}). Creating your first
account migrates it automatically, so no money silently disappears.
6Security
⚠️
Never commit your data
Keep mylana.db (and its -wal/-shm
companions), the seed JSON files and the .json.bak backups out of version
control. Only expenses.example.json is tracked, as a template.
All amounts are in Mexican Pesos (MXN). Interest rates accept either
fractions (0.24) or percents (24) — both normalize to the same
value.
mylana serve starts a localhost-only server and opens your
browser. It is the daily front end — server-rendered, embedded in the binary, no JS
framework, works offline.
Localhost onlyEmbedded, no CDNSix tabs
1One binary, no toolchain
The UI is built from server-rendered Go templates embedded straight into the binary with
go:embed. It listens on localhost only (default port 8787) — nothing
leaves your machine. There is no JavaScript framework: HTMX (for
hx-boost page swaps) and Lucide icons are vendored, pinned to an
exact version, and served locally — never from a CDN. So the app works fully offline.
Every money mutation — logging a transaction, paying a card, a transfer, an income deposit —
goes through one shared entry point (store.ApplyEffects), the same
code the report reads. The web UI and the report can never disagree.
go run ./cmd/mylana serve # opens http://127.0.0.1:8787
2The tabs
Six tabs, one job each. They read and write the same SQLite data the pipeline computes over.
🧾
Transactions
The daily diary: log, edit and delete day-to-day movements. Each row says what it was paid
with — an account or a card, never a blank cell. Shows the month's variable spending and
what's charged to cards.
🔁
Recurring
Your monthly commitments as an actionable dashboard: pay an item, mark it paid, edit it,
domicile it to a card, or cancel a subscription. Sectioned tables (fixed / subscriptions /
debts / cards' commitment) with status pills, an overdue banner, and a Totals overview.
Debts show a resolved payoff line.
💳
Cards
One row per credit card with balance, available credit (color-coded), the live minimum
still owed, and a clear settlement status — paid, cut-and-payable in amber, overdue in red.
The card detail adds pay + edit, the interest-free installments (MSI), and the subscriptions
domiciled to that card, so it mirrors your bank app.
🏦
Accounts
Liquid money split by account (new account, adjust balance, transfer — an ATM withdrawal is
bank → cash), plus Income: one-time deposits and perpetual income sources (fixed or variable,
MXN or USD).
📊
Statistics
Read-only analytics on the current month: consumption by category, largest movements, and
per-card utilization. Charts are pure CSS bars — no chart library.
📄
Report
Run the analysis and read the Monthly Financial Report, with one-click copy of the clean
Markdown.
3Settings & theme
A Settings menu — the gear at the bottom of the sidebar — holds the language
(English / Español / Français), the demo/privacy mask that hides every money
amount so the app can be shown safely, the currency list (the extra currencies
you also handle, e.g. USD), and your custom expense categories.
The light/dark theme is remembered in the browser, separate from the language —
switching one never touches the other.
4Design notes
🎨
One design system, questions answered
Semantic ok/danger colors, a single indigo accent, and reusable components
(metric cards, tables, badges, dialogs) hold the whole UI together. The guiding rule: the UI
answers questions instead of raising them — dates are resolved ("pay Jul 27 (15d)") and
unknowns are stated explicitly rather than left blank. It also lives in the system tray on
Windows/Linux (Open / Quit); on macOS the Dock icon plays that role.
Three parts share one data layer over a single SQLite database: the Go analysis
pipeline, the Go web UI, and a reference Python oracle that keeps the math honest.
One SQLite filePure-Go, no CGOMath triplicated on purpose
1The map
Two entry points, one shared data layer, one analysis pipeline. The web UI and the CLI both
call the same Go code; nothing reaches the database except through store/.
web/web.go ──> store/ (read/write db/mylana.db, SQLite)
──> pipeline.Run (the analysis, in-process)
cmd/mylana ──> web.Serve (the `serve` subcommand: the localhost server)
──> pipeline.Run (the bare CLI: run the analysis once)
pipeline/run.go ──> rate.go (network: rate APIs)
──> math.go (pure math, no I/O)
──> report.go ──> math.go
──> advisor.go (network: local Ollama)
──> store (shared load/date/format helpers)
──> i18n/ (all translations, UI + report)
src/mylana/main.py mirrors pipeline/run.go with the same module split.
Where things live: arithmetic is in pipeline/math.go, report text in
pipeline/report.go, web handlers in web/web.go, data access in
store/, and translations in i18n/.
2pipeline/ — the Go analysis
The canonical implementation of the pipeline. Pure computation and rendering; the only I/O is
the exchange-rate fetch, the optional LLM call, and writing the report.
File
Does
run.go
Orchestration — loads .env and data, fetches the rate, renders the report, optionally appends the LLM advice, and writes outputs/report.md.
rate.go
GetRate — exchangerate-api.com, with an automatic fallback to frankfurter.app (ECB).
math.go
All arithmetic: CategoryTotals, MonthlyOutflow, CardsReserved, DebtInventory, the Avalanche/Snowball order, Payoff, DebtMotion, UpcomingEvents, HealthIndicators, and more. Pure functions.
report.go
MonthlyReport — sections 1–6, translated via i18n.
advisor.go
GetAdvice — one POST to Ollama; never fails, returning an ADVICE UNAVAILABLE placeholder if the server is unreachable.
3store/ — the shared data layer
Reads and writes db/mylana.db and holds every cross-UI rule; the web UI and the
pipeline both call it, and there is no private copy anywhere. Connections open per operation
and close immediately — never cached — because the data dir changes between calls and a held
handle would lock the file on Windows. The first open imports any legacy db/*.json
files in one transaction and renames them *.json.bak.
File
Does
db.go
The SQLite layer — schema, per-operation connections, the legacy-JSON importer, and ExpensesRaw/SetExpensesRaw (the only bridge to the document shape).
Spending, obligations, card and recurring mutations, cash, and the pipeline-shared helpers (VariableTotal, CardCharges, CardPayments, CardsReserved, NextDue, MXN/Commas). ApplyEffects is the single money-mutation entry point.
accounts.go
Liquid money per account.
income.go
Perpetual income sources.
settings.go
In-app preferences.
paths.go
BaseDir() and every *Path() helper.
4web/ — the Go web UI
The daily front end. Server-rendered Go templates embedded with go:embed, bound
to localhost only; HTMX and Lucide are vendored and pinned (never a CDN). web.go
holds the routes and handlers and the per-tab view builders; templates/*.html is
the shell plus the tabs; static/ holds the vendored JS. Every money mutation goes
through store.ApplyEffects, never a direct balance edit in a handler.
5cmd/mylana/ — the CLI and server entry point
serve starts the web server; a bare invocation runs the pipeline once. A second
serve recognizes an already-running instance by the Server: mylana
response header and just opens the browser. On Windows and Linux it can put a small system tray
icon (Open / Quit); the tray never blocks the server, and a desktop without one degrades to a
plain foreground server. The embedded Windows icon and version resources live here too.
6src/mylana/ — the Python oracle
The same pipeline with the same module split — main, exchange_rate,
finance_math, report, advisor, i18n — and
the same report, byte for byte. It has no UI, no accounts, and no web server: it exists only to
keep the math honest. It reads the JSON shape produced by mylana export, never the
SQLite schema directly.
7The three-sided math
⚠️
The reserve/outflow/calendar logic is triplicated on purpose
The same rules live in three places: Go store, Go
pipeline, and Python finance_math.py. Any change to the math must
land in all three, and both reports must stay byte-identical — verify with the parity
command (go run ./cmd/mylana --no-advice against
uv run mylana --no-advice, modulo line endings).
Tests follow the toolchain: Go tests live inside each package they cover
(pipeline/, store/), while Python tests live in tests/.
What's built and what's planned. Detailed feature designs get their own pages —
the first is flexible cadence payments.
1Shipped
Accounts Done
Liquid money split by account, with transfers between them (replaced the single cash number).
Payment control (core) Done
Pay or mark recurring obligations, with paid/overdue status, the home agenda and the
open-the-app reminders.
SQLite storage Done
All data in one transactional db/mylana.db; export/import for JSON backup and
the Python oracle.
Income & the fixed/variable taxonomy Done
Declared income sources (one-time plus perpetual, fixed or variable); the AMOUNT
environment variable was removed.
Multi-currency (Phase 1) Done
Income and expenses record how the money entered versus how it landed — which yields the
real exchange rate you got.
Subscription cancellation Done
An ends_on date with the strictly-before charge rule, so a final paid cycle is
never invented or dropped.
2Planned
Flexible cadence payments
Design ready
Schedule a payment on a weekly / biweekly / monthly / every-N-days rhythm — "fixed but
variable", flexible, and counted only when it is actually paid.
Read the design →
Monthly close & statements Planned
A frozen month-end "estado de cuenta" as a clear money-flow map (in → out, with
percentages), exportable to PDF, with 2–3 months of read-only history.
Essential vs. discretionary Planned
A necessary/unnecessary tag on every expense, feeding a "needed vs. not" split (with %) in
the report and the Statistics tab.
Installment purchases (MSI/MCI) Planned
Interest-free (or financed) month plans tied to a card, created straight from the
add-expense flow.
Investments & surplus Planned
Net worth, an emergency-fund target, and a surplus waterfall for what's left over.
Managed people (dependents) Planned
Give money to a spouse, parent or kid and track their running balance.
3Design pages
Each planned feature earns a full design page once its shape is settled.
Schedule a payment on a rhythm the monthly model can't express — weekly,
biweekly, monthly or every N days. "Fixed but variable": the cadence is fixed,
the amount changes each time, and the payment may or may not happen.
PlannedDesign readyBackward compatible
1The problem
Today every recurring item in mylana assumes a monthly cycle keyed to a
day‑of‑month (due_date = "01"…"31")
with a month-boundary paid marker (paid_through: "YYYY-MM"). There is no
concept of frequency.
But some payments follow another rhythm. The motivating case, in the user's words:
💬
Real use case
"Tengo un pago que hago en efectivo cada semana, puede ser o
puede no ser, es flexible. […] Tipo de pago: fijo pero variable."
Three things the monthly model can't represent: a sub-monthly cadence
(weekly), an amount that varies each time, and the fact that an occurrence
is optional — it may simply not happen.
2Design decisions
Three choices shape the whole feature:
①
It counts only when paid
A flexible payment is not forecast into "committed
this month" — it never inflates what's left. When you log a real payment, that amount
lowers the month. Honest with "may or may not happen", and it keeps the math simple.
②
Soft status, never red
A past occurrence you didn't log shows as a muted
pending / optional hint — never a red "overdue" badge, and never in the
overdue banner. It respects that the payment is flexible.
③
Not married to a date
No rigid calendar of fixed dates. The cadence only paces the
reminder from your last payment: next occurrence = last paid +
interval. You say "done or not done" — the app just nudges at the right rhythm.
3Data model
New optional fields on a recurring item. All are absent in today's data, so nothing
changes for existing monthly items — the feature is fully backward compatible.
Field
Type
Default
Meaning
frequency
string
absent → monthly
weekly · biweekly · monthly · custom
interval_days
int
7 / 14 / 30
Days between reminders. Explicit for custom; derived otherwise.
flexible
bool
false
Optional-cadence: soft status and excluded from the forecast sum.
since
date
today (at creation)
Start of the rhythm — the reference for the first reminder before any payment.
paid_until
date
—
Last payment ("paid up to this moment"). Next occurrence = paid_until + interval.
⚠️
paid_until is not paid_through
Monthly items keep paid_through (a
YYYY-MM boundary). Cadenced items use paid_until (a date).
The two boundary semantics are deliberately not unified.
4How it counts (the exclusion trick)
The monthly reserve math is triplicated across three implementations (Go store,
Go pipeline, and the Python oracle finance_math.py) and must stay
in sync — see Architecture. Rather than teach all three to
multiply a weekly amount by "occurrences in the month", a flexible item is simply
excluded from the forecast sum — the same pattern already used for
card-domiciled items. Its real payments then count in full through an existing code path.
A normal monthly item
forecast in "committed" · replaced by real payment
A flexible item — forecast
excluded → contributes 0
A flexible item — once paid
real payment counts in full
Concretely, all three recurringExtra implementations compute:
extra = Σ(all recurring payments)
for each matched payable item:
extra -= min(paid, monthly_amount) // a real payment REPLACES the forecast
A payment whose name matches no item stays in extra in full.
So if a flexible item is (a) kept out of the committed sum and (b) skipped in this matched
loop, its payment is never subtracted — it counts exactly like "a payment matching
no item." No occurrence multiplication anywhere, and because every guard keys on
flexible (absent in all current data) the report stays byte-identical for
existing users.
🔎
Honest tradeoff
Because it isn't forecast, the month doesn't pre-reserve
anything for a flexible payment — it appears as spending only once logged. That's the
correct reading of "may or may not happen". A soft reserve could be added later.
5The flow
Schedule
Create the payment, pick a frequency (weekly / biweekly / monthly / every N days) and mark it flexible.
Remind
The recurring tab and agenda show a soft nudge at the cadence — "toca esta semana" / "in N days" — never a red overdue.
Mark done
You paid it: enter the real (variable) amount and the account it came from. Missed it? Do nothing — it just nudges again next cycle.
Deduct
The existing pay flow logs a recurring-payment entry and ApplyEffects debits the chosen account. paid_until = today resets the rhythm.
6Phases
Phase 1 this design
Cadence fields + the flexible exclusion (three synced implementations).
Next-reminder resolver: last paid + interval.
Pay flow advances paid_until; soft status & agenda.
Web forms: frequency selector, interval, "mark paid now"; i18n.
Phase 2 deferred
Firm (non-flexible) sub-monthly obligations that are reserved — these need occurrence-multiplication in the report.
Flexible occurrences in the report calendar with expected amounts.
Multi-occurrence agenda (all weekly occurrences within 14 days).