Reference

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.

What the user types is redacted before it reaches the model. The values behind the placeholders are encrypted and stay in Frankfurt; only the redacted text crosses to the model provider.Stays in eu-central-1 (Frankfurt)Leaves itWhat the user typedemail me at sam@ex.comon their deviceRedactionemail me at [EMAIL_1]card numbers dropped, not maskedThe modelAnthropic, USnot used for trainingStored, in placeholder formkept for your app’s retention windowThe values, encryptedAES-256-GCM, deleted when the thread closesBack to the devicethat owns thisconversation, andonly that deviceNever into a URL field
Emails and phone numbers become typed placeholders and card-like numbers are dropped before anything crosses to the model. The values behind the placeholders are encrypted, stay in Frankfurt, and are substituted back only on the way to the device that owns the conversation.

Keys

KeyWhere it livesWhat it can do
Publishable (rd_pk_live_…, rd_pk_test_…)Inside your app binaryStart conversations, upload a manifest, read published config, identify a device, rate a turn. Public by design. Nothing it reaches is a secret.
Identity secretYour backend, never your appSigns HMAC_SHA256(userId, secret) so the server can believe a claimed user id.
Console credentialsYour team's browserEverything 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:

  1. Every tenant-scoped query filters on the app id.
  2. Postgres row-level security policies filter on a session variable the API pins at the start of every transaction, with FORCE ROW LEVEL SECURITY so 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_state and context.current_screen if 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:

RiskBehaviour
readRuns on the device without asking.
writeThe server synthesizes a confirmation card. Nothing runs until the user taps it.
destructiveSame, 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 provedTurn it on when
DefaultThe 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-mediatedThe 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

DataWhereKept
Conversations, messages, action invocationsSupabase Postgres, AWS eu-central-1 (Frankfurt)Your app's retention window, 365 days by default, settable per app
Redaction valuesSame database, encryptedDeleted when the conversation closes
Knowledge baseSame databaseUntil you delete the source
Model trafficAnthropicNot 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

WhoForWhere
AnthropicThe modelUS
Supabase (on AWS)Databaseeu-central-1
VercelHostingGlobal edge, functions in eu
ResendTransactional emailUS
An embeddings providerKnowledge searchSee 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.