Add research docs about nerves deployment

This commit is contained in:
Claudio Ortolina
2026-05-04 16:12:43 +01:00
parent eb8d4776a5
commit 7efe277fba
3 changed files with 841 additions and 0 deletions
@@ -0,0 +1,222 @@
---
id: doc-10
title: 'Deepseek v4 Pro, xhigh analysis'
type: other
created_date: '2026-05-04 15:11'
---
# Nerves Deployment Feasibility Analysis for Music Library
> Research report — do not implement. May 2026.
## Overview
This report assesses the viability of deploying the Music Library application (a
Phoenix LiveView app backed by SQLite + Litestream) as a Nerves firmware image.
Three areas were identified as potential challenges:
1. **Libraries with native extensions (NIFs)** — do they offer precompiled ARM
binaries or can they be cross-compiled?
2. **SQLite extensions**`vec0` (sqlite-vec) and `unicode` — already support
Linux/ARM on paper, but are they tested on Nerves targets?
3. **Data replication** — the production instance uses Litestream for backup. How
would a Nerves device replicate data, and what happens with write conflicts?
Each area is analysed below, followed by a summary of risks and recommended next
steps.
---
## 1. Native-extensions audit
Every dependency that ships a NIF or binary artefact was reviewed for ARM /
Nerves compatibility. Devonly tools (esbuild, tailwind, credo, etc.) are
excluded — they never reach the firmware.
| Dependency | Purpose | NIF type | Precompiled ARM? | Nerves feasibility |
|---|---|---|---|---|
| **exqlite**`ecto_sqlite3` | SQLite driver | C NIF (`cc_precompiler`) | ✅ `aarch64-linux-gnu/musl` (no `armv7-*`) | **🟢 Good** — `force_build: true` compiles from source inside the Nerves toolchain. Explicitly mentioned in Exqlites README as an embedded use-case. |
| **vix** → libvips | Image processing (covers, thumbnails) | C NIF (`elixir_make`) | ❌ No ARM binaries advertised | **🔴 High risk** — libvips is ~30 MB with transitive deps (libjpeg, libpng, libwebp, etc.). Each needs Buildroot integration or crosscompilation. Biggest blocker for a lean firmware. |
| **mdex** → comrak + ammonia | Markdown → HTML | Rust NIF (RustlerPrecompiled) | ⚠️ Standard Rustler targets (`aarch64-unknown-linux-gnu`, possibly `arm-*`) | **🟡 Needs crosscompilation** — RustlerPrecompiled targets use different triples than Nerves (`aarch64-nerves-linux-gnu`). Precompiled `.so` likely wont load. Crosscompile from source with a Nervesaware Rust toolchain. |
| **lumis** → treesitter | Syntax highlighting (in mdex) | Rust NIF (RustlerPrecompiled) | Same as mdex | **🟡 Same situation** — part of the mdex stack. Treesitter grammars add perlanguage compilation but the NIF itself is manageable. |
| **typst** | PDF generation (tracklists) | Rust NIF (RustlerPrecompiled) | Same as mdex | **🔴 Heavy** — a full typesetting engine. Large binary, plus the same targettriple mismatch. Likely overkill for embedded PDFs. |
| **dominant_colors** → kmeans_colors | Colour extraction from covers | Rust NIF (`rustler_precompiled ~> 0.7`) | Same as mdex | **🟢 Small** — trivial NIF. Crosscompilation is straightforward. Low risk. |
| **esbuild** | JS bundling | Go binary | — | **N/A** — dev only, never deployed. |
| **tailwind** | CSS generation | Go binary | — | **N/A** — dev only, never deployed. |
### Targetarchitecture specifics
| Target | CPU | Nerves toolchain triple | Precompiledbinary match? |
|---|---|---|---|
| RPi 4 / 5 | CortexA72 / A76 (64bit) | `aarch64-nerves-linux-gnu` | ⚠️ `aarch64-linux-gnu` *may* be ABIcompatible (exqlite). Rust NIFs need rebuild. |
| RPi 3 | CortexA53 (32bit) | `armv7-nerves-linux-gnueabihf` | ❌ No precompiled binaries match. Everything must be compiled from source. |
**Recommendation**: Target RPi 4 or 5 (64bit) first — the precompiled ecosystem is
far better and the 32bit path adds unnecessary friction.
---
## 2. SQLite extensions
The application loads two runtime extensions via Exqlites `load_extensions`
config:
| Extension | Source | ARM status | Notes |
|---|---|---|---|
| **vec0** | [sqlite-vec](https://github.com/asg017/sqlite-vec) | ✅ **Good** | “Written in pure C, no dependencies, runs anywhere SQLite runs (… Raspberry Pis, etc.)” — explicitly tested on ARM. Compilable for any Nerves target. |
| **unicode** | likely [sqlean](https://github.com/nalgeon/sqlean) (`text` module or bundle) | ✅ **ARM64 available** | Sqlean ships `sqlean-linux-arm64.zip`. For ARMv7, compile from C source. |
### Loading mechanism
The project already uses an architectureaware directory layout:
```elixir
config :myapp, Myapp.Repo,
load_extensions: ["./priv/sqlite/#{arch_dir}/vector0"]
```
This pattern extends naturally to Nerves — compile the `.so` files with the Nerves
crosscompiler, place them in the appropriate arch subdirectory, and Exqlite will
load them at connection time.
**Unknown**: Whether the `unicode` extension is built from the exact same sqlean
release as production, or compiled independently. Sync the build flags and version
to avoid subtle behavioural differences.
---
## 3. Data replication & conflict resolution
This is the **hardest problem** and the most architecturally open of the three.
### What Litestream does (and doesnt)
| Capability | Litestream | Needed for device ↔ cloud sync |
|---|---|---|
| Unidirectional backup (app → S3) | ✅ | ❌ |
| Disaster recovery (restore from S3) | ✅ | ❌ |
| Bidirectional sync | ❌ | ✅ |
| Conflict detection / resolution | ❌ | ✅ |
| Multiwriter support | ❌ | ✅ |
Litestream is purely an asynchronous backup tool. It continuously streams WAL pages
to S3. It has **no mechanism** for a second instance to push writes back, no merge
strategy, and no conflict detection. Adding a Nerves device that writes to the same
database file would corrupt the Litestream generation (or create a parallel one that
cannot be merged).
### What LiteFS does (and doesnt)
[LiteFS](https://github.com/superfly/litefs) (same creators, different project)
provides multinode SQLite replication with a primary/replica model:
- ✅ Automatic failover via distributed lease (Consul)
- ✅ LTXbased transactionlog replication
- ✅ Builtin HTTP proxy for writeforwarding to the primary
- ❌ Requires FUSE (needs a Buildroot kernel module)
- ❌ Requires Consul or static leasing
-**Replicas are readonly** — only one writer at a time
- ❌ Designed for sameregion clusters, not device ↔ cloud
- ❌ No offlinefirst or intermittentconnectivity support
**LiteFS does not fit** the devicesync use case. If the Nerves device goes offline,
accumulates writes, then reconnects, LiteFS has no way to merge those writes onto
the primary.
### Realistic approaches
#### A. Readonly Nerves device (simplest, safest)
The Nerves device is a readonly replica. All writes happen on the production
server. The device periodically pulls a fresh database snapshot.
- **Sync**: `GET /api/v1/backup` (already exists), Litestream restore, or HTTP
rangerequest to fetch a compressed DB.
- **Pros**: Zero conflicts, simple to implement, leverages existing backup API.
- **Cons**: No local writes — cant add records, update notes, or scrobble from
the device.
#### B. Separate write domains (pragmatic)
Never write the same record type from both sides:
- **Production server**: records, embeddings, notes, record sets, artist info.
- **Nerves device**: scrobbles, listening stats, playback logs.
- Each instance has its own SQLite database. They sync via API calls (e.g., device
pushes scrobbles to production, production pushes new records to device).
- **Pros**: No databaselevel conflicts. Clear ownership boundaries.
- **Cons**: Applevel routing logic. Two separate databases on the device.
#### C. CRDTbacked data model (most robust)
Model the subset of data writable on both sides using Conflictfree Replicated Data
Types (CRDTs).
- **Libraries**: [Automerge](https://automerge.org/), [Yjs](https://yjs.dev/),
custom CRDT stores.
- **Pros**: True offlinefirst, automatic conflict resolution, no central
authority needed.
- **Cons**: Requires wrapping your data in CRDT types. No mature Elixir CRDT ↔
SQLite bridge. Significant architecture change.
#### D. Event sourcing with log shipping
Write all mutations as an appendonly event log. Ship the log between instances.
Replay to reconstruct state.
- **Pros**: Natural fit with SQLite (append to a `mutations` table). Full audit
trail.
- **Cons**: Requires eventschema design, log compaction, eventual consistency
handling.
#### E. Applicationlevel sync protocol
Define a custom sync protocol with:
- Vector clocks or timestamps for causality tracking.
- Lastwritewins (LWW) or manual conflict resolution.
- Periodic pull/push of changed records since the last sync point.
- **Pros**: Domainspecific, can optimise for exact data shapes.
- **Cons**: Most implementation effort. Easy to get wrong.
### Litestreams role on the device
Even with a sync strategy in place, Litestream could still run on the Nerves device
for **local disaster recovery** — streaming the devices own WAL to a separate S3
path. This is independent from the production Litestream instance.
---
## Summary of risks
| Risk | Severity | Mitigation |
|---|---|---|
| **vix / libvips on Nerves** | 🔴 High | Replace image processing with a lighter alternative, or offload to the production server via API. |
| **Datasync strategy** | 🔴 High | No offtheshelf solution. Start with readonly replica (option A), iterate toward separate write domains (option B). |
| **typst on embedded** | 🟡 Medium | Switch to serverside PDF generation; client downloads the result. |
| **Rust NIF crosscompilation** | 🟡 Medium | Set up Nerves Rust crosscompilation. Works but adds build complexity. Each library needs individual testing. |
| **ARMv7 (RPi 3) vs AArch64 (RPi 4/5)** | 🟡 Medium | RPi 4/5 (64bit) has much better precompiledbinary support. RPi 3 requires everything compiled from source. |
| **Buildroot package deps (libvips)** | 🟡 Medium | Each transitive C library needs a Buildroot package definition or a custom crosscompile step. |
| **Firmware size** | 🟡 Medium | libvips + typst + sqlite extensions → image could exceed 200 MB. Typical Nerves firmware is 2080 MB. |
| **sqlitevec on ARM** | 🟢 Low | Explicitly supported. Pure C, no deps. Compiles trivially with the Nerves toolchain. |
| **exqlite on Nerves** | 🟢 Low | Designed for embedded use. Source compilation wellsupported and documented. |
| **dominant\_colors** | 🟢 Low | Tiny Rust NIF. Straightforward crosscompilation. |
---
## Recommended next steps (if proceeding)
1. **Target RPi 4 or 5 (64bit)** — significantly better precompiled binary
ecosystem and larger RAM for Phoenix + Oban.
2. **Start with a readonly firmware** — pull database snapshots from the
production server via the existing `/api/v1/backup` (or `/health`) endpoint.
Dont write locally until a sync strategy is decided.
3. **Audit which features to keep ondevice** — consider dropping image
processing (vix) and PDF generation (typst) from the embedded build, or
replacing them with lighter alternatives / serverside APIs.
4. **Benchmark firmware size incrementally** — start with bare Phoenix + SQLite,
add extensions, measure. Set a hard size budget (e.g., 120 MB).
5. **Design the sync protocol as a separate project** — this is the most complex
piece. Prototype with a readonly client first, then evaluate CRDTs, event
sourcing, or applicationlevel sync based on actual write patterns.