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:
parent
2487d8a2f9
commit
e3891f4dc1
301
README.md
301
README.md
@ -1,13 +1,37 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
@ -15,15 +39,15 @@ Actual Budget (browser)
|
||||
│ GoCardless API calls
|
||||
▼
|
||||
actual-server
|
||||
│ GOCARDLESS_BASE_URL → http://gocardless-proxy:3456
|
||||
│ GOCARDLESS_BASE_URL=http://gocardless-proxy:3456
|
||||
▼
|
||||
gocardless-proxy ◄── this repo
|
||||
│ Enable Banking PSD2 API
|
||||
│ Enable Banking PSD2 API (RS256 JWT auth)
|
||||
▼
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
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.
|
||||
Store `private_key.pem` securely. You will reference it during setup.
|
||||
|
||||
### 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 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)
|
||||
The script prompts for:
|
||||
|
||||
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:
|
||||
|
||||
Secret ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
Secret Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
Secret Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
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
|
||||
services:
|
||||
actual-server:
|
||||
image: actualbudget/actual-server:latest
|
||||
# ... your existing config ...
|
||||
environment:
|
||||
GOCARDLESS_BASE_URL: http://gocardless-proxy:3456
|
||||
# ... your other env vars ...
|
||||
# ... rest of your actual-server env vars
|
||||
depends_on:
|
||||
- gocardless-proxy
|
||||
|
||||
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
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@ -113,7 +133,7 @@ services:
|
||||
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
|
||||
|
||||
@ -121,45 +141,176 @@ services:
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Verify the proxy is running:
|
||||
Verify the proxy is up:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3456/health
|
||||
# → {"status":"ok","configured":true}
|
||||
# {"status":"ok","configured":true}
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
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
|
||||
{ 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 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
|
||||
↓
|
||||
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 }
|
||||
← { session_id, accounts: [uid, ...] }
|
||||
→ fetches account details, stores GC account IDs
|
||||
→ marks requisition status "LN" (linked)
|
||||
→ redirects → http://actual:5006/...?requisition_id=<req_id>
|
||||
→ 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>
|
||||
↓
|
||||
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/
|
||||
GET /api/v2/accounts/<id>/balances/
|
||||
@ -187,27 +338,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 full list.
|
||||
See [enablebanking.com/docs/markets/fr/](https://enablebanking.com/docs/markets/fr/) for the complete list.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Proxy not configured" error
|
||||
**"Proxy not configured" error**
|
||||
Run `npm run setup` and restart the container.
|
||||
|
||||
### "Unknown institution_id" when creating a requisition
|
||||
actual-server caches institutions. Go to **Settings → GoCardless → Reset** and re-fetch banks.
|
||||
**"Unknown institution_id" when linking an account**
|
||||
actual-server caches the institutions list. Go to **Settings → GoCardless → Reset** and re-fetch.
|
||||
|
||||
### 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.
|
||||
**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.
|
||||
|
||||
### 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.
|
||||
**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.
|
||||
|
||||
### Private key errors
|
||||
Make sure the key is **PKCS8** format. To convert from traditional RSA format:
|
||||
**Private key parse error**
|
||||
The key must be in **PKCS8** format. To convert from a traditional RSA key:
|
||||
```bash
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user