Setting up GnuCash Sync
This walks you through connecting 's optional GnuCash Sync feature to your own GnuCash ledger. Once set up, expenses you log on your phone show up as real transactions in your GnuCash desktop book.
Who this is for
You're comfortable with a command line, and you either already have GnuCash using a
PostgreSQL-backed book (not the default local file), or you're willing to set
one up. If your GnuCash book is currently a local .gnucash file, you'll need to
migrate it to Postgres first — GnuCash's own documentation covers this (File → Save As,
choosing a PostgreSQL connection).
What you'll need
- A GnuCash book already stored in PostgreSQL (version 13 or newer).
- A server or always-on machine to run PostgREST on — the same machine as your Postgres database works fine, or a different one on the same network.
- Basic comfort with a terminal, SSH, and copy-pasting commands.
psql(comes with any PostgreSQL install) and Python 3 (for the JWT step — no extra packages needed).
Download the setup files
These four files are all you need besides PostgREST itself:
- self_hosting_setup.sql — creates the roles and function GnuCash Sync needs
- postgrest.conf.example — PostgREST configuration template
- postgrest.service.example — systemd service template
- make_jwt.py — dependency-free script to generate your access token
1Run the setup SQL script
Open self_hosting_setup.sql in a text editor and replace the three
CHANGE_ME_... passwords near the top with strong passwords of your own — write
them down somewhere safe, you'll need one of them again in Step 3.
Then run it against your GnuCash database, as a Postgres superuser:
psql -U postgres -d your_gnucash_database -f self_hosting_setup.sql
This creates three things:
- An
authenticatorrole — the only role PostgREST itself ever connects as. - A
gnucash_app_userrole — a deliberately narrow role your phone impersonates for each request. It can only read your chart of accounts (account names, not balances or transaction history) and call one specific function to write new expenses. It can never read a balance, a transaction, or anything else in your ledger. - A
gnucash_ownerrole, and async_transaction()function that does the actual work of writing a balanced double-entry transaction to your ledger — the same functiongnucash_app_useris allowed to call, but nothing more.
If you'd rather point GnuCash desktop itself at this database using a role with full
access (recommended, so GnuCash owns its own ledger the way it normally would), connect it
using the gnucash_owner role and password you set above — Data Format
postgres, your host/port, your database name, username gnucash_owner.
2Install PostgREST
PostgREST ships as a single static binary — no Docker or complex install needed.
- Download the release matching your server's architecture from the PostgREST GitHub releases page.
- Extract it and place the binary somewhere on your
$PATH, e.g./usr/local/bin/postgrest. - Confirm it runs:
postgrest --help
3Configure PostgREST
Copy postgrest.conf.example to somewhere like
/etc/postgrest/postgrest.conf, and fill in its placeholders:
db-uri— plug in theauthenticatorpassword you set in Step 1.jwt-secret— generate a fresh random secret:openssl rand -base64 32. Save this value; you'll pass it to the JWT script next.server-host/server-port— where PostgREST listens. See "Deciding how to expose this" below before picking this.
4Generate your access token (JWT)
python3 make_jwt.py "the-jwt-secret-from-step-3" gnucash_app_user
This prints a long string starting with eyJ... — that's your JWT. Copy it
somewhere safe. This token doesn't expire, so treat it like a password: it's the only thing
(besides your PostgREST URL) that lets a device write to your GnuCash ledger.
5Run PostgREST
For a quick test:
postgrest /etc/postgrest/postgrest.conf
For it to stay running (including after a server reboot), copy
postgrest.service.example to /etc/systemd/system/postgrest.service,
fill in the placeholders, then:
sudo systemctl daemon-reload
sudo systemctl enable --now postgrest
sudo systemctl status postgrest
Test it's working:
curl http://YOUR_SERVER:3000/accounts -H "Authorization: Bearer YOUR_JWT_FROM_STEP_4"
You should get back a JSON list of your GnuCash accounts. If you get an error, see Troubleshooting below.
6Deciding how to expose this
Your phone needs to be able to reach PostgREST's address. How you make that possible is entirely your call:
- VPN-only (recommended for most people). Set up a personal VPN (WireGuard is a popular, simple choice) between your phone and your server, and bind PostgREST to the VPN's private IP address only. Nothing about PostgREST is reachable from the public internet this way.
- Public with a reverse proxy + HTTPS. If you want to reach it without a VPN, put a reverse proxy (e.g. Caddy, nginx) in front of PostgREST with a real TLS certificate, and only expose the proxy publicly, not PostgREST directly. PostgREST itself only speaks plain HTTP — your JWT is a bearer token, so it should never travel over an unencrypted public connection.
Whichever you choose, note the final URL (e.g. http://10.x.x.x:3000 for a VPN
setup, or https://yourdomain.com for a reverse-proxied one) — you'll need it next.
7Connect the app
In : Settings → 🔗 GnuCash Sync → Configure GnuCash connection.
- Turn the sync toggle on.
- Enter your PostgREST URL from Step 6.
- Enter the JWT from Step 4.
- Tap Test Connection — you should see your account count confirmed.
- Tap Save Connection.
8Map your wallets and categories
The first time you save an expense with sync enabled, the app will prompt you to map the wallet and category involved to a matching GnuCash account (search by name — GnuCash's full account tree is available, with breadcrumb paths to disambiguate similarly-named accounts in different places). This mapping is saved per wallet/category, so you only do it once per pairing, not per transaction. You can also do this ahead of time from the GnuCash Sync settings screen's account list.
If you run a multi-currency GnuCash book, mapping a wallet to an account warns you if the wallet's currency doesn't match that account's actual currency — sync writes amounts as-is with no conversion, so a mismatched mapping (e.g. an IDR wallet mapped to a USD account) would otherwise silently record the wrong number. You can dismiss the warning and map anyway if that's genuinely what you want.
From here on, expenses you log will appear in your GnuCash ledger as normal, balanced transactions — indistinguishable from ones entered directly in GnuCash desktop.
One-way sync: what happens if you edit in GnuCash afterward
Sync only ever flows → GnuCash, never the other direction. That's fine — editing a transaction in GnuCash desktop after it's been synced (correcting the account, the amount, the date, whatever) won't break anything. But the app won't notice on its own; there's no live connection watching your ledger for changes.
What it does do: any time you open an already-synced, single-item transaction for editing in the app, it checks that transaction against GnuCash's current copy. If they've diverged, you'll see a warning banner with a "Pull values from GnuCash" option, which copies GnuCash's current amount/category/wallet/date/note into the local record. This only updates the app's own copy — it never writes anything back to GnuCash.
Multi-item receipts (individually editable items aren't supported yet) get a lighter version of this: opening one flags whether GnuCash's total for that receipt no longer matches what's saved locally, but without saying which item changed or offering anything to pull in — check GnuCash desktop directly if you need the specifics.
Troubleshooting
"Anonymous access is disabled" / 401 errors — you forgot the
Authorization: Bearer ... header, or your JWT doesn't match the
jwt-secret in postgrest.conf. Double check both.
PostgREST doesn't see a function/table you just created or changed — PostgREST
caches its schema at startup. Run NOTIFY pgrst, 'reload schema'; via
psql to refresh it live, or restart the postgrest service.
GnuCash desktop won't open the database ("could not obtain the lock") — check
SELECT * FROM gnclock; in psql. A stale row from an earlier failed
connection attempt can block new ones; if you're sure nothing else has the file open,
DELETE FROM gnclock; clears it.
App can't connect over plain http:// — this is expected and
already handled on Android for a VPN-only http:// PostgREST setup. If you're using
a reverse proxy with HTTPS instead, use the https:// URL and this doesn't apply.
"permission denied for table ..." when calling sync_transaction —
this actually means it's working as designed. gnucash_app_user has no direct table
access; all its capability comes through the one function. If a different error like
this shows up from GnuCash desktop itself, double check you connected desktop using
gnucash_owner, not gnucash_app_user.
FAQ
Yes — generate one JWT per device if you'd like to be able to revoke
access to just one later (there's no built-in revocation besides rotating the
jwt-secret, which invalidates every JWT at once), or share the same one, since
this is designed for single-user/self-hosted use.
Turn the sync toggle off in Settings — the app falls back to local-only saving immediately, nothing about your GnuCash setup needs to change.
No — GnuCash Sync is expense-only by design. Income and budgets stay in the app only.
No — see "One-way sync" above. The app only checks for drift when you open that specific transaction for editing, and only offers to pull GnuCash's values in, never push the app's out.