actual-gocardless-proxy/README.md
jeanGaston e3891f4dc1 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>
2026-04-13 11:00:53 +02:00

14 KiB

actual-gocardless-proxy

AI-generated code — see disclaimer below.

A drop-in GoCardless Bank Account Data API proxy that lets Actual Budget sync with French (and other European) bank accounts via Enable Banking — 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
  2. Setup
  3. Ansible deployment
  4. OAuth redirect flow
  5. Supported banks
  6. Troubleshooting
  7. Data & privacy
  8. Disclaimer
  9. License

How it works

Actual Budget (browser)
    │  GoCardless API calls
    ▼
actual-server
    │  GOCARDLESS_BASE_URL=http://gocardless-proxy:3456
    ▼
gocardless-proxy          ◄── this repo
    │  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. All state (tokens, requisitions, account mappings) is persisted in ./data/store.json so Docker restarts do not break existing Actual Budget account links.


Setup

1. Sign up for Enable Banking

Go to enablebanking.com and create a free personal account. Under your application 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.

# 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 the Enable Banking dashboard
openssl pkey -in private_key.pem -pubout -out public_key.pem

Store private_key.pem securely. You will reference it during setup.

3. Run the setup script

git clone https://github.com/yourname/actual-gocardless-proxy
cd actual-gocardless-proxy
npm install
npm run setup

The script prompts for:

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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

And set this environment variable on actual-server:

  GOCARDLESS_BASE_URL=http://gocardless-proxy:3456
──────────────────────────────────────────────────────────

All credentials are written to ./data/store.json.

4. Add the proxy to docker-compose

Paste the gocardless-proxy service block into your existing actual-server docker-compose.yml and add GOCARDLESS_BASE_URL to actual-server:

services:
  actual-server:
    image: actualbudget/actual-server:latest
    environment:
      GOCARDLESS_BASE_URL: http://gocardless-proxy:3456
      # ... rest of your actual-server 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
    container_name: gocardless-proxy
    restart: unless-stopped
    volumes:
      - /path/to/actual-gocardless-proxy/data:/app/data
    environment:
      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.

5. Start the services

docker compose up -d

Verify the proxy is up:

curl http://localhost:3456/health
# {"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.


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.

# 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:

# Create vault file
ansible-vault create group_vars/actual_host/vault.yml
# 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:

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/
                    body: { redirect: "http://actual:5006/...", institution_id }
                  ↓
2. proxy          POST https://api.enablebanking.com/auth
                    { 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> }
                  ↓
4. User           visits the link, authenticates at their bank
                  ↓
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 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-uuid-1", ...] }
                  ↓
8. actual-server  GET /api/v2/accounts/<id>/details/
                  GET /api/v2/accounts/<id>/balances/
                  GET /api/v2/accounts/<id>/transactions/

Supported banks

All French banks supported by Enable Banking, including:

Bank BIC
BNP Paribas BNPAFRPP
Société Générale SOGEFRPP
Crédit Agricole AGRIFRPP
LCL CRLYFRPP
Banque Populaire various
Caisse d'Épargne various
La Banque Postale PSSTFRPPPAR
Crédit Mutuel CMCIFRPP
CIC CMCIFRPP
Boursorama BOUSFRPPXXX
Fortuneo FTNOFRP1
Hello bank! BNPAFRPP

See enablebanking.com/docs/markets/fr/ for the complete list.


Troubleshooting

"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.

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 array if no transactions fall in the requested date range. Try widening the range in Actual Budget.

Private key parse error The key must be in PKCS8 format. To convert from a traditional RSA key:

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.

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 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.