SplitClone - Software Requirements Specification
trusted group of people (e.g. a household, a couple, a trip group). It deliberately avoids any dedicated
backend service. Instead, the shared state lives in a single folder that the
users share among themselves through an existing consumer file-sync service,
primarily Microsoft OneDrive.
This document specifies the requirements for the Minimum Viable Product (MVP).
Features that are out of scope for the MVP are listed
explicitly in section "Out of Scope".
expenses, automatically compute who owes whom, and settle balances - without
any of the users having to operate, pay for, or trust a central server.
- Participant: A person identified within a ledger. Not necessarily an app user.
- App user: A person who runs SplitClone on an iOS device and is authenticated against the shared folder.
- Expense: A single recorded payment by one participant on behalf of one or more participants.
- Split: The distribution of an expense amount across participants.
- Balance: The net amount one participant owes another, derived from all expenses and settlements.
- Settlement: A recorded transfer of money between two participants that reduces their balance.
- Label: A short user-defined tag (e.g. "cash", "trip-paris", "groceries") that can be attached to expenses for filtering and organisation. Labels carry no semantic effect on splits or balances.
- Execution date: The calendar date on which an expense actually occurred in the real world (e.g. the day the ice cream was bought).
- Entry timestamp: The wall-clock instant at which an expense was recorded in the app, which may be later than the execution date.
- Cash-basis view: A per-participant projection of the ledger that contains only real money movements from that participant's perspective (amounts they personally paid out or received), suitable for reconciling against a real bank or card account.
- Virtual-account view: A per-participant projection of the ledger intended to be imported as a separate "virtual" sub-account in a personal finance app. Each joint expense moves the virtual balance by (participant's outlay - participant's share); settlements move it back toward zero. The virtual balance at any moment equals the participant's net position inside the shared ledger.
- Shared folder provider: The underlying file-sync service (OneDrive in MVP) that hosts the ledger files.
- Event log: The append-only, per-device record of all changes made on that device, stored on disk as one or more ordered segment files (see Log segment).
- Log segment: A single file holding a contiguous slice of one device's event log. A device writes to exactly one "open" segment; older segments are closed and immutable. Segments are concatenated in chronological order to reconstruct the device's full log.
- PWA: Progressive Web App. A web application built on standard browser APIs (HTML/CSS/JS, Service Worker, Web App Manifest) that can be "installed" to the home screen / launchpad on iOS, Android, and desktop and can run offline once cached.
- Device: In a browser context, a "device" is the (browser, origin, IndexedDB partition) tuple. Two browsers on the same physical iPhone, or the same browser in private-browsing mode, are considered distinct devices.
- Data key: The 256-bit AES key, one per ledger, used to encrypt all event-log segments at rest in the shared folder. Generated on the device that creates the ledger; never written to the shared folder.
- Join code: A serialised representation of the data key (as a base64 string with checksum, and as a QR code) that the creator of the ledger shares out-of-band with each new participant device so it can decrypt the ledger contents.
- Key fingerprint: A short hash derived from the data key (e.g. truncated SHA-256) stored in the ledger's plaintext metadata. A joining device computes the same hash from its entered join code; a mismatch means the wrong code was entered, without revealing the key itself.
- File format: The collection of on-disk and on-the-wire layouts that SplitClone reads or writes (metadata file, segment directory layout, segment encryption envelope, JSONL event schema, join code encoding, CSV export, local backup file). Enumerated in SC-ARC-FMT-1.
- Schema version: The monotonically increasing integer that identifies a snapshot of the file format. Stored in the plaintext metadata file (SC-FR-LED-3) so that any device can decide before reading whether it understands the format.
or family plan) and wants to track shared expenses with 2 to ~8 other people
they trust (e.g. flatmates, partner, travel companions).
Larger groups are not a goal and may exhibit degraded sync performance.
~10 participants, sync and merge cost stops being negligible on mobile.
folder inside their connected shared folder provider. The app shall write an
initial ledger metadata file into that folder.
already contains SplitClone ledger metadata. The app shall validate the
metadata and refuse to open folders that are not recognised SplitClone ledgers.
metadata file shall declare at minimum:
- The ledger UUID
- The schema version
- The creation timestamp (UTC)
- The encryption marker "encrypted": true (see SC-ARC-ENC-1)
- The key fingerprint (see SC-ARC-ENC-3)
The metadata file shall NOT contain the ledger's human-readable name, its
participant list, label list, or any other field that could leak sensitive
information about the ledger's contents to anyone who can read the folder
but does not hold the data key. The ledger name and the participant /
label state shall instead be reconstructed from the encrypted event log
(see SC-ARC-LOG-2).
the only file in the folder that is visible without the key. Keeping it
minimal limits what an accidentally-over-shared folder leaks.
device. Switching ledgers shall be possible but is not a frequent operation.
to that ledger by providing a display name. The system shall assign a stable
participant UUID.
let them either (a) claim an existing unclaimed participant entry, (b) create
a new participant and claim it, or (c) re-claim a participant that is already
claimed on another device, when the same person is adding a further device
(e.g. PC + phone). A device shall be bound to exactly one participant per
ledger; a participant may be bound to more than one device. Claiming moves
the claiming device only — other devices already bound to a participant are
unaffected. The claim screen shall present (c) candidates separately from (a)
and make clear that picking one links the new device to the same person
rather than creating a duplicate.
participant a given device claims. Claiming is what links a real human (and
their OneDrive identity) to a participant UUID inside the ledger.
remain unchanged so existing expenses are not orphaned.
a flatmate who is included in splits but does not own an iPhone). Such
participants can be added by any other user and never claim a device.
- Title (free-form string, max 200 chars)
- Amount (decimal with 2 fractional digits, > 0)
- Execution date (calendar date when the expense actually occurred, defaulting to today; user-editable)
- Payer (exactly one participant)
- Participants in the split (one or more participants, defaulting to all)
- Labels (zero or more, see section Labels)
The system shall additionally and automatically assign:
- A stable expense UUID
- An entry timestamp recording the wall-clock instant at which the expense was recorded in the app (not user-editable)
The execution date and the entry timestamp shall be stored as two distinct
fields. Example: ice cream bought on 2026-04-22 but entered in the app on
2026-04-28T15:00:00Z yields execution date 2026-04-22 and entry timestamp
2026-04-28T15:00:00Z.
spending happen?" (matters for which trip/month it belongs to) versus
"when did this record enter the system?" (matters for sync/merge ordering
and audit trail).
and stored in ledger metadata. Per-expense currency override is out of scope.
shall be displayed but does not participate in any calculation.
overwrite the original record in place; it shall be stored as a new event
that supersedes the previous version of the expense (see SC-ARC-LOG-1).
tombstone event so that other devices converge on the deletion when they sync.
ledger state and is therefore visible to all users of that ledger. Labels are
not shared across ledgers.
(free-form string, max 40 chars, case-insensitive uniqueness within the
ledger). The system shall assign the label a stable UUID.
unchanged so that existing expense-to-label assignments are preserved.
recorded as a tombstone event. Expenses that reference a deleted label
shall continue to exist; the dangling reference shall be silently elided
from the derived state so the label simply no longer appears on those
expenses.
assignments converges cleanly without forcing the user to resolve a
conflict.
time and shall be able to add or remove labels via editing an existing
expense (see SC-FR-EXP-4). Label assignments are stored as part of the
expense record.
in the current ledger and for creating, renaming, and deleting labels from
that screen. The same screen shall show, for each label, the number of
expenses currently carrying it.
participants in the split, each participant's share shall be A/N rounded to
the smallest unit of the ledger currency.
remainder shall be assigned deterministically to the payer so that the sum of
shares always equals the expense amount exactly.
function of the ledger state.
user may exclude the payer at expense entry time, in which case the payer is
fully reimbursed by the other participants.
amount A owes B based on all non-deleted expenses and settlements in the
ledger. The computation shall be a pure function of the event log.
summary listing for each other participant whether the current participant
owes or is owed and how much.
to another, with an amount and a date. A settlement shall reduce the recorded
balance between the two participants accordingly.
re-routing of payments through third parties is out of scope.
newest first, showing at minimum: date, title, amount, payer, and split size.
the split, the amount each owes, the note (if any), the creator, and the
creation timestamp.
participant (as payer or as split member).
labels are selected, the list shall show expenses carrying any of the
selected labels (logical OR). The label filter shall be combinable with the
participant filter (logical AND between filter dimensions).
than AND ("show me anything tagged trip-paris or holiday-2026") and matches
the way most consumer apps handle multi-tag filtering. Revisit if user
testing disagrees.
either bound optional). The filter shall apply to the execution date
(SC-FR-EXP-1), not the entry timestamp.
Users who track their personal cashflow in apps such as WISO Mein Geld,
Quicken, YNAB, or similar need a way to pull the relevant slice of the
shared ledger into those tools without re-typing every line. This section
specifies that export path.
chosen participant's transactions. The export shall reflect only that
participant's individual money movements (see SC-FR-EXR-3), not the raw
shared expense list.
claimed by the current device (SC-FR-PRT-2). The user may override this
selection and export on behalf of any participant in the ledger.
selected participant P. Both modes share the same CSV schema (see
SC-FR-EXR-4) and the same sign convention (positive = inflow to P / debit
to P's external account that mirrors the export; negative = outflow /
credit). Deleted expenses and deleted settlements shall be excluded in both
modes.
Mode A - Cash basis:
Contains only real money movements involving P. Intended to be reconciled
against P's real bank or card account in the external finance app.
- Expense E paid by P: one row, amount = -E.amount, date = E.execution date.
- Expense E paid by someone else, P in the split: NOT exported.
- Settlement S where P paid: one row, amount = -S.amount, date = S.date.
- Settlement S where P received: one row, amount = +S.amount, date = S.date.
Mode B - Virtual account (share basis):
Intended to be imported into a dedicated "virtual" sub-account in the
external finance app, where the running balance represents P's net position
inside the shared ledger.
- Expense E in which P is the payer with share s out of total amount T:
one row, amount = +(T - s), date = E.execution date. The row is omitted when T = s (P is the sole participant in the split).
- Expense E paid by someone else, P in the split with share s: one row,
amount = -s, date = E.execution date.
- Expense E paid by someone else, P not in the split: NOT exported.
- Settlement S where P paid amount A: one row, amount = +A, date = S.date.
- Settlement S where P received amount A: one row, amount = -A, date = S.date.
The two modes are mutually exclusive within a single export: each invocation
of the export action produces exactly one file in exactly one mode.
in a personal finance app require different shapes of data:
(a) Reconcile against P's real bank/card account: cash basis, because that
is what the bank statement also shows.
(b) Track the shared ledger as a separate virtual sub-account: share
basis, because each joint expense should debit the virtual account by
P's share (driving its balance negative when P consumes without
paying) and each settlement should bring it back toward zero.
Both projections are pure functions of the same event log, so providing both
costs little and covers the two main user workflows. The MVP defaults the
mode selector to Mode A (cash) for new users; the most recently chosen mode
shall be remembered per device.
the following columns, in this order:
1. Date - ISO-8601 calendar date (YYYY-MM-DD)
2. Description - Expense title for expense rows; "Settlement to <name>" or "Settlement from <name>" for settlement rows
3. Amount - Signed decimal with two fractional digits, period decimal separator, no thousands separator. Positive = inflow to selected participant, negative = outflow.
4. Currency - ISO 4217 currency code, taken from the ledger (SC-FR-EXP-2)
5. Counterparty - For expense rows: comma-separated display names of the OTHER participants in the split (i.e. those who owe a share). For settlement rows: the display name of the other participant.
6. Labels - Semicolon-separated list of label display names attached to the source expense; empty for settlement rows.
7. Note - The expense note (SC-FR-EXP-3), with embedded CR/LF characters replaced by single spaces; empty for settlement rows.
8. ExpenseUUID - The stable expense or settlement UUID, so re-exports can be deduplicated by the importing tool.
Field values shall be RFC 4180 quoted (double quotes, doubled internally)
when they contain commas, double quotes, or newlines.
label filter (SC-FR-HIS-4) that is active in the UI at the moment the export
is initiated, so the user can scope an export to a single month, trip, or
label set.
download triggered from a Blob object and an anchor element carrying the
"download" attribute. Where the Web Share API with file support is
available (notably iOS Safari 15+), the app may additionally offer "Share
via..." to route the file into the OS share sheet (Mail, Files,
third-party apps).
The default filename shall follow the pattern
"splitclone_<ledger-slug>_<participant-slug>_<mode>_<YYYYMMDD-HHMMSS>.csv",
where <mode> is "cash" for Mode A and "virtual" for Mode B (see
SC-FR-EXR-3). Including the mode in the filename prevents users from
accidentally importing the wrong projection into the wrong account.
statements (CSV, MT940, CAMT.053), or other SplitClone instances. Data flow
between SplitClone and other tools is one-way (export only) in the MVP.
opened or brought to the foreground, and additionally at a periodic
interval while it remains in the foreground and connected, so that a
peer's change appears without any manual action. It shall push local
changes to the shared folder within 10 seconds of the user committing
them, network permitting. Periodic polling shall pause while the app is
hidden or offline (the backend offers no push channel, so a bounded poll
is the propagation mechanism; the interval is a tunable constant balancing
freshness against request volume and battery, SRS Q2).
pull and push cycle.
syncing, offline, or sync error (with the error reason).
approved by the project owner before implementation, per the
SC-ARC-FMT-3 pre-1.0 rule. The backup file is a
file-format member, SC-ARC-FMT-1 item (g); its on-the-wire schema is
documented field-by-field in docs/backup-format.md, which is the
authoritative reference for a future hand-restore.
complete local application database needed to reconstruct app state on
another device: every ledger's full event log, its data key in join-code
form, key fingerprint, optional sync hint and recovery-acknowledged flag.
Export is whole-database, not per-ledger; it is distinct from the
per-ledger CSV export (SC-FR-EXR-*), which is a lossy accounting report.
A ledger whose data key is not present on this device cannot be restored
and shall be omitted from the file, with the user informed.
restore: (a) merge - union the selected ledgers into existing state, which
shall be idempotent (re-importing the same file changes nothing) and shall
never overwrite an existing ledger whose id matches but whose key differs
(key conflict is reported and skipped); or (b) replace - wipe the local
database, then restore the selected ledgers (clean device migration). The
user shall be able to select which ledgers in the file to restore.
(JSON). It contains every ledger's join code (data key) in clear text;
anyone holding the file can decrypt every ledger in it. This is an
explicit, accepted exception to SC-ARC-ENC-*. Securing the exported file
is the user's responsibility, not the app's. The app shall state this
prominently at export time and require an explicit acknowledgement before
producing the file, and shall repeat the warning inside the file itself.
The app shall never transmit the backup to any network endpoint.
event-schema version its contents use. On restore the app shall refuse
loudly (no partial or best-effort import) if either exceeds the version
the app understands, telling the user to update the app; older versions
shall be accepted. Every event in the file shall be re-validated through
the canonical decoder before any data is written; any failure aborts the
whole restore. (Mirrors SC-ARC-FMT-2 for item (g).)
shall never adopt the identity of the device that wrote the backup
(SC-ARC-IDN-1, SC-ARC-LOG-1 sole-writer). Restored events retain their
original authorship in history; new events append under the restoring
device. After a restore the user re-claims their participant per ledger
via the normal multi-device claim flow (SC-FR-PRT-2 (c)).
shared state is stored. They exist to make the design tractable on top of a
consumer file-sync service that offers no atomic multi-writer primitives.
"SharedFolderProvider" interface. The interface shall be defined narrowly
enough that multiple OneDrive back-ends (Microsoft Graph API; iOS Files /
Document Picker) and future non-OneDrive back-ends (iCloud Drive, Dropbox,
etc.) can coexist as independent implementations selectable at runtime,
with no changes required in the rest of the app when a new back-end is
added.
Graph API and the iOS Files picker without committing to one in the spec.
Confirmed in a later iteration: both shall be shipped in the MVP where the
host platform permits (see SC-ARC-PRV-3).
Microsoft Graph API provider that authenticates the user via OAuth 2.0
(Authorization Code with PKCE) and accesses the ledger folder directly
through the Graph REST API. ETags returned by Graph shall be used to
implement the conditional-write precondition in SC-ARC-PRV-2.
The iOS Files / Document Picker provider that was considered alongside
Graph API in earlier drafts is not buildable in a web app and is
explicitly removed from MVP scope.
SPA cannot integrate with the iOS Files app the way a native app can, so
the dual-provider plan collapses to Graph-only. If the project later
migrates to a native app (see Roadmap), the dropped provider can be
revived under the same SharedFolderProvider interface.
- List files in the ledger folder (with last-modified timestamps and ETags or equivalent)
- Read a file by name
- Create or replace a file by name, with an optional precondition (ETag / If-Match)
- Delete a file by name
All operations shall be asynchronous and shall report transport errors
distinctly from semantic errors.
per-device, append-only event logs stored in the ledger folder. Each device
shall write only to log files that it owns (named with that device's UUID,
see SC-ARC-LOG-4). No device shall ever write to another device's log
files.
atomic multi-writer semantics. Because no two devices ever write the same
file, the provider never has to merge concurrent edits and never produces
"filename-DESKTOP-XYZ" conflict copies for the data files.
- LedgerRenamed (changes the ledger's human-readable name; sensitive content lives here, not in the plaintext metadata file - see SC-FR-LED-3)
- ParticipantAdded
- ParticipantRenamed
- ParticipantClaimed (binds a device UUID to a participant UUID)
- LabelCreated
- LabelRenamed
- LabelDeleted (tombstone)
- ExpenseCreated (payload includes execution date, entry timestamp, and label UUID list)
- ExpenseUpdated (carries a full new version of the expense, including its label UUID list)
- ExpenseDeleted (tombstone)
- SettlementRecorded
- SettlementUpdated (carries a full new version of the settlement)
- SettlementDeleted (tombstone)
Each event shall carry: event UUID, event type, author device UUID, author
participant UUID, wall-clock timestamp (which is the entry timestamp for
expense events), schema version, and a type-specific payload.
per line, no trailing comma, newline-terminated). Files shall be
append-only: existing lines shall never be modified or removed, only new
lines appended. No compression shall be applied; segments are stored as
plain UTF-8 text.
the ledger folder. Segment files shall be named
"events/<device-uuid>/<segment-open-timestamp>.jsonl", where the timestamp
is the UTC instant the segment was opened, formatted as
"YYYYMMDDTHHMMSSsss" (millisecond precision) so that lexicographic file
ordering matches chronological order.
A device shall have at most one "open" segment per ledger at any time; all
other segments belonging to that device are "closed" and immutable. New
events are appended to the open segment. When the open segment's size on
disk would exceed the segment size threshold (SC-ARC-LOG-5) after writing
the next event, the device shall:
1. Close the current segment (no further writes to it, ever).
2. Open a new segment with the current UTC timestamp as its filename.
3. Append the new event to the new segment.
A device's full event log is reconstructed by reading every segment file
under that device's folder, sorted by filename. The concatenation is the
device's complete, append-only history.
"append to file" primitive. Every write rewrites the whole file. Without
segmentation, the cost of adding a single expense grows linearly with the
total log size, becoming user-visible after a few years of use on poor
mobile connections.
Segmentation caps the per-append upload at one segment's worth of bytes,
keeping write latency bounded regardless of the ledger's age.
This value is a configurable constant in the implementation, not a wire
format invariant: a device that uses a different threshold remains fully
interoperable with devices using the MVP value, because every device folds
all segments regardless of how their boundaries were drawn.
segment, which corresponds to ~1-2 years of activity for an active ledger
and ~5 years for a quiet one - so segment count stays low even over the
long term. 1 MiB uploads in ~1-3 seconds on LTE and ~10 seconds in
worst-case cellular conditions, which is comfortably within the
"committed write" UX budget. Round power-of-two value chosen for
simplicity.
compression that would reduce or transform historical event data:
- Closed segments shall be retained verbatim for the lifetime of the
ledger. They shall never be merged, rewritten, or deleted by the app.
- Tombstone events (ExpenseDeleted, SettlementDeleted, LabelDeleted)
shall remain in the log alongside the events they tombstone.
- No segment shall be compressed (gzip, zstd, etc.) at rest.
- The derived-state cache (SC-ARC-CCH-1) is a performance optimisation
only; it shall be reconstructible at any time by folding all segments
from scratch.
The full event history is the source of truth and shall remain forensically
reconstructible from the segment files alone.
The expected lifetime data volume per ledger (a few tens of MiB over
decades, see SC-ARC-LOG-5 rationale) is small enough that compaction or
compression buys nothing meaningful but introduces irreversibility risks
(a buggy compaction could lose audit trail forever) that the project
explicitly refuses to accept.
a deterministic order (events sorted by wall-clock timestamp, breaking ties
by event UUID lexicographic order). Given the same set of event log files,
all devices shall compute the same derived state.
expense, the event with the later wall-clock timestamp shall win in the
derived state. Ties shall be broken by event UUID lexicographic order. The
loser's event shall remain in the log for auditability.
are out of scope for the MVP. Last-write-wins is acceptable because expense
records are small and concurrent edits to the same expense are expected to
be rare.
of the per-segment read offsets (or hashes) used during the last fold.
Re-syncing shall only re-read segments whose remote state has changed, and
within a changed segment shall only fold the new tail. Closed segments
(SC-ARC-LOG-4) are immutable, so once their hash has been recorded they
shall not be re-fetched again.
it in browser IndexedDB under the app's origin. This UUID shall be the
device's identity across all ledgers and shall not leave the device
except as the directory name of that device's log-segment folder inside
each shared OneDrive folder (SC-ARC-LOG-4).
If the IndexedDB record is lost (browser data cleared, private-browsing
session, iOS Safari long-disuse eviction, fresh browser install), a new
device UUID shall be generated on the next launch. The user shall be
prompted to claim a participant identity again as if it were a new device
(SC-FR-PRT-2). The previous device's log segments in the shared folder
remain valid history and shall continue to contribute to the derived
state; they simply receive no further appends.
A new device id does not have to mean a new participant: the same person
re-running the app (after eviction, or simply on an additional device)
re-claims their existing participant via SC-FR-PRT-2 (c). The participant
then has both device ids bound to it; balances and "you" attribution stay
on the one identity. Authorship is still per device id (SC-ARC-IDN-2);
only the participant↔device binding is many-to-one.
(e.g. OneDrive user account) to identify the author of an event. Authorship
shall be established entirely by the device UUID and the claimed participant
UUID inside the event payload.
different devices. Decoupling event authorship from the provider identity
keeps the model correct in that case.
shall be distributed as immutable static assets from a third-party static
hosting provider. The current target is GitHub Pages; equivalent
alternatives such as Cloudflare Pages, Netlify, or any S3-equivalent
object store are acceptable substitutes. The choice of host shall not
require code changes; only the deployment URL changes.
The project shall not operate any application server, database server,
job queue, or other persistent backend infrastructure. All ledger state
lives exclusively in (a) per-user OneDrive folders, accessed via Graph
(SC-ARC-PRV-3), and (b) per-device browser storage (SC-ARC-IDN-1,
SC-NFR-SEC-2).
zero server cost, zero data-residency liability for the project, and a
trust model in which only Microsoft (OneDrive) and the static host
(GitHub or equivalent) sit between the user and their data. The static
host sees only standard web-access metadata - never the ledger contents -
because the JavaScript runs entirely in the user's browser and talks to
Graph directly.
from a public source repository. The repository shall include the build
configuration, dependency lockfiles, and a release procedure that pins
the exact commit deployed for each released version. Subresource Integrity
(SRI) hashes shall be used on all script and style tags whose contents
are loaded from the static host, so a tampered asset is detected and
refused by the browser.
JS that steals OAuth tokens or the data key) is mitigated by giving
anyone the ability to verify that the served bundle matches the public
source. SRI further ensures a silently-modified asset cannot execute in
the browser.
the device shall generate a fresh random 256-bit AES key (the "data key")
using the browser's cryptographically secure random number generator
(crypto.getRandomValues). The data key shall never be transmitted to or
stored on the shared folder provider in any form. It shall exist only
in volatile memory on devices that have joined the ledger and in
IndexedDB on those same devices (see SC-ARC-ENC-5).
The MVP shall not offer an "unencrypted ledger" option. Encryption is
mandatory.
- Two formats (encrypted and plaintext) doubles the code paths in the
storage layer for negligible benefit; the encryption cost is small.
- A user opting out of encryption is almost always doing so by accident
or under-estimation of the threat; the default should be the safer
one and there is no compelling case for the unsafer alternative.
AES-256-GCM using the ledger's data key. The on-disk file format for an
encrypted segment shall be:
- 12 bytes: a freshly-generated random IV (nonce)
- N bytes: the GCM ciphertext, where N equals the plaintext JSONL length
- 16 bytes: the GCM authentication tag
A fresh IV shall be generated for every encryption operation, including
every re-upload of the same logical segment after an append. Random 96-bit
IVs are acceptable because the collision probability over the lifetime of
any realistic ledger is negligible.
Decryption shall fail loudly on auth-tag mismatch. A segment that fails
to decrypt shall be reported as an error to the user, never silently
skipped, because it indicates either a wrong key, a corrupted file, or
tampering.
No plaintext segment file shall ever leave the device.
in a single primitive, available natively in every modern browser via
crypto.subtle. Encrypting whole segments (rather than individual JSONL
lines or individual fields) is the simplest layering: the storage codec
wraps the existing fold algorithm and the rest of the system stays
oblivious to the fact that data is encrypted.
joining device can identify the folder as a SplitClone ledger before
holding the key. The metadata file shall include a "key fingerprint":
the lowercase hexadecimal representation of the first 16 bytes of
SHA-256(data_key).
When a user enters a join code, the app shall compute the fingerprint of
the imported key and compare it to the stored fingerprint. A mismatch
indicates a wrong join code and shall be reported as such; the key shall
not be persisted in IndexedDB until the fingerprint matches.
The truncated SHA-256 does not reveal the key (256-bit input space, only
128 bits of output displayed) and is short enough to log or display in
diagnostics if needed.
the key as a join code in two interchangeable forms:
(a) A QR code suitable for camera scanning device-to-device.
(b) A base64url-encoded string with a 4-character truncated-SHA-256
checksum appended (so typos are detected before the fingerprint
check in SC-ARC-ENC-3).
A device that has not yet joined a ledger shall accept the join code via
either form (scan-to-camera or paste). On successful import, the
fingerprint check (SC-ARC-ENC-3) shall pass before the key is persisted.
The join code shall never be transmitted by the app to any network
endpoint. It shall be communicated between users entirely out-of-band
(messaging app, in-person QR scan, etc.). The app shall include a clear
in-UI warning that the join code grants full access to the ledger's
contents and shall be shared only over a channel the user trusts.
IndexedDB under the app's origin, scoped to the ledger UUID. Where the
browser supports it, the key shall be imported as a non-extractable
CryptoKey via crypto.subtle.importKey so that crypto.subtle.exportKey on
the resulting object fails. The decryption and encryption operations
continue to work with non-extractable keys; only direct read-out of the
raw key material is prevented.
If the IndexedDB record holding the key is lost (browser data cleared,
private-browsing session, iOS Safari long-disuse eviction, fresh browser
install), the device shall be unable to read the ledger and the user
shall be required to re-enter the join code.
or a developer mistake that exfiltrates "everything in IndexedDB" will
get an opaque key handle, not the raw 32 bytes. It is not a defence
against a fully-compromised app bundle (the malicious script can still
encrypt/decrypt with the key on the device's behalf) - that threat is
addressed by SC-ARC-HST-2.
until acknowledged, the app shall prominently prompt the user to save
the join code as a recovery code in a safe place (their password
manager, a downloaded file, a printed copy). The prompt shall remain
visible until the user explicitly confirms they have saved it.
The app shall make it easy to:
- Reveal the current data key as a join code on demand from a joined
device.
- Download the join code as a small text file.
- Copy the join code to the clipboard.
The user shall be able to re-display the prompt at any later time from
the ledger settings.
device that ever held the key has since cleared its storage". A pushy
recovery-code prompt at the moment the key first exists, plus an easy
on-demand re-display, is the standard mitigation. It accepts a small UX
nag in exchange for avoiding the worst-case "ledger irretrievably lost"
outcome.
segments under a new key, or revoking a member's access to an existing
ledger. A user who needs to revoke a former member shall be advised, via
in-UI guidance, to create a new ledger and re-enter the relevant expense
history manually (or by exporting and re-importing, when the relevant
import feature exists - see Roadmap).
key (which violates the "closed segments are immutable" rule in
SC-ARC-LOG-4) or moving to a per-member envelope-encryption scheme
(Option C in the design discussion). Both are large complications for a
case that, in a 2-10-person trusted-group ledger, is rare. Deferring is
the right MVP trade.
app reads from or writes to non-volatile storage shared with users or
external tools. Specifically:
(a) The plaintext metadata file structure (SC-FR-LED-3).
(b) The event-log segment directory layout and filename pattern (SC-ARC-LOG-4).
(c) The encrypted segment envelope (IV / ciphertext / GCM tag, SC-ARC-ENC-2).
(d) The plaintext JSONL event schema inside a segment, including the payload schema of every event type (SC-ARC-LOG-2).
(e) The join-code encoding, in both QR and base64url-with-checksum forms (SC-ARC-ENC-4).
(f) The CSV export schema (SC-FR-EXR-4).
(g) The local backup file schema (SC-FR-BAK-1; docs/backup-format.md).
A single monotonically increasing integer schema version (declared in the
metadata file, SC-FR-LED-3) shall identify the current snapshot of the
format as a whole. A change to any of (a) - (f) requires incrementing the
schema version.
Item (g), the backup file, carries its OWN independent monotonically
increasing version integer ("splitcloneBackup"), declared inside the
backup file itself, plus an embedded copy of the (a)-(f) event-schema
version its contents are written in. It is versioned separately because a
backup is a self-describing one-shot artefact, not part of the
continuously-synced shared folder; a backup reader gates on those two
integers exactly as SC-ARC-FMT-2 gates the shared format (refuse a newer
file loudly; accept older). All other governance (SC-ARC-FMT-3,
changelog) applies to (g) unchanged.
The local browser-side caches (IndexedDB layout, derived-state cache
structure) are NOT part of the file format. They are private to a single
device and may evolve freely as long as the user-visible behaviour is
preserved.
SC-ARC-FMT-3 hinge on what counts as "the format". Anything visible to
another device or to an external tool counts; anything purely local does
not.
in the metadata file (the "ledger version") against the highest schema
version it has been built to understand (the "device version"):
- If ledger version > device version: the device shall refuse to fold,
read, or write to the ledger. It shall display a clear error
explaining that the ledger was written by a newer version of the app
and shall suggest the user update the app. It shall not attempt
partial reads or "best effort" parsing.
- If ledger version == device version: normal operation.
- If ledger version < device version: the device shall read all events
and segments and shall perform any necessary upgrades to the older
schemas in memory only. New events shall continue to be written at
the ledger's current schema version (i.e. the older version), so
that other devices still on the older app continue to participate.
The ledger version shall never be incremented implicitly. Promoting a
ledger to a newer schema version shall be an explicit user action
("Upgrade ledger format") that re-confirms compatibility implications
and warns that older devices will stop being able to read the ledger
until they update.
Closed segments (SC-ARC-LOG-6) shall never be rewritten to convert their
schema version. Schema upgrades apply to new events going forward, not
to history.
half-understanding device might silently corrupt structure it does not
fully comprehend. "Continue writing at the ledger's version when the
device is newer" is the rule that lets a group upgrade their phones one
at a time without breaking the ledger for everyone else.
SC-ARC-FMT-1 shall be treated as stable. After v1.0:
- Any proposed change to the file format requires explicit, recorded
approval from the project owner before implementation begins. Format
changes shall not be introduced as part of unrelated work or as a
side effect of refactoring.
- The project shall maintain a written, public changelog that
enumerates every defined schema version, the changes between
consecutive versions, and the in-memory migration logic that an
upgraded device applies when reading the older version.
- Schema-version increments shall be infrequent and shall batch
related changes together where possible, rather than incrementing
repeatedly for small additions.
Before v1.0, the format may evolve more freely, but every change shall
still be a deliberate decision and shall update the changelog.
OneDrive folders that the project does not control. Once that data
exists, changing the format silently or casually risks rendering
real ledgers unreadable or, worse, subtly corrupted. The cost of
discipline here is paid in slightly slower velocity for format changes;
the benefit is that users' historical data remains readable for the
lifetime of the project.
browsers, with no native iOS, Android, or desktop binary. Minimum
supported targets are:
- Mobile: iOS Safari 17+ (covers iPhone and iPad), Chrome on Android 14+
- Desktop: current and previous major versions of Chrome, Edge, Firefox, Safari
The app shall be installable to the home screen via Safari's "Add to Home
Screen" on iOS and via the standard PWA install prompt on Chromium
browsers, including a Web App Manifest declaring icon, name, theme
colour, and standalone display mode.
Program cost and to obtain Android/iPad/desktop support for free. iPhone
remains the primary target form factor; the others come along because the
runtime is the browser.
~320 px (smallest iPhone) up to typical desktop browser widths. iPad and
desktop are not separate code paths; they are simply wider breakpoints of
the same layout. The phone-portrait viewport is the design baseline; other
breakpoints are progressive enhancements.
internet connection. Local changes shall be queued in IndexedDB and pushed
to OneDrive automatically once connectivity is restored. A registered
Service Worker shall cache the static app shell so that the app launches
when offline.
Known limitation: iOS Safari evicts PWA storage (IndexedDB and Service
Worker caches) after extended periods of non-use (currently around 7
weeks). Users who open the app at least every few weeks will not be
affected; users returning after a long absence may need to re-download
the app shell and re-authenticate. The app shall detect lost local state
gracefully (re-fetch from OneDrive, re-prompt for OAuth) rather than
data-loss.
render within 1 second on supported iPhone hardware. Network sync may
complete asynchronously after the UI is interactive.
under a per-ledger 256-bit AES-GCM data key (see SC-ARC-ENC-1 through
SC-ARC-ENC-7). The only plaintext file in a ledger folder shall be the
minimal metadata file specified in SC-FR-LED-3.
The shared folder provider's own access controls (folder sharing,
permissions) remain in force and are the first line of defence; the
at-rest encryption layer is additional protection against accidental
over-sharing of the folder and against compromise or insider access at
the provider level. The two layers protect against different threats and
neither replaces the other.
Members who legitimately hold the data key (every joined device) can
read the full ledger. Encryption is not intended to compartmentalise
access between members of the same ledger.
the device UUID shall be stored in browser-local storage scoped to the
app's origin: IndexedDB for refresh tokens and the device UUID, in-memory
or sessionStorage for short-lived access tokens. None of these values
shall ever be written to the shared OneDrive folder.
The MVP shall use the OAuth 2.0 Authorization Code flow with PKCE (no
client secret), as appropriate for a public single-page application.
Refresh tokens shall be requested with the smallest scope set that
satisfies the SharedFolderProvider operations (SC-ARC-PRV-2).
The MVP acknowledges that browser-local storage is materially weaker than
a hardware-backed secure enclave (e.g. iOS keychain):
- IndexedDB contents are readable by any JavaScript executing on the same origin (XSS risk).
- No biometric gating is available to the browser.
- Browser extensions with sufficient permissions may inspect storage.
Mitigations: a strict Content Security Policy restricting which sources
can execute scripts; no third-party analytics or trackers (SC-NFR-PRV-1);
keeping refresh-token lifetime as short as the OAuth provider permits;
clearing tokens on sign-out.
downgrade. Future migration to a native iOS or Android app would restore
hardware-backed storage; the spec leaves that door open.
party. Diagnostic information shall remain on-device unless the user
explicitly exports it.
user-facing strings through an iOS localisation mechanism so that other
languages can be added later without code changes.
but are not currently committed:
- Multiple groups / multiple concurrent ledgers per user
- Unequal splits (by share, by percentage, by exact amount)
- Multi-currency expenses and FX conversion
- Aggregate reporting and charts by label (the MVP supports user-defined labels and filtering by label, but not totals/breakdowns)
- Receipt OCR and image attachments
- Debt simplification (re-routing payments through third parties)
- In-app payment integration (PayPal, Venmo, etc.)
- Recurring expenses
- Import from external finance / banking files (the MVP only exports, see SC-FR-EXR-7)
- Additional export formats (QIF, OFX, MT940, CAMT.053, Excel)
- Per-row double-entry export (one debit row + one credit row per transaction); the MVP collapses each transaction into a single signed row in both Mode A and Mode B
- Push notifications between users
- Shared-folder providers other than OneDrive (iCloud Drive, Dropbox, plain WebDAV)
- Native iOS / Android app, which would restore hardware-backed secure storage for both OAuth tokens (SC-NFR-SEC-2) and ledger data keys (SC-ARC-ENC-5) and would unlock the iOS Files / Document Picker provider previously envisaged in SC-ARC-PRV-3. Requires a paid Apple Developer Program subscription and is deferred until budget priorities change.
- Per-member envelope encryption with per-user keypairs, enabling key rotation and member revocation without rebuilding the ledger (option C in the encryption design; see SC-ARC-ENC-7).
phase must close before implementation:
- Q1: RESOLVED (2026-05-14). Following Q7's resolution in favour of a
web app, only the Microsoft Graph API provider is implementable and
is therefore the sole MVP SharedFolderProvider (SC-ARC-PRV-3). The
iOS Files / Document Picker provider is deferred to a potential
future native-app release (see Roadmap).
- Q2: RESOLVED (2026-05-14). No compaction, no snapshotting, no
compression - all historical events are kept verbatim
(SC-ARC-LOG-6). The performance concern around large files is
addressed instead by segmenting each device's log at 1 MiB
(SC-ARC-LOG-4 / SC-ARC-LOG-5), which caps the per-append upload cost
without losing data. Threshold is implementation-tunable and does not
affect interoperability.
- Q3: RESOLVED (2026-05-14). Schema-version handling is specified in
SC-ARC-FMT-2: refuse loudly when reading a newer version; migrate
in memory only when reading an older version; never rewrite closed
segments to change their version. The complementary governance rule
(any post-v1.0 format change requires explicit project-owner
approval) is captured in SC-ARC-FMT-3.
- Q4: UX for resolving the case where two users add a new participant with
the same display name independently (currently treated as two distinct
participants because UUIDs differ).
- Q5: Whether the per-device "last used export mode" memory (SC-FR-EXR-3)
should be replaced by a per-ledger or per-participant default once
users have a sense of how often they switch modes.
- Q6: Whether to ship a WISO-Mein-Geld-tuned CSV variant (semicolon
delimiter, comma decimal separator, possibly localised header names) in
addition to the generic RFC 4180 CSV defined in SC-FR-EXR-4, or whether
the WISO generic CSV import mapper is sufficient.
- Q7: RESOLVED (2026-05-14) - web app (PWA). The MVP is a Progressive
Web App distributed as static assets (SC-NFR-PLT-1, SC-ARC-HST-1).
The decision was driven by the desire to avoid the paid Apple
Developer Program; it also brings iPad, Android, and desktop support
for free. Accepted trade-offs are recorded in the corresponding
requirements:
- Weaker secret storage than the iOS keychain (SC-NFR-SEC-2).
- Trust in a static-host CDN, mitigated by a verifiable build with
SRI (SC-ARC-HST-2).
- iOS Safari PWA storage eviction after long disuse (SC-NFR-OFF-1).
- Loss of the iOS Files / Document Picker provider (SC-ARC-PRV-3,
Q1).
Hybrid options (Capacitor, React Native, Flutter, Tauri Mobile) were
considered and deferred; revisiting any of them would require a paid
Apple developer account for iOS distribution, which is the cost the
web-app choice exists to avoid.
- Q10: Splitwise-Export-Import. The app shall be able to parse a CSV
file exported from Splitwise and create (or restore into) a SplitClone
ledger from it - participants, expenses, splits and settlements
reconstructed as the equivalent SplitClone events. Stated user intent:
(a) Input is the Splitwise CSV export. The exact column layout /
dialect will be supplied later; the parser must be written
against that real format, not guessed.
(b) Open: whether the import creates a brand-new ledger or can also
merge into an existing one; how Splitwise rows map onto the
event model (ExpenseCreated / SettlementRecorded / participants /
labels); currency handling (MVP is single-currency EUR,
SC-FR-EXP-2); and how unmapped Splitwise concepts are surfaced.
(c) This is an import mapper over user-provided data, not a new
member of the SplitClone file format (SC-ARC-FMT-1) - it writes
ordinary events through the existing model, so no schema-version
change. It is, however, a deliberate feature and will get its
own requirement(s) once the format is known.
Out of MVP scope; recorded so the design is deliberate when scheduled
and the exact Splitwise format is available.