Architecture
Eutepio rests on three decisions: a local database as the single source of truth, sync on the local network only, and a strict split between shared and personal data.
Offline-first
Every device holds its own complete copy of the library. There is no server that owns the data and no account it lives under. Sync is how you keep the band on the same page during a rehearsal or a gig — not a backup, and not cloud storage.
Consequences:
- The app works without internet
- Connection quality has no effect on stage behaviour
- You have to make your own backups — that is what the
.eutbackupformat is for - Lose the phone without a backup and you lose the library
Storage
The database is Isar (community fork, version 3), an embedded database running inside the app process.
Since 0.2.0 it is not one database but one database per library plus a separate registry:
| Where | What lives there |
|---|---|
| Registry | The list of libraries and your user profile — name, instrument, stage appearance, pedal and MIDI mappings |
| Library | Songs, setlists, annotations and that library's own state |
The profile is therefore global: switching library does not rearrange your stage or your controls. Details in Multiple libraries.
| Platform | Database location |
|---|---|
| Android, iOS | app documents directory |
| Windows, Linux, macOS | app support directory |
Binary attachments are not stored in the database. PDFs, MusicXML and images are written as files into
pdfs/, musicxml/ and images/ subfolders; the database only keeps a relative path. Each library has its own attachment root.Resilience
- If the database fails to open, the damaged file is set aside under a name ending in
.corrupt-<timestamp>and the app starts with a fresh database instead of crashing. - The database records the schema version it was last written with. Opening an older build of the app on top of a newer database locks the app on a warning screen instead of corrupting data.
- The app distinguishes a locked database (another process holds it — it retries) from a corrupted one (it sets it aside), so a locked file is never discarded needlessly.
Data model
Song
The shared part, the one the whole band has a reason to know:
| Field | Type | Note |
|---|---|---|
title, artist | text | required |
variantName | text | optional variant, part of the song's identity |
key | text | musical key |
bpm | number | tempo |
timeSignature | text | time signature |
capo | number | defaults to 0 |
tags | list of text | indexed for search |
chordProContent | text | the song body in ChordPro |
durationSeconds | number | length |
version | number | increments when shared fields change |
createdAt, updatedAt, deletedAt | timestamp | deletion is soft — the record stays and is only flagged |
On top of that, three identifiers used for sync and backups:
syncId (unique), matchKey (derived from title, artist and variant) and contentHash (SHA-256 of the shared fields).Setlist
name, eventDate, venue, startTime, estimatedDurationMin, songIds (ordered list), breaks, songNotes, customFieldsJson, version, createdAt, deletedAt and a local sortOrder.⚠
A setlist has no
syncId — across devices it is matched by name and event date alone. Rename the same setlist on one device and sync will produce a duplicate on the other.Shared and local fields
Alongside the shared fields, a song carries a set of per-device values that never sync and can never be overwritten by anyone else:
privateNotes— your personal notes on the songlocalTranspose,localTransposeXml— chord and MusicXML transpositionfontSizeScale— text size- auto-scroll speeds, stored separately for each of the five display modes
defaultViewMode,pdfStartPage,pdfFitWidth,musicXmlZoom- attachment paths (
pdfPath,musicXmlPath,imagePaths) and shelved alternative versions midiJson— the MIDI command sent when the song opens
Annotations are local outright — they never enter sync at all, although you will find them in a backup.
ℹ
This split is why your guitarist cannot overwrite your transposition or your notes. The singer can keep the same song a third higher and it bothers nobody.
Sync layers
1. Discovery
The leader announces itself on the local network over mDNS/Bonjour under the service
_eutepio._tcp. A fallback scan of the local address range runs in parallel so the session can be found on networks where multicast does not work. You can also join by scanning a QR code or typing the IP address by hand.2. Transport
The leader starts an HTTP server on port 8765. If that port is taken it tries 8766 through 8769 in turn. The server exposes four endpoints — session announcement, join, WebSocket upgrade and a health check. The actual traffic then runs over the WebSocket.
3. Application protocol
Messages are JSON objects with a
command key. The leader broadcasts them and members execute them; a member that wants to reach the others sends the message to the leader, who re-stamps it and forwards it. Every message carries the leader's clock stamp and an executeAt time (300 ms in the future by default) so all devices act at the same moment.The full command list is in the Leader/member model.
What leaves the device
To be precise, because "data never leaves your device" would not be true:
| Situation | What is sent | Where |
|---|---|---|
| Sync | Songs, setlists and attachments that members request | Other devices on the same local network |
| Automatic backup | A full library backup | Into a folder you pick yourself — which may well be one that syncs to a cloud |
| URL import | Only an HTTP GET to the address you typed | The server you specified |
| First setlist PDF export | Downloads the Noto Sans font | Google Fonts |
What is never sent anywhere: no analytics, no crash reports, no telemetry, no account. The app contains no Firebase, Supabase, Sentry or anything comparable.