Share the data your insulin pump loop is already writing — with your partner, your parent, your clinician — revocably, and with nobody who can read it in the middle.
Your phone keeps looping exactly as it does now. What is added is the ability to hand someone a key to your record, and to take it back.
Status: working proof of concept, running on a live closed loop. An AAPS add-on seals its own history and serves it; other devices replicate it and serve it onward; a granted reader opens it from any of them.
Done end to end on real hardware, with real data:
-
A follower's graph. A second phone, set up from nothing, showed its invite once and drew the subject's own trend line — 5.7 mmol/L, two minutes old, against the subject's own 7.0 target band, which arrived in the shared record rather than being typed in. The history it was granted arrived on first contact and then two readings per sync pass. That is the Nightscout job, done with no Nightscout. It is a separate app, Ayni, that contains no AAPS at all.
-
A carrier that cannot read. A stranger peer, granted nothing, replicated 146 segments and all 219 wraps, could open none of it, and served the complete history to a granted reader while the originating phone was switched off. 33,800 records over 73 days.
-
A pool. Phones on a network, with nothing configured between them, found each other, agreed independently on the size of the pool, and each worked out the same share of the subject space to carry. Turning the feature on is the whole instruction — there is no address to exchange and nobody to ask.
-
Two networks. The sharer was the live loop phone on a mobile carrier with wifi off, behind CGNAT; the follower was on home wifi. No shared network and no mDNS. Readings kept arriving every two minutes,
0m old, and the graph ran unbroken through the moment the wifi was cut (D23).
Until 2026-09-11 that last one was impossible: no relay was configured, so devices could only ever meet on one wifi by mDNS. An earlier version of this file nevertheless claimed the two-phone test ran "over the open internet". It did not, and that claim is the reason this paragraph exists.
Not reviewed cryptography. See Limits before trusting it with anything.
Not affiliated with, endorsed by, or part of the AndroidAPS project. This is an independent add-on that compiles against AAPS's plugin interfaces. It contains no AAPS source. AndroidAPS is the work of its own maintainers, and problems with this add-on are not theirs — report them here.
No loop APK is distributed here, and none ever will be. Running a loop is running a medical device you assembled; you build it yourself, from source you have read. Releases in this repository version this project, not AndroidAPS.
The follower is released as an APK, and is a different proposition. It is its own app — Ayni,
nz.diaswarm.ayni, in follower/ — built from this repository alone, containing no AndroidAPS source, no pump driver and no database AAPS would read. It is structurally unable to dose: there is nothing in it to dose with. It is a viewer for somebody else's glucose, it installs alongside AAPS rather than over it, and handing a person a file they can install is reasonable in a way that handing them a loop is not. CI builds it and the workflow refuses to produce a loop build at all.Ayni is Quechua for the reciprocal exchange that holds a community together — you give because you will need, and someone else gives because they will too.
If you loop, your data already leaves your phone — usually to Nightscout, which means running a web server, keeping it patched, and trusting whoever hosts it. Sharing is all-or-nothing and mostly permanent: a URL and a token, handed out and rarely taken back.
The alternative is normally framed as "self-hosting versus a company's cloud". That is the wrong axis. The question is who decides who can read — and in both cases it is whoever runs the server, not you.
diaswarm removes the server. Data is published as ciphertext to whatever devices care to hold it. Access is decided by who holds a key, and by nothing else. Withdrawing access does not ask a server to stop serving; it stops wrapping the next key.
Follow one reading — 5.7 mmol/L, taken at 07:32 — from the phone that recorded it to the phone that draws it. The padlock is on it for the whole of the middle:
The same path, as text:
records ──▶ epochs ──▶ segments ──▶ sealed ──▶ served to anyone
│ │
└─▶ wrapped per reader ────┘
│
grant log (signs, names nobody)
- Records are normalised into a canonical stream — one line per event, stable
bytes, defined in
spec/records.md. - Epochs are days, cut at your fixed UTC offset rather than UTC, so a day is a day where you live.
- Segments are the unit of key custody. A day can hold several; withdrawing access cuts a new one immediately rather than waiting for midnight.
- Each segment is sealed under its own key (ChaCha20-Poly1305), and that key is wrapped separately to each reader (X25519 + HKDF-SHA256).
- Grants are signed and hash-chained, and filed under a tag derived from the shared secret — so the log proves what happened without naming anyone in it.
- Peers hold a share of the pool and serve what they hold. A peer stores segments, every reader's wraps, and the grant log — and can read none of it.
Revocation is the absence of a file. Nobody is told; the departing reader is simply not wrapped for the next segment. What they already downloaded stays readable, and nothing here pretends otherwise.
That is the same picture again, one day later, with one box changed:
Nothing on the network was asked to stop, because nothing on the network could be.
Both diagrams are generated from docs/img/src/ — edit the HTML and
re-export rather than editing the PNG.
Turning swarm on is the whole instruction. There is no address to exchange and nobody to ask: a phone finds the other members, works out which slice of the subject space is its share, and holds what falls there — for people it has never met and cannot read.
A topic is a bucket of the subject space, not a person. A subject lands in a bucket by the first bits of its hash; each peer carries a handful of buckets chosen from its own id, and everyone sharing a bucket holds everything in it. Redundancy is simply how many peers share a bucket.
| peers | buckets | each carries | held per phone | copies |
|---|---|---|---|---|
| 5 | 8 | 5 | 78 MB/yr | 3 |
| 100 | 128 | 4 | 78 MB/yr | 3 |
| 100,000 | 131,072 | 4 | 76 MB/yr | 3 |
Roughly 1.6 MB a month, and three copies of everything, at any size. Bucket depth tracks the number of peers, so a peer's share does not grow as the pool does — being an early member is not a tax.
These figures assume a five-minute sensor, and are the floor rather than the number. A subject's stream is 13.1 MB a year at 288 readings a day. The sensor behind these measurements reports every 59 seconds — 1,586 a day — and every reading is published, because thinning them could not be done on a phone without losing data (
spec/records.md§3.3). On that sensor a subject is 54.9 MB a year and a peer carrying six of them is 329 MB, not 78. Cheap sensors are cheap to carry and fast ones are not.
Self-healing is arithmetic, not a process. A peer disappears, the pool is smaller, depth and share adjust, and the survivors cover the gap on their next pass. Nothing has to notice a loss, nobody is elected to repair it, and two peers cannot repair the same thing twice.
Membership, discovery and gossip are p2panda-net over iroh — the decision recorded in D2, and the reason this repository does not contain a hand-written DHT.
On the same network, mDNS. Anywhere else, an iroh relay: both phones hold a connection to one, it introduces them, they hole-punch to a direct connection, and it drops out of the path. If hole-punching fails it stays as a fallback.
A relay cannot read anything. Traffic reaching it is encrypted twice: your records are sealed under epoch keys before they touch the network, and that ciphertext travels inside a QUIC/TLS connection keyed to the two endpoints. In iroh's words, relays "can not decode any traffic for other iroh endpoints and only forward it".
What a relay does see, and this is the honest cost:
- the two node ids exchanging bytes — stable, so correlatable over time;
- both IP addresses, hence rough location and ISP;
- timing and volume — that a phone syncs every two minutes, that 80 KB moved at 07:30, that a device went quiet.
The default is aps1-1.relay.n0.iroh.link, run by n0 and
open to anyone without an account. Verified 2026-09-11: it resolves into
2a01:4ff:2f0::/48, registered to Hetzner Online GmbH, netname CLOUD-SIN,
country SG. If that jurisdiction or that operator is not acceptable to you —
and for health data it reasonably might not be — run iroh-relay yourself and
put your own URL in the invite. Note that it defaults to AccessConfig::Everyone;
a relay of your own carries anybody's traffic until you give it an allowlist of
node ids or a shared token.
Nothing else leaves the phones. p2panda builds its endpoint with iroh's
Minimal preset, so there is no DNS or DHT publishing of your address, and
address lookups come only from the peers you already know. The relay is the
single third party in the design.
An invite is one string, checksummed, that also fits in a QR code:
diaswarm:2:<subject-key>:<endpoint>:<purpose>:<relay>:<check>
The relay travels in the invite rather than being compiled in, so moving off
the default costs a new invite and not a rebuild of every phone. It is per
subject: two people you follow may be reachable through different relays. v1
invites, which predate the field, are still read and mean the default.
Its subject field is exactly the key someone would grant, so one code works in both directions:
| Subject's phone | Follower's phone | |
|---|---|---|
| 1 | Show my invite → show the code | |
| 2 | Scan a code → Share with them | |
| 3 | grants them, then hands its own invite back over the wire | the trend line appears |
One scan, not two. Showing the code is what opens a three-minute window in which this phone will accept an invite pushed back at it; outside that moment an uninvited offer is refused. So the subject's single scan finishes the job in both directions — it learns the reader's key and the address to hand its own invite to, because both were in the string it just read. Storing only the key is what used to make the other person scan a second code.
Scanning never guesses which direction was meant — following someone and sharing with them are opposites, and both are ordinary — so it names the key and asks. The phone doing the scanning is the one taking the action.
A camera is not always in the room, so an invite can also be pasted as text: it is short enough to send in a message, and Your invite offers to copy it.
The follower is a different app. Not a different build of the same app — a
different app. Drawing somebody else's glucose inside the software that drives a
pump is not a display bug: a new glucose row triggers the loop. The follower was
once an aapsclient flavour of AAPS (D22); it is now
Ayni, which shares this repository's Rust core and nothing else. It has no
AAPS classes, no pump driver, no profile editing and no database the loop would
read — one screen, one instruction, and a reading always shown with its age.
An invite is not a secret and not a grant. Anyone holding it can download your ciphertext and open none of it.
A follower polls every two minutes while awake, which is comfortably under the five-minute cadence a CGM produces. Measured, not intended: 105s and 123s between unattended refreshes.
Android decides the rest. Doze batches background work, so two minutes means two minutes while the phone is awake and something longer while it is in a pocket. A follower phone should be excused from battery optimisation, or it will be quiet for much longer than that. This is why every reading is shown with its age rather than as a bare number that implies it is current: a follower's dangerous failure is not an error on screen, it is a value that looks fresh and is nine hours old.
Nothing needs re-granting as time passes. Every segment sealed after a grant is wrapped for that reader as it is written, so a follower keeps receiving data indefinitely until it is withdrawn.
The whole design is one bargain, and it is worth reading before anything else.
| You get | You give up |
|---|---|
| Data available to your people when your phone is off | Any record of someone reading — reads are invisible |
| Revocation enforced by key, not by a server's goodwill | Deletion. Publication is permanent |
| A signed, tamper-evident record of every grant | A leaked key never expires |
| No server holds your data, and no operator can read or withhold it | A relay operator sees which node ids talk, and when. Run your own to change who that is |
| Peers find each other, so one phone sleeping is survivable | Membership is visible. Being in the pool is not private, though what you hold is unreadable |
| Works off your own network, not just on your wifi | Which needs a relay, and so a rendezvous point somebody runs |
| Three copies of everything, for ~1.6 MB a month | You carry strangers' ciphertext too — the deal runs both ways |
Language that must never be used about this — and the true version of each claim — is in docs/feasibility.md §11. It is not decoration. Someone may make a 3 a.m. decision based on what this says.
The AAPS add-on ships disabled in the app that drives a pump, and is
structurally incapable of dosing there. It is a
DataSyncSelector: it drains a queue outward. It holds no pump reference,
implements no constraint, and has no path into the loop. The residual risk is not
dosing — it is that a plugin which fails to construct takes the app with it, and an
app that will not start is a loop that has stopped. That is why it is off by
default, why the constructor does nothing, and why the native library is not loaded
until something is actually shared.
The one write into an AAPS database is fenced by the build flavour. A
follower puts the person it follows onto its own glucose graph, which means
inserting rows — and a glucose row landing in a looping phone fires EventNewBG,
which extends EventLoop, which runs the loop. That would not be a wrong line on
a graph; it would be dosing on somebody else's blood. So it refuses unless
Config.AAPSCLIENT, and a build flavour cannot be turned on by a setting, a
settings import, or a bug in this add-on. Two further barriers: mirrored rows are
tagged so the drain can never re-publish them as the follower's own, and nothing
is mirrored until somebody says whose line it is.
cd crates/diaswarm-core
cargo run --bin diaswarm -- keygen /tmp/me.id
cargo run --bin diaswarm -- keygen /tmp/friend.id
cargo run --bin diaswarm -- init /tmp/vault /tmp/me.id 12 # your UTC offset
cargo run --bin diaswarm -- seal /tmp/vault /tmp/me.id records.ndjson
cargo run --bin diaswarm -- grant /tmp/vault /tmp/me.id "$(cargo run -q --bin diaswarm -- pub /tmp/friend.id)"
cargo run --bin diaswarm -- read /tmp/vault /tmp/friend.id # what they can open
cargo run --bin diaswarm -- log /tmp/vault # every grant, verifiedNo AAPS database? tools/mkfixture.py builds a synthetic one, and
tools/canon.py db --stats turns it into records and reports what it dropped and
why.
Two peers, one relaying:
cargo run --bin diaswarm-net -- serve /tmp/store /tmp/node.key # peer 1
cargo run --bin diaswarm-net -- keep /tmp/store2 '<invite>' # peer 2 follows
cargo run --bin diaswarm-net -- peer /tmp/store2 /tmp/n2.key /tmp/friend.iddiaswarm-peer is a machine that does not sleep. Every other holder is a
phone, and the promise that your data stays reachable while your phone is
asleep rests on somebody else having the bytes — so one mains-powered peer makes
that structural rather than probabilistic.
Run with no arguments and it carries. It can read nothing it carries.
diaswarm-peer # carry a share of the pool
diaswarm-peer identity # the string a subject grants
diaswarm-peer read --subject <identity> --days 1
diaswarm-peer export --subject <identity> --out day.csv
diaswarm-peer summary --subject <identity> --days 90 --patient <id>
diaswarm-peer nightscout --subject <identity> --out ./ns
# and the three ways to hand a summary to a clinician
diaswarm-peer summary --subject <identity> --patient <id> --out s.json \
--report agp.html \ # prints; the clinic needs nothing
--shl-url https://files.example/cgm # a SMART Health Link; they need only a receiverread and export are the raw records, one row each. summary is the
clinician's view and is deliberately not those: a Libre 3 reports every
minute, so ninety days is about 143,000 readings, and handing a clinic that is
handing them nothing. It emits an HL7 CGM FHIR
bundle — time in range, mean glucose, GMI,
variability, days of wear, sensor active — which validates against HL7's own
validator with zero errors.
It is a FHIR document: self-contained, carrying its own subject, and holding
no HTTP requests. The IG's submission bundle is a queue of POSTs for a server,
which is not what this is; --as transaction produces one anyway for somebody
who has arranged to submit.
nightscout writes entries.json and treatments.json in the shapes the
ecosystem already speaks.
API_SECRET, or syncs. These are files,
written locally by a machine that already holds a granted key — and what leaves
is plaintext with no access control, because the software on the other side
has no idea what a group secret is.
If you then upload those files to a Nightscout instance, you have chosen to create a custodian: a server holding readable data behind a shared password, which is precisely the model this project exists to avoid. That is a legitimate choice about your own copy and it is not an extension of the trust model. The handover is a person moving a file, deliberately.
It states the window it actually holds, read off the keys rather than off a flag it was passed, so it cannot claim ninety days it was not granted. And it says when a period is too short to act on: the consensus asks for 14 days at 70% sensor active, and a report quietly summarising four days looks exactly like one summarising ninety.
spec/records.md The wire contract. Versioned in-band
docs/feasibility.md The assessment: architecture, costs, what must not be claimed
docs/decisions.md What is settled (D1–D32), and what would reopen each
docs/migration.md Moving onto p2panda: what is proven, and what a cutover still needs
docs/rights.md The Diabetes Data Rights Charter, and where this fails it
crates/diaswarm-core Records, sealing, the vault, grants. The reference implementation
crates/diaswarm-net The pool: membership and buckets (pool.rs, swarm.rs) over
p2panda-net, and the vault protocol they carry
crates/diaswarm-android The JNI surface the phone calls
crates/diaswarm-peer The desktop: an always-on carrier, a reader, and the
clinician's FHIR summary
plugin/ The AAPS add-on: settings screen, QR scanner, sync worker
follower/ Ayni — the standalone follower app. No AAPS in it at all
tools/canon.py AAPS SQLite → canonical records, with a dropped-and-why report
tools/seal.py The sealing construction in Python, byte-identical to Rust
tools/mkfixture.py A synthetic AAPS database, so everything runs with no real data
spike/p2panda-net What p2panda-net 0.7.1 actually does, measured
spike/p2panda-seal What p2panda-encryption 0.7.1 actually does, measured
The Python and Rust implementations are checked against each other over real record streams, byte for byte, because two implementations that agree are evidence and one implementation is an assertion.
The plugin lives here; AAPS itself stays untouched, on its own branch, in a git
worktree. You need the AAPS source, the Android SDK and NDK, and rustc 1.96+
(p2panda's floor — rust-toolchain.toml pins it), then:
CAMAPS=/path/to/your/aaps-checkout ./plugin/build-apk.sh --installIt refuses to install if the signing certificate no longer matches the device, because on a looping phone a mismatch costs you a pump re-pairing.
Its own gradle project, needing no AAPS checkout at all — only the SDK and the NDK, because it compiles the same Rust core the add-on uses:
cd follower
ANDROID_NDK_HOME=/path/to/ndk ./gradlew assembleRelease
# build/outputs/apk/release/ayni-arm64-v8a-release-unsigned.apk
# build/outputs/apk/release/ayni-armeabi-v7a-release-unsigned.apk
# — one per ABI; sign the one your phone needs before installingOne APK per architecture, not one carrying both. The record core is a Rust
library packaged uncompressed so Android can map it straight out of the APK —
15.7 MB for arm64, 10.1 MB for armv7 — so a single APK made every phone download
the other architecture's copy as well. Splitting takes arm64 from 28.2 MB to
18.0 MB and armv7 to 12.4 MB, and it stops the app being offered at all
to an x86 device it could only have crashed on. Each gets its own versionCode
(base × 10 + ABI index, arm64 higher) because two APKs of one release cannot
share a number.
Released builds are signed by CI on a follower-v* tag; the workflow is
.github/workflows/ayni.yml.
- The cryptography has not been reviewed. Standard primitives, composed by hand. Do not rely on this where being wrong would matter.
- No forward secrecy. A key that leaks opens everything it was ever wrapped for.
- Reads are invisible. Nobody can tell you who has read their copy, or when.
- Peers find each other; people do not. Joining the pool needs nothing, but to read someone you still exchange a code and they still have to grant you. There is no directory and no way to search for a person — deliberately.
- Being in the pool is visible. Membership is a gossip topic anyone can join, and so are the bucket topics where peers announce what they hold — so who participates, and roughly what they carry, is not secret, even though every byte of it is unreadable ciphertext (D19).
- The relay is a metadata observer. It cannot read a byte, but it sees which node ids talk, from which addresses, and how often. The default is operated by a third party in Singapore. Running your own moves that to you — and shrinks the crowd you were hiding in to exactly your own family.
- The pool has only ever been three phones. Peers carrying genuinely disjoint shares, and one adopting a stranger's subject unasked, are tested on a laptop and unproven on hardware — that needs four or more devices.
- Metadata leaks. The number of grants and roughly when they happened are visible in the log, even though who they name is not.
- Background sync is at Android's mercy. Two minutes while awake; Doze stretches it, and a follower not excused from battery optimisation will be much slower. There is no push — a subscribe-and-notify protocol would fix the foreground case and Doze would still govern the rest.
- Storage only grows. Around 78 MB a year for a peer's share of the pool, and more if you also follow people directly. There is no pruning and no way to hold only recent days.
- No pause. The only controls are withdrawing the grant or turning the plugin off. There is nothing between "sharing" and "not sharing".
- The follower's memory grows and it is not yet fixed. Measured over three
hours on a phone: live allocation roughly doubles, and it is retained objects
rather than an allocator holding pages back. The suspect is a per-topic
broadcast channel in
p2panda-netthat is re-created for topics already subscribed. A separate and much worse leak inirohis worked around invendor/iroh. - The clinical summary has never been accepted by a real system. It validates against HL7's own validator with zero errors, which is not the same as one FHIR server ingesting it, and no clinic has been handed one.
- iOS is out of scope.
AGPL-3.0, matching AndroidAPS, whose interfaces the add-on compiles against.

