OpenE2EE

Relay and prekey infrastructure

What a relay must do, what it never needs, and the exact prekey batch sizes, thresholds, and rotation intervals the SDK uses.

Status
pre-1.0
Applies to
0.1.0
Platforms
Expo · Browser · Node
Prereqs
A working client from Start → Quickstart
Reading time
12 min

Signal Protocol encrypts content between devices. It does not provide account authentication, device discovery, mailbox delivery, or remote object storage. An authenticated relay provides those functions. The relay boundary keeps private keys and plaintext outside your backend.

A relay is a device, prekey, and envelope service. It is not a key escrow, not a message archive you can read, and not a trust anchor. The formula the SDK holds to, exactly as written:

the relay never needs message plaintext or device private keys

This statement defines what the protocol requires from a relay. It does not define what operators can observe or logs can retain. It also does not limit what an attacker can see after a deployment compromise. Your operations determine those properties. See Limits and metadata.

Two devices exchange envelopes through a relay. A public prekey bundle moves between them, while private keys stay on each device.device Adevice Brelaypublic prekey bundle: readable and mobileprivateprivate
Metadata stays outside the envelope. Private keys stay on each device. Only the public key bundle moves.

What a relay implementation must preserve

ISignalProtocolRelayServer is the contract. An implementation must preserve the package's protocol semantics for:

  • identity and prekey upload
  • prekey bundle fetch
  • device registration and listing
  • envelope delivery
  • linked-device provisioning state
  • stale-device and unlink cleanup

Two invariants sit on top of that list, and both are correctness requirements rather than recommendations.

One-time-prekey consumption

A bundle fetch must consume one-time prekeys atomically.

The relay must not give one unused prekey to multiple callers. Reuse removes the forward-secrecy property that the prekey provides. The responder's store rejects the second use as a replay. The second use causes session establishment to fail. It does not cause a silent reduction in protection. Use one atomic read-and-consume operation under concurrency.

Device ownership

The server allocates linked-device slots. Clients must not select linked deviceId values. Unlink operations and stale cleanup must remain consistent across active identity types. Readers need public-key access to create sessions. Writers can change only their own account and device state. If a write trusts a client-supplied user ID, any authenticated caller can publish another user's keys.

For a custom backend:

import type { ISignalProtocolRelayServer } from '@open-e2ee/signal-protocol-sdk/remote/relay/types';

For local development, inMemoryRelay() from /remote/relay/memory gives multiple clients a shared in-memory relay. It is development only. For a concrete production path, Convex relay integration maps a Convex deployment onto this contract.

Prekeys

This is the part that decides whether your product degrades gracefully or silently.

A peer uses a prekey bundle to create a session with an offline device. The bundle contains the device's composite identity, signed prekey, and last-resort Kyber prekey. When available, it also contains a one-time prekey that the relay consumes on fetch. This one-time prekey gives the first message forward secrecy before the ratchet turns. The following controls keep one-time prekeys available.

The device generates all private halves and keeps them in the local store. Only public halves reach the relay.

The numbers

These are the SDK's actual values. Use them. Do not extrapolate.

ValueSettingMeaning
100ONE_TIME_PREKEY_BATCH_SIZEOne-time prekeys generated per replenishment, EC and Kyber alike
10MIN_PREKEY_REPLENISHMENT_THRESHOLDInternal trigger — below this, replenishment runs
50preKeyLowThreshold (client config default)Product-facing low-watermark for your own alerting
200MAX_EC_PREKEYSCeiling on server-held EC prekeys; replenishment fills available slots
172800000keyRefreshIntervalMs (2 days)Signed-prekey and Kyber-prekey rotation interval
1209600000maxPreKeyAgeMs (14 days)Maximum age before prekey material is treated as stale
43200000preKeyCheckThrottleMs (12 hours)Minimum spacing between status checks

The 10 and 50 values have different purposes. The SDK acts at 10. Your application should detect the condition at 50. The checkPreKeyStatus() method compares against this threshold, and the onPreKeyLow callback fires there. This telemetry shows the decline before the SDK reaches its internal minimum.

The rotation interval is two days. A stale SDK source comment claims a longer period. The keyRefreshIntervalMs default of 172800000 is authoritative.

The API

// Replenish and publish. Idempotent enough to run at app start.
await client.syncToServer();

// Report, without mutating.
const status = await client.checkPreKeyStatus();
// { oneTimePreKeysRemaining: number, needsReplenishment: boolean }

syncToServer() fetches the server-held counts. If EC or Kyber one-time prekeys are below the internal threshold, it generates a batch. It uploads the public halves without exceeding the 200-key limit. checkPreKeyStatus() reports the status. The SDK limits real checks to one per preKeyCheckThrottleMs.

Rotate the longer-lived keys explicitly:

await client.rotateEcSignedPreKey();
await client.rotateKyberPreKey();

Both methods check age against keyRefreshIntervalMs. Use forceCompleteKeyReset() when you cannot repair published device keys. Causes include a restored backup, a reclaimed slot, or an identity mismatch. The method regenerates and republishes all key material. It destroys existing sessions with that device.

Peers will see a safety-number change. Key rotation covers the operational schedule.

Opacity ledger

ArtifactStays on deviceSent to relayIn object storeVisible as metadata
identityKeyPair private halfyesnonono
compositeIdentity (canonical CompositeIdentityV1)yesyesnoyes
Signed prekey public half + signatureyesyesnoyes
Signed prekey private halfyesnonono
EC one-time prekey public halves (batch of 100)yesyesnocount
EC one-time prekey private halvesyesnonono
Kyber one-time prekey public halvesyesyesnocount
Kyber last-resort prekey public halfyesyesnoyes
Kyber prekey private halvesyesnonono
registrationIdyesyesnoyes
deviceIdyesyesnoyes
encryptedDeviceNameyesyesnociphertext length
Envelope ciphertextyesyesnosize, sender, recipient, timestamp
Message plaintextyesnonono
SessionRecord (version: 4)yesnonono

Next

On this page