Iceblock SDK

The first app's needs

Agreed after three rounds, and validated by the first app on 20 October 2026: what follows, with the decisions of the second round at the end. Changes from here on go through the same pages. In short:

  • Data: immutable records, one file each; a handoff chain whose records carry Iceblock’s checked writer (network and device); looking for it after two days without contact: any member network may, and it comes to whoever finds it, a handoff beating a find and two finds settled by the lower id; finished days rolled up once by the current holder, their records deleted to the trash. One group Collection, and one Collection per network, both hidden from Files.
  • Sync: no push service; sync now, or one Collection, for what’s urgent; background sync on Android; Iceblock and the app waking each other.
  • Sharing: groups only, with the app’s own invitation URLs, which open the app, so people without Iceblock can join; removing the app from a group by Owners’ approval.
  • Joining Iceblock: offered when Iceblock is found; adoption hands the app’s network and its group’s home over, several devices together; group changes afterwards as requests.
  • Identity: a static app_id (on Android, the package name), pinned to the app’s signing certificate where the platform can check it; unverifiable installs never inherit grants silently.
  • Permissions: a manifest at pairing, asked in context, enforced by withholding, per app across the network: its Collections, presence, group-presence:<group>, group-contact:<group> (required), devices, group:<id>, wake. The capability list further down is everything an app could ask for; the first app leaves out collections and group-devices:<group> because it doesn’t need them: it appears only on its own installs, and hands off between networks, never to another network’s devices.
  • Presence (stage 3): off unless needed, passive, from use of any app built on Iceblock and never attributed, with last contact; one state per network in a group, per device only for the app’s own installs on its own network; 15 minutes, 30 or 90 minutes, 5-minute steps, two hours’ expiry.
  • Packaging: its own bridge on iceblock-sdk, a platform package without Rust, feature flags, the registries on git.tricauda.com; Android and the Linux desktop first.
  • Order: the facade and packages; adoption and permissions; presence.

The first app replied to the open questions and proposed features of its own. Here’s each, with where it stands on Iceblock’s side and what Iceblock would change or ask. Its answers to the questions are under Decided there.

In short: its data model and sharing fit Iceblock as it is; adoption, permissions and waking need building, much of which was planned anyway; presence is new and the largest piece, and comes last.

Fits as it is

  • Immutable records, never edited (Q1, Q2, Q5). No conflicts to handle. Built
  • Who wrote a record. Each file version records the device that wrote it (writer) and, in a group’s Collection, the network that wrote it (network), which the group’s home sets on every version it takes from a member, having checked it came from that member. Within a network, a device below Full trust can only write versions as itself. Built in the facade: FileInfo::writer and FileInfo::network. So the app needn’t sign records with keys of its own: which person’s network handed on is checked; which of their devices, as their network says. A version the group’s home writes has no network, as it’s the home’s own: every member knows the home’s fingerprint (Group::home). Adopted, an app’s network has a new fingerprint, but its records name the old one: every member also has the group’s successions, each checked against the old network’s signature (Group::successions, and Group::current to follow them), so the adopting network reads as the one it succeeded. Built, in r78.
  • One group Collection, plus one Collection per network (Q3). Built
  • Groups only, no plain shares (Q6), and leaving a group losing its Collection. Built
  • Data only, no ready-made pairing widgets (Q13). The facade gives links, QR payloads and events; the app draws them.
  • No thumbnails, mounting or relay serving: feature flags. Built in stage 1.
  • Android in the background (Q12) and a Linux desktop app (Q14). Built in stage 1, in iceblock_platform: a WorkManager job, as the Iceblock app’s.

To adjust

A recall and a handoff can cross. A holder that’s only out of touch, not gone, may hand on while the home recalls. “The holder accepts the recall when it next syncs” doesn’t cover a holder that’s really gone, which never syncs again, and a recall that takes effect without it can fork the chain. A rule every device works out the same way, from the records alone, avoids that: where a handoff and a recall both follow the same record, the handoff wins. A holder that handed on before it saw the recall was alive, and the new holder has it; a holder that comes back without having handed on finds itself recalled. The home’s copy may show it home for a while, then gone on: “called home, but it had already gone on”. The app’s to decide; Iceblock has no part in it.

Changed after validation (the first app): a recall became “looking for it”. Any member network may look once the holder has been out of touch for two days, and it comes to whoever finds it rather than going home. The two days are judged by the home’s view of last contact, never the looker’s clock: the looker must have heard from the home in the last 30 minutes, and the home must not have heard from the holder for two days or more. A handoff still beats a find on the same record, and two finds are settled by the lower id. The rule above holds with “find” for “recall”.

Asked of Iceblock: a coarse fact a group’s app can read: the group’s home hasn’t heard from this member network for two days or more, with no times. Without it, a holder that never shared its presence could only be found by the home. Decided by Iceblock’s maintainer: a permission of its own, coarse presence, group-contact:<group>, which each member network grants or not, like any other, apart from presence. An app may require it: its manifest marks it required, so taking part in the app’s group means granting it, and the app tells a member who hasn’t that it can’t take part until they do. The fact is one bit, nothing under two days, from the home’s view, never times; Iceblock’s group page says which members’ apps can see it. Built, in r76: off by default for every member network; a group made by an app that requires it (its manifest says so) asks each member, who can’t use the app’s group until it agrees; one only asking for it works without. The home says, with the roster, which of the members that agreed it hasn’t heard from in two days or more, by when each last asked it anything about the group. Since #737 it’s per app: a group has apps, each added by an Owner for everyone in it, and each member chooses whether to use each, agreeing to group-contact for that app alone.

The first app’s answers on it:

  1. It requires group-contact:<group>. A member who declines can’t join the group’s app: without it, a holder that goes silent would keep it stuck for good. The app says so in its own words, naming no setting, and the member can grant it and join later.
  2. Two days, and the 30-minute check, suit: long enough that ordinary gaps (a phone off overnight, a laptop closed for a day) never trigger it, short enough that it isn’t lost for long. The app offers to look only when the home reports the holder out of touch and it heard from the home in the last 30 minutes.
  3. No notifications. The finder sees it found, and nobody is told where it was. The holder’s network, back in touch, simply finds it gone; to the other members a find is an ordinary handoff.
  4. Roll-ups with deletion to the trash suit, and each is named by an id of its own (d/<date>/<id>), like records, so two devices rolling up the same day never share a path or make Conflict copies. Iceblock’s note: only the holder rolls up, but while a handoff and a find cross, two devices may each think they hold it, so readers should take a day’s records from every roll-up of that day, by record id. The app’s other need, when it last heard from the home, is the group’s last sync: Group::synced_secs, when any of the member network’s devices last had the roster from the home (now, at the home itself), after Group::sync to be sure. Built, in r79.

Records add up. Every file costs an entry in its Collection’s document, kept with its history for good, and its own encryption and erasure coding, however small. A record every few minutes, over years, makes a large document that every device holding the Collection syncs. Two ways round it: roll records up (a day’s in one file, older ones purged), or a records API in the facade, many records to a file. Question: how many records a day, and how large?

Prompt delivery has limits. Iceblock has no cloud, and no push service. sync_now (or one Collection’s sync) goes at once to every device it can reach, nearby, directly or through the relay. A phone in the background can’t be reached until its next background run: about every 15 minutes on Wi-Fi, hourly on mobile data, longer in Doze. So treating delay as travel time is honest: minutes, not seconds, to a phone in a pocket. Iceblock won’t use a push service (decided): no FCM, no UnifiedPush. Built in stage 1: sync_now, and one Collection’s sync.

What a Scoped app saw before r76. An app device got the network document: the roster with device names and kinds, and every Collection’s name. Only Collection documents (files, folders, comments) were withheld. Decided: device names are a permission (devices), which the user grants, as any other; so is knowing of other Collections (collections): without it, an app knows nothing of any Collection outside its scope, not its name, its id, that it exists, how many there are, or where they link. So app devices get a cut-down network document: the roster’s keys and trust, with device names only once devices is granted, and nothing of other Collections (their records, Links, key chains’ records) unless collections is, and then only their names. Two edges, both decided: a Collection linked inside the app’s own is in its scope, as scope already works, so the app sees it as its own; and the app doesn’t know where its own Collection sits in the user’s Files (linked inside another, say). Stage 2.

How, decided by Iceblock’s maintainer (21 October): thin app devices. An app never holds the network document. At each sync, one of the user’s Full-trust or Metadata-only devices makes it a view of the network: the members’ keys and trust (their names only with devices), its own Collections and the Links among them (others’ names only with collections), their key records, and its own grants, denials and requests. The app reads that as it would the network document; it learns nothing of other Collections, not even that they exist, nor anything of other apps’ grants. What it asks goes to a Full-trust device at its next sync with one. Its files are named within its Collection, as the facade already does. Without the user’s devices in reach, it reads and writes what it holds, and syncs with its own other installs. Built, in r76.

To build

Adoption, finished Built, in r76: the app’s network succeeded by the user’s, its groups carrying on from there (the group’s home handed over, or the user’s network in its place); several of a person’s devices adopted together; the Applications section under My network; hiding its Collections from Files (asked for by the app, Q10). In r78, every member of its groups is given the succession, to read the old network’s records as the new one’s.

An app device in a group’s Collection Built, in r81. Adopted, the app holds everything its network had: its own Collections, linked anywhere or nowhere, and its groups’ Collections, and reads and writes them. It reads its groups through the user’s network, in its view of it: who’s in them, their home, their apps, successions and when the network last heard from their home, never their invitations. Found by the first app’s own adoption test against a real Iceblock, which now passes. Group changes after adoption (inviting, removing) go as requests the user approves, the group:<id> permission below.

The app in its group Built, in r76: Applications in a group. A group’s roster lists its apps, each added by an Owner for everyone in it, with Collections of its own, used by the members that choose to. Taking an app out, or moving it to another group, goes through the Owners’ approval, as taking a Collection out does; moving it to a group with another home, in r77.

Permissions, generalising scope requests Built, in r76: the manifest, the static id, requests, grants and denials, and taking them back. Withholding what isn’t granted too, by thin app devices, above. A good fit: today’s requests for a Collection become requests for a capability (collection:<id>, a Collection’s files; collections, that your other Collections exist and their names, not their files; presence, whether you’re around, from your use, on your network; devices, your devices’ names; group:<id>; group-presence:<group> and group-devices:<group>, the same from the group’s other members; group-contact:<group>, coarse presence, whether the group’s home hasn’t heard from a member for two days or more; wake), declared in a manifest the user sees when pairing it, asked for in context, and enforced by withholding (keys, documents, presence) rather than trust. In a group, the same: an app can ask for what other members’ networks share with the group for that app (group-presence:<group>, group-devices:<group>, their devices’ names), and each member network decides for itself whether to share it, as with presence. Decided: an app has a static id, checked where the platform can check it. Each app declares an app_id in reverse-domain form (com.example.notes), the same on every install and platform; on Android it must be the app’s package name. Iceblock records it on the app device’s roster entry, and on a group made through the app; grants, the Applications section and a group’s apps all go by it. Claiming another app’s id is stopped where the platform allows:

  • Verified (Android): the first time an app joins Iceblock on a phone, Iceblock records its app_id with the digest of its signing certificate. A later Android install of the same id with the same certificate is another install of that app, and inherits its grants. An impostor would need the real developer’s signing key.
  • Contradicted (a different certificate): refused as that app. Iceblock says another app claims to be it, and the user can only pair it as a separate app.
  • Unverifiable (Linux, or over the network): never inherits grants silently. Pairing shows what the app it claims to be has been given, says Iceblock can’t check it on that device, and the user decides, or pairs it as a separate app with nothing granted.
  • Either way an app device is Scoped and never holds the network’s keys, it gets into no group without an invitation, and the user can remove it, which changes its keys. The first app sets app_id to its Android package name. Built, in r76 (breaking: app_id goes on the wire when an app pairs): recording the id, pinning a certificate’s digest, inheriting or not, and the user deciding for an unchecked install. Checking the certificate, when Iceblock and the app meet on the same phone: Built, on Android, in r84.

Waking the app Built, in r76. After adoption, the Iceblock app, which syncs in the background anyway, can wake the app on the same phone when something arrives for it (an explicit broadcast to its package on Android, which then runs its own background job). The app still syncs itself, as a device of its own, but only when there’s something to fetch. The wake permission.

Detecting Iceblock (Q9) Built in stage 1: iceblock_installed() in iceblock_platform. On Android the app declares Iceblock in its manifest’s <queries> to be allowed to see it.

Presence: new, and last

Decided: presence is off unless an app needs it, and only passive. Nothing is noted or sent while no app holds a presence permission; the user’s switch is off until they turn it on. It comes from use, not from watching the device. Iceblock won’t check whether a device is being used: no idle times, screen state, do-not-disturb or quiet hours. Instead, a device says it’s been used when the person uses the app (or Iceblock) on it, and the network’s other devices, and the group’s other members, see how long it’s been. Someone who hasn’t used it in a while is away; nothing is measured to know it.

  • A network’s presence is the most recent use on any of its devices, coarsened in the core: around (used in the last few minutes), away (not for a while), with its age. Never per device, never a history. The app’s own “not now” (a pause the user sets in the app) can go with it.
  • Ephemeral state next to the documents, not in them: new session messages within the network, and new group messages through the group’s home. Both are Breaking. Rounds carry it, so a change takes up to a round to arrive (ten minutes, half an hour on mobile data), inside its expiry of two hours, after which it reads as unknown.
  • Last contact. Beside last use, each network knows when it last heard from another: from the group’s home, when each member last synced. Shown with presence, it says whether away means not using it, or not reachable at all. Built, in r83: the home passes on each member’s last sync with the roster, under the same consent.
  • Consents, as permissions: on your own network, presence, which you grant the app; in a group, your network’s switch per group and app (“Let the app know when I’m around”), and the reading app’s group-presence:<group>. Sent only while someone reads it. Built, in r83, except that it’s sent with every sync while the switch is on, not only while read.
  • Asleep can’t be known this way, short of a history, which isn’t kept. A long away stands for it.

Iceblock would build the transport for any app to use, presence its first state: ephemeral, signed, for a group and an app, expiring, consented. Built, in r83, for presence: it goes in the sessions the devices already hold, signed by them, through the group’s home, never in a document.

Order

  1. The facade and the package: start; Collections (made by name, hidden on request); write, read, list; a per-Collection change feed; who wrote each file; sync now; groups (make, invite, accept, members, leave); events; conditions and background work on Android; feature flags. The app’s travel and messages can be built on this, running alone and in groups.
  2. Adoption finished, and permissions: handing over, several devices, the app device in its group’s Collection, Applications, hiding from Files, the capability manifest and grants, the cut-down network document, the app in its group, waking.
  3. Presence: from use, the ephemeral transport, the group service, its consents.

Second round

The first app accepted the adjustments above (the recall rule, the writer from the facade, no push, the cut-down document, the order) and answered the questions back:

  1. Records: tens a day per group, each well under 1 KB, rolled up a day to a file. No records API for now. The roll-up’s rules are under Decided in the second round.
  2. Recall: the rule suits. A holder counts as gone after two days without contact (decided by Iceblock’s maintainer; the app proposed three). Since changed to looking for it (above).
  3. Presence from use, around or away with an age: suits, with the additions below; long gaps in last contact (a phone dozing overnight) stand in for asleep.
  4. Presence last suits: until stage 3, the app chooses by its own records (its last handoffs and visits).
  5. Its own bridge on iceblock-sdk, so it wants the package’s platform parts (Android’s conditions and background work) as a package without Rust. Built in stage 1: iceblock_platform, the platform parts alone. The full package, the library and its Dart API on top, is Planned, until an app needs it.

Publishing (Q15) is decided: the iceblock organisation’s registries on git.tricauda.com.

Its additions to presence, and Iceblock’s answers

  • Use of any app built on Iceblock counts, and of Iceblock itself, never saying which app: “used recently”, nothing more. The SDK reports it by itself while an app is in front and being used, so every app contributes alike; no permission is needed to contribute, only to read; and nothing is reported unless some app holds a presence permission. Accepted: it fits “off unless needed, only passive”. Built, in r83: the app calls app.used() while in front (the platform package will on Android); Iceblock counts its own use.
  • Last contact, under the same consent as presence, coarsened in the core. Accepted: recent use is around; old use but recent contact, devices on but not in use; old contact, away, off or asleep. Built, in r83.
  • Per device, within the user’s own network only, for the app’s own installs. An app holding presence sees last used and last contact for each device of the network that runs that app, so it can choose which to land on. It shows nothing beyond its own installs, ephemeral and coarsened like the rest; across a group, still one state per network. Accepted. Built, in r83.
  • Following the user as they switch devices, from their use of any app built on Iceblock on each: Accepted by Iceblock’s maintainer as a permission of its own, device-presence, since it says roughly where the user is from their use of other apps. Each of the app’s installs sees when any of them was last used on its machine (never which), and the app is woken within seconds when a machine is picked up. Built, in r90 (#769).

Decided in the second round

Iceblock’s maintainer agreed both proposals:

  1. Rolling up, without conflicts. Only days that are over are rolled up, and only by the current holder, the one device allowed to act at a time: it makes the day’s file once and never changes it, and deletes that day’s records to the trash (the first app’s change: not purged, so the holder needs no Owner’s role; the trash’s own retention clears them). Today’s records stay a file each until then.
  2. Thresholds, to tune once it runs: around is use in the last 15 minutes; last contact is recent within 30 minutes on Wi-Fi and 90 on mobile data; both are coarsened to 5-minute steps; everything expires after two hours, reading as unknown. (The first app’s third reply moved it from one hour: a phone on mobile data syncs about hourly in the background, so an hour would read it as unknown in normal use.)