Skip to content
marcodonghiaaPublic

About

Self-hosted PSD2 bank sync, FX rates, and AI transaction categorization — the backend pipeline behind My Wealth View.

Topics

Resources

Contributing

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

249 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

💸 Alopex

A self-hosted net worth tracker for European bank accounts, brokerage and crypto. Your data lives in your own Supabase project. No SaaS, no subscription.

Tests License: MIT Node 22 Supabase PSD2

Dashboard

This repo is the backend: sync scripts, Supabase Edge Functions and migrations. The dashboard is alopex-web.

✨ Features

🏦 Bank sync PSD2 open banking via Enable Banking: Revolut, Wise, Fineco and 2000+ other European banks. Multi-user.
🏷️ Categorization Your own category_rules first, Gemini for the rest. Hand-set categories are never overwritten. The Gemini key is recommended but optional: without it, or when Gemini fails, your rules still run.
🔁 Subscription detection Every transaction is marked Subscription or One-time.
🔀 Flow detection spend, income or transfer, so exchanges and top-ups don't count as income.
📈 Portfolio and crypto Prices from Yahoo Finance, Coinbase (read-only key) and CoinGecko.
💱 FX Daily EUR rates from Frankfurter/ECB, history backfilled on first run.
🔔 "Worth it?" prompts Web Push after discretionary purchases.

Trade-offs: European (PSD2) banks only, and you renew bank access every 180 days. The sync runs inside your own Supabase project (pg_cron + Edge Functions): no computer has to stay on.

🧭 How it works

flowchart LR
    Bank[(Banks<br/>Enable Banking)] --> SYNC[sync-bank]
    CRON[pg_cron<br/>every 6 hours] --> SYNC
    SYNC --> DB[(Supabase<br/>Postgres + RLS)]
    DB --> POST[post-sync<br/>rules, Gemini, flows, refunds]
    POST --> DB
    PRICES[sync-prices<br/>FX, Yahoo, Coinbase, CoinGecko] --> DB
    DB --> REC[detect-recurring]
    DB --> UI[alopex-web<br/>dashboard]
    DB --> PUSH[notify<br/>Web Push]
Loading

