EmoSend
I built EmoSend because my kid wanted a way to move photos from his iPad to a Windows PC.
It had to work in a browser, without an account, and without first uploading the files somewhere.
Those constraints led to three basic principles.
1. It should be usable by anyone
Pairing with emojis
The default pairing method is a sequence of eight emojis. One device shows the sequence. On the other, you tap the same emojis in the same order.
I initially considered using emoji but Apple, Microsoft and Google emojis do not look the same.
EmoSend therefore uses its own set of images. The sequence looks the same on both devices regardless of fonts, operating system or language settings.
cat
dog
frog
penguin
fish
butterfly
apple
banana
pizza
cake
star
moon
tree
rocket
car
heart
boat
key
gift
book
QR code and link
The same pairing information can also be carried in a QR code or a link.
Scanning the QR code or opening the link avoids entering the emoji sequence manually.
No app or account is required.
2. The server should not be able to receive the files
The files are transferred over a WebRTC data connection between the two browsers.
The application server is unable to accept file data.
What the server does
The server is used for signaling: enough information for the two browsers to establish a WebRTC connection.
It accepts small JSON messages. HTTP request bodies are limited to 4 KiB. Binary WebSocket messages are rejected, and the service has no object storage for transferred files.
The actual file data is sent over the peer connection.
On the same network
If both devices are on a normal local network, WebRTC will usually find a local route between them.
In that case a transfer between an iPad and a PC on the same Wi-Fi stays on that network.
Across different networks
If the devices are on different networks, WebRTC uses STUN to discover possible public routes.
With ordinary NAT configurations, it can often establish a direct connection between the browsers over the internet.
The file is still transferred over the encrypted WebRTC connection, not through the EmoSend server.
When a direct connection cannot be established
Some networks make direct peer-to-peer connections impossible.
Common examples are guest Wi-Fi with client isolation, symmetric or carrier-grade NAT, and networks that block UDP.
If the direct attempt has not succeeded after a few seconds, EmoSend offers a TURN relay while it keeps trying.
The relay is only used after both sides agree to it. It forwards the encrypted WebRTC traffic but does not have the keys needed to decrypt the transferred file. That rests on the setup messages arriving unchanged, which the section below is about.
A less obvious failure
Firefox on Linux can advertise its local address using mDNS. If the system has no working mDNS responder, Chrome on another machine may be unable to resolve that address.
This can make a local connection fail even when both computers are on the same network.
EmoSend detects this case and suggests a workaround.
Checking the selected path
Once WebRTC connects, both browsers inspect the selected ICE candidate pair.
If the selected route is a relay and the users did not both consent to using one, the transfer is stopped before file data is sent.
The intended behavior: use a direct connection when one can be established, and use a relay only when both sides have explicitly allowed it.
Encrypting the setup
The offer and answer pass through the server. They contain the certificate fingerprints the two browsers check each other against. When using the emoji sequence, a malicious server could swap those and trick both devices into talking to it instead of each other. The workaround would require 30 taps instead of 8.
For increased security the QR code or link can be used. It carries a random key in its fragment. The server never sees that key, and the setup messages are encrypted with it.
The sender is told which case it is by the server. A lie there hands the server the sender's messages in the clear.
The files never go through the server, in any of these cases.
3. Pairing should be difficult to guess
A pairing room exists for five minutes.
The server keeps only a small amount of state for it: creation time, expiry time, two random 128-bit tokens, and a flag for whether the receiver arrived holding a link key.
If a relay is agreed, it also records that each side consented. That record is what makes "both must agree" something the server enforces. The room's expiry is extended to cover the transfer it enabled. Expired rooms are deleted.
The emoji sequence
The visible pairing code consists of eight different emojis chosen from a set of twenty.
- 2



- 6



- 7


- 8


- 3

- 1




- 5


- 4



That gives:
20P8 = 5,079,110,400 possible sequences
or a little over 32 bits.
Join attempts are also rate-limited to ten per minute per IP address.
Why twenty emojis
Removing repeated symbols from a code makes it easier to enter, but it also reduces the number of possible codes.
With sixteen emojis and eight positions, allowing repetition gives:
16⁸ = 4,294,967,296
possible sequences.
If every emoji must be different, the number becomes:
16P8 = 518,918,400
which is about 28.9 bits.
So removing duplicates reduces the search space by roughly a factor of eight. Using twenty emojis brings it back above 32 bits while keeping the no-duplicates property.
QR codes and share links
The QR code and share link contain the pairing sequence and the key that seals the setup. Both sit in the URL fragment, the part after #.
For example:
https://emosend.com/#b4ig715j-L9J2_qQbwOLa_Nc9Hqryg
It expires with the room.
What else the page loads
Two things reach a third party. Never the files or the pairing code.
The page loads Cloudflare Web Analytics. It counts page views without cookies and without identifying visitors. The server also increments a few aggregate counters - rooms created, whether a join succeeded, whether a relay was used or declined. They tell me whether pairing works for people. Those counters only count things - no IP address, pairing sequence, token or filename is written.
You will see the beacon in DevTools. The counters are written inside the server and make no request the browser can show.
EmoSend itself runs on Cloudflare. The server is a Worker, the STUN server is Cloudflare's, and a relay, when both sides agree to one, is Cloudflare's too. The relay forwards traffic it has no key for.
Verifying it
Most of this can be checked from the browser.
Open DevTools, start a transfer, and look at the Network and WebRTC information.
You should see a small signaling WebSocket connection, the analytics beacon described above, and a WebRTC peer connection carrying the transfer. There should not be an HTTP request containing the file itself.
That is the design - make the common case simple for my kid, and keep the file transfer and pairing easy to inspect.