Lars Nolden dc767799bc Import ING and Kontist statements behind a reviewed column mapping
CSV import is now mapping-driven: N26, ING (metadata preamble, Windows-1252,
German decimals) and Kontist exports are recognized locally, and any other
layout can have its columns proposed by the configured model from a sample in
which letters are replaced by x and digits by 0. Proposals are untrusted: every
column must name a supplied header, money must come from one signed column or
one debit/credit pair, and formats must be from a closed list.

Uploading no longer imports. /api/import is replaced by prepare/confirm/cancel:
prepare parses, deduplicates and previews the exact facts, and only confirming
at the reviewed revision writes them. ING and AI-mapped facts carry no
transaction reference, because repeating SEPA mandate references must never
become a transaction identity.
2026-09-11 17:49:03 +02:00
2026-09-10 12:30:42 +02:00
2026-09-10 12:30:42 +02:00
2026-09-10 12:30:42 +02:00
2026-09-10 12:30:42 +02:00
2026-09-10 12:30:42 +02:00
2026-09-10 12:30:42 +02:00
2026-09-10 12:30:42 +02:00

Finance Duck

A self-hosted personal finance dashboard with a Go backend, React frontend, and DuckDB analytics. Human-readable .finance journals are the source of truth; DuckDB is a disposable index.

Imported bank facts are separate from editable merchant, category, and tag classifications. N26, ING, and Kontist CSV imports and bank synchronization work without AI. Optional OpenRouter enrichment uses restrictive provider routing and omits amounts by default, and can map the columns of an unrecognized CSV layout from a redacted sample.

There is no application login. Keep Finance Duck behind your VPN. The default service and Docker Compose port bindings are loopback-only. Setting a hostname does not provide authentication or firewall protection.

Quick start on NixOS

From the repository root:

nix-shell
npm --prefix web ci
npm --prefix web run build
go build -o bin/finance-duck ./cmd/finance-duck
./bin/finance-duck -data ./finance

Open http://localhost:8080. For CSV imports, create a local account in Accounts, then import an N26, ING, or Kontist statement. For automatic bank imports, follow the setup below; authorization discovers the bank's accounts for you.

Build React before building Go: its production assets are embedded in the binary. shell.nix supplies Go, Node.js, and the CGO toolchain needed by DuckDB. Node.js is not needed at runtime.

Connect your bank

Bank connections use Enable Banking. There are two separate credentials:

  • Application ID + RSA private key: authenticate Finance Duck to Enable Banking.
  • Bank session: grants access to the accounts you approve. Finance Duck obtains and stores this automatically after bank login.

The callback's one-time authorization code is not a reusable API key or the application private key.

1. Choose the callback URL

Use the address you open Finance Duck at through your VPN, followed by /api/banking/callback. For example:

https://finance.example.internal/api/banking/callback

Replace finance.example.internal throughout these examples with your actual hostname. Settings → Enable Banking credentials shows the exact callback URL to register, including scheme, hostname, port and path. On the deployed server it is https://nixos.taile9e6d9.ts.net:8444/api/banking/callback.

For local use, the corresponding URL is:

http://localhost:8080/api/banking/callback

localhost works only when the browser completing bank login can reach Finance Duck on that computer. For a remote server, use its VPN-accessible hostname instead.

This is a browser redirect, not a webhook. Your browser must be connected to the VPN and able to reach the callback. Finance Duck does not need to be publicly exposed. The server needs outbound HTTPS access to Enable Banking.

Settings shows a copyable callback URL for the current browser origin and saves it with the application credentials. Accounts displays the configured callback URL, or suggests the current origin before configuration.

2. Generate an application key and certificate

If you already have the private key for your registered Enable Banking application, use it instead of generating a replacement.

Otherwise, on NixOS:

umask 077
mkdir -p secrets
chmod 700 secrets

nix-shell -p openssl --run '
  openssl genrsa -out secrets/enablebanking.key 4096 &&
  openssl req -new -x509 -days 365 \
    -key secrets/enablebanking.key \
    -out secrets/enablebanking.crt \
    -subj "/CN=Finance Duck"
'
  • Upload secrets/enablebanking.crt, the public certificate, to Enable Banking.
  • Upload secrets/enablebanking.key through Finance Duck's Settings, and back up your original key securely. Never upload it to Enable Banking in place of the public certificate.
  • Do not rerun the generation command over a key already in use. A different private key will not authenticate against the existing registered certificate.