npx create-alopex schedules everything with pg_cron inside your Supabase project (20261118000000_schedule_sync.sql). Every 6 hours (banks typically cap unattended access at about 4 calls a day per account), for every linked user:

  1. sync-bank pulls balances and new transactions, with a 7-day overlap because banks post late, and marks payments between your own accounts.
  2. post-sync fills merchant keys, applies your rules and past choices, then Gemini, for categories; marks Subscription or One-time; categorizes Splitwise expenses; marks spend, income or transfer (your flow rules, your own accounts, the bank's code); suggests transfer pairs; then pairs each card refund with its original charge (same account, merchant, amount and currency, within 90 days) and marks both refunded so neither counts. A flow you set by hand, or a transfer from a rule, is never changed. Refund pairing also covers rows a rule marked "not a transfer" and rows synced before flows were tracked (legacy).
  3. sync-prices stores daily EUR rates and prices your stocks, ETFs and crypto.
  4. detect-recurring updates the subscription radar.

A newly linked bank doesn't wait for the next run: its first sync starts right after linking.

notify sends "worth it?" prompts hourly and the bank-access reminder daily. Everything covers every user; only the Coinbase sync is single-user (your own API key).

The Node scripts in this repo (sync-transactions.js, classify-flow.js, ...) are the older Docker, cron or launchd path. They do less than the Edge Functions: no flow rules, own-account or transfer pairing, merchant keys or subscription radar. New installs should use npx create-alopex.

Bank linking and the dashboard's other server-side actions (account deletion, holdings, statements, rules) run as Supabase Edge Functions, see supabase/functions/README.md.

Built to keep running: every outbound call has a 60 s timeout, each scheduled script a 10 minute limit, sync cursors are written atomically (and kept on a volume in Docker), and one user's failing rows can't block anyone else's classification.

🚀 Getting started

Want to look first? Try the live demo: fake data, no sign-up.

Your own copy takes about 30 minutes, all in the browser: no computer has to stay on and nothing to install. Most of the time goes into Enable Banking's signup.

1. Create the accounts (about 15 minutes)

Account What it's for Cost
GitHub runs the setup (Codespaces) and the sync (Actions) free
Supabase your database and login free tier is enough
Enable Banking the PSD2 connection to your banks free for your own accounts
Vercel (sign in with GitHub) puts the dashboard online over HTTPS free Hobby plan
Gemini API key AI categories and subscription detection free tier; optional, recommended. Each new transaction (name, description, amount), each Splitwise expense description and any statement PDF you import go to Google; on the free tier Google may use them to improve its models, a paid-tier key opts out

In Enable Banking, sign up only; setup tells you when to create the app and gives the exact redirect URL.

2. Run the setup (about 10 minutes)

One command (Mac, Linux or Windows, needs Node 20+):

npx create-alopex

It downloads both apps into ./alopex, signs you in to Supabase and Vercel in your browser, creates the project, tables, Edge Functions and your login, turns on the scheduled sync inside Supabase (pg_cron: no computer, fork or GitHub Actions needed), walks you through Enable Banking and puts the dashboard online. Run it again in the same folder to update. See installer/README.md.

Or the guided script in Codespaces:

Open in GitHub Codespaces

This opens a terminal in your browser.

Free: 60 hours a month on a personal account, no card needed. First open takes 2-3 minutes. Codespaces has Node 22, the Supabase CLI and gh ready.

Run:

./setup.sh

Choose 1, GitHub Actions. Setup forks this repo to your account, creates or links your Supabase project, applies the schema, deploys the functions, creates your login (the app has no sign-up screen), switches off public sign-ups, stores your keys as secrets of your fork and turns the sync on. When it asks for the .pem, drag it from your Downloads onto the file list on the left.

At the end it prints three values for the next step (also saved in dashboard-values.txt) and waits for you to open the dashboard and sign in once. Keep the Codespace open.

3. Put the dashboard online (about 5 minutes)

Deploy with Vercel

Click GitHub first, then Install Vercel (All repositories is fine). Paste the three values setup printed and deploy, then open the address under Domains on the project page (not a deployment URL) and sign in. Your dashboard tells your database its own address on that first sign-in, so there is nothing to copy back: setup sees it, prints Done! Open <your address>, sign in as <email> and bank linking sends you back there. Closed the terminal first? Nothing is lost, the sign-in alone finishes it.

Then delete the Codespace, since it holds your keys: github.com/codespaces, the ... menu, Delete. GitHub warns about unsaved changes: that's expected, delete anyway. Everything you set up stays in Supabase, GitHub and Vercel.

4. Link a bank and add your investments

Sign in on your dashboard. On Accounts, use Connect a bank: balances show at once, and the first sync of transactions starts right away (a minute or two). On Portfolio, Add an investment finds yours by name, ticker or ISIN; on Crypto, Add a coin. A checklist on the dashboard shows what's left.

Bank access lasts 180 days. Two weeks before it ends the Accounts page shows a Renew access banner, and you get a push notification the day the banner appears, then 7 days, 1 day and on the day it expires.

Run ./setup.sh again any time: it keeps what's in .env, asks only for what's missing, and applies new migrations and functions. Forgot your password? ./setup.sh --reset-password.

On your own computer or server instead (Docker)

Needs Docker, the Supabase CLI and Node 22. The sync runs only while that machine is on.

git clone https://github.com/marcodonghiaa/alopex.git
git clone https://github.com/marcodonghiaa/alopex-web.git
cd alopex
./setup.sh          # choose 2, Docker
docker compose up -d --build

Open http://localhost:3000 (only this computer; set WEB_BIND=0.0.0.0 in .env to reach it from your phone on the same network). With Docker, a newly linked bank's last 90 days of transactions arrive within about 5 minutes. Check your configuration any time with node env-check.js.

Manual setup
  1. Create a Supabase project, then supabase link --project-ref <ref> and supabase db push --linked.
  2. In Enable Banking, create a Production application with redirect URL https://<ref>.supabase.co/functions/v1/bank-consent-callback (must match exactly). Activate it by linking one of your own accounts in their panel.
  3. Deploy the functions and set ENABLE_BANKING_APP_ID and ENABLE_BANKING_PRIVATE_KEY as Edge Function secrets. (FRONTEND_URL is optional: the dashboard registers its own address when you sign in.)
  4. In the dashboard, Authentication -> Users -> Add user (tick auto-confirm) to create your login, and put its id in .env as USER_ID.
  5. Authentication -> Sign In / Providers -> User Signups: turn off Allow new users to sign up. Supabase leaves it on, which lets anyone who finds your project URL create an account.
  6. npm install, then cp .env.example .env and fill it in.

🛠️ Everyday commands

Task Command
Run a full sync by hand bash run-daily-sync.sh
Send "worth it?" prompts now bash run-notify.sh
Re-apply changed category rules node categorize.js --reapply-rules
Check your .env node env-check.js
Run the tests npm test

--reapply-rules recategorizes rows the AI or an earlier rule set. Categories you set by hand are never touched (tracked in transactions.category_source; categories that predate the column count as hand-set).

⏰ Scheduling

With npx create-alopex there is nothing to schedule: pg_cron in your Supabase project runs the sync. The options below are for the older setups.

On GitHub, no computer needed: in your fork, add the secrets APP_ID, PRIVATE_KEY (the whole .pem), SUPABASE_URL, SUPABASE_SECRET_KEY, USER_ID and, if you use them, GEMINI_API_KEY, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT, COINBASE_API_KEY_NAME, COINBASE_API_PRIVATE_KEY (Settings -> Secrets and variables -> Actions). Then add the variable SYNC_ON_GITHUB = true and enable Actions on the fork. .github/workflows/sync.yml then syncs every 6 hours, sends "worth it?" prompts hourly and the bank-access reminder daily; run any of them by hand from the Actions tab. Your fork is public, so its list of runs is public too: the sync logs only counts, never names or amounts, and setup keeps logs for 1 day. GitHub can start scheduled runs 10-30 minutes late.

Docker runs everything on its own schedule (scheduler.js). Without Docker, use cron or launchd:

0 */6 * * *    cd /path/to/alopex && bash run-daily-sync.sh
*/30 * * * *   cd /path/to/alopex && bash run-notify.sh
0 9 * * *      cd /path/to/alopex && node consent-notice.js

🗄️ Data model

Every table is Row Level Security scoped to auth.uid() = user_id.

Area Tables
Banking accounts, transactions (primary key (account_uid, entry_reference)), net_worth_snapshots, fx_rates
Rules category_rules, subscription_billing_overrides
Investments portfolio_holdings / portfolio_snapshots, crypto_holdings / crypto_snapshots

The dashboard reads security_invoker views such as v_net_worth_daily, v_transactions_eur and v_subscriptions. Schema changes live in supabase/migrations.

🔒 Security

  • .env, *.pem and last-sync.json* are gitignored. The Enable Banking key authenticates your app, so treat it like a password.
  • The dashboard only uses the publishable key. The service-role key stays in the sync scripts and Edge Functions.
  • A Postgres trigger blocks users from editing bank-sourced columns on transactions. Only category, type, flow, split and worth-it fields are writable.
  • Two-step sign-in (authenticator app, set up in Settings) is enforced by the database: once a user has an authenticator, a session without the code gets nothing from the API. See UPGRADING.md for recovery.
  • The bank-linking state is a single-use token, not a user id, and error text is never reflected into the redirect.
  • Bank text is sent to the LLM as untrusted data, in per-user batches.
  • The Coinbase integration uses a read-only key.

🤝 Contributing

Tests run on every push and pull request. See CONTRIBUTING.md.

📄 License

MIT

About

Self-hosted PSD2 bank sync, FX rates, and AI transaction categorization — the backend pipeline behind My Wealth View.

Topics

Resources

Contributing

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages