Phase 3b: Flutter client¶
Status: implementation direction accepted; milestone acceptance remains pending, with parts of M3b.0–M3b.4 already implemented. See ADR-009 and the implemented foundation. Versión española.
The Spanish plan is the normative source of acceptance criteria. This condensed English translation must be updated in the same review; the Spanish text prevails if they diverge.
Current execution priority — October 1, 2026¶
Beta 6 (0.1.0+27) is published. One invitation, from installation to the first conversation is implemented and distributed; the implementation record separates automated/native checks from pending physical acceptance. Publication does not close M3b.5: physical devices, notifications and three external users remain open. Beta readiness records packages, compatibility and tracked tasks.
Scope and structure¶
Deliver a usable macOS/Android beta for enrollment, pairing and conversation, including offline work and understandable errors. Extend the same Flutter application to Windows, Linux and iOS. The client opens encrypted profiles and enrolls an identity through invitation/bootstrap fields, resumes a failed enrollment and recognizes completion after reopening. Pairing with manual code comparison and encrypted identity-kit export/restore are implemented. Dated KeyPackage availability, exhaustion and resumable replenishment are implemented. Conversation creation, paginated history, offline text and sync are implemented. Separate release apps passed Mac ↔ Android-emulator messaging, upgrade and offline/reconnect checks on September 24; physical-device acceptance remains pending. See the platform record. SwiftUI, UniFFI, calls, federation, protocol redesign and GUI/CLI IPC are outside this phase's initial scope.
clients/flutter/ Adaptive UI and platform adapters
core/crates/arveil-flutter/ Thin bindings and contract translation
core/crates/arveil-app/ Application operations and executor
core/crates/arveil-core/ Identity, MLS and persistence
Rust owns durable state and domain decisions. Dart holds presentation projections, without a second durable domain database. The Go relay keeps its role; ADR-009 permits a bounded enrollment-idempotency correction in M3b.2.
Milestones and exit criteria¶
| Milestone | Work and acceptance |
|---|---|
| M3b.0 — Native build and bridge | Compile Flutter, bridge and Rust/SQLCipher/OpenSSL on macOS and a physical Android device. Implement the minimum configuration/lifecycle contract below. Open, query, receive a typed error and close without blocking UI; another process can immediately open the closed profile. Pin reproducible toolchains before acceptance. |
| M3b.1 — Application contract and platform integration | Secure key storage, explicit TLS/policies, asynchronous calls, incremental events, pagination, bounded admission and failure handling. Restart preserves access; absent/wrong keys never recreate or silently decrypt the profile. Show ProfileInUse. A post-commit failure preserves the same pending message and cannot duplicate it on retry. Implement and test the contracts below. |
| M3b.2 — Enrollment and pairing | Identity, invitation/bootstrap, session approval, code comparison, confirmation, cancellation and resumed completion without a terminal. Test wrong code, expiry, lost responses and duplicate confirmations, including invitation enrollment under the concurrency contract below. Offer kit export during onboarding with explicit deferral and visible recovery risk; demonstrate restore. Test KeyPackage exhaustion/replenishment when joining multiple groups. |
| M3b.3 — Conversation (Mac ↔ Android emulator verified; physical device pending) | Conversation list, verified contact/route, group creation, paginated history, sending and sync. Mac ↔ Android conversation; offline reading and local acceptance; reconnect without duplicates. Distinguish local/relay acceptance from human reading. Local history remains responsive during slow network. |
| M3b.4 — Daily use (contacts/aliases, explicit attachments, device revocation and history archive implemented; platform acceptance open) | Attachments identified by event_id, explicit download/private-storage/export policy using platform permissions; resume interrupted transfers and expose cancellation/expiry. Contacts, naming, verification, device revocation and kit/archive API. Demonstrate device loss and recovery; do not claim MLS rejoin is solved by sync. |
| M3b.5 — macOS/Android beta | Accessibility, desktop navigation/shortcuts, mobile layouts, permissions, packaging, suspension and secret-free diagnostics. Reproducible external installation with signing/distribution limits documented. Three external users complete principal flows; before using an identity they intend to retain, each verifies kit export/restore in a test profile (without promising recovery of all history or MLS groups); record and fix blockers. Initial beta guarantees foreground/on-reopen sync only, not immediate suspended/closed delivery. Test Android Doze, suspension and reconnect on hardware. Any background promise needs a prior push/service decision and verification. |
| M3b.6 — Windows/Linux | Native builds, packaging, keys, files, notifications, lock/restart tests. Verify M3b.2–M3b.4 on identified systems; distinguish built, tested and distributed. |
| M3b.7 — iOS | Native build/signing, Keychain, permissions, suspension and documented APNs/extension decision. Test on a physical device and distinguish active/suspended/closed reception. An extension accessing the profile needs an ownership design compatible with exclusive locking. |
| M3b.8 — Phase exit and production | External security review covering client, bridge and protocol integration, signed updates and final platform matrix. Record review scope/commit, verify fixes for blocking findings and document residual risks. Signed builds/updates and tested platforms are required. M3b.5 is a limited beta, not audit evidence. |
Complete M3b.0 before designing every screen. Windows/Linux and iOS may change order according to usage. macOS/Android priority does not establish compatibility elsewhere.
The visual system, personalization and the redesign plan that precedes M3b.5's external-user test are in Client design. That plan covers M3b.5's accessibility, desktop navigation, mobile layouts and secret-free diagnostics, and adds the data the UI needs from the Rust contract. It does not replace this document's acceptance criteria.
Installation and distribution contract¶
Simple installation is a product requirement. Keep the README's installation entry point current as M3b.0–M3b.3 evolve; package trial builds alongside usable client flows instead of leaving all installation work until the end. This does not waive missing milestone or platform acceptance.
M3b.5 must deliver versioned server images for Linux x86-64/ARM64, a packaged macOS app and an Android APK, English/Spanish instructions, checksums and a verified first-install/update path that preserves profile access. End users must not need developer toolchains or undocumented terminal steps to install the app or enroll. Server instructions include prerequisites, network access, invitations, persistence, health, reboot, backup and recovery. Demonstrate installation from a clean machine/phone using only published instructions.
No paid Apple Developer membership is assumed for local development or the initial macOS evaluation path. Verify the selected key store, first launch, reopen and rebuild behavior; document signing and notarization limits. Android releases need a persistent private signing key and an update test; debug APKs are not release artifacts. iPhone distribution is a separate milestone and must not inherit Android installation claims. Follow the complete acceptance criteria in the installation guide.
Minimum M3b.0 contract¶
ProfileConfigsupplies path, key and transport/policy configuration. The library does not depend on global environment variables; CLI may translate them. Never log secrets or include them in events. GUI rejects absent keys before opening/creating a database. Any plaintext development mode must be explicit. The spike injects a test key; secure platform storage follows in M3b.1.- Each session owns immutable configuration.
openvalidates actual database access/key before returning a handle and reserves the profile during initialization. A second independent open of the same canonical path returnsAlreadyOpen, even with a different key/policy; sharing requires explicit handle cloning. Never silently reuse configuration or expose/compare secrets. Opening during shutdown returnsClosing; reopening after closure validates the key again. Test simultaneous opens, wrong keys on free/occupied profiles, different policies, symlink aliases and opening during shutdown. - Distinguish an OS-lock reservation (
ProfileGuard) from a configured executor session (Application).AlreadyOpenrejects a second session, not a reservation without a session. In M3b.0 adapt CLI dispatch: application commands acquire one session owning its reservation; legacy commands keep an exclusive guard. Do not retain generic guard acquisition followed byopenas implicit sharing. Test chat, onboarding/pairing, legacy commands and cross-process exclusion. - Explicit, idempotent session close stops admission and drains bounded pending work or returns
Busywithout prematurely releasing the lock. Define clones, streams and post-close calls. Successful close means executor shutdown and resource release, independent of Dart GC. Lock lifetime must cover all executor work even when UI handles disappear; retain ownership in the worker or an owner with verifiable shutdown. Await thread termination using a JoinHandle or equivalent. Test explicit close and dropping the last handle with a held write: neither local nor external reopen can acquire the profile until work ends. Then verify the last durable write, no in-flight work and immediate external acquisition. Also test double close. - Exploration may use provisional versions; acceptance requires pinned Flutter SDK/channel, FRB/generator, Rust toolchain/targets, lockfiles, NDK, ABIs, compilers and minimum Android/macOS versions. Record SQLCipher/OpenSSL versions/features and verify encryption, reopen and wrong-key rejection on both systems. Separate built, tested and distributed targets; iOS validation belongs to M3b.7.
- Keep
unsafe_code = "forbid"in core/application. The future adapter cannot inherit that lint if generated bindings requireunsafe: use adapter-local lints,deny(unsafe_code)for handwritten code and a scoped generated-module exception with diff review. Verify edition 2024 and pinned toolchain compatibility. Any required handwritten unsafe integration needs a specific justified/reviewed exception, not a workspace-wide relaxation. This policy does not certify dependencies as unsafe-free. - If vendored OpenSSL fails, evaluate explicitly built/linked target-native SQLCipher/OpenSSL libraries. Record reproducibility, licenses and maintenance before changing strategy. Never silently substitute plaintext SQLite. Unverified targets cannot pass.
M3b.1 contracts¶
- Do not expose SQLite connections, MLS engines or generic private-key objects to UI. Keep key integration minimal and explicit. Run today's blocking
Application::executein the background adapter, preserving single-executor exclusion while adapting lock ownership and shutdown under M3b.0. - Extend lifecycle with operation cancellation; cancelling a UI wait cannot undo a durable commit or revoke a credential. Correlate results, partial results and events by operation and relevant conversation/message/entity.
- Incremental events during
Sync,SendFileand pairing. The adapter may useStreamSink; application code must not depend on FRB. Define ordering, bounded capacity, unsubscribe and snapshot recovery after lost progress. Events do not replace durable state or partial receipts. - Define a UI event projection instead of exporting all internal
StateChangevariants automatically. Separate diagnostics from state changes; specify schema evolution and unknown-event behavior without losing durable results/errors. Cross threads through a bounded channel of transferable DTOs or a callback with the concurrency guarantees required by that boundary. KeepStreamSinkin the adapter and executor!Sendfutures on their own thread. - History takes conversation, stable cursor and capped page size. Specify total ordering and test concurrent insertion between pages before building the M3b.3 screen.
- Bound admission and active work. Keep one active sync per profile, bound its waiting queue and return a typed saturation error. Verify local queries remain responsive under load.
- Panic handling invalidates the affected session and reports a recognizable failure where the compilation mode permits containment.
catch_unwindplus blind restart is insufficient: guarantee rollback or discard connections, reload MLS from durable state and remove dead registry entries before reopening. Test transaction panic and pending commands. Do not promise recovery for process-aborting runtimes. - Define profile migration/backup and client update policy before external distribution. Inventory legacy operations and retry semantics instead of running CLI as a GUI backend.
Keys, backups and clock handling in M3b.1¶
- Initial policy excludes the private profile (database, WAL/SHM, credentials, attachments and temporary files) from automatic cloud backup and app-managed data migration. Kit/archive exports are explicit user actions. Document platform coverage without promising control of manual/administrator backups. Backup exclusion does not replace encryption.
- Android: configure
allowBackup, legacy backup rules anddataExtractionRulesfor cloud/device transfer according to targets. Do not assumeallowBackup=falsedisables all manufacturers' transfers. Check packaged rules, restore and transfer on the device matrix, including exclusion of exportable keys or wrapped-key files. - Apple: non-synchronizable local key (
kSecAttrSynchronizable=false) and a suitableThisDeviceOnlyaccessibility class; verify the Keychain implementation on macOS. Keychain sync and backup migration are separate policies. On iOS exclude private files viaisExcludedFromBackup, reapply after replacement operations and verify it. Absence ofThisDeviceOnlydoes not imply synchronization. - Define policy in M3b.1; verify packaged Android/macOS in M3b.5 and iOS in M3b.7. Test reinstall, restore without key and device loss without silent profile recreation. Explain the need for a kit or another authorized device for actually implemented recovery flows; neither guarantees full history recovery.
- Distinguish typed temporal errors (not-yet-valid/expired) from invalid signatures/authorization where the protocol provides this information. Show clock skew as a possible cause, not a proven diagnosis from
credential rejected. Never relax validity/TTL/expiry. Test clocks ahead/behind; define trust and limits for any remote time signal.
References: Android backup, Keychain synchronization, device-bound accessibility, iOS backup exclusion.
Concurrent enrollment in M3b.2¶
One active enrollment per profile. Persist phase and operation identity (realm/invitation/credential) without logging tokens. Equivalent callers wait within limits and reread durable results; incompatible callers receive a typed conflict without changing active enrollment. Resume the same operation after restart/lost response. Serialization alone is insufficient: skip completed steps and verify relay InviteRedeem idempotency for the same credential, including consumed invitations with lost responses. ADR-009 permits the bounded relay idempotency correction required here. Persist the result and its token/identity/credential binding atomically with consumption; existing membership alone is insufficient. Define retry behavior for exhausted/expired invitations and revoked credentials without reactivating authorization or consuming another use. Verify compatibility with existing clients and resolve missing protocol support before acceptance. Conflicting identity/pairing mutations must respect enrollment state.
With a delayed relay, two equivalent Enroll calls produce one identity/mailbox/route; a different invitation cannot alter state. Test lost response, restart between phases and retry after completion while local queries remain available.
The guarantee also covers MailboxCreate: persist request identity before sending, bind it to the authorized device and atomically create the mailbox/idempotency record. Equivalent retries retain the same mailbox and identical read/write capability bytes, without rotation or expiry extension as a retry side effect; key reuse with different parameters conflicts. Explicitly design safe capability recovery and expiry/revocation behavior: the relay currently stores only hashes and cannot reconstruct the original response. Do not implicitly introduce plaintext capability storage as a shortcut. Test lost responses after relay commit and failure before client persistence, including restarts of both processes. Retries preserve mailbox/route without duplicates; review subsequent steps for repeated completed effects.
Finalize capability design before implementing M3b.2. The preferred option to validate is independent 32-byte tokens from the client's cryptographic RNG, persisted encrypted with the request before sending. Generate in Rust using getrandom::fill, not Dart; RNG failure aborts preparation without a weak fallback. The relay validates format, authorization and reuse conflicts and retains hashes; it cannot certify the entropy of a client-supplied token. If it receives only hashes, it cannot validate original token length either: specify exactly what the transport carries and validates. Keep cap_hash globally unique within the realm: matching hash/mailbox/scope only permits retry under the same authorized operation and original parameters; another mailbox or scope conflicts. Hash equality is not authorization. Do not relax the primary key or use conflict-hiding INSERT OR IGNORE to implement retries. Test both cases and rejection of identical read/write tokens. This is not the only possible solution, but avoids a relay derivation master key. Document compatibility, secret handling and retry tests before changing the protocol. Routes embed the write capability: actual rotation requires explicit contact-route updates, never silent rotation on creation retry.
Prototype capabilities have a 365-day TTL from creation, not contact import, and may be revoked earlier. Renewal is not implemented. M3b.2 defines expiry behavior and decides whether the beta adds authorized renewal or explicitly retains this limitation. M3b.4 presents access failure and available recovery without treating every Forbidden as expiry or promising automatic route redistribution. M3b.5 tests expiry with a controlled test clock instead of waiting a year and publishes the limitation if renewal is absent. Retries never reactivate expired/revoked capabilities.
Compatibility includes relay data, not only clients. Prefer additive changes; implement ordered transactional migrations before any required transformations. Check schema version before modifying data and reject unsupported future versions. Test upgrading a populated existing database, reopening, and restoring an older backup with the new binary. Document rollback: an old binary that does not validate schema versions cannot be promised to reject a newer schema; restore the previous backup when backward compatibility is unproven.
Legacy API inventory¶
Assign every legacy command in M3b.1. Kit export/restore moves to application in M3b.2; contacts/naming/verification and archive export/import in M3b.4. Expose required status as typed queries. probe, notify and low-level mailbox create/send/fetch remain CLI unless a concrete product flow needs them.
M3b.2 defines KeyPackage thresholds, replenishment and exhaustion behavior for the GUI, including publication failure/retry. The initial enrollment batch is now persisted with its private MLS state and reused after lost responses; an acknowledged enrollment does not generate another batch. The relay applies quota after deduplication and never revives consumed packages. Regression tests cover lost acknowledgements, profile restart, completed enrollment retries and quota boundaries. The GUI now shows the last dated relay count, distinguishes unknown from empty, and offers replenishment at three or fewer packages towards a target of ten. CLI sync and GUI replenishment persist the retry batch with private MLS state and reuse it after lost acknowledgements. Physical-device acceptance remains pending; see the implementation record.
Verification and delivery¶
Strict documentation builds already run on publication; CI must also check pull requests from this revision.
CI from M3b.0 builds the native bridge for macOS/Android with pinned dependencies. From M3b.1 it checks FRB regeneration without drift, flutter analyze, Dart/Rust tests. Add platform jobs as support expands; hardware evidence is separate from CI compilation.
Each milestone tests new Rust behavior, relevant widget states and end-to-end flows. Preserve workspace tests with --locked, Clippy and existing acceptance phases when changing application contracts. Record commit, OS and device; historical test counts do not certify GUI behavior.
Demo installation, enrollment/pairing, sending, disconnection, pending state and reconnect delivery. Publish screenshots, limits and platform matrix. Push, external security review and measured performance are separate deliverables. Track relay risks, real MLS group recovery and historical security claims before production; Flutter does not resolve them.
References: FRB disposal, FRB concurrency, Flutter platforms, Android Doze/App Standby.