Security summary
What the copilot can reach, what it stores, and where the boundaries actually are.
The page to send to whoever reviews vendors. It says what is true today, including the parts that are not finished. For the contractual version see the privacy policy and the DPA.
Keys
| Key | Where it lives | What it can do |
|---|---|---|
Publishable (rd_pk_live_…, rd_pk_test_…) | Inside your app binary | Start conversations, upload a manifest, read published config, identify a device, rate a turn. Public by design. Nothing it reaches is a secret. |
| Identity secret | Your backend, never your app | Signs HMAC_SHA256(userId, secret) so the server can believe a claimed user id. |
| Console credentials | Your team's browser | Everything in the console for your organization. |
Rotating a publishable key dates the old one's revocation into the future — seven days for a live key, one for a test key — so builds already on phones keep working while you ship the new one. An immediate revocation is a fleet-wide outage, which is why it is not the default.
A key is stored as a SHA-256 hash. We cannot show you a key again after it is created.
Tenant isolation
Two independent layers, and the second exists because the first is written by hand:
- Every tenant-scoped query filters on the app id.
- Postgres row-level security policies filter on a session variable the API pins at the start of every transaction, with
FORCE ROW LEVEL SECURITYso even the table owner is subject to them.
A forgotten WHERE clause returns zero rows rather than another tenant's data. This is tested rather than asserted: the test suite runs the real migrations against an in-process Postgres, as the API's own restricted database role, so a table added without the right grants fails in CI.
What reaches the model
Everything on this list, and nothing else:
- The user's message, after redaction
- The conversation's recent history
- Your persona (name, tone, instructions, guardrails)
- The names, descriptions and parameter schemas of the actions your app declares
- Knowledge chunks retrieved for that turn
context.app_stateandcontext.current_screenif you send them
App state and user text are given to the model as data, never as instructions. Your guardrails are instructions to a model, not a security boundary — anything that must not happen belongs behind an action's risk level, where the server enforces it.
Redaction
Before a message reaches a model:
- Email addresses and phone numbers become typed placeholders (
[EMAIL_1],[PHONE_1]) - Card-like numbers that pass a Luhn check are removed entirely and never stored
Placeholder values are encrypted with AES-256-GCM and stored separately from the conversation. They are substituted back only on the way to the device that owns the conversation — never into a URL field, so a block cannot be used to exfiltrate a value by being rendered.
The API refuses to start without an encryption key configured. A deployment that would store "encrypted" values in plaintext does not run.
Actions
Three risk levels, and the server decides what each one costs:
| Risk | Behaviour |
|---|---|
read | Runs on the device without asking. |
write | The server synthesizes a confirmation card. Nothing runs until the user taps it. |
destructive | Same, and worded for a decision the user cannot undo. |
The confirmation is enforced server-side: a result posted back for a write or destructive action without a confirmation is recorded as declined, so an old SDK, a third-party client or a replayed request gets the same answer as a well-behaved one. Confirmations expire after 10 minutes.
There are two levels, and the difference is who decides.
| How a confirmation is proved | Turn it on when | |
|---|---|---|
| Default | The client posts confirmed: true beside the result. Everything around it is checked here — the card is built from validated parameters, the TTL is enforced, a missing flag is a decline — but the decision itself is a boolean the device asserts. | It already is. |
| Server-mediated | The device calls POST /v1/invocations/:id/confirm when the button is pressed, before the handler runs. The server checks the invocation is yours, that the calling device owns the conversation, and that the card is still inside its TTL, then records the answer itself. The confirmed field in the result is ignored. | Every build in the wild is on SDK 0.1.0 or newer. Older ones never call the endpoint, so their confirmations would all read as declines. |
Server-mediated is the setting to hold us to if a wrong action is expensive: with it on, a compromised client cannot manufacture a confirmation, because the record it would have to forge is written on this side. Switch it on in the console under Settings → Security.
Console actions that change something are always server-mediated, whatever the setting above says: the server makes the call itself, so it acts only on a tap the confirm endpoint recorded. It may change something only as the signed-in user, with the token your app passes for them, never with a key you stored; the database refuses a write with any other kind of credential. It runs only for the person the card was drawn for: the confirming request must name the same signed-in person, proved by HMAC when identity verification is on, so an account switch on the phone cannot carry a confirmation across. It claims the invocation before the call goes out, so a retried continuation cannot send it twice, and an answer lost on the way back is reported as an unknown outcome, never retried. See Actions from the console.
Parameters are validated against the JSON Schema in your manifest before an invocation is created. Your console can disable an action or raise its risk level, and that governance is per app rather than per manifest — so it survives your next release.
Known limits
- On the default setting, the confirmation is the client's attestation, so a malicious client controlling the device can claim one the user did not give. Server-mediated confirmation is the answer to exactly this, and it is one switch away.
- A console write runs with the signed-in person's token, so your API's own rules decide what that person may change. If your API lets a user change something they should not, the copilot can too, once they confirm.
- Handlers run in your app, with your app's authority. The copilot cannot do anything your own code could not — and it cannot stop your own code doing something wrong.
- A confirmation proves a tap happened on that device. It does not prove who was holding it. If that distinction matters, require a verified identity as well.
Storage and retention
| Data | Where | Kept |
|---|---|---|
| Conversations, messages, action invocations | Supabase Postgres, AWS eu-central-1 (Frankfurt) | Your app's retention window, 365 days by default, settable per app |
| Redaction values | Same database, encrypted | Deleted when the conversation closes |
| Knowledge base | Same database | Until you delete the source |
| Model traffic | Anthropic | Not used for training |
A nightly job deletes conversations past their window and the encrypted values behind them. The maintenance run records every pass in a table, because a job that has never run and a job with nothing to do are otherwise indistinguishable — that is a lesson from this system, not a hypothetical.
Subprocessors
| Who | For | Where |
|---|---|---|
| Anthropic | The model | US |
| Supabase (on AWS) | Database | eu-central-1 |
| Vercel | Hosting | Global edge, functions in eu |
| Resend | Transactional email | US |
| An embeddings provider | Knowledge search | See the DPA |
Access by us
Staff access to the console and to transcripts is an explicit allowlist of named people, revocable without touching anyone's mailbox. Every time a member of our staff opens one of your conversations, a row is written to your audit log — visible to you in Settings → Activity, alongside every change made to your organization by you or by us.
Backups
The database is on a managed provider's own backup and point-in-time recovery, on that plan's schedule, in the same region. Ask us for the current retention window and we will tell you the number rather than a range.
What we will also tell you: there is no second region, no offsite copy beyond the provider's, and no tested restore drill. The schema is reproducible from migrations and that part is exercised on every CI run; the data is not reproducible, which is why deletion here is real deletion and why the retention window is yours to set rather than ours to impose.
What we do not have
Said plainly, because a vendor review will ask:
- No SOC 2 report. Not started.
- No penetration test report. Not commissioned.
- No uptime commitment on current plans.
- No self-hosted or on-premise deployment.
- No customer-managed encryption keys.
- No SSO or SCIM for the console.
- No tested disaster-recovery drill, and therefore no RTO or RPO worth quoting.
- No on-call rota or status page. You will hear about an incident from a person.
Reporting something
Email support@neonapps.co with SECURITY in the subject. We will acknowledge within one working day. There is no bug bounty; we will credit you if you would like us to.