Skip to content
PaulBayfieldPublic

Latest commit

Β 

History

209 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Prisme Logo

Prisme

Track your money across LCL accounts, assets, debts, and goals, self-hosted and behind your own SSO.

Status License Next.js Python PostgreSQL Demo

Prisme dashboard screenshot

⚠️ β€’ Disclaimer

This relies on an unofficial, reverse-engineered LCL API and deals with real banking credentials. It's built for personal use against my own account, so use it at your own risk, and never commit or share captured identifiers, keypads, or session tokens. This is also a personal project, not a polished product: expect missing pieces, sharp edges, and breaking changes.

πŸ“‹ β€’ Table of Contents

πŸ“– β€’ Overview

Prisme is a personal money-tracking app built around LCL, a French bank with no official public API. It reverse-engineers LCL's private web API (monespace.lcl.fr/api) to pull account balances and transactions into a PostgreSQL database you control, then displays everything in a dashboard behind your own SSO.

Key features:

  • Sync current and savings accounts, balances, and transactions straight from LCL
  • Hierarchical, user-defined transaction categories, with local (non-cloud) ML category suggestions
  • Net worth tracking: manually-tracked assets, debts, cash on hand, and Cheques-Vacances
  • Savings goals (fixed target or recurring) and per-category monthly budgets
  • Income forecasting from your own categorized salary history
  • Shared/joint account support: multiple Prisme users can sync the same LCL account
  • Authentication via Authentik (OIDC SSO); no separate Prisme accounts/passwords
  • Per-device session duration (15 minutes to 30 days of inactivity), with a list of signed-in devices you can sign out remotely from Account β†’ Security
  • Installable as a PWA ("Add to Home Screen" / "Install app") for quick access on mobile

🧩 β€’ Components

The project is split into three pieces under src/, meant to run as a pipeline:

flowchart TD
    LCL[("LCL<br/>monespace.lcl.fr")]
    You(("You"))
    Ext["Extension<br/>captures session &amp; encrypts"]
    FE["Frontend (Next.js)<br/>behind Authentik SSO"]
    Worker["Worker (Python)<br/>sync Β· categorize Β· forecast"]
    DB[("PostgreSQL")]

    You -- "logs in" --> LCL
    LCL -. "webRequest captures<br/>login + account data" .-> Ext
    Ext -- "encrypted payload<br/>(manual copy-paste,<br/>passphrase from frontend)" --> FE
    FE -- "stores encrypted credentials,<br/>enqueues sync" --> DB
    Worker -- "polls queue,<br/>decrypts credentials" --> DB
    Worker -- "fetches accounts &amp;<br/>transactions" --> LCL
    Worker -- "persists data,<br/>categorizes, forecasts" --> DB
    FE -- "reads &amp; displays" --> DB
    You -- "views dashboard" --> FE
Loading
Component Status Description
src/extension Working A browser extension you run once while logging into monespace.lcl.fr. It captures the session credentials from that login, encrypts them, and lets you hand them off to the frontend.
src/worker Working A Python service that decrypts those credentials, calls LCL's API (balances, transactions, savings accounts), persists everything to PostgreSQL, predicts transaction categories, and forecasts income.
src/frontend Working A Next.js dashboard behind Authentik SSO: accounts, transactions, budgets, goals, assets/debts, and the onboarding flow that pairs a user with their LCL credentials.

πŸ› οΈ β€’ Tech Stack

Layer Technology
Frontend Next.js 16 (App Router, React 19), TypeScript 5
Styling / UI Tailwind CSS v4, shadcn/ui, Radix / base-ui
Charts Recharts
Auth NextAuth.js + Authentik (OIDC SSO)
Worker Python 3.13, managed with uv
LCL client aiohttp (reverse-engineered LCL API, lib/LCLPy)
Categorization scikit-learn (local TF-IDF + logistic regression, per user)
Database PostgreSQL 17 (pgcrypto for credential encryption, pg_trgm for search)
Extension WebExtension Manifest V3, bundled with webpack
Containerization Docker Compose

πŸš€ β€’ Getting Started

Prerequisites

  • Node.js β‰₯ 20 and npm (frontend + extension)
  • Python β‰₯ 3.13 and uv (worker)
  • Docker & Docker Compose (PostgreSQL, and optionally the worker)
  • A browser (Chrome or Firefox) to load the extension
  • An Authentik instance (or another OIDC provider, if you adapt src/frontend/lib/auth.ts) for SSO

Installation

# 1. Clone the repository
git clone https://github.com/PaulBayfield/Prisme.git
cd Prisme

# 2. Install the extension's dependencies
cd src/extension && npm install && cd ../..

# 3. Install the worker's dependencies
cd src/worker && uv sync && cd ../..

# 4. Install the frontend's dependencies
cd src/frontend && npm install && cd ../..

Environment Variables

How Prisme's .env files are scoped depends on whether you run it via Docker or locally:

Docker deployment - a single .env.example β†’ .env at the repo root, passed to all three services (prisme-db, prisme-worker, prisme-frontend) by docker-compose.yml:

