Compare commits

..

No commits in common. "e739f97056bf000c8543f8e495860c0621393d99" and "2487d8a2f95bdc0dd59c5f717a4e901dfdd8d456" have entirely different histories.

2 changed files with 60 additions and 266 deletions

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

301
README.md
View File

@ -1,37 +1,13 @@
# actual-gocardless-proxy
> **AI-generated code — see disclaimer below.**
A drop-in [GoCardless Bank Account Data API](https://developer.gocardless.com/bank-account-data/quick-start-guide/) proxy server that lets [Actual Budget](https://actualbudget.org/) sync with French (and other European) bank accounts via [Enable Banking](https://enablebanking.com/) — a **free** PSD2 aggregator.
A drop-in [GoCardless Bank Account Data API](https://developer.gocardless.com/bank-account-data/quick-start-guide/) proxy that lets [Actual Budget](https://actualbudget.org/) sync with French (and other European) bank accounts via [Enable Banking](https://enablebanking.com/) — a **free** PSD2 aggregator.
**Zero modifications to actual-server.** The only change required is one environment variable:
**Zero modifications to actual-server.** The only change is setting one environment variable:
```
GOCARDLESS_BASE_URL=http://gocardless-proxy:3456
```
---
## Table of contents
1. [How it works](#how-it-works)
2. [Setup](#setup)
- [1. Sign up for Enable Banking](#1-sign-up-for-enable-banking)
- [2. Generate an RSA key pair](#2-generate-an-rsa-key-pair)
- [3. Run the setup script](#3-run-the-setup-script)
- [4. Add the proxy to docker-compose](#4-add-the-proxy-to-docker-compose)
- [5. Start the services](#5-start-the-services)
- [6. Configure Actual Budget](#6-configure-actual-budget)
3. [Ansible deployment](#ansible-deployment)
4. [OAuth redirect flow](#oauth-redirect-flow)
5. [Supported banks](#supported-banks)
6. [Troubleshooting](#troubleshooting)
7. [Data & privacy](#data--privacy)
8. [Disclaimer](#disclaimer)
9. [License](#license)
---
## How it works
```
@ -39,15 +15,15 @@ Actual Budget (browser)
│ GoCardless API calls
actual-server
│ GOCARDLESS_BASE_URL=http://gocardless-proxy:3456
│ GOCARDLESS_BASE_URLhttp://gocardless-proxy:3456
gocardless-proxy ◄── this repo
│ Enable Banking PSD2 API (RS256 JWT auth)
│ Enable Banking PSD2 API
Enable Banking → French banks (BNP, SG, CA, LCL, BP, …)
```
The proxy translates GoCardless API shapes ↔ Enable Banking shapes on the fly. All state (tokens, requisitions, account mappings) is persisted in `./data/store.json` so Docker restarts do not break existing Actual Budget account links.
The proxy translates GoCardless API shapes ↔ Enable Banking shapes on the fly and persists all state (tokens, requisitions, account mappings) in `./data/store.json` so Docker restarts don't break existing account links.
---
@ -55,49 +31,53 @@ The proxy translates GoCardless API shapes ↔ Enable Banking shapes on the fly.
### 1. Sign up for Enable Banking
Go to [enablebanking.com](https://enablebanking.com) and create a **free personal account**. Under your application settings, note your **Application ID**.
Go to [enablebanking.com](https://enablebanking.com) and create a **free personal account**. Under your app settings, note your **Application ID**.
### 2. Generate an RSA key pair
Enable Banking authenticates API calls with an RS256 JWT signed by your application's private key. You upload the public key to Enable Banking; the private key stays on your server.
Enable Banking uses RS256 JWT auth. You need to provide your **public key** in the Enable Banking dashboard and keep the **private key** on your server.
```bash
# Generate a PKCS8 RSA private key
# Generate a 2048-bit RSA private key in PKCS8 format
openssl genpkey -algorithm RSA -pkcs8 -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# Extract the public key — upload this to the Enable Banking dashboard
# Extract the public key (upload this to Enable Banking)
openssl pkey -in private_key.pem -pubout -out public_key.pem
```
Store `private_key.pem` securely. You will reference it during setup.
Upload `public_key.pem` in the Enable Banking dashboard under your application's API settings.
Keep `private_key.pem` somewhere safe — you'll need it in the next step.
### 3. Run the setup script
Clone this repo and install dependencies:
```bash
git clone https://github.com/yourname/actual-gocardless-proxy
cd actual-gocardless-proxy
npm install
```
Run the interactive setup:
```bash
npm run setup
```
The script prompts for:
The script will ask for:
- Your Enable Banking **Application ID**
- Path to your **private key PEM** file
- The **proxy base URL** (the URL that Enable Banking will redirect to after OAuth — must be reachable from your browser; e.g. `http://localhost:3456` for local dev or `https://yourserver.example.com` for production)
| Prompt | Example |
|--------|---------|
| Enable Banking Application ID | `abc123-...` |
| Path to private key PEM | `/home/user/private_key.pem` |
| Proxy base URL | `https://yourserver.example.com` or `http://localhost:3456` |
The proxy base URL is the address Enable Banking redirects to after OAuth — it **must be reachable from the user's browser**, not just from within Docker.
On completion the script prints:
It will generate a random `secret_id` and `secret_key` pair and print them:
```
──────────────────────────────────────────────────────────
Enter these values in Actual Budget → Settings → GoCardless:
Secret ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Secret Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Secret Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
And set this environment variable on actual-server:
@ -105,26 +85,26 @@ And set this environment variable on actual-server:
──────────────────────────────────────────────────────────
```
All credentials are written to `./data/store.json`.
All credentials are stored in `./data/store.json`.
### 4. Add the proxy to docker-compose
### 4. Add the proxy to your docker-compose.yml
Paste the `gocardless-proxy` service block into your existing `actual-server` `docker-compose.yml` and add `GOCARDLESS_BASE_URL` to actual-server:
In your existing `actual-server` `docker-compose.yml`, add the proxy service and wire it to actual-server:
```yaml
services:
actual-server:
image: actualbudget/actual-server:latest
# ... your existing config ...
environment:
GOCARDLESS_BASE_URL: http://gocardless-proxy:3456
# ... rest of your actual-server env vars
# ... your other env vars ...
depends_on:
- gocardless-proxy
gocardless-proxy:
build: /path/to/actual-gocardless-proxy
# or use a pre-built image:
# image: ghcr.io/yourname/actual-gocardless-proxy:latest
# or: image: ghcr.io/yourname/actual-gocardless-proxy:latest
container_name: gocardless-proxy
restart: unless-stopped
volumes:
@ -133,7 +113,7 @@ services:
PORT: 3456
```
> **Changing the proxy base URL later:** Re-run `npm run setup`, or edit `config.proxy_base_url` directly in `data/store.json` and restart the container.
> **Note on PROXY_BASE_URL:** The proxy base URL is saved in `data/store.json` during setup. If you need to change it later (e.g. you added a reverse proxy), re-run `npm run setup` or edit `data/store.json` directly and update `config.proxy_base_url`.
### 5. Start the services
@ -141,176 +121,45 @@ services:
docker compose up -d
```
Verify the proxy is up:
Verify the proxy is running:
```bash
curl http://localhost:3456/health
# {"status":"ok","configured":true}
# {"status":"ok","configured":true}
```
### 6. Configure Actual Budget
Go to **Settings → GoCardless** in Actual Budget and enter the `Secret ID` and `Secret Key` printed in step 3. Then link your bank via **Accounts → Link account**.
In Actual Budget, go to **Settings → GoCardless** and enter the `secret_id` and `secret_key` printed in step 3.
---
## Ansible deployment
The playbook below deploys the proxy to a remote host that already runs `actual-server` via Docker Compose. Adjust variables at the top to match your environment.
```yaml
# deploy.yml
---
- name: Deploy actual-gocardless-proxy
hosts: actual_host
become: true
vars:
proxy_dir: /opt/actual-gocardless-proxy
data_dir: /opt/actual-gocardless-proxy/data
proxy_port: 3456
# Set proxy_base_url to the URL reachable from the user's browser
proxy_base_url: "https://actual.example.com"
eb_app_id: "{{ vault_eb_app_id }}"
eb_private_key_src: "{{ playbook_dir }}/files/private_key.pem"
gc_secret_id: "{{ vault_gc_secret_id }}"
gc_secret_key: "{{ vault_gc_secret_key }}"
tasks:
- name: Install git and Docker dependencies
ansible.builtin.package:
name:
- git
- python3-docker
state: present
- name: Clone / update repository
ansible.builtin.git:
repo: https://github.com/yourname/actual-gocardless-proxy.git
dest: "{{ proxy_dir }}"
version: main
force: true
- name: Create data directory
ansible.builtin.file:
path: "{{ data_dir }}"
state: directory
mode: "0700"
- name: Copy RSA private key
ansible.builtin.copy:
src: "{{ eb_private_key_src }}"
dest: "{{ data_dir }}/private_key.pem"
mode: "0600"
- name: Write store.json (credentials + proxy config)
ansible.builtin.copy:
dest: "{{ data_dir }}/store.json"
mode: "0600"
content: |
{
"config": {
"eb_app_id": "{{ eb_app_id }}",
"eb_private_key": {{ lookup('file', eb_private_key_src) | to_json }},
"gc_secret_id": "{{ gc_secret_id }}",
"gc_secret_key": "{{ gc_secret_key }}",
"proxy_base_url": "{{ proxy_base_url }}"
},
"gc_tokens": {},
"institutions_cache": null,
"institutions_cache_at": 0,
"agreements": {},
"requisitions": {},
"accounts": {}
}
# Skip this task if store.json already exists (preserves live state:
# tokens, requisitions, linked accounts). Remove the condition if you
# want to reset credentials on every deploy.
args:
creates: "{{ data_dir }}/store.json"
- name: Build proxy Docker image
community.docker.docker_image:
name: actual-gocardless-proxy
tag: local
build:
path: "{{ proxy_dir }}"
source: build
force_source: true
- name: Run gocardless-proxy container
community.docker.docker_container:
name: gocardless-proxy
image: actual-gocardless-proxy:local
restart_policy: unless-stopped
ports:
- "127.0.0.1:{{ proxy_port }}:{{ proxy_port }}"
volumes:
- "{{ data_dir }}:/app/data"
env:
PORT: "{{ proxy_port | string }}"
STORE_PATH: /app/data/store.json
- name: Ensure actual-server has GOCARDLESS_BASE_URL set
ansible.builtin.debug:
msg: >
Remember to add
GOCARDLESS_BASE_URL=http://gocardless-proxy:{{ proxy_port }}
to your actual-server environment and restart it.
```
**Sensitive variables** (`eb_app_id`, `gc_secret_id`, `gc_secret_key`) should be stored in an Ansible Vault file:
```bash
# Create vault file
ansible-vault create group_vars/actual_host/vault.yml
```
```yaml
# group_vars/actual_host/vault.yml
vault_eb_app_id: "your-enable-banking-app-id"
vault_gc_secret_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
vault_gc_secret_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
Run the playbook:
```bash
ansible-playbook deploy.yml --ask-vault-pass
```
> **First deploy only:** `store.json` is written with `creates:` so it is not overwritten on subsequent runs — this preserves live requisitions and linked accounts. On a brand-new host the credentials are bootstrapped automatically; no need to run `npm run setup` manually.
You can now go to **Accounts → Link account** and search for your French bank.
---
## OAuth redirect flow
```
1. actual-server POST /api/v2/requisitions/
body: { redirect: "http://actual:5006/...", institution_id }
1. actual-server POST /api/v2/requisitions/ { redirect: "http://actual:5006/..." }
2. proxy POST https://api.enablebanking.com/auth
{ aspsp: { name, country },
redirect_url: "http://proxy/callback",
{ aspsp: { name, country }, redirect_url: "http://proxy/callback",
state: <requisition_id> }
← { url: "https://tilisy.enablebanking.com/..." }
3. proxy returns requisition { id, status: "CR", link: <EB auth URL> }
3. proxy returns requisition with link = EB auth URL
4. User visits the link, authenticates at their bank
5. Enable Banking → http://proxy/callback?code=xxx&state=<requisition_id>
5. Enable Banking redirects → http://proxy/callback?code=xxx&state=<req_id>
6. proxy POST https://api.enablebanking.com/sessions { code }
← { session_id, accounts: [uid, ...] }
→ fetches account details per UID
→ stores gc_id ↔ eb_uid mapping in store.json
→ sets requisition status "LN" (linked)
→ 302 → http://actual:5006/...?requisition_id=<req_id>
→ fetches account details, stores GC account IDs
→ marks requisition status "LN" (linked)
→ redirects → http://actual:5006/...?requisition_id=<req_id>
7. actual-server GET /api/v2/requisitions/<req_id>/
← { status: "LN", accounts: ["gc-uuid-1", ...] }
← { status: "LN", accounts: ["gc-acc-id-1", ...] }
8. actual-server GET /api/v2/accounts/<id>/details/
GET /api/v2/accounts/<id>/balances/
@ -338,27 +187,27 @@ All French banks supported by Enable Banking, including:
| Fortuneo | FTNOFRP1 |
| Hello bank! | BNPAFRPP |
See [enablebanking.com/docs/markets/fr/](https://enablebanking.com/docs/markets/fr/) for the complete list.
See [enablebanking.com/docs/markets/fr/](https://enablebanking.com/docs/markets/fr/) for the full list.
---
## Troubleshooting
**"Proxy not configured" error**
### "Proxy not configured" error
Run `npm run setup` and restart the container.
**"Unknown institution_id" when linking an account**
actual-server caches the institutions list. Go to **Settings → GoCardless → Reset** and re-fetch.
### "Unknown institution_id" when creating a requisition
actual-server caches institutions. Go to **Settings → GoCardless → Reset** and re-fetch banks.
**OAuth callback fails with "Enable Banking error"**
- `proxy_base_url` in `data/store.json` must be reachable from your browser, not just from inside Docker.
- Make sure the callback URL (`{proxy_base_url}/callback`) is whitelisted in your Enable Banking application settings.
### OAuth callback fails with "Enable Banking error"
- Make sure `proxy_base_url` in `data/store.json` is reachable from your browser (not just from Docker).
- Check that the redirect URL is whitelisted in your Enable Banking application settings.
**Transactions not appearing**
Enable Banking may return an empty array if no transactions fall in the requested date range. Try widening the range in Actual Budget.
### Transactions not appearing
Enable Banking may return an empty `transactions` array for accounts with no activity in the requested date range. Try widening the date range.
**Private key parse error**
The key must be in **PKCS8** format. To convert from a traditional RSA key:
### Private key errors
Make sure the key is **PKCS8** format. To convert from traditional RSA format:
```bash
openssl pkcs8 -topk8 -nocrypt -in old_key.pem -out private_key.pem
```
@ -367,46 +216,12 @@ openssl pkcs8 -topk8 -nocrypt -in old_key.pem -out private_key.pem
## Data & privacy
All credentials and session data are stored locally in `./data/store.json`. No data is sent to any third party other than Enable Banking (the licensed PSD2 provider) and your bank.
All bank credentials and tokens stay on your server inside `./data/store.json`. Nothing is sent to any third party except Enable Banking (which is the PSD2 provider) and your bank.
The `data/` directory is git-ignored and must not be committed.
---
## Disclaimer
**This project was entirely generated by an AI assistant (Claude, by Anthropic) without manual code authorship.**
It has not been independently audited, penetration-tested, or validated against production bank APIs. Use it at your own risk. In particular:
- The Enable Banking integration is based on publicly available documentation and may require adjustments as the API evolves.
- No warranty is given regarding correctness, security, or fitness for any purpose.
- Your bank credentials and PSD2 access tokens are handled by this software — review the code before deploying in a sensitive environment.
Contributions, corrections, and reviews from human developers are very welcome.
The `data/` directory is git-ignored and should not be committed.
---
## License
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT