> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reflecto.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Threat model

> What Reflecto protects against, what it explicitly does not, and what you can verify for yourself without trusting us.

Most security pages list what a product defends against. This one has both
halves, and the second is longer.

Read the [protocol page](/guides/protocol) for the mechanisms referenced here.

## What Reflecto is

A bridge between devices you already own. Your phone's notifications appear on
your computer; clips and files move both ways; an AI assistant can reach you on
your phone. There are no accounts — a "user" is a set of devices that have paired
with each other.

## What we're protecting

* **Notification content** — messages, codes, call alerts, everything your phone shows you
* **Clipboard content** — often passwords, tokens, addresses
* **Files** in transit between your devices
* **Device secret keys** — compromise means everything above, and see [no forward secrecy](#no-forward-secrecy-no-key-rotation)
* **Metadata** — who you talk to, when, and how often, which is frequently as sensitive as content

## What this protects against

| Threat                                                             | Mechanism                                                                                                                                                                                                                                                |
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A network attacker** — hostile Wi-Fi, an ISP, anyone on the path | TLS for transport, and independently, payload encryption underneath it. Breaking TLS yields ciphertext                                                                                                                                                   |
| **A compromised Reflecto server** reading your mirrored content    | The server holds public keys only. It never possesses a shared secret and cannot derive one. Stolen database plus stolen disk yields opaque message blobs — though it also yields the device graph, labels, tokens and status; see [metadata](#metadata) |
| **Us reading your mirrored content**                               | Same mechanism. We cannot produce plaintext we do not have. This does not extend to the [server-as-sender path](#messages-an-ai-agent-sends-you)                                                                                                         |
| **Google reading your notifications via FCM**                      | Content is encrypted before it reaches FCM. Google relays ciphertext, plus the cleartext routing fields [enumerated on the protocol page](/guides/protocol#how-messages-travel)                                                                          |
| **A stolen phone backup**                                          | Android excludes the content databases and the key store from cloud backup and device transfer. **Not true of the CLI or browser profiles** — see [key storage](/guides/protocol#identities-and-keys)                                                    |
| **A key substituted during pairing**                               | Detectable — but only if you check. Compare safety numbers on both devices                                                                                                                                                                               |
| **Mirroring your most sensitive apps**                             | Per-app filters run on the phone; a filtered app's notifications are dropped before encryption, with no exceptions — not even for calls or alarms. Privacy mode additionally collapses the panel to app names and counts                                 |
| **A leaked `rfk_live_` API token flooding your devices**           | Tokens are individually revocable and rate-limited per token, per IP, and per receiving user. **Hosted MCP connectors are not covered by the first and third** — see [rate limiting](#rate-limiting-is-uneven)                                           |

## What this does not protect against

### Your device being compromised

The most relevant threat to almost everyone, and the one we can do least about.
Reflecto decrypts on your devices — that is the entire design. Malware with
sufficient privileges, a rooted phone, a compromised browser profile, or someone
with your unlocked device reads everything Reflecto shows you.

Related and often overlooked: **your history on the computer is plaintext at
rest.** Notification history, clip history — including the passwords and tokens
named above — and agent messages persist in browser storage inside your profile
with no additional encryption. Any process that can read the profile can read
them.

### You cannot inspect the code

Reflecto is closed source. You are trusting a binary we built. There are no
reproducible builds, so you cannot confirm the Play Store app was built from any
particular source, and a build targeted at one user would not be detectable by
inspection. [What you can verify](#what-you-can-verify-yourself) is below.

### Messages an AI agent sends you

When you or an assistant calls the public `POST /v1/send` API, or uses the hosted
MCP connector, **the server holds that message in plaintext** for as long as it
takes to encrypt it to each of your devices. An agent holds no keys, so somebody
composes the message; that somebody is our server.

The plaintext is never written to disk, never persisted, and never logged. But
"no long-term visibility" is weaker than "no visibility," and this path gets the
weaker one. Mirrored notifications, clipboard and files never touch it. Full
detail on [the encryption page](/guides/encryption).

### Guessing a live pairing code

A six-digit code is a 10⁶ space with a 300-second window. An attacker who guesses
a live code pairs **as a phone** to a stranger's computer: they receive that
computer's outbound dismiss, reply and clipboard traffic, and can inject
arbitrary notifications into it.

What currently stands in the way is a per-IP rate limit, and it is weaker than it
looks: the limiter is in-process, so its effective ceiling multiplies by the
number of server instances. We consider this the weakest structural point in a
design with no accounts, and we would rather say so than wait to be told.

### Metadata

The server necessarily sees who is talking to whom and when: device IDs and
labels, message sizes, timestamps, delivery targets, FCM tokens, Web Push
endpoints, and per-device status (battery, charging, ringer mode, network type).
Encryption hides content, not the fact or shape of communication.

Google additionally sees delivery timing, dismissed message IDs, disconnect
device IDs, and registration nonces — all outside the encrypted envelope.

Peer-to-peer file transfer reveals each device's **public IP and port** to
**Google's public STUN servers** during connection setup, along with the fact
and timing of the attempt. Local network addresses are not sent there — they
travel only between the two peers inside the encrypted envelope.

### What we do with IP addresses

More precisely than we put it before. Our application logs do not contain client
IPs. But:

* Coarse geography (country, region, city) is derived from your IP and **sent to
  Google Analytics** with routing events.
* The public API's rate limiter stores counters keyed by literal client IP in
  Redis (`rl:api:ip:<ip>`) for **up to an hour**.
* Our hosting provider and its edge keep their own access logs, outside our code.

"We do not log IPs in our application" is true. "Your IP never persists anywhere"
would not be.

### Analytics, precisely

The analytics opt-out in settings is **client-side only**. Turning it off stops
the client SDK. It does not stop server-side analytics, which has no opt-out path
at all.

The server emits routing events — pairing initiated and completed, device
unpaired, notification relayed and dropped — to **Google Analytics 4** and
**PostHog**, keyed to a pseudonymous per-pair identifier and tagged with the
coarse geography above. No notification content, clip content, or on-device AI
output is ever collected on any path.

### An unverified pairing

Pairing is trust-on-first-use. A malicious server could hand each device a key it
controls. Safety numbers make that detectable — but only when someone compares
them, and most people never will.

### Verifying two computers against each other

You can verify your phone against a computer. You **cannot** verify two computers
against each other: they learn each other's public keys from a server-provided
directory. Concretely, **PC-to-PC clipboard sync and PC-to-PC file transfer are
trust-the-server today**, with no detection path. This is a known v1 gap and the
weakest point in the verification story.

### No forward secrecy, no key rotation

Device keypairs never rotate and there is no ratchet. One compromised device
secret key retroactively decrypts **every ciphertext for that pair that an
attacker captured**, not merely what is still queued. The server keypair does not
rotate either.

### Checking the Android app's traffic yourself

The Android app pins no certificates, but it also does not trust user-installed
certificate authorities — the platform default since Android 7. You therefore
cannot put a proxy in front of it on an unmodified device. The other three
surfaces do not have this limitation.

### The server controls delivery

Encryption stops the server reading your messages. It does not stop the server
**dropping, delaying, reordering or replaying** them. Each message is
individually authenticated; there is no integrity check across the stream, so a
missing message is not detectable by the client.

Related: the queue holds 200 entries per device and evicts the oldest on
overflow. Someone able to generate traffic to your devices can push undelivered
notifications out of the queue before you read them.

### Rate limiting is uneven

Public API tokens (`rfk_live_`) are limited per token, per IP, and per receiving
user. **Hosted MCP connectors are not**: connector sends currently carry no
per-connector quota and do not increment the per-receiver daily counter. They are
bounded by per-IP limits only, which are in-process and therefore multiply by
instance count.

### Traffic analysis

Message sizes and timing are visible to anyone watching the connection. There is
no padding and no cover traffic.

### A targeted attacker with real resources

If your adversary is a state actor, a well-resourced intelligence service, or
anyone who can compel a specific build to be served to a specific person,
**Reflecto is not the right tool.** It is built to keep an ordinary hosted service
— including us — out of your notifications. That is a real guarantee. It is not
the same as resisting a targeted adversary.

### Things we have not done

* **No third-party security audit.** libsodium and TweetNaCl-js have public
  audits; Lazysodium is an unaudited binding; our use of all of them is
  unaudited. We have not commissioned one. If we do, our intent is to publish
  the report rather than summarise it — an audit you cannot read is weak
  evidence.
* **No server or device key rotation.**
* **No warrant canary.**

## Who else touches your data

| Processor                                        | Role                        | Sees                                                                   |
| ------------------------------------------------ | --------------------------- | ---------------------------------------------------------------------- |
| Google (FCM)                                     | Push wake-up to Android     | Ciphertext, delivery timing, cleartext routing fields                  |
| Google (Web Push relays: Mozilla, Apple, Google) | Push to browsers            | Encrypted push payloads, delivery timing                               |
| Google (STUN)                                    | Peer-to-peer setup          | Each device's public IP and port, and the timing of a transfer attempt |
| Google Analytics 4                               | Product analytics           | Routing events, pseudonymous pair ID, coarse geo                       |
| PostHog                                          | Product analytics           | Same class of events                                                   |
| Sentry                                           | Error monitoring            | Scrubbed error reports (a shared denylist keeps payload data out)      |
| Cloudflare R2                                    | File-transfer relay storage | Ciphertext parts only                                                  |
| Render                                           | Hosting                     | Everything the server sees, plus its own edge logs                     |

## What you can verify yourself

Without a repository, and without our cooperation:

**Compare safety numbers.** Settings → Privacy & security → Verify encryption on
the computer; device detail on Android. Matching 60-digit numbers mean the key
exchange was not tampered with. The
[algorithm is published in full](/guides/protocol#safety-numbers), so you can
recompute it yourself from the two public keys.

**Watch what actually leaves.** The Chrome extension and the web app are
JavaScript running in your own browser — open devtools, watch the network tab,
and compare against the
[documented wire format](/guides/protocol#wire-format). You should see
`[0x01 | nonce | ciphertext]` going out and nothing legible. The CLI is an
ordinary Node process you can put behind a proxy.

**Check that decryption is local.** Every readable string in the panel came from
a blob that arrived opaque. Plaintext reaching the server would have to leave
your browser as plaintext, and you would see it.

**Read the protocol page against reality.** It documents nonce length, version
byte, minimum blob size, FCM cleartext fields and payload shapes precisely enough
that a mismatch would be obvious. If what you observe disagrees with what we
published, that gap is the story — please tell us.

## Data retention

| What                                           | Where             | How long                                                                               |
| ---------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------- |
| Queued messages (ciphertext)                   | Redis, per device | 24h TTL, 200-entry cap. **Removed on dismissal — delivery alone does not remove them** |
| Message plaintext                              | Nowhere           | Never stored, on any path                                                              |
| File-transfer parts (ciphertext)               | Cloudflare R2     | 12h                                                                                    |
| Large clip blobs (ciphertext)                  | Redis             | 1h                                                                                     |
| Interaction records                            | Redis             | Expiry plus a 24h grace period                                                         |
| Pairing codes                                  | Redis             | 300s, single use                                                                       |
| Pairing result (phone public key, device name) | Redis             | 24h                                                                                    |
| Rate-limit counters keyed by client IP         | Redis             | Up to 1h                                                                               |
| Device records                                 | SQLite            | Until that device disconnects — **see below**                                          |
| Web Push subscriptions                         | SQLite            | With the device record                                                                 |
| OAuth grants and connector records             | Server            | Until revoked                                                                          |
| On-device history                              | Your devices      | Until unpair; the wipe is best-effort and a failure is logged, not surfaced            |

**Device deletion is asymmetric and worth knowing.** Disconnecting a computer
deletes that computer's record and its pair rows. **The phone's record is
deliberately spared** — including its FCM token, label and status columns — so it
is not stranded from its other computers. The phone's record goes when the phone
itself disconnects, or when the reaper collects a device that never paired.

You can [delete your data](https://reflecto.dev/delete-data) at any time.

## Reporting a vulnerability

Email **[security@reflecto.dev](mailto:security@reflecto.dev)** with what you found and how to reproduce it.

We will not pursue or threaten anyone for good-faith security research: testing
against your own devices and account, without degrading service for others and
without accessing other people's data. Tell us before you publish and give us a
reasonable window to fix it; if you would like to encrypt the report, ask and we
will arrange a channel.