The application accepts a single unencrypted PEM RSA key in PKCS#1 or PKCS#8 format, at least 2048 bits and no larger than 32 KiB. It creates signed application JWTs itself; you do not need to generate daily tokens.

3. Register and activate the Enable Banking application

Open the Enable Banking application control panel:

  1. Register a Production application for real bank data, rather than a Sandbox application.
  2. Upload your public certificate and register the callback URL.
  3. Copy the resulting application ID.
  4. Use Activate by linking accounts to link your own accounts.

Enable Banking documents restricted production access for individual, non-commercial use. In that mode, only accounts linked to the application can be accessed. Follow its current registration requirements and terms: whitelisting your own accounts.

Linking accounts in their control panel activates your application; it does not create Finance Duck's bank session. You must also authorize from Finance Duck in step 5, even for the same account.

4. Configure Finance Duck in Settings

Open Settings → Enable Banking credentials:

  1. Enter the application ID from Enable Banking.
  2. Select the matching private-key .pem or .key file.
  3. Register the displayed callback URL in Enable Banking if you have not already.
  4. Click Save configuration, then proceed to Accounts to authorize your bank.

Saving validates the local key and callback format and applies the provider immediately; it does not contact Enable Banking, register an application, or grant bank consent. Configured means locally configured, not externally verified. No SSH, environment file, Nix rebuild, or restart is required.

For the same application ID, leave the upload empty to keep the saved key, or upload a replacement to rotate it. Existing bank sessions and sync cursors are preserved. Changing the application ID requires a new upload and invalidates old local bank connections, so reconnect afterward. Remove configuration disables banking and invalidates local connections after confirmation; accounts and transaction history stay intact. It does not revoke consent at your bank. Successful configuration changes also cancel pending authorization flows.

The private key and application settings are stored atomically in state/enablebanking.json beneath the data directory, with permissions 0600. The key is never returned by the API or saved in browser storage. Banking sessions are bound to that configuration's application generation so old sessions cannot be reused after switching applications or removal, even across restart. Protect backups of the entire data directory.

Optional environment-managed setup

For headless configuration, all three variables below remain a startup fallback only when no saved banking configuration exists. A UI-saved configuration, including explicit removal, overrides them. Environment changes require a restart; malformed saved credentials fail closed rather than reverting to the environment.

export ENABLEBANKING_APP_ID="your-application-id"
export ENABLEBANKING_KEY_FILE="$PWD/secrets/enablebanking.key"
export ENABLEBANKING_REDIRECT_URL="https://finance.example.internal/api/banking/callback"

./bin/finance-duck \
  -data ./finance \
  -listen 127.0.0.1:8080 \
  -public-url https://finance.example.internal

This example assumes a VPN-accessible reverse proxy forwards to 127.0.0.1:8080. For local browser use without a proxy, set the redirect to http://localhost:8080/api/banking/callback and omit -public-url.

Stop the old process before restarting; only one process may write a finance directory. Native runs do not automatically load .env files. Shell exports also do not configure an already-running process or a systemd service—configure that service's environment separately.

For Docker, use the container-side key path and the configuration in Docker Compose.

5. Authorize from the UI

Before continuing: link every account you want to use in Finance Duck through the Enable Banking control panel. The Connect your bank flow cannot connect an account until it has been linked to the Enable Banking application there.

Open Accounts → Connect your bank:

  1. Enter the bank's exact Enable Banking institution name.
  2. Set the country to DE for a German bank.
  3. Set History to import (months): 12 by default, or another whole number from 1 to 120.
  4. Click Authorize bank.
  5. Log in on your bank's page and approve account access.

Finance Duck verifies the callback state, exchanges the returned code for a session_id, stores the session locally with restrictive permissions, and wakes the synchronization worker immediately. You do not need to copy the code or session ID manually.

Initial synchronization requests the selected number of calendar months of booked transactions per account, defaulting to 12 months. The bank may provide less history. The choice is saved with the bank connection and reused on reconnection. Subsequent daily synchronization overlaps each account's last successful sync by 14 days. Sync now starts a manual synchronization. Existing accounts keep their successful-sync cursors: changing the history choice or reconnecting does not backfill them. Older history can be imported with CSV.

