Neloa architecture
Neloa is local-first. Every client should remain useful without an account or an internet connection.
Current implementation
- One React interface and one design system for every platform.
DesktopShellandMobileShellshareviews/andui/; macOS and Windows differ only in window controls, system font stack, and frame radius. Colour comes from a single set of CSS custom properties with a light and a dark value, so component rules never branch on the colour scheme. - Tauri 2 desktop and mobile shells, with generated Android and iOS native projects.
- Stable X25519 device identity stored in the native OS credential store: Keychain, Credential Manager, Android Keystore, or iOS Keychain.
_neloa._udp.local.service advertising and discovery:mdns-sdon desktop/Android and an Apple Bonjour adapter on iOS.- Live peer updates from Rust to the interface.
- Simultaneous QUIC client/server endpoint on UDP port
48631. - Noise XX handshake over every QUIC stream.
- Six-digit short authentication string derived from the Noise handshake hash.
- Explicit confirmation on both devices before persisting the peer public key.
- Trusted-device verification and revocation.
- End-to-end encrypted test text with application-level acknowledgement.
- Native multi-file selection, desktop drag and drop, bounded send queues, and file metadata inspection. Android content URIs are opened through the platform file-descriptor bridge, staged only when sending, and deleted after the transfer; iOS uses sandbox copies.
- File offer/accept flow tied to a trusted device identity.
- Encrypted binary chunks with strict offsets, size limits, progress, and cancellation.
- SHA-256 verification before publication and an authenticated completion receipt.
- Temporary
.partwrites followed by atomic, no-overwrite publication inDownloads/Neloa. - Transfer result history and retry for interrupted outgoing files.
- Opt-in clipboard monitoring on a dedicated thread using the native system pasteboard. Mobile synchronization is foreground-only because the operating systems restrict background clipboard reads.
- Plain-text clipboard events sent to every online trusted device through authenticated Noise sessions.
- Event UUID deduplication and normalized line-ending comparison to prevent rebroadcast loops.
- An 8 KB payload limit, strong credential-marker blocking, and no clipboard-body persistence.
- Protocol range and capability negotiation in discovery and authenticated handshakes.
- Explicit rejection of legacy, malformed, or feature-incompatible peers before application data is accepted.
- A user-readable local connection status for QUIC, mDNS, identity, clipboard, peer capability, and firewall checks.
- An optional self-hosted relay client with persistent authenticated WebSocket connections, same-token device presence, and bounded virtual streams.
- An explicit per-device route choice. An unset route rejects the connection instead of recommending, automatically selecting, or silently changing paths.
- Native Android Wi-Fi Direct service discovery, group formation, and address rendezvous. It is enabled only when a nearby Android peer is actually visible and reuses the same QUIC, Noise, pairing, and transfer protocols.
- Apple CoreBluetooth discovery and GATT transport. A fixed service exposes public discovery metadata; two data characteristics use negotiated-MTU fragmentation, packet sequencing, half-close, and native write backpressure before reusing the same Noise, pairing, and transfer protocols.
Advertising declares protocolVersion=1, minProtocolVersion=1, and the base capabilities discovery,pairing,noise-xx,test-message,file-transfer,streaming-file-hash,clipboard-text; Android also declares transport-peer-to-peer-wifi, while macOS and iOS declare transport-bluetooth. The same protocol range and capability list is authenticated inside the Noise XX handshake; discovery values are presentation and early-filtering hints only.
Interface layout
src/
bridge.ts typed Tauri commands/events, plus the browser preview simulation
lib/useNeloa.ts all application state and actions; both shells consume it
ui/ icons, kit primitives, overlays
views/ RadarView | HistoryView | SettingsView — shell-agnostic
shell/ DesktopShell (title bar) | MobileShell (tab bar, safe areas)
styles/ tokens | base | components | desktop | mobileviews/ and ui/ contain no platform or shell branches except through useShell(), which only switches a modal between a centred dialog and a bottom sheet. The shell is chosen in the frontend from ?platform=, pointer type, viewport width, and user agent. The Rust backend reports macos, windows, linux, ios, or android; protocol behavior remains shell-independent.
Security boundary
QUIC uses a per-launch self-signed certificate only as a reliable encrypted datagram transport. It is not treated as the long-term device identity. Relay, Wi-Fi Direct, and Bluetooth adapters all expose the same bidirectional byte-stream interface. All application payloads are wrapped by Noise XX, and no plaintext application message may be written directly to any transport.
Remote relay URLs must use wss://; plaintext ws:// is accepted only for a loopback development server. The shared access token is stored in the native OS credential store, while relay-settings.json contains only the enable flag and public URL. Relay presence includes devices connected with the same token so initial pairing can work across networks; the relay source ID must match the authenticated Noise metadata. Paired sessions must also match the pinned peer public key before application data is accepted.
The Noise static private key is stored under the application service name in macOS Keychain, Windows Credential Manager, Android Keystore-backed storage, or iOS Keychain. trusted-devices.json contains only peer public keys, fingerprints, device metadata, and timestamps.
Desktop builds retain the original credential-store service name when the public application identifier changes. On first launch under com.kure29.neloa, Neloa copies a missing device-id, trust store, and clipboard settings from the legacy app.neloa.desktop data directory without overwriting any data already created by the new version. Mobile operating systems treat the new bundle/application ID as a separate sandbox, so mobile upgrades still require a fresh install and pairing.
During first pairing, both devices compare a six-digit value derived from the same Noise handshake transcript. Trust is committed only after both sides explicitly accept. Later sessions must match both the discovered device ID and the stored Noise public key.
Every Noise handshake carries the sender's current and minimum supported protocol versions plus its feature capabilities. Version ranges must overlap, malformed ranges fail closed, and the capability required by the session purpose must be present. A peer that omits these fields is treated as legacy protocol 0 and receives an actionable incompatibility error instead of entering pairing or transfer flows.
Peers that advertise streaming-file-hash offer only the sanitized base name and byte size. The sender computes SHA-256 while reading, encrypting, and sending the file, then commits the digest in the authenticated completion message; the receiver computes and compares its digest in parallel. Older peers without the capability retain the pre-transfer hash flow. The receiver writes only to a transfer-specific temporary file, rejects invalid offsets or excess bytes, verifies the final digest, flushes it to disk, and publishes it without replacing an existing file. Cancellation and failure remove only that transfer's temporary file.
Clipboard synchronization is fail-closed and disabled by default. Enabling it records the current clipboard as a local baseline without transmitting it. Subsequent text updates receive UUIDs, are checked for size and strong credential markers, and are sent only to currently discovered trusted peers. Remote updates are acknowledged only after the OS clipboard write succeeds. Received content becomes the new local baseline, preventing it from being sent back; CRLF, CR, and LF are canonicalized for comparison across Windows and macOS. Events retain only peer, direction, byte count, status, and time—not clipboard text.
Android requires CHANGE_WIFI_MULTICAST_STATE plus a held WifiManager.MulticastLock while the Activity is alive so mDNS packets are delivered reliably. The Wi-Fi Direct adapter uses Android DNS-SD discovery, system group formation, and a local rendezvous port that exchanges only device IDs and group addresses. These are untrusted routing hints; the subsequent Noise XX handshake still authenticates the actual peer. Native device addresses and group IPs stay inside Rust and are not sent to the WebView. Android 13+ uses Nearby devices permission; earlier versions use location permission.
macOS and iOS use one fixed 128-bit CoreBluetooth service UUID. Its read-only identity characteristic contains the same public device metadata used by Bonjour and capability negotiation; it maps a BLE peripheral into the device list but is not a trust anchor. A central-write characteristic and a peripheral-notify characteristic carry versioned packets with monotonic sequence numbers. Missing, duplicate, reordered, or malformed final packets close the session. Sending honours canSendWriteWithoutResponse and updateValue readiness, while Noise XX still verifies the device ID, static key, and all application content.
iOS does not open a raw multicast socket: NWBrowser browses _neloa._udp, while NetService publishes and resolves the Bonjour service that points at the existing Rust QUIC listener on UDP 48631. Swift forwards resolved IPv4 addresses and TXT metadata to the shared Rust peer store. This path uses the declared Bonjour service and local-network privacy prompt without the restricted multicast entitlement.
Architecture layers
React UI
-> typed Tauri commands/events
Application services
-> discovery | pairing | transfer | clipboard | history
Platform adapters
-> mDNS/Bonjour | QUIC | filesystem | system clipboard | secure key storeThe relay server lives in relay/, with shared wire definitions in crates/neloa-relay-protocol. It authenticates a small single-user deployment with a server token, keeps presence and tunnel state in memory, and forwards bounded binary frames without inspecting their Noise-encrypted contents. The client maintains one WebSocket connection, maps relay tunnels to bounded local byte streams, and reuses the existing pairing, transfer, test-message, and clipboard Noise protocol without transport-specific envelopes. The shared token only scopes device visibility; trust is still established by the Noise handshake, the six-digit code, and explicit confirmation on both devices.