Under the hood

Everything runs on your machine. No accounts, no cloud, no trust required. Here is how the pieces fit together.

Wise together, private always

Federated learning lets your devices learn from each other without ever sharing your raw data. The wisdom travels. Your records stay home.

1

Train locally

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.

2

Share only gradients

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.

3

Aggregate and improve

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.

What this means for you

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 devices, in sync

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.

Identity

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.

Discover

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.

Sync

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.

Append-only sync log

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.

NAT piercing with STUN

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.

Relay fallback

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.

Data provenance

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.

One app, every device

The same interface runs on your desktop, your phone, and your browser. Your data and your dashboard, wherever you are.

Desktop

Linux, macOS, Windows

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.

Mobile

Android

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.

Web

Any browser

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.

Same frontend, different engines

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.

Everything is a plugin

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.

Discovery, not configuration

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:

Pipe Connects to an external service and syncs data into your local database
Schema Defines canonical tables as Python dataclasses -- DDL is generated, never hand-written
Component A Lit web component that renders a dashboard widget or visualization
Theme CSS custom properties that style everything at once -- only one active at a time
UI The frontend shell itself -- tabs, command palette, navigation

Signed and verified

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.

Write your own

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.