Variable Required Description
POSTGRES_DATABASE Yes Database name
POSTGRES_USER Yes Database user
POSTGRES_PASSWORD Yes Database password
CREDENTIALS_ENCRYPTION_KEY Yes (unless DEMO_MODE=true) pgcrypto passphrase used to decrypt stored LCL credentials, shared by the worker and the frontend
DEMO_MODE No (default false) When true, runs the frontend against in-memory demo fixtures instead of PostgreSQL/Authentik - see Demo Mode. The AUTHENTIK_* variables below are ignored in this mode
NEXTAUTH_URL Yes Public URL of the frontend
NEXTAUTH_SECRET Yes Run openssl rand -base64 32
AUTHENTIK_ISSUER Yes (unless DEMO_MODE=true) Your Authentik issuer URL
AUTHENTIK_CLIENT_ID Yes (unless DEMO_MODE=true) OIDC client id (Authentik admin β†’ Applications)
AUTHENTIK_CLIENT_SECRET Yes (unless DEMO_MODE=true) OIDC client secret
AUTHENTIK_USER_INFO_URL Yes (unless DEMO_MODE=true) Authentik userinfo endpoint

POSTGRES_HOST/POSTGRES_PORT aren't in this file - docker-compose.yml hardcodes them to prisme-db/5432 for the worker and frontend services, since that's how they reach PostgreSQL over the compose network regardless of what's in .env. NEXT_PUBLIC_APP_VERSION isn't here either - it's baked into the published ghcr.io/paulbayfield/prisme-frontend image at CI build time instead, from package.json - see Docker Deployment.

Local (non-Docker) development - two independent scopes, each with its own .env.example to copy from. This split isn't optional: npm run dev and uv run python __main__.py each load their own scoped file by convention (Next.js auto-loads .env.local from src/frontend/; the worker's load_dotenv(dotenv_path=".env") resolves relative to src/worker/), so the root .env above has no effect on either when run outside Docker.

1. src/worker/.env.example β†’ src/worker/.env, read by the worker and its scripts:

Variable Required Description
POSTGRES_HOST No (default localhost) PostgreSQL host
POSTGRES_PORT No (default 5432) PostgreSQL port
POSTGRES_DATABASE Yes Database name
POSTGRES_USER Yes Database user
POSTGRES_PASSWORD Yes Database password
CREDENTIALS_ENCRYPTION_KEY Yes pgcrypto passphrase used to decrypt stored LCL credentials, must match the frontend's value

2. src/frontend/.env.example β†’ src/frontend/.env.local:

Variable Required Description
DEMO_MODE No (default false) When true, runs the frontend against in-memory demo fixtures instead of PostgreSQL/Authentik - see Demo Mode. All variables below are ignored in this mode
POSTGRES_* Yes (unless DEMO_MODE=true) Same PostgreSQL connection info as above
NEXTAUTH_URL Yes Public URL of the frontend
NEXTAUTH_SECRET Yes Run openssl rand -base64 32
AUTHENTIK_ISSUER Yes (unless DEMO_MODE=true) Your Authentik issuer URL
AUTHENTIK_CLIENT_ID Yes (unless DEMO_MODE=true) OIDC client id (Authentik admin β†’ Applications)
AUTHENTIK_CLIENT_SECRET Yes (unless DEMO_MODE=true) OIDC client secret
AUTHENTIK_USER_INFO_URL Yes (unless DEMO_MODE=true) Authentik userinfo endpoint
CREDENTIALS_ENCRYPTION_KEY Yes (unless DEMO_MODE=true) Must be the exact same value as in src/worker/.env
NEXT_PUBLIC_APP_VERSION No Version shown in the help dialog's badge when running npm run dev/npm run build directly

Running Locally

# 1. Start PostgreSQL
docker compose up -d prisme-db

# 2. Run the worker (polls the sync queue, syncs every connected user hourly)
cd src/worker
uv run python __main__.py

# 3. Run the frontend
cd src/frontend
npm run dev

The frontend is available at http://localhost:3000. After onboarding, go to Settings β†’ Compte to connect an LCL account: generate a passphrase there, paste it into the extension popup, then paste the encrypted result back.

To build and load the extension:

cd src/extension
npm install
npm run dev   # one-off development build into dist/

Then load src/extension/dist as an unpacked extension: chrome://extensions β†’ Developer mode β†’ Load unpacked (Chrome), or about:debugging β†’ Load Temporary Add-on (Firefox).

πŸ§ͺ β€’ Demo Mode

Set DEMO_MODE=true in src/frontend/.env.local to run the frontend against in-memory fixtures (lib/demo) instead of PostgreSQL and Authentik. This is what powers the live demo: the sign-in page shows a single "Essayer la dΓ©mo" button (components/demo-auth-form.tsx) that authenticates into a fixed demo identity, no real LCL account, database, or Authentik instance required.

cd src/frontend
echo "DEMO_MODE=true" >> .env.local
npm run dev

In this mode:

  • POSTGRES_*, AUTHENTIK_*, and CREDENTIALS_ENCRYPTION_KEY are not read and can be left unset
  • NEXTAUTH_URL and NEXTAUTH_SECRET are still required - NextAuth needs them to sign/verify the session JWT regardless of which provider (Authentik or the demo credentials provider) is active
  • All reads/writes (lib/data.ts, lib/actions.ts) are routed to fixture data instead of the real Postgres-backed implementations, so nothing is persisted between restarts
  • No data ever leaves the fixtures - the extension, worker, and LCL are never involved

❀️ β€’ Health Check

The frontend exposes an unauthenticated health check at /health:

curl http://localhost:3000/health
{
  "status": "ok",
  "uptime": "42s",
  "timestamp": "2026-07-04T12:00:00.000Z",
  "unix": 1783598400
}

Useful for uptime monitors, container health checks, or a reverse proxy readiness probe.

πŸ“ β€’ Project Structure

Prisme/
β”œβ”€β”€ .github/
β”‚   β”œβ”€β”€ dependabot.yml
β”‚   └── workflows/
β”‚       └── extension-release-please.yml   # Builds, zips & releases the extension
β”œβ”€β”€ docker-compose.yml                      # PostgreSQL + worker
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ extension/                          # WebExtension (MV3) - captures LCL credentials
β”‚   β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”‚   β”œβ”€β”€ background.js               # webRequest listener, captures login/account data
β”‚   β”‚   β”‚   β”œβ”€β”€ popup.js                    # encrypts captured data for export
β”‚   β”‚   β”‚   └── main.js                     # content script injected on monespace.lcl.fr
β”‚   β”‚   β”œβ”€β”€ popup.html
β”‚   β”‚   β”œβ”€β”€ manifest.json
β”‚   β”‚   └── webpack/
β”‚   β”œβ”€β”€ worker/                             # Python service - syncs LCL data into PostgreSQL
β”‚   β”‚   β”œβ”€β”€ lib/LCLPy/                      # Reverse-engineered LCL API client
β”‚   β”‚   β”œβ”€β”€ script/                         # decrypt.py, seed_credentials.py, import_legacy_transactions.py
β”‚   β”‚   β”œβ”€β”€ tests/test_client.py            # manual smoke test (not run in CI)
β”‚   β”‚   β”œβ”€β”€ worker.py                       # syncs one user's accounts/balances/transactions
β”‚   β”‚   β”œβ”€β”€ categorizer.py                  # local ML category prediction
β”‚   β”‚   β”œβ”€β”€ income_forecast.py
β”‚   β”‚   β”œβ”€β”€ schema.sql
β”‚   β”‚   └── __main__.py                     # sync queue daemon entry point
β”‚   └── frontend/                           # Next.js dashboard behind Authentik SSO
β”‚       β”œβ”€β”€ app/(app)/                      # App Router pages (accounts, budgets, goals, ...)
β”‚       β”‚   └── health/route.tsx            # GET /health - status, uptime, timestamp
β”‚       β”œβ”€β”€ components/                     # UI components (shadcn/ui based)
β”‚       β”œβ”€β”€ lib/                            # data.ts / actions.ts / auth.ts / db.ts
β”‚       β”‚   └── demo/                       # In-memory fixtures used when DEMO_MODE=true
β”‚       β”œβ”€β”€ app/manifest.ts                 # PWA web app manifest
β”‚       └── proxy.tsx                       # NextAuth middleware, excludes /health from auth
└── LICENSE

🐳 β€’ Docker Deployment

docker compose up -d

This starts:

Both images are built and published to ghcr.io on every push to main (see worker-ci.yml/frontend-ci.yml), tagged latest and by commit SHA. To pick up a new release:

docker compose pull prisme-worker prisme-frontend
docker compose up -d

If you'd rather build the images locally instead of pulling from ghcr.io (e.g. to test an unmerged change), swap the image: line for the corresponding service in docker-compose.yml for a build: block (context: ./src/worker or ./src/frontend).

πŸ“¦ β€’ Releases

The extension has its own release-please workflow (extension-release-please.yml, scoped to src/extension/**): on merge to main, it builds the production bundle, zips it, renames it to .xpi, attaches it to a GitHub release, and updates .ff_updates.json (the Firefox self-hosted update manifest). The worker and frontend are built and published as container images on every push to main - see Docker Deployment.

🀝 β€’ Contributing

This is a personal, single-maintainer project (see Disclaimer), so there's no formal CONTRIBUTING.md or process. Issues and pull requests are still welcome if something's broken or you have a suggestion.

πŸ™Œ β€’ Credits

Person Role
Paul Bayfield Creator & maintainer

Prisme also relies on a few external services:

Service Used for
LCL (unofficial, reverse-engineered API) Syncing account balances and transactions (src/worker/lib/LCLPy)
Authentik OIDC single sign-on
Frankfurter (European Central Bank rates) Live exchange rates for the currency converter (src/frontend/lib/exchange-rate.ts)

πŸ“„ β€’ License

Prisme is licensed under the AGPL 3.0.

Prisme
Copyright (C) 2026 Paul Bayfield

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.