# Delivery, mailboxes, and push (https://docs.open-e2ee.dev/relay/delivery-and-push.md)



Relay owns one durable mailbox for each device and mailbox generation. A send is durable only after the destination mailbox stores the encrypted envelope or shared-body reference. Pull and acknowledgment remove stored work through the same device authority.

For a durable direct send (`user-visible` or `background-sync`), 202 means stored. Relay answers HTTP 202 only after the destination mailbox commits the encrypted envelope. An `ephemeral` send stores no ciphertext, and Relay answers 202 only after the online recipient device accepts it.

## Delivery units [#delivery-units]

One delivery unit is one stored encrypted envelope or shared-body reference for one destination device. Each stored Relay delivery receipt is also one delivery unit.

An exact retry with the same stable operation identifier gets the stored answer. It uses no new unit while Relay keeps its retry identity. Relay keeps it while the destination mailbox holds the message. After removal by acknowledgment or expiry, Relay keeps the retry identity of the last 256 messages removed from that mailbox. A later retry is a new delivery and uses a new unit, and the recipient session refuses the replayed ciphertext.

A direct message to one account with two destination devices uses two delivery units. A group send uses approximately one unit for each destination device that stores its shared-body reference. The direct encrypted-envelope limit is 256 KiB.

## Mailbox behavior [#mailbox-behavior]

Each device mailbox has a 3,000-message working ceiling. At the maximum envelope size, that ceiling is 786,432,000 bytes. This is 78.6432% of the Free plan's storage allowance before other ciphertext.

Relay never silently evicts a live message to make space. A full mailbox still permits pull, acknowledgment, expiry, deletion, and revocation. A new direct send receives a typed `MAILBOX_FULL` refusal, nothing is stored, and the send is not charged.

## Relay delivery receipts and E2EE receipts [#relay-delivery-receipts-and-e2ee-receipts]

Two receipts can tell a sender that a message arrived. They mean different things.

* A **Relay delivery receipt** is the Relay's record that the recipient device stored and acknowledged a message. It never proves decryption. It sets the delivery status to `delivered` with `source: 'relay'`.
* An **E2EE receipt** is the end-to-end encrypted receipt that a recipient device sends. It sets the delivery status to `delivered` with `source: 'e2ee'` and `decryptionConfirmed`.

The delivery status only moves forward, and it keeps every source that reported it.

### Project policy [#project-policy]

`relayReceipts` is a policy of each project environment, and it is on by default. Change it on the project settings page in the console. When it is on, Relay makes a Relay delivery receipt when the recipient device acknowledges a message. Relay makes none for sealed-sender, self, background-sync, ephemeral, or expired messages.

When `relayReceipts` is off, Relay makes no Relay delivery receipt. The `receipted` list is empty, and the SDK sends its own E2EE receipt under the `'auto'` rules. A message that a send stored before the change still makes a receipt when the recipient device acknowledges it. The SDK option `deliveryReceipts` is a separate setting.

### Wire frames [#wire-frames]

Relay stores one receipt for each sender account and acknowledgment batch, at each active device of the sender. A `delivery-receipt` frame carries at most 32 receipts. Each `receiptId` has 43 characters.

```json
{"type":"delivery-receipt","receipts":[{"receiptId":"Zm9v…","recipient":{"accountId":"acc_b","deviceId":2},"messageIds":["m1","m2"],"deliveredAt":1791300000000}]}
```

The sender device removes receipts with `acknowledge-receipts`. One frame takes 1 to 100 IDs, and Relay sends no reply. A frame with more than 100 IDs closes the socket with code 1008.

```json
{"type":"acknowledge-receipts","receiptIds":["Zm9v…"]}
```

Relay answers the recipient device's acknowledgment with an `acknowledged` frame. Its `receipted` list holds the message IDs for which Relay started a receipt.

```json
{"type":"acknowledged","messageIds":["m1","m2"],"receipted":["m1"]}
```

Over HTTP, `/mailbox/pull` returns `receipts` next to `messages`. `/mailbox/acknowledge` takes an optional `receiptIds` list and returns `receipted` and `receiptsAcknowledged`. The `mailbox-capabilities` frame and the pull field `relayReceipts: true` state protocol support only, not the project policy.

### Retention and limits [#retention-and-limits]

A Relay delivery receipt has the same retention as its message, and never more than 30 days. Delivery is best effort. Relay makes one attempt to store each receipt and never retries it. When the sender mailbox is full, Relay drops the receipt. A receipt never makes a send fail with `MAILBOX_FULL`. Each device mailbox keeps at most 3,000 receipts and removes the oldest one past that limit.

### Billing [#billing]

Each stored Relay delivery receipt is one delivery unit for each sender device that stores it. A dropped receipt uses no unit. A message is one unit for each recipient device.

Receipt units depend on acknowledgment batches and active sender devices. They do not always double the message units. Turn `relayReceipts` off to stop these Relay receipt units.

An E2EE receipt is an encrypted envelope, so it uses delivery units as a message does. The Console delivery estimate takes a separate total for stored Relay receipt deliveries. Include E2EE receipt envelopes in its message count.

## Push and notification profiles [#push-and-notification-profiles]

Expo, APNs, FCM, and Web Push carry best-effort wakes. Provider payloads contain
no message plaintext, ciphertext, sender or recipient identity, device or message
identifier, group or conversation identifier, attachment reference, or receipt
state. A wake tells the SDK to authenticate, pull all pending mailbox work,
process each envelope, and acknowledge accepted work.

Push providers can lose, delay, duplicate, or reorder a best-effort wake. Delivery correctness therefore depends on mailbox pull, not on a push provider. Relay removes an invalid provider token after the provider returns its terminal invalid-token result.

The device registers one profile that the project allows:

| Profile           | iOS behavior                                                          | Special Apple approval |
| ----------------- | --------------------------------------------------------------------- | ---------------------- |
| `background-only` | Discretionary background wake                                         | No                     |
| `visible-alert`   | Generic visible alert, then application pull                          | No                     |
| `nse-visible`     | Generic alert that invokes an eligible Notification Service Extension | No                     |
| `nse-filtering`   | The extension can modify or suppress the alert                        | Yes                    |

`nse-filtering` stays unavailable until Apple approves the filtering entitlement
and a signed physical-device build proves extension execution and suppression.
The entitlement does not improve APNs transport or guarantee execution. A
Notification Service Extension does not require that entitlement when it only
modifies visible content.

## Configure iOS [#configure-ios]

Use the [CLI](https://docs.open-e2ee.dev/reference/cli.md#notifications) after the first acknowledged encrypted message:

```bash
oe notifications setup ios --profile background-only
oe notifications setup ios --profile visible-alert
oe notifications add-nse
oe notifications verify ios
```

Expo applications need a development or native build. Expo Go cannot receive
this remote-push configuration or contain OpenE2EE's extension. Expo CNG uses a
generated Apple target. Bare React Native uses the same generated, generic Swift
source and an exact Xcode target handoff.

The generated extension has a timeout-safe generic fallback. It does not receive
App Group or Keychain access by default and does not decrypt a preview. Use
`oe notifications verify ios --app-bundle PATH` to inspect a signed build. A
Simulator build proves target generation and compilation, not physical-device
execution, locked-device behavior, or Apple-approved filtering.