HTTP 429 is a provider rate limit, not evidence that bank consent has expired. Bank reads honor Retry-After and use bounded exponential retries. A longer or exhausted limit pauses further requests until the reported retry time; failed accounts keep their previous sync cursors and imported data. Session checks use the saved account metadata rather than fetching every account's details again. A failed session is reported once instead of also marking each of its accounts unavailable. After the cooldown, Sync now can retry; the warning clears after a successful sync. One-time authorization and code-exchange requests are never automatically replayed.

Manual Sync now, Import older history, and balance requests forward the requesting user's IP, browser User-Agent, and available Accept headers to the bank as PSU metadata. Scheduled syncs never claim a user is present. This distinction matters: Enable Banking documents background limits of roughly four fetches per day at many banks. A confirmed background ASPSP_RATE_LIMIT_EXCEEDED defers that account's affected endpoint for at least six hours, preserving longer provider hints; it does not block an eligible user-initiated fetch. General provider limits still apply to both. Bank requests are spaced by at least one second, with longer learned spacing after throttling.

Import older history for a connected account

Open Accounts, find the account, and click Import older history. Choose Months back (default 12, whole numbers from 1 to 120) and confirm. This requests that account's booked transactions from the selected number of calendar months ago through today; the bank may provide less history.

The dialog reports how many new transactions were imported, including zero when nothing new was found. Repeated or overlapping ranges skip existing transactions. New records use the normal import and classification process. The account's regular sync cursor, last-sync time, and saved initial-history choice stay unchanged; other accounts are not fetched. Inactive connected accounts can also use this explicit action. If authorization has expired, reconnect first.

When consent expires or is revoked, the dashboard shows a warning such as “ING needs reconnection.” Open Accounts and click Reconnect ING. The bank and country are already selected.

Reconnect → bank login and approval → callback code → new session
                                                      ↓
                                    replace old consent and resume sync

Renewal preserves local account identities and existing transaction history. It does not create a second copy of the account or its transactions. Transient provider failures are displayed separately from expired consent.

Reconnecting renews bank consent, not your application registration. Correct certificate registration in the Enable Banking control panel and update the matching private key in Settings (or your optional environment-managed setup).

Import a CSV statement

Open Accounts → Import a statement, choose the account, select the export, and click Review statement. Uploading imports nothing: it parses the file and opens a review dialog showing the detected export, the column mapping, how many records are new or already imported, and a sample of the parsed transactions with their dates, descriptions, counterparties, and signed amounts. Import N transactions commits exactly those records; Cancel, a reload, or a journal change in between commits nothing.

N26, ING (Umsatzanzeige, including its metadata preamble and Windows-1252 encoding), and Kontist exports are recognized on your own machine, with no AI involved. Comma, semicolon, and tab separators, UTF-8 with or without BOM, CRLF, quoted multiline descriptions, ISO and German dates, and both decimal separators are accepted. Use the bank's original export rather than a spreadsheet-reformatted copy. Uploads are limited to 2 MiB, and a prepared statement expires after one hour.

Any other layout needs a saved OpenRouter key and model, which maps the columns rather than reading the transactions: the request carries the delimiter, the column names, and up to four sample rows in which every letter is replaced by x and every digit by 0. Descriptions, counterparties, references, IBANs, and amounts are never sent. The proposal must name existing columns, choose exactly one money convention (one signed amount column, or a debit and credit pair), and use a supported date and decimal format; anything else is rejected instead of guessed. Because a proposed mapping can still be wrong, check the sample's dates, signs, and currency before confirming.

Reimporting the same statement adds nothing: the review dialog reports the overlap as already imported. A statement whose currency conflicts with the account, or whose records cannot be parsed, is rejected whole rather than imported in part.

Deployment options

Option Best fit Included support
Native NixOS service Deployment on your own NixOS machine Pinned flake package and one systemd service, with persistent state and runtime credentials.
Docker Compose A private server using containers Dockerfile and Compose configuration package the binary and frontend together.
Manual native run Development or troubleshooting The quick-start command runs in your terminal.

Run one application instance per canonical finance directory. React is compiled and embedded in the Go binary: production needs neither Node.js nor a separate frontend service.

Add this input to your host flake after committing and pushing the deployment files:

inputs.finance-duck.url = "git+https://git.larsnolden.com/lars/finance-duck.git";

Include finance-duck in your flake's outputs arguments, then add these entries to your existing nixosSystem.modules list:

finance-duck.nixosModules.default
{
  services.finance-duck = {
    enable = true;
    publicURL = "http://localhost:8080";
    # Optional, once you have installed the runtime credential file:
    # environmentFile = "/var/lib/finance-duck-secrets/environment";
  };
}

