Collectivenix fleet

30 machines · 4 platform classes · 1 registry

Devices

Android

Five phones and tablets are full fleet members. They take the same Home Manager modules as a laptop, run supervised services, join the tailnet as their own nodes, host nested Wayland desktops on the phone's GPU, and update from prebuilt closures. Everything runs as an ordinary app, without root.

The shape of it

Nix-on-Droid runs Nix under proot inside an Android app. Collective builds on top of it: a service manager, bridges to the Android side, and the same Home Manager configuration as every other machine.

Android and Nix-on-Droid, bridgedTwo panels. On the left, the Android host: the display and input, the native Termux app, Shizuku, Tasker and widget apps, the PulseServer audio app, and the package installer. On the right, Nix-on-Droid running under proot: supervisord and its programs (sshd on port 8022, userspace tailscaled, a secrets watcher, the shell-history daemon, liveness, an SSH SOCKS tunnel and the nod-exec command server), Home Manager, and a nested Hyprland or Sway session with GPU drivers. Between them, bridges: termux-exec runs native Termux commands from Nix-on-Droid over local TCP; rish reaches Shizuku's shell; tasker-run triggers Tasker tasks by broadcast; nod-exec lets Tasker, widgets and apps run commands inside Nix-on-Droid; nod-install installs APKs through Shizuku; audio goes to PulseServer; Wayland clients draw into Android through Waylandroid. android host Termux app Shizuku package installer Tasker, widgets, apps PulseServer app display and input nix-on-droid proot · Home Manager supervisord sshd :8022 tailscaled (user) secrets watcher history daemon liveness SOCKS tunnel nod-exec server nested session Hyprland or Sway Waybar, kitty turnip + zink GPU Mesa per Adreno gen termux-exec TCP, local rish Shizuku shell nod-install through Shizuku tasker-run am broadcast nod-exec TCP, apps to Nix PulseAudio audio out Waylandroid Wayland display the station marks the side being called
Two worlds, bridged.Nix-on-Droid cannot see Android's apps or hardware directly. Each bridge is a small, declared program that crosses the line one way.

Services without systemd

There is no init system and Android may kill the app at any time, so services cannot be started once at activation and trusted to stay up. The public supervisord module gives Nix-on-Droid a small service manager. Modules declare programs much as they would declare systemd units, and session start-up idempotently starts whatever is not running.

supervisord.programs.atuin-daemon = {  command = "${atuinDaemonStart}";  autostart = true;  autorestart = true;  startsecs = 0;};

sshd (on port 8022), the userspace Tailscale daemon, a secrets watcher that re-checks SOPS every minute, the shell-history daemon, liveness reporting, a single SSH SOCKS tunnel and the command server all run this way. A Termux:Boot script starts the session when the phone boots.

One module, three supervisorsThe agnostic daemon module declares a long-running service once as daemon.name and renders it as a systemd unit, a launchd daemon or agent, or a supervisord program, at system or home level depending on the platform.

Bridges to Android

ToolDirectionDoes
termux-execNix → TermuxRuns a command in the native Termux app over a local TCP daemon, for things proot cannot do, such as Termux's Android API helpers
rishNix → ShizukuA shell with Shizuku's elevated permissions, through termux-exec
nod-installNix → AndroidStages an APK where SELinux allows and runs pm install through Shizuku; can also install on another device over SSH
tasker-runNix → TaskerTriggers a named Tasker task with an am broadcast
nod-execAndroid → NixA local TCP command server for Tasker, home-screen widgets and custom apps, with a pipe mode for one-shot commands and a terminal mode for floating shells
am-supervisorNix → AndroidRuns a health check on an interval and, on failure, a restart action such as an am start intent, with exponential backoff
PulseServerNix → AndroidAudio leaves through the native PulseServer app, which owns the speakers and the user's virtual sinks

Its own tailnet node

Android allows one VPN at a time, and the phone's Tailscale app already holds it. So Nix-on-Droid runs a second, userspace tailscaled under supervisord, as its own node named machine-nod:

Userspace Tailscale gives other apps no address to bind and does not route the shell's traffic, so existing Android-side addresses stay in use; the new identity is for serving.

A desktop on the phone

Android owns the physical display and input. Hyprland or Sway runs as a nested Wayland client inside Waylandroid, not as a DRM session, using the same Home Manager module as the laptops with a few platform switches:

Termux-X11 and VNC are available for X11 sessions.

Keyboards as code

Layouts for the Unexpected Keyboard app are written in Nix with a small layout library in collective-public, rendered to the app's XML and installed under /etc/unexpected_keyboard/layouts. Variants include a code-oriented QWERTY, a compact one and a window-manager layer.

cltv config keyboard unexpected --layouts code-qwerty --copy

Updates

A phone never builds its own system. Every Android machine has a prebuild assignment, so its closure is built under emulation on an x86 cache node and the phone runs:

cltv switch --cached

That needs SSH to the cache node and a download, no evaluation (see cached activation). Apps released as GitHub release assets are kept current by an APK watcher: named targets with a repository and an asset pattern are polled, downloads are staged, and nod-install installs them.

CommandDoes
cltv apk statusWatcher state as JSON: releases, staged downloads, failures
cltv apk checkPoll releases once, stage new APKs and install them
cltv apk installRetry installing everything staged
cltv apk daemonRun the polling loop in the foreground