DEVELOPER TUTORIAL
Run the source and trace one change.
1. Run the full source loop
source_unpack=$(mktemp -d /tmp/lettres-0.20.0.XXXXXX)
tar -xzf lettres-0.20.0.tar.gz -C "$source_unpack"
cd "$source_unpack/lettres"
npm test
node --test relay/test/runtime-config.test.mjs relay/test/relay-compatibility.test.mjs relay/test/connection-policy.test.mjs relay/test/update-policy.test.mjs relay/test/service-worker-lifecycle.test.mjs relay/test/notification-policy.test.mjs relay/test/render-policy.test.mjs relay/test/viewport-policy.test.mjs relay/test/sync-side-effects.test.mjs relay/test/v20-source-contract.test.mjs relay/test/relay.test.mjs relay/test/release.test.mjsUse a fresh directory for every archive so two releases cannot be combined accidentally. Node 24 is required; the browser is a no-build ES-module PWA. This focused command covers 0.20 ownership; run npm test before packaging. Its VM/source checks do not replace a real browser worker/PWA run, native picker, OS notification, physical pinch/keyboard, suspension, or physical Android/iOS/postmarketOS test.
2. Follow the owners
A normal send crosses web/conversation-drafts.mjs → web/outgoing.mjs → web/db.mjs → the bounded preparation worker in web/app.mjs → web/sync.mjs. The stable operation is visible before expensive work and persists its exact account, sender/recipient key, submission order, reply, and source files. The shared projection keeps every nonterminal operation until its matching confirmed message is actually present, and rejects a delayed IndexedDB snapshot that is older than the in-memory state. Reload resumes preparing records. Album outbox rows are grouped by operation: every resource part must be acknowledged before the final publication row is eligible. RECIPIENT_UNKNOWN moves the same prepared envelopes into recoverable waiting; authorization lets bounded reconnect publish them idempotently. Cancellation stays definitive until final publication starts, then uses honest delivery-unknown semantics.
Audio preparation measures the encoded bytes and stores finite seconds on the normal clone-safe outgoing record and encrypted manifest. web/audio-player.mjs is shared by conversation, pending, and Shared-media cards; its slider writes the active media element's currentTime, media events read it back, and bounded WebM metadata repair/error handling prevents an endless loading label. Known duration may display immediately, while seek waits for media readiness.
The browser owns keys, encryption, IndexedDB, rendering, outbox, and media reconstruction. Node 24 plus built-in SQLite owns signature/role validation, append sequencing, receipts, tombstones, enrollment, filtered REST pages, and SSE wake-ups.
Canonical layers
shared/crypto.mjs identities, contacts, envelopes
shared/events.mjs all message/state types and body rules
shared/message-state.mjs deterministic edit/delete projection
shared/media.mjs manifests, chunks, albums, verification
relay/src/store.mjs SQLite and registration transitions
web/db.mjs local replica and suppression
web/sync.mjs transport, outbox/pages, reconnect
web/sync-side-effects.mjs ordered refresh and isolated notification hook
web/outgoing.mjs durable send lifecycle and timeline merge
web/contact-policy.mjs pending, delete and block boundaries
web/composer.mjs one-line empty sizing
web/selection-policy.mjs native selection and visual-viewport toolbar placement
web/emoji-policy.mjs renderer and safe fallback diagnostics
web/media-state.mjs availability, diagnostics, outbox media order
web/audio-player.mjs duration validation, readiness, real seeking
web/identity-import.mjs actionable identity-input classification
web/conversation-drafts.mjs revision-safe device-local drafts
web/recipient-readiness.mjs exact waiting classification/backoff
web/reply-preview.mjs type-aware stable-reference summaries
web/viewer-fit.mjs drawable-area minimum image fit
web/service-worker-policy.mjs app/document/network cache routing
web/runtime-config.mjs validated local startup configuration
web/relay-compatibility.mjs directional relay contract
web/connection-policy.mjs actionable connection reasons
web/update-policy.mjs candidate identity and action states
web/render-policy.mjs transport-independent keyed render policy
web/viewport-policy.mjs shared visual-viewport CSS-pixel geometry
web/notification-policy.mjs worker presentation readiness and grouping
web/browser-recovery.mjs scoped cache refresh and deliberate full reset
web/appearance.mjs local/shared/global precedence
relay/admin/app.mjs background admin refresh and action merge
issue-bot/* store, deterministic service, transport, CLI
Protocol and persistence
chat.message.edit and chat.message.delete remain signed encrypted append-only controls. Readers accept 1.3–1.6 and writers produce protocol 1.7. Relay/browser schema stays 2. Issue schema 2 additively stores bot workflow grants and bounded attempt windows while retaining issues, contacts, cursors, flows, processed IDs, and reply intent.
The exact media threshold is 16777216 bytes per original file, not per album. Files become independent 192 KiB encrypted envelopes. Bot integration tests send a fresh approved human through the real relay, persist the signed sender contact, accept a durable reply, and decrypt it as the human.
Render, notification, and resume boundaries
Routine syncing/online phases update only the connection indicator; they are absent from the conversation fingerprint. A changed queue explanation can update pending status. renderMessages() uses active-account/generation guards, keyed in-place reconciliation, stable content tokens, one listener set per message article, and one replaced media observer per committed revision. Receipt/reaction changes update metadata without detaching text, loaded images, or playing audio; a stale decrypt result cannot commit over another conversation.
Incoming records and cursors persist, the changed UI refreshes, and only then does the best-effort notification hook run. notification-policy.mjs requires explicit granted permission plus the normal active worker and uses showNotification(); it filters/group-bounds conversation events and records presentation rejection without converting it to sync_failed. Worker clicks accept only a validated account identifier and the locally started app still enforces accepted/unblocked contact state before navigation. The worker has no push handler or background message-sync task.
The document owns the reconnect loop, SSE hints, outbox, and media work. Diagnostics call SSE event_stream_*, not push. Visible return, pageshow, and restored networking coalesce one wake that replaces a stale EventSource and resumes the existing loop/preparation worker. Hidden work is best effort; a frozen, closed, discarded, or force-stopped document does no message processing. Closed-app Web Push remains a separate consent, subscription, relay-delivery, payload-privacy, and worker-handler design.
Performance and safety
Normal synchronization excludes chunks. Indexed queries compose cursor/date/key/conversation/type/kind/resource/size filters. A viewport observer prefetches nearby resources and digest checks them. Resource loading coalesces concurrent requests and makes at most three short-backoff attempts; zero returned records is temporary evidence, not a deletion claim. Reconstruction filters sender/conversation, decrypts and deduplicates consistent chunk indexes, rejects conflicts, and verifies size plus digest. The 0.16 album bug was manifest-first operation retirement: lexical resource-group ordering could acknowledge the album, delete its operation, and strand later chunks. Operation grouping, transactional dependency checks, last-step publication, and legacy orphan-chunk resume correct that boundary. One bounded reconnect owner separates transport reachability from authorization. Plaintext rendering creates allowlisted DOM nodes only. QR/fonts/media are same-origin and self-hosted.
Local/network boundary
boot() opens IndexedDB, selects a validated last-usable/cached/default configuration, validates the retained identity, and renders local conversations before any release probe, registration check, relay request, or worker registration. Relay access and compatibility gate transmission, not reading or safe local enqueue. The synchronizer classifies registration, metadata, directional protocol/capabilities, trust pin, publication, history/receipt, and local persistence phases with status/code/request ID. Retry clears only session metadata and wakes one bounded loop. A relay-key mismatch remains a hard warning; explicit exact-fingerprint replacement retains the old public contact for historical receipt validation.
Fresh installation runs lettres relay initialize after building and before enrollment, accepted-user mutation, doctor, or startup, so relay-identity.json and lettres.sqlite3 are created and validated as one bootstrap step. The explicit v0.19.0 recovery accepts a database without an identity only when its complete schema, sequence, and operational tables prove it has never handled relay traffic; otherwise the matching identity backup is required.
Update boundary
appVersion and buildId are deployment evidence; directional protocol/capabilities remain the network gate. The self-contained worker cannot inherit stale imported constants. Registered with updateViaCache: none, it fetches every reader/crypto asset into a candidate build cache, validates exact headers, types, and both JSON markers, and writes readiness only after complete storage. Failure deletes the candidate, not the active cache or IndexedDB. The page handshakes for exact candidate/controller identity, flushes drafts, coordinates tabs, and performs one reload only after verified control. Exact supplied 0.18 worker and policy fixtures prove compatibility with its legacy activation message and that its lettres-static-* cleanup cannot match the new lettres-reader-* candidate. Live web evidence and actual relay metadata are tracked separately; test both clean and retained profiles.
Checks
npm test
node --check web/app.mjs
node --check relay/src/server.mjs
for f in install.sh scripts/*.sh live-tests/run.sh; do sh -n "$f"; done
docker compose configA release must align `VERSION`, package metadata, constants, UI/config/API, Compose tags, service-worker cache, changelog, prompt history, and archive filename.