docs(design): specify delivery when the recipient is not running

Sender-held offers with a bounded foreground pull instead of push
infrastructure. Records that APNs is out of scope and that mobile-to-mobile
with both apps closed is unsupported.
This commit is contained in:
2026-08-06 15:51:00 +02:00
parent 7afc7d0892
commit 0388422318

View File

@@ -25,8 +25,9 @@ Local network discovery was considered and deliberately dropped. See
### Non-goals
- No automatic acceptance of transfers, under any configuration.
- No store-and-forward: an offer to an unreachable device fails; nothing is
queued and nothing is uploaded anywhere.
- No server-side store-and-forward and no push infrastructure. An offer to an
unreachable device is held **on the sender's own device** or fails; nothing is
uploaded anywhere. See §11 for what this means in practice.
- No presence or "who is online" indicator. Knowing it requires probing, and
probing tells every contact when you opened your list. Reachability is
resolved lazily, at send time.
@@ -237,7 +238,8 @@ plainly rather than presenting an opaque failure. Local-only users in particular
should be told that contacts depend on a cached address.
Reachability is never polled in the background. It is determined when the user
actually sends.
actually sends — and, for incoming offers, when the app next comes to the
foreground (§11).
### 5.1 Identity lifetime
@@ -329,6 +331,9 @@ No new OS permissions, entitlements, or platform bridges are required.
cleanly and reports an actionable error, rather than hanging.
- **Persistence:** grants and contacts survive a core shutdown and reopen of the
same data dir, following the existing recovery-test pattern.
- **Sender-held offers (§11):** an offer to an unreachable contact is retained,
is cancellable, is collected on the receiver's next pull, and is not
double-delivered if the receiver pulls twice.
- Per `AGENTS.md`, any bug found gets a regression test at the lowest layer.
---
@@ -352,6 +357,67 @@ with their rationale so the reasoning is not lost.
---
## 11. Delivery when the recipient is not running
An offer is a live connection to a running app. This section states plainly what
that costs and how far it is mitigated.
### 11.1 The constraint
Notifying the user is not the problem — `LocalNotificationService` and the
existing `ApprovalCoordinator` already turn an incoming approval request into a
user-visible prompt, and an incoming offer reuses that path unchanged.
*Receiving* the request is the problem. `BackgroundActivityController` holds an
iOS background assertion only while there is active work and releases it as soon
as that drains, so a suspended app has no listening socket: the sender's dial
fails and there is nothing to notify about.
Waking a suspended iOS app from the network requires a remote push through APNs,
which means a server holding device tokens and observing who contacts whom. That
is infrastructure plus a metadata leak, both of which contradict the product's
no-cloud posture. **APNs is out of scope.** (This is also why AirDrop can do it
and a third-party app cannot: AirDrop is an OS daemon, not an app.)
### 11.2 Sender-held offers with a foreground pull
When the target is unreachable, the sender holds the offer **locally** — the
share stays on the sender's disk exactly as today, with no copy anywhere else —
and the receiver collects it when its app next comes to the foreground, raising
a local notification at that point.
Resulting coverage:
| Scenario | Result |
|---|---|
| Phone → always-on desktop | Immediate; the desktop is listening |
| Desktop → phone, app closed | Delivered on the phone's next launch |
| Phone → phone, both apps closed | **Not supported** |
Desktop platforms are unaffected by any of this and are always reachable while
the app runs.
### 11.3 The presence cost of pulling
Dialing contacts on launch tells them when the app was opened and reveals the
device's address to them — precisely the leak §1 avoids by refusing background
presence polling. The pull is therefore bounded rather than automatic:
- It runs only for contacts the user has explicitly marked as allowed to be
checked, or on an explicit "check for incoming" action.
- It never runs in the background, only on an actual foreground transition.
- It is rate-limited per contact, so repeated app switching does not turn into a
presence beacon.
### 11.4 Scope statement for the UI
Mobile-to-mobile transfer with both apps closed is not supported and must not be
implied. The contact list distinguishes "reachable now" from "will be delivered
when they next open VniDrop", and an offer awaiting pickup is visible and
cancellable on the sender's side.
---
## Appendix A — Deferred: local network discovery
An earlier draft specified AirDrop-style discovery: three visibility tiers