Keep your existing host modules, including OpenClaw, unchanged. Do not make this input's nixpkgs follow the host's NixOS 24.11 input: Finance Duck needs Go 1.24 or newer. The package uses its own pinned toolchain; the service module works on the older host.

Deploy and inspect:

sudo nixos-rebuild switch --flake /etc/nixos#nixos
systemctl status finance-duck
journalctl -u finance-duck -f

The service runs as finance-duck, listens on 127.0.0.1:8080, and stores all canonical data, settings, provider state, and the disposable DuckDB cache in /var/lib/finance-duck. It creates that directory with mode 0700; rebuilds do not replace it. No firewall ports, VM, container, reverse proxy, or Tailscale configuration are created.

For access from another machine, an SSH tunnel is sufficient: ssh -L 8080:127.0.0.1:8080 your-server, then open http://localhost:8080. For your existing Tailscale setup, instead set publicURL to the host's actual full MagicDNS HTTPS origin, for example https://nixos.YOUR-TAILNET.ts.net:8444, rebuild, and publish the loopback listener:

sudo tailscale serve --bg --yes --https=8444 http://127.0.0.1:8080

Choose an unused port; 8444 leaves the existing OpenClaw 8443 endpoint alone. Keep access restricted to trusted tailnet users. This application has no login; do not enable Funnel or expose it publicly. The Enable Banking callback must use that exact HTTPS origin plus /api/banking/callback.

Optional environment-managed credentials: both Enable Banking and OpenRouter can be configured directly in Settings, without these steps. If you prefer a banking startup fallback, create a private runtime directory and install the key after the first rebuild creates the service account:

sudo install -d -o root -g finance-duck -m 0750 /var/lib/finance-duck-secrets
sudo install -o root -g finance-duck -m 0640 secrets/enablebanking.key \
  /var/lib/finance-duck-secrets/enablebanking.key
sudo touch /var/lib/finance-duck-secrets/environment
sudo chmod 0600 /var/lib/finance-duck-secrets/environment
sudoedit /var/lib/finance-duck-secrets/environment

Put the three ENABLEBANKING_* variables from bank setup in that file together, using ENABLEBANKING_KEY_FILE=/var/lib/finance-duck-secrets/enablebanking.key. Enable environmentFile in your Nix configuration and rebuild. Later environment-file changes require sudo systemctl restart finance-duck. An optional OPENROUTER_API_KEY provides the equivalent AI startup fallback. For each provider, saved Settings configuration or explicit removal takes precedence over its environment fallback. Never place private keys or credential values in Nix configuration or the Nix store.

Existing local data: this service does not automatically adopt ./finance. Stop the previous instance, back it up, stop finance-duck.service, and copy the entire data directory into /var/lib/finance-duck, owned by finance-duck:finance-duck. Never run two writers against the same directory.

Updates: update the host's Finance Duck input with nix flake update finance-duck in /etc/nixos, then run nixos-rebuild switch again. Normal NixOS rollback restores the binary, not financial data; keep separate data backups. To build without installing a service, run nix build in this repository (the new flake and nix/ files must be tracked by Git).

Deployed server

The server at bender@100.87.224.3 serves Finance Duck at https://nixos.taile9e6d9.ts.net:8444, accessible only through Tailscale. Port 443's existing Funnel and OpenClaw on port 8443 are separate and unchanged.

ssh -t bender@100.87.224.3 'sudo finance-duck-update'
# Deploy edits already in the server checkout without pulling:
ssh -t bender@100.87.224.3 'sudo finance-duck-update --no-pull'

The command pulls ~/projects/finance-duck with git pull --ff-only, snapshots its Git-tracked working files to /var/lib/finance-duck-source, updates only the host's finance-duck flake input, rebuilds NixOS, and checks backend health. New files must be tracked with Git to enter the snapshot. Ignored data, untracked secrets, and .git are excluded; never track credentials. The host's Nixpkgs and NixVirt pins are not updated.

Host configuration lives in /etc/nixos/flake.nix and /etc/nixos/modules/finance-duck.nix; the latter defines the service settings, tailnet proxy, and updater. Application data lives separately in /var/lib/finance-duck, owned by finance-duck with mode 0700. The initial deployment has no bank/AI credentials and does not import workstation data.

Application and deployment source are tracked in the project repository. Commit and push changes before running the normal updater; --no-pull can deploy tracked local edits. Git refuses an update that would overwrite conflicting local edits. The updater never stashes, resets, commits, or pushes automatically. See OPERATIONS.txt for service inspection, backup, and rollback commands.

