Everything runs on your machine. No accounts, no cloud, no trust required. Here is how the pieces fit together.
Federated learning lets your devices learn from each other without ever sharing your raw data. The wisdom travels. Your records stay home.
Each device trains a model on its own data. Your watch data, journal entries, and spending records never leave your machine. Training happens in PyTorch using only what is already in your local DuckDB database.
After training, each device sends its updated model weights -- not data -- to a coordination server. These are just numbers: the direction the model moved, stripped of anything that could identify individual records.
The server averages the weights from all participating devices using Federated Averaging. The result is a smarter model that has learned from everyone's patterns without seeing anyone's data. Each device then downloads the improved model and the cycle repeats.
A single device can only learn from its own history. With federated learning, your model benefits from patterns discovered across many people -- better sleep predictions, sharper spending alerts, more relevant suggestions -- without a single row of your data leaving your device. The coordination server never sees your records. It only sees the mathematical summary of what each device learned.
The system is built on Flower, an open-source federated learning framework. Model plugins can be added through the same entry point system as everything else.
Your laptop syncs Garmin. Your phone has Duolingo. A mesh daemon running on each device keeps them all in sync -- peer to peer, centrally orchestrated, and through any NAT.
Each device generates an Ed25519 keypair on first run and registers with the coordination server. The server stores only the public key and a device ID -- the private key never leaves your machine.
Devices discover each other through the server's registry. Each one reports its reachable endpoints -- local IPs for same-network connections, and public addresses resolved via STUN for NAT traversal.
A background daemon wakes up every 60 seconds, finds peers with unseen events, and exchanges changes directly over UDP. If the peer is unreachable, events route through an encrypted server relay instead.
Every data mutation -- a pipe sync, a transform run -- appends
an immutable event to a local log. Each event carries the
originating device ID, the affected table, and a timestamp.
When two devices connect, they compare cursors: "I last saw
your event evt-abc, send me everything after
that." Only new events travel. The log is never rewritten.
Most home networks sit behind NAT, making direct connections between devices difficult. Each device sends a lightweight UDP probe to a public STUN server, which reflects back the device's externally visible IP and port. These endpoints are published to the coordination server so peers can attempt direct connections. When both devices are on the same local network, the local address takes priority for the fastest possible path.
When direct connections fail -- symmetric NAT, firewalls, devices on different schedules -- events are pushed to the coordination server as opaque encrypted payloads. The receiving device polls and consumes them on its next sync cycle. The server cannot read the contents. It only knows which device sent a message and which device should receive it. Messages are deleted after delivery.
Every row produced by a transform is tagged with the device that created it. When your laptop syncs Garmin data and your phone syncs Duolingo data, each device's contributions are clearly attributed. This enables deduplication, conflict awareness, and a clear audit trail of where every piece of data came from.
The same interface runs on your desktop, your phone, and your browser. Your data and your dashboard, wherever you are.
A Tauri 2 app wraps the web frontend in a native window. Three PyInstaller-built binaries are bundled as sidecars: the main server, the scheduler daemon, and the CLI. The app starts the server on launch, and the frontend talks to it over localhost. Everything runs in-process -- no external services, no Docker, no containers.
On mobile, Python is replaced entirely by Rust. An axum HTTP server runs in-process alongside an embedded DuckDB instance, exposing the same API contract as the Python server. The frontend is identical -- same Lit components, same queries, same dashboard. The only difference is the engine underneath.
The FastAPI server can also run standalone on any machine. Point a browser at it and you get the full dashboard with plugin discovery, telemetry, and the complete API. Useful for headless setups, remote access, or development.
The frontend is a set of Lit web components that communicate with the backend over HTTP and Arrow IPC. They do not care whether the server behind localhost is Python or Rust. This means the desktop app, mobile app, and web server all share the exact same UI code. Features built for one platform automatically work on all of them.
Pipes, schemas, components, themes, and UIs are all independent Python packages. You install only what you need, and nothing runs unless you put it there.
Each plugin declares a standard Python
entry point
in its package metadata. When shenas starts, it calls
importlib.metadata.entry_points() and finds every
installed plugin automatically. No config files, no hardcoded
imports, no plugin registry to maintain.
Five plugin kinds exist, each with its own entry point group:
Every plugin is distributed as a standard Python wheel served
from a PEP 503 package repository that you host yourself.
Before a wheel reaches that repository, it is signed with an
Ed25519 key. When you run
shenasctl pipe add garmin, the CLI downloads the
wheel and its .whl.sig signature file,
verifies the signature against the known public key, and only
then installs it. If the signature does not match, nothing gets
installed.
You always know exactly what is running on your machine and where it came from. Trust by design, not by promise.
A pipe is a Python class with two methods:
build_client() to create an API client from stored
credentials, and resources() to return
dlt resource objects that
know how to fetch and yield data. Add a
pyproject.toml with a hatchling build backend,
declare your entry point, and
shenasctl pipe add your-pipe just works.