Partage: an encrypted, local-first bill splitting app made with Elm

Hi everyone,

I just deployed Partage on its final domain: https://onpartage.eu

What it is

Partage helps a group of people keep track of who paid for what. Think of a trip with friends, a shared flat, or a family. You write down each expense, who paid, and who it was for. Partage does the math and tells you who owes who, and the simplest set of payments to settle everything.

If you already use an app like Tricount or Splitwise, Partage is that, with two main differences. Everything is encrypted in your browser, so the server only ever stores ciphertext and cannot read your group. And there are no accounts. No email, no password, no sign up. Your identity is a keypair created on your device the first time you open the app, and you invite people by sharing a link.

It is free and open source: GitHub - mpizenberg/partage-elm · GitHub

Features

You can record expenses, direct transfers between two people, and income. An expense can have several payers, and you can split it by shares or by exact amounts. Groups can mix currencies, with one default currency for the balances. The settlement plan is optimized to minimize the reimbursements transfer needed. You can add people who have not joined a group yet as virtual members to easily keep track of expenses. Nothing is ever really erased: editing an entry creates a new version and keeps the old one, so there is a full activity feed and a per member audit log. There is also search and filtering, CSV export, push notifications, and English and French translations (for now). It installs as a Progressive Web App on Android, iOS, macOS and desktop, and it works offline.

A bit of architecture

The frontend is Elm 0.19.1. It is around 38k lines of Elm, or about 65k if you count the vendored dependencies the compiler builds along with it, plus 31 test modules. A few things may be interesting to people here.

elm-ui 2.0 all the way. The whole interface is built with @mdgriffith elm-ui 2.0. I love working with it, and I would make that choice again without hesitating. It is not published yet, so it is vendored in the repo, and that is part of why the line count above jumps when you include vendored code.

The domain is a fold. A group is an append only log of signed events. All the state comes from replaying that log:

applyEvents : List Envelope -> GroupState -> GroupState

Balances, the settlement plan, the activity feed, the audit log, the member graph, all of it is derived from that one pure function. Two devices that hold the same set of events derive exactly the same state, which is what makes offline editing converge without any server side merge. It is also very pleasant to test, because the whole domain is a function from a list to a record.

Almost no ports. All the async browser work goes through @andrewMacmurray elm-concurrent-task: Web Crypto, IndexedDB, HTTP, compression. So a flow like “sign this event, encrypt it, store it, push it to the relay, update the cursor” is a single typed Task chain in Elm, instead of a port ping-pong spread across the update function. I wrote three small packages for that, currently vendored in the repo: elm-webcrypto, elm-indexeddb ( GitHub - mpizenberg/elm-indexeddb: IndexedDB support for Elm via elm-concurrent-task · GitHub ) and elm-pwa ( GitHub - mpizenberg/elm-pwa: PWA integration for Elm apps · GitHub ), not published yet. In the end public/index.js, the entire JavaScript side of the frontend app, is about 650 lines.

The server is deliberately dumb. The backend is a small Hono and SQLite relay. It stores encrypted blobs and signed metadata, it authenticates with a bearer token derived from the group key, and it can decrypt nothing. Clients are the source of truth, so if the relay loses history a client can push it back.

Elm’s “no runtime exceptions” promise matters more than usual in this design. The user’s data lives in their browser, and if the app crashed in the middle of a write there would be no server copy to fall back on.

Translations are Fluent files compiled to Elm at build time with @andreasmolitor travelm-agency, and the build is @lydell elm-watch plus esbuild.

One last Elm note: the feedback button you will find on every screen opens a form hosted by @kantuni Feedback.one, which is itself a product built in Elm. Nice to be able to stay in the family for that part too.

How it was built

Partage started in January 2026 as a JavaScript app that I mostly vibecoded. I was struggling to keep the bugs in check as it became a Whack-A-Mole game for something that is important to be bug-free, so I restarted from scratch in Elm. I architected the app myself: the core data structures, the design decisions, the properties that have to hold, and LLMs were the help. Elm makes that help safer to accept. The compiler rejects whatever a model gets wrong about types and missing cases, and @jfmengels elm-review keeps them in check on the rest, especially by refusing dead code before it piles up.

Happy to answer any questions, and feedback is very welcome.

19 Likes

This is really cool and there’s a lot to learn from in the application. Congrats on getting it completed and deployed. Please consider submitting it to Built with Elm.

1 Like

Yep PR in preparation.

BTW, the app is using elm-field, elm-form, and elm-validation :slight_smile: which are great!

1 Like

I played around with Elm and indexeddb some time ago but never pushed it forward, will look at your implementation soon, been watching your posts re elm-pwa, really interested in that, hope you’re able to publish at some point :+1:

1 Like

This is awesome!

As a side note, does it include any implementation of Bistromathics? I know that has long been considered an open problem and I was just wondering how you might have solved it.

In all seriousness, this is really nicely engineered along several different dimensions! Wow. Thank you for sharing.

2 Likes

Never heard of that ahah ^^

Thank you for trusting us with your feedback! :heart:

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.