Everything so far assumed we could reach Bob. But Bob's phone is asleep, the app is suspended, and the only way to wake it is to ask Apple to ring a doorbell, using a token that, in the wrong hands, ties Bob's phone number to every account he talks to. This is the hardest privacy problem in the relay, and the one where we have to be most honest about the floor.
The ephemeral relay (the server that carries sealed messages between phones while storing none of them) drops a message when no device is live. For that to be safe rather than lossy, we need to make a device live: to wake Bob's phone. This article is about doing that without rebuilding the social graph (the map of who talks to whom) in the one place we can't fully control: the push provider, the company whose servers deliver push notifications. On iOS, that company is Apple.
Modern phones aggressively suspend backgrounded apps. Our relay cannot keep a socket (an open network connection) to a sleeping iPhone; iOS won't allow it. The single sanctioned way to wake an app is a push notification through Apple Push Notification service (APNs). To send one, you present Apple a device token (an opaque handle Apple issued to that specific phone) and Apple rings it.
Here is the trap. That device token is minted by Apple and tied to a physical device, and that device is tied to a person, through the App Store account, the phone number on the SIM, the whole Apple identity. The token is not itself a name, but it is one short hop from one.
So whoever holds (the device token) + (the list of which
conversations should wake it) can join Bob's phone-linked identity to his account's
traffic: exactly the phone↔account link the rest of the system is built to prevent. A
naive push service is a table of (device token, which subjects wake me) (a subject
being the named channel one conversation's messages travel on). That table is a social graph
wearing a hard hat.
And there is a second, subtler leak: Apple itself sees the pattern of wakes. Every push we send is a timing event Apple observes: this device, woken now, again in ten minutes. Even if Apple never learns the content, a dense enough timing series is a signal.
So there are two adversaries to design against: our own push infrastructure (which we can engineer away) and Apple (which we can only minimize and then disclose).
The damage requires one party to hold both the seal-opening key and the account-linked registry. So we split the push path into two services that each hold only half, plus a client rule that keeps them honest.
push-svc: the blind registry. This service stores the mapping the relay needs: "to wake subject S, here is a wake token." But every stored token is sealed, encrypted so push-svc itself cannot read it. Each row is sealed with a fresh key-encapsulation (a public-key step that wraps a one-time encryption key), so two stored tokens for the same device are mutually unlinkable at rest: they look like independent random blobs. push-svc holds no seal-opening key, no APNs credentials, and no account identity. Rows carry a short time-to-live (an expiry clock) and are admitted by an anonymous token (postage that proves the writer is entitled to a row without saying who they are; see Anonymous Admission). So even the registry is not a durable identity map. It is a pile of per-row-encrypted, expiring blobs. A dump of push-svc yields only ciphertext, encrypted bytes: no account, no phone number, no name, and no way to open a single token.
It is not, however, entirely ungroupable, and the reason is a defence rather than an oversight. Each row also records the key that first claimed its subject. Without that, any account could look up a victim's published discovery address (the standing address that lets new contacts reach them), re-point its push registration at an attacker's device, and swallow every offline wake the victim was owed. So the registry pins each subject to its first claimant.
That pin costs something. For a pair subject (the channel between two particular devices), the claiming key belongs to one device, so a dump does group that device's rows together. For a discovery inbox the claimant is the account-wide claim key rather than a device key. What the registry still holds is no identity, no content, and nothing that survives the row's expiry. Who Gets to Listen is the argument for why the trade is worth making.
push-sender: the decrypt-and-forget worker. This is the only component that can open a sealed wake token, and it is deliberately minimal: it holds the seal-opening key, receives one sealed blob, decrypts it in memory, extracts the device token, fires the one push at APNs, and forgets. It keeps no registry, never links one wake to another, and writes nothing down. It is a pure function from one sealed wake to one API call. We call it "the TEE workload" because its intended home is a TEE: a trusted execution environment, a CPU-enforced sandbox whose contents even the operator of the machine cannot read (more on that, honestly, below).
The client rule: seal only to attested keys. None of the above helps if a malicious operator can just publish their own seal key and collect readable wake tokens. So the client will only seal a wake token to a seal key that comes with a valid attestation: a signed statement that the key lives inside the expected software. The statement names that software by a hash of the exact image it is running (its measurement) and chains up to a root the client already trusts. If the attestation is missing or wrong, the client refuses to seal, fail-closed, and the wake simply doesn't get set up. The exact attestation construction (the sealed box and the signed measurement a client checks) is its own chapter: The Attested Push Seal.
The shape is a sealed pneumatic tube through a doorman. The registry is a wall of locked tubes it cannot open. The sender is a doorman who opens exactly one tube, rings exactly one buzzer, and remembers nothing. And you only drop your tube in if the doorman can prove he is the real, bonded doorman and not an impostor with a photocopier.
Recall the coarse WakeClass the relay reads. It is precisely the signal that decides how to wake, and it is tuned to spend Apple's channel, and Apple's observation of us, as sparingly as possible:
voip (a call) wakes hard and is never suppressed: a ringing phone that
sometimes doesn't ring is a broken phone, so a call always rings.alert / sync / silent use a quieter wake that is deduplicated until
reconnect (next section).call_tail / ack never wake: call teardown and tiny receipts are not worth a
push, so they wait for the next time the app is up.Matching wake aggressiveness to the coarse class is what keeps the push volume, and thus Apple's timing view of us, as low as the feature set allows.
If Alice sends Bob ten background messages while he's asleep, we do not want to fire ten pushes. That would drain the silent-push budget (the limited allowance of silent wakes iOS grants an app), and it would hand Apple a ten-event timing series about Bob.
So background wakes are deduplicated: the first miss for a sleeping device fires one wake; every subsequent miss is suppressed until that device reconnects, at which point the mark resets. It is implemented as an atomic mark-and-test (a single indivisible check-and-set step, so even sends that race each other cannot each conclude they were first), and a burst of simultaneous sends therefore still yields a single doorbell. The woken app reconnects, pulls everything waiting obliviously, and the counter arms again.
Suppression also lifts on a short timer (well under a minute) if the device never came back. That safety valve matters: a push is a best-effort request to a vendor, and one that is simply dropped must not silence the recipient indefinitely. So the honest summary is one wake per burst rather than one per sleep; a phone that stays away through continuing traffic is nudged again occasionally rather than once and never. (Calls, again, are exempt: each ring is real-time and must fire.)
This is where we refuse to overclaim, because the temptation to say "sealed in a hardware enclave, nobody can touch it" is strong and, today, would be a lie.
What is shipped and tested: the registry/sender split; the per-row sealing that makes stored tokens unlinkable; the client's fail-closed refusal to seal to a non-attested key; and a software attestation path (a signed measurement over an Ed25519 key, Ed25519 being a standard digital-signature scheme) with end-to-end tests. The compelled-operator join (the phone↔account link an operator could be forced, by court order or coercion, to produce) is closed as long as push-sender is actually running the attested image, because that image is the only thing that decrypts-and-forgets, and the client won't seal to anything else.
What is a marked seam, not yet shipped: the hardware enclave itself. The design's destination is a real trusted execution environment: an AWS Nitro Enclave or AMD's SEV-SNP, two hardware enclave technologies from Amazon and AMD. Hardware of that kind makes the seal key non-exportable even by the operator running the box, and roots its attestation in the CPU vendor rather than in our own software. That provider is stubbed (the hook in the code exists but is deliberately unimplemented), and it fails loud: ask push-sender to run in Nitro mode today and it refuses to start; ask the client to trust a hardware-rooted attestation and it throws an error. So the precise, honest statement of today's guarantee is "gated on running the attested software image," not "the key is sealed in hardware." The enclave is the remaining hardware-gated work, and the code has the seam cut for it.
The irreducible floor: even a perfect enclave does not remove Apple. To ring Bob's phone, someone must hand Apple Bob's device token, and Apple sees the wake happen. We minimize what reaches Apple (content-free pushes: the notification carries no message, just "wake up and go pull"), we ration the timing (dedup, WakeClass), and we seal everything at rest so no one else can join the graph. But Apple remains a join oracle for the wake event (a party that can be asked, for any wake, to put the phone half and the traffic half back together), and we disclose that rather than pretend a design trick removes it. It is the same posture we take on the parts of the stack where a vendor sets the floor, and on the attested push seal: name the limit precisely instead of hiding it.
That is the most private way we know to wake a sleeping phone through a channel we do not own, and an explicit, honest account of the one place we cannot.