Docker Compose

For an initial local deployment without bank credentials:

docker compose up --build -d
docker compose logs -f finance

Open http://localhost:8080. The supplied configuration:

  • Publishes 127.0.0.1:8080, not a public host interface.
  • Persists financial data in a named volume mounted at /data.
  • Runs as UID/GID 10001:10001 with a read-only root filesystem.
  • Drops Linux capabilities and enables no-new-privileges.
  • Restarts the service unless you explicitly stop it.

To enable banking, create a private .env file in the repository root:

FINANCE_PUBLIC_URL=https://finance.example.internal
ENABLEBANKING_APP_ID=your-application-id
ENABLEBANKING_KEY_FILE=/run/secrets/enablebanking.key
ENABLEBANKING_REDIRECT_URL=https://finance.example.internal/api/banking/callback

Protect it with chmod 600 .env. .env* and secrets/ are excluded from Git and the Docker build context. Compose reads .env for variable substitution; an already-exported shell variable takes precedence, so remove conflicting exports when switching from a native run.

Uncomment the example key mount in compose.yaml, under services.finance.volumes:

- ./secrets/enablebanking.key:/run/secrets/enablebanking.key:ro

The private key must be readable by the container service user, while remaining inaccessible to unrelated users. A host-owned key with mode 0600 is not automatically readable by container UID 10001. Set ownership or a restrictive ACL for the service's mapped host UID; rootless Docker and user-namespace remapping may use a different host UID. Do not solve this by making the private key world-readable.

Then recreate the service with its new environment and mount:

docker compose up --build -d

Existing native data: Compose's named volume does not automatically use ./finance. If migrating an existing installation, stop the native service, back it up, and either copy the full data directory into the named volume or replace finance-data:/data with ./finance:/data. A bind-mounted data directory must be writable by the container's mapped service UID. Do not point two running instances at it.

Useful commands:

docker compose logs -f finance
docker compose stop
# Start again or rebuild after updating the source:
docker compose up --build -d

Do not use docker compose down -v unless you intend to delete the named data volume.

Native binary + systemd

Build the frontend and binary as in the quick start, then install the binary on a compatible Linux host. The React assets are embedded, and DuckDB is in-process: no separate frontend server or database service is needed.

Configure a systemd service with:

  • A dedicated unprivileged user and a writable persistent data directory, such as /var/lib/finance-duck.
  • An absolute binary path and -data /var/lib/finance-duck.
  • -listen 127.0.0.1:8080 behind your VPN-only reverse proxy.
  • -public-url https://finance.example.internal matching the browser origin.
  • Provider credentials configured through Settings, or optional banking environment variables and a private key readable by that service user.
  • Automatic startup and a restart policy, such as Restart=on-failure.

The binary uses CGO/native DuckDB dependencies; it is not a universally portable static Go executable. Build for your target OS/architecture and provide the required native runtime libraries. On NixOS, use the native flake package and service module above to retain its runtime closure—copying a Nix-linked binary alone to another machine, or garbage-collecting its unrooted runtime paths, is not a reliable deployment method.

NixOS declarative OCI container

If you prefer nixos-rebuild to manage the service, use the included Docker image with virtualisation.oci-containers and Docker or Podman. Mirror the Compose configuration: one instance, loopback port mapping, persistent /data, a read-only private-key mount, runtime environment files, and the explicit -public-url command argument.

Build the image from the repository:

docker build -t finance-duck:local .

Make it available to the daemon used by the NixOS service, or publish it to a private registry and pin a version/digest. A rootless Docker image is not automatically available to the system Docker daemon or Podman.

Keep private keys and API keys in protected runtime files or a secrets manager. Do not embed secret values in Nix expressions or copy them into the world-readable Nix store. The included NixOS module runs the native package; an OCI deployment requires your own container configuration.

VPN and reverse-proxy requirements

For any deployment:

  • Keep the application and proxy private to your VPN/firewall. A public DNS name or -public-url is not an access-control mechanism.
  • Point your chosen hostname at the VPN-accessible proxy and configure trusted HTTPS there. Finance Duck itself serves HTTP.
  • Forward to the application and preserve the public Host header. The application checks Host and mutation origins; forwarding only X-Forwarded-Host is insufficient.
  • For native runs, configure -public-url; for the supplied Compose configuration, set FINANCE_PUBLIC_URL.
  • Register the callback shown in Settings → Enable Banking credentials: the browser origin plus /api/banking/callback. For optional environment-managed setup, set ENABLEBANKING_REDIRECT_URL to that same URL.
  • Allow outbound HTTPS to Enable Banking, and optionally OpenRouter. No public inbound bank webhook is required.
  • Allow sufficiently long proxy requests for sequential bulk AI previews.

A direct bind to a VPN interface is also supported with -listen <VPN-IP>:8080 and a matching -public-url, provided the network is private and your registered callback uses that same browser address. HTTPS through your existing private reverse proxy is the preferred setup.

Optional OpenRouter setup

Open Settings → OpenRouter credentials, paste your API key, and click Save key. Then choose an exact OpenRouter provider/model identifier under Classification preferences and save those preferences. No SSH, Nix configuration changes, or service restart is needed.

Use the complete identifier, for example deepseek/deepseek-v4.1-flash, not just deepseek-v4.1-flash. Verify identifiers in OpenRouter's model catalog rather than relying on a model's display name.

Use Replace key to rotate the credential or Remove key to disable AI. Changes apply to future classifications immediately and survive restart; an already-running classification keeps the key it started with. “Configured” means a key is present, not that OpenRouter has accepted it. A successful AI classification → Analyse request checks the key, model, and private routing together.

The key is stored separately from preferences and journals in state/openrouter.json beneath your data directory (/var/lib/finance-duck/state/openrouter.json on the deployed server), as a private 0600 plaintext file. The API never returns it, and the UI clears the password field after saving rather than storing it in browser storage. Treat data-directory backups as containing credentials. Anyone able to access this no-login application can replace or remove the key: keep access restricted to trusted tailnet users.

For administrator-managed startup configuration, OPENROUTER_API_KEY in the server environment or Compose .env remains an optional fallback only while no saved credential file exists. A UI-saved key takes precedence; Remove key persists an explicit disable, so a restart cannot silently restore the environment key. Environment-only changes require a restart/recreate.

Bank synchronization and recognized N26, ING, and Kontist CSV imports do not require this key; only mapping an unrecognized CSV layout does. Without AI, explicit merchant-default rules still work; unresolved transactions remain unclassified and editable.

Every AI classification requests provider.data_collection = "deny", provider.zdr = true, and provider.require_parameters = true. Unsupported private routing fails rather than falling back to a less restrictive provider. Amount sharing is off by default. Keep OpenRouter account prompt logging disabled as well. Automatic redaction minimizes data; it is not a guarantee that arbitrary transaction prose is anonymous.

Classification spaces request starts by at least three seconds, including successful requests, rather than sending a burst between 429s. This is a conservative application policy, not a published quota for every model. On HTTP 429, backoff starts at 15 seconds and increases across consecutive failures; Retry-After seconds or HTTP dates can extend the wait. Successful retries retain the learned spacing (up to 30 seconds) instead of immediately bursting again. Each operation makes at most four attempts, with at most two minutes of automatic retry waiting, preserving the same model, sanitized prompt, and privacy controls. Imports and previews share this pacing and cooldown. Long or exhausted limits leave records unclassified with a retry-time error; local merchant rules still work. After the cooldown, run AI classification → Analyse again for previously failed records—repeating a bank import does not reclassify existing transactions.

Data, backups, and recovery

Back up the entire canonical finance directory, including registry files, journals, config.toml when present, and operational/recovery state, plus any separately stored environment-managed secrets. state/openrouter.json and state/enablebanking.json contain UI-managed credentials: protect backups accordingly, including the matching banking session state. Stop the service for a consistent filesystem backup. DuckDB under cache/ can be excluded and rebuilt.

Use Settings → Rebuild index while the app runs. For an offline rebuild, stop the service first and run:

./bin/finance-duck -data ./finance -rebuild

See OPERATIONS.txt for the journal grammar, CSV formats and identity limitations, transfer matching, recovery, privacy boundaries, and reclassification behavior.

Development checks

After building the frontend:

nix-shell --run 'go test ./... && go vet ./...'
nix-shell --run 'npm --prefix web run build'

Tests cover deterministic financial invariants and mocked provider HTTP behavior. Real bank consent and real OpenRouter access require your own application registration and credentials.

S
Description
No description provided
Readme
2.5 MiB
Languages
Go 69.5%
TypeScript 25.8%
CSS 4%
Nix 0.6%