docs: rewrite README — add ToC, Ansible playbook, disclaimer, full MIT licence

- Table of contents with anchor links
- Ansible deploy.yml: clones repo, copies private key, bootstraps store.json
  via Ansible Vault variables, builds image, runs container; uses creates:
  guard to preserve live requisitions on re-deploy
- AI-generated disclaimer (prominent, at the bottom)
- Full MIT licence text (not just the word "MIT")
- Improved prose and formatting throughout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jeanGaston 2026-04-13 11:00:53 +02:00
parent 2487d8a2f9
commit e3891f4dc1

301
README.md
View File

@ -1,13 +1,37 @@
# actual-gocardless-proxy # actual-gocardless-proxy
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. > **AI-generated code — see disclaimer below.**
**Zero modifications to actual-server.** The only change is setting one environment variable: 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:
``` ```
GOCARDLESS_BASE_URL=http://gocardless-proxy:3456 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 ## How it works
``` ```
@ -15,15 +39,15 @@ Actual Budget (browser)
│ GoCardless API calls │ GoCardless API calls
actual-server actual-server
│ GOCARDLESS_BASE_URLhttp://gocardless-proxy:3456 │ GOCARDLESS_BASE_URL=http://gocardless-proxy:3456
gocardless-proxy ◄── this repo gocardless-proxy ◄── this repo
│ Enable Banking PSD2 API │ Enable Banking PSD2 API (RS256 JWT auth)
Enable Banking → French banks (BNP, SG, CA, LCL, BP, …) Enable Banking → French banks (BNP, SG, CA, LCL, BP, …)
``` ```
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. 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.
--- ---
@ -31,53 +55,49 @@ The proxy translates GoCardless API shapes ↔ Enable Banking shapes on the fly
### 1. Sign up for Enable Banking ### 1. Sign up for Enable Banking
Go to [enablebanking.com](https://enablebanking.com) and create a **free personal account**. Under your app settings, note your **Application ID**. Go to [enablebanking.com](https://enablebanking.com) and create a **free personal account**. Under your application settings, note your **Application ID**.
### 2. Generate an RSA key pair ### 2. Generate an RSA key pair
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. 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.
```bash ```bash
# Generate a 2048-bit RSA private key in PKCS8 format # Generate a PKCS8 RSA private key
openssl genpkey -algorithm RSA -pkcs8 -out private_key.pem -pkeyopt rsa_keygen_bits:2048 openssl genpkey -algorithm RSA -pkcs8 -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# Extract the public key (upload this to Enable Banking) # Extract the public key — upload this to the Enable Banking dashboard
openssl pkey -in private_key.pem -pubout -out public_key.pem openssl pkey -in private_key.pem -pubout -out public_key.pem
``` ```
Upload `public_key.pem` in the Enable Banking dashboard under your application's API settings. Store `private_key.pem` securely. You will reference it during setup.
Keep `private_key.pem` somewhere safe — you'll need it in the next step.
### 3. Run the setup script ### 3. Run the setup script
Clone this repo and install dependencies:
```bash ```bash
git clone https://github.com/yourname/actual-gocardless-proxy git clone https://github.com/yourname/actual-gocardless-proxy
cd actual-gocardless-proxy cd actual-gocardless-proxy
npm install npm install
```
Run the interactive setup:
```bash
npm run setup npm run setup
``` ```
The script will ask for: The script prompts 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)
It will generate a random `secret_id` and `secret_key` pair and print them: | 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:
``` ```
────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────
Enter these values in Actual Budget → Settings → GoCardless: Enter these values in Actual Budget → Settings → GoCardless:
Secret ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Secret ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Secret Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Secret Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
And set this environment variable on actual-server: And set this environment variable on actual-server:
@ -85,26 +105,26 @@ And set this environment variable on actual-server:
────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────
``` ```
All credentials are stored in `./data/store.json`. All credentials are written to `./data/store.json`.
### 4. Add the proxy to your docker-compose.yml ### 4. Add the proxy to docker-compose
In your existing `actual-server` `docker-compose.yml`, add the proxy service and wire it to actual-server: Paste the `gocardless-proxy` service block into your existing `actual-server` `docker-compose.yml` and add `GOCARDLESS_BASE_URL` to actual-server:
```yaml ```yaml
services: services:
actual-server: actual-server:
image: actualbudget/actual-server:latest image: actualbudget/actual-server:latest
# ... your existing config ...
environment: environment:
GOCARDLESS_BASE_URL: http://gocardless-proxy:3456 GOCARDLESS_BASE_URL: http://gocardless-proxy:3456
# ... your other env vars ... # ... rest of your actual-server env vars
depends_on: depends_on:
- gocardless-proxy - gocardless-proxy
gocardless-proxy: gocardless-proxy:
build: /path/to/actual-gocardless-proxy build: /path/to/actual-gocardless-proxy
# or: image: ghcr.io/yourname/actual-gocardless-proxy:latest # or use a pre-built image:
# image: ghcr.io/yourname/actual-gocardless-proxy:latest
container_name: gocardless-proxy container_name: gocardless-proxy
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@ -113,7 +133,7 @@ services:
PORT: 3456 PORT: 3456
``` ```
> **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`. > **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.
### 5. Start the services ### 5. Start the services
@ -121,45 +141,176 @@ services:
docker compose up -d docker compose up -d
``` ```
Verify the proxy is running: Verify the proxy is up:
```bash ```bash
curl http://localhost:3456/health curl http://localhost:3456/health
# {"status":"ok","configured":true} # {"status":"ok","configured":true}
``` ```
### 6. Configure Actual Budget ### 6. Configure Actual Budget
In Actual Budget, go to **Settings → GoCardless** and enter the `secret_id` and `secret_key` printed in step 3. 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**.
You can now go to **Accounts → Link account** and search for your French bank. ---
## 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.
--- ---
## OAuth redirect flow ## OAuth redirect flow
``` ```
1. actual-server POST /api/v2/requisitions/ { redirect: "http://actual:5006/..." } 1. actual-server POST /api/v2/requisitions/
body: { redirect: "http://actual:5006/...", institution_id }
2. proxy POST https://api.enablebanking.com/auth 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> } state: <requisition_id> }
← { url: "https://tilisy.enablebanking.com/..." } ← { url: "https://tilisy.enablebanking.com/..." }
3. proxy returns requisition with link = EB auth URL 3. proxy returns requisition { id, status: "CR", link: <EB auth URL> }
4. User visits the link, authenticates at their bank 4. User visits the link, authenticates at their bank
5. Enable Banking redirects → http://proxy/callback?code=xxx&state=<req_id> 5. Enable Banking → http://proxy/callback?code=xxx&state=<requisition_id>
6. proxy POST https://api.enablebanking.com/sessions { code } 6. proxy POST https://api.enablebanking.com/sessions { code }
← { session_id, accounts: [uid, ...] } ← { session_id, accounts: [uid, ...] }
→ fetches account details, stores GC account IDs → fetches account details per UID
→ marks requisition status "LN" (linked) → stores gc_id ↔ eb_uid mapping in store.json
→ redirects → http://actual:5006/...?requisition_id=<req_id> → sets requisition status "LN" (linked)
→ 302 → http://actual:5006/...?requisition_id=<req_id>
7. actual-server GET /api/v2/requisitions/<req_id>/ 7. actual-server GET /api/v2/requisitions/<req_id>/
← { status: "LN", accounts: ["gc-acc-id-1", ...] } ← { status: "LN", accounts: ["gc-uuid-1", ...] }
8. actual-server GET /api/v2/accounts/<id>/details/ 8. actual-server GET /api/v2/accounts/<id>/details/
GET /api/v2/accounts/<id>/balances/ GET /api/v2/accounts/<id>/balances/
@ -187,27 +338,27 @@ All French banks supported by Enable Banking, including:
| Fortuneo | FTNOFRP1 | | Fortuneo | FTNOFRP1 |
| Hello bank! | BNPAFRPP | | Hello bank! | BNPAFRPP |
See [enablebanking.com/docs/markets/fr/](https://enablebanking.com/docs/markets/fr/) for the full list. See [enablebanking.com/docs/markets/fr/](https://enablebanking.com/docs/markets/fr/) for the complete list.
--- ---
## Troubleshooting ## Troubleshooting
### "Proxy not configured" error **"Proxy not configured" error**
Run `npm run setup` and restart the container. Run `npm run setup` and restart the container.
### "Unknown institution_id" when creating a requisition **"Unknown institution_id" when linking an account**
actual-server caches institutions. Go to **Settings → GoCardless → Reset** and re-fetch banks. actual-server caches the institutions list. Go to **Settings → GoCardless → Reset** and re-fetch.
### OAuth callback fails with "Enable Banking error" **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). - `proxy_base_url` in `data/store.json` must be reachable from your browser, not just from inside Docker.
- Check that the redirect URL is whitelisted in your Enable Banking application settings. - Make sure the callback URL (`{proxy_base_url}/callback`) is whitelisted in your Enable Banking application settings.
### Transactions not appearing **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. Enable Banking may return an empty array if no transactions fall in the requested date range. Try widening the range in Actual Budget.
### Private key errors **Private key parse error**
Make sure the key is **PKCS8** format. To convert from traditional RSA format: The key must be in **PKCS8** format. To convert from a traditional RSA key:
```bash ```bash
openssl pkcs8 -topk8 -nocrypt -in old_key.pem -out private_key.pem openssl pkcs8 -topk8 -nocrypt -in old_key.pem -out private_key.pem
``` ```
@ -216,12 +367,46 @@ openssl pkcs8 -topk8 -nocrypt -in old_key.pem -out private_key.pem
## Data & privacy ## Data & privacy
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. 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.
The `data/` directory is git-ignored and should not be committed. 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.
--- ---
## License ## License
MIT 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.