How MSRX Split works
No server holds your group. Your devices keep it, talk to each other directly, and lock everything with a key made from your group code.
Last updated 7 October 2026
In plain words
Where is my data?
On your phone or computer, inside your browser. Nothing is stored on an MSRX server, because there is none holding your groups. Every member's device keeps its own full copy of the group.
That is why the app is free to run and why it can keep working with no signal: you add an expense, it is saved on your device, and it reaches the others when you are next online together.
How do everyone's phones stay in step?
When two members have the group open at the same time, their devices connect to each other directly and swap whatever the other is missing. A small introduction service helps them find each other, like a receptionist who says who is in the room but never sees what you say.
If nobody else is online, your changes simply wait on your device. Tap Sync now, or open the group when a friend does, and they go across.
What is encrypted?
When two devices in your group talk, the connection between them is encrypted, and the details they swap to set it up are locked with a key made from your group code. The introduction service only ever sees a scrambled room name and those locked details. It cannot read your expenses, names or amounts. Backup files are locked with your group code too.
The lock symbol at the top of the app is a reminder of this: communication within your group is encrypted.
What is the group code?
A random 10-character code made on your device when you create the group. It is both the key and the address of the group. Anyone who has it can open the group, so share it only with people on the trip.
Each person you add also gets their own short personal link, so the right name is set up on the right phone. The personal part is a label, not a password.
Who is the Admin?
Whoever creates the group. Admins can add and remove people, make others admin, rename the group and close the trip. If the last admin leaves, the next person who joined takes over, and so on down the line.
These roles are rules the app follows. They keep honest groups tidy; they are not a vault. If you need to be sure someone is shut out, the admin moves the group to a new code and shares it only with the people who stay.
What if I lose my phone?
The group is still on everyone else's devices. Download a backup file from the Data tab now and then: with the file and the group code you can bring the whole trip back on a new device. Without a backup and with every device lost, there is nothing to recover from, and that is the price of keeping no copy anywhere.
What is Ask Split?
A helper you can ask about the trip's spending: the total, what you owe, who paid most, where the money went, what the trip will cost by the end and whether you are on budget. Those quick questions are worked out on your phone, from your group's own data, and work with no signal.
Typed questions use AI and are off unless an admin turns them on and you agree on your own phone. Before anything is sent, names are swapped for labels like P1 and P2, and expense titles, the group name and the code stay behind. The answer comes back with the names put back in on your phone.
What does it not do?
- It does not move or collect money. It only keeps the tally; you settle up yourselves.
- It has no accounts, no passwords to reset and no email sign-up.
- It has no cookies, no advertising and no analytics.
- It cannot recover a lost group code.
What does it cost?
Nothing. It runs on free plans of the hosting and introduction services, which is why those services have daily limits and the app is built to keep working on your devices even when they are unavailable.
For engineers
Architecture
MSRX Split is a local-first single-page app. There is no application server holding data and no database. The static bundle is served from Vercel; the server-side code is a tiny signalling relay on Cloudflare Workers that introduces peers, and one stateless function for the optional Ask Split AI. Group state lives in each browser's IndexedDB and converges by exchanging CRDT updates over WebRTC data channels.
- Client: React 19 + TypeScript, built with Vite, installable as a PWA.
- State: one Yjs document per group, persisted with y-indexeddb.
- Transport: y-webrtc over WebRTC data channels, introduced by a WebSocket relay.
- Relay: a Cloudflare Worker with one Durable Object (WebSocket hibernation) that forwards messages between sockets subscribed to the same topic. It never writes to storage.
- Hosting: Vercel (static, plus the /api/ask function) behind split.msrx.co.in; relay on workers.dev.
Data model
Each group is a Y.Doc with three Y.Maps keyed by id (meta, people, expenses) and a Y.Array activity log. Records are objects replaced whole, so concurrent edits to the same record resolve last-writer-wins by Yjs's deterministic ordering, while concurrent additions never conflict.
Nothing is ever hard-deleted. Removing an expense, a person or a member sets a flag (tombstone), which is what lets history merge cleanly after long offline periods and lets a device that was away for months catch up without losing or resurrecting anything.
Money is held as integer minor units (paise) and split with a largest-remainder allocation so shares always sum exactly to the total. The ledger (who owes whom, and a short list of payments that settles everyone) is derived from the expenses on every render and is never stored.
Keys and encryption
The group code is 10 characters drawn uniformly from a 56-character alphabet without look-alike characters (no 0, O, 1, I, l, L), about 58 bits of entropy. It is run through PBKDF2-HMAC-SHA-256 with 600,000 iterations and a fixed domain-separated salt, and the result is split by HKDF-SHA-256 into three independent secrets:
- roomId (128 bits): the public rendezvous topic on the relay. It reveals nothing usable about the code or the keys.
- signalSecret (256 bits): the password y-webrtc uses to encrypt signalling messages (the WebRTC offers and answers) that pass through the relay.
- data key: a non-extractable AES-256-GCM key that encrypts backup files, with a fresh random 96-bit IV each time.
Group updates travel over WebRTC data channels, which are encrypted with DTLS between the two devices; a TURN relay carries only those encrypted bytes. The DTLS certificate fingerprints are inside the offers and answers, which are encrypted with signalSecret, so the signalling relay can neither read them nor swap in its own to sit in the middle. PBKDF2 was chosen over Argon2 because it ships in every browser's WebCrypto with no extra download; the iteration count is what makes guessing a code from a leaked room name expensive. Derived keys are never stored: they are recomputed from the code each time a group opens.
Sync and the relay
On opening a group the client derives keys, fetches ICE servers, and joins the room through y-webrtc. Peers exchange Yjs state vectors and send only the missing updates. The connection status shown in the app is computed from real RTCPeerConnection state, not from peer discovery, so it never claims a link that does not exist.
The relay is a Cloudflare Worker routing WebSocket upgrades to a single Durable Object. It implements the y-webrtc signalling protocol (subscribe, unsubscribe, publish, ping) over WebSocket hibernation, keeps topic membership in memory only, and holds no persistent state. It enforces per-socket limits: 64 KiB per message, 8 topics per connection, 60 connections per topic and 120 messages per 10 seconds, and caps open connections at 3,000 in total and 64 per address. An origin allow-list restricts it to the app's own site.
Because the relay is only a rendezvous, the free-plan daily limits affect discovery of new connections, not data. Existing direct links, and everything on each device, keep working if the relay is unavailable.
STUN and TURN
Devices find their public address with a Google STUN server (stun.l.google.com:19302). When a network blocks direct links, traffic falls back to Cloudflare's TURN service. TURN credentials are minted by the relay Worker at GET /ice: the long-lived API token never leaves the Worker, each credential expires after 12 hours, requests are origin-checked and rate-limited (30 per address and 3,000 in total per hour), and only hosts under cloudflare.com are accepted. If anything fails, the client fails open to STUN only. A relayed connection carries only DTLS-encrypted bytes it cannot read.
Invite links
A link is https://split.msrx.co.in/#/join/<code>~<tag>. Everything after the # stays in the browser and is never sent to any server. The 4-character tag is derived from a member id with FNV-1a; it pairs a device with a person and is not a secret.
An optional longer form appends a snapshot of the group (raw deflate, base64url, at most 3,000 characters, inflate capped at 256 KB) so a new device can show the group, and who runs it, before any peer is online. A snapshot is applied only to a new or still-empty group, so a stale link can never overwrite live data. Opening a link first shows a consent screen; nothing is saved until the person agrees.
Roles, and what they do not guarantee
The creator is flagged admin. Add, remove, restore, promote, demote, rename and close are admin-only in the app, every one is written to a readable activity log, and when the last admin leaves the member with the earliest join time inherits the role. Groups created before roles existed treat every active member as an admin until one is chosen.
These are application rules, not cryptographic ones: the document is one shared CRDT that anyone holding the group code can write, so a modified client could ignore them. The log makes tampering visible rather than impossible. The real revocation tool is moving the group: a new code is generated, history is copied, the old group is closed, and the new code is shared only with people who stay. Making roles tamper-proof would need per-member signing keys and a signed history, which is a larger design than this app takes on.
Ask Split and the AI path
Insights are computed on the device from the same per-expense split as the balances: daily average over the calendar days the spending covers, a straight-line forecast to the trip's last day, and the budget measured against both. The eight quick questions are answered by code on the device; there is no model involved and no request made.
Typed questions, when an admin has set the group's ai flag and this device has recorded its own consent, take two steps through a stateless Vercel function, /api/ask. First the question alone, with member names masked to P1, P2 and so on, is classified by TypeSafe's Jev model (one Choice question): a question that is one of the quick ones at probability 0.8 or above is answered on the device, an unrelated one gets a fixed reply, and only the rest go on. Then the device sends a summary built in code (labels, amounts in major units, categories, dates, budget, forecast and settle-up transfers, plus the budget verdict and the daily allowance to stay within it, both computed in code so the model never has to judge them; no titles, group name or code) and Groq's openai/gpt-oss-20b writes the answer at low reasoning effort, which the device unmasks. If the classifier is unavailable, questions go straight to the model.
The function holds the GROQ_API_KEY and TYPESAFE_API_KEY server-side, accepts only same-origin requests from the app's own hosts, caps the question at 300 characters and the summary at 16 KB, rate-limits per address and per instance, and never logs a request body. Answers are rendered as plain text, so nothing a model returns can run as code in the page.
Storage and backups
Updates are written to IndexedDB by y-indexeddb and the app asks the browser for persistent storage. Backups are the encrypted Yjs state in a single file, openable only with the group code, and a plain CSV export is available for spreadsheets. The app nudges for a backup after a few entries or a day. Removing a group from a device deletes its IndexedDB database and registry entry from that device only.
Browser security
- A strict Content-Security-Policy: script-src and style-src 'self' only, no inline script, connect-src limited to the site and the relay, object-src 'none', frame-ancestors 'none'.
- X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, X-Frame-Options: DENY, Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy set to same-origin, Strict-Transport-Security, and a Permissions-Policy that denies camera, microphone, geolocation and payment.
- Everything the app reads from the shared group document is checked before use: records of the wrong shape are skipped, text is cut to length and numbers must be finite, so a malformed or hostile entry from another device cannot stop the group opening for everyone.
- No cookies, no third-party scripts, fonts or analytics; all icons are drawn in the bundle.
- The privacy notice and terms are generated to static HTML so they load without the app bundle, and a test fails if the notice stops matching the code.
Testing
Unit tests (Vitest) cover the ledger and money maths, key derivation, the CRDT model, roles and succession, invite encoding and the relay itself. A claims test checks the privacy notice and this page against the code. Playwright end-to-end suites run real browsers (Chromium and an iPhone-profile WebKit) through creating, joining, syncing, backup and restore, admin succession, legacy groups and moving a group, plus a check of the live site under the production CSP and a run that forces traffic through Cloudflare's real TURN service.
Known limits
- Admin roles are not cryptographically enforced (see above).
- Whoever holds the code can read the group; removal needs a move to a new code to be effective.
- Syncing needs two devices online at once; the link can carry a snapshot for the first join.
- On networks that block both direct links and TURN, devices cannot connect until the network changes.
- Browsers can evict storage; backups are the safety net.
- AI answers can be wrong, and the forecast is a straight line from the spending so far.
Built with
Versions are the ranges this build declares.
In the app
| Package | Version | What it does |
|---|---|---|
| React | ^19.3.0 | The screens you see |
| React DOM | ^19.3.0 | Draws React in the browser |
| ws | ^8.22.0 | WebSocket server for running the relay locally and in tests |
| y-indexeddb | ^9.0.12 | Saves each group in the browser's own storage |
| y-webrtc | ^10.3.0 | Connects members' devices directly and exchanges Yjs updates |
| Yjs | ^13.6.33 | The shared-document engine (CRDT) that merges everyone's changes |
To build, test and deploy
| Package | Version | What it does |
|---|---|---|
| Vite React plugin | ^6.1.2 | Compiles React for Vite |
| Playwright | ^1.63.0 | Real-browser end-to-end tests |
| TypeScript | ^7.0.2 | Typed JavaScript |
| Vite | ^8.3.3 | Build tool and dev server |
| vite-plugin-pwa | ^2.0.0 | Makes the app installable and available offline |
| Vitest | ^5.0.3 | Unit tests |
| Wrangler | ^4.147.0 | Deploys the relay to Cloudflare |
Services
- Vercel: serves the static app and the legal pages.
- Cloudflare Workers and Durable Objects: the signalling relay, which stores nothing.
- Cloudflare Realtime TURN: fallback relay for blocked networks, carrying only encrypted bytes.
- Google STUN: tells a device its public address.
Questions about any of this: mrinalsinghraja@gmail.com. The Privacy notice and Terms are the legal side.