Graine AI

Secure your agent: your app and your users

Make sure only your own app uses your agent, prove that it is your genuine app, and prove who each user is — which way to choose, set up per agent in the dashboard, rolled out report-only first with development builds that keep working, tested end to end, and traced check by check in the Security log.

Your app talks to its agent with the agent's publishable key. The key is public by design: it ships inside your app, and anyone who unpacks the app can read it. On its own it says which agent — not which app or which person. This page adds both, per agent, from Agent → Embed & Widgets → Install → Your app and your users:

What it checksWhat it stops
Your appsThe app says which app it is: bundle ID and Team ID on iPhone, package name and signing certificate on AndroidYour key copied into somebody else's app
Verified appThe app proves it: Apple App Attest on iPhone, Google Play Integrity on AndroidA modified app, a script pretending to be your app
Verified usersThe person your app names is proven by your own sign-in: a signed user ID from your server, or your sign-in provider's ID tokenSomeone claiming to be another user to read their memory, history or booked calls

Each one is off until you turn it on, starts report-only — nothing is refused, you see what would be — and is enforced only after you confirm. Chats and calls follow the same rules, as do booking, memory, past conversations and device registration.

Requires SDK 0.14.4 on iOS, Android, Kotlin Multiplatform, Flutter and React Native (React Native also needs the calls package for app checks). Websites keep Allowed domains, and Verified users works on websites too.

What each level promises

  • Your apps stops casual reuse, not a determined attacker. The app declares its identity; someone writing their own client can declare anything. Use it to keep your key out of other apps and to see who uses it.
  • Verified app is a proof. App Attest proves the request comes from your signed iPhone app on a real Apple device; Play Integrity proves your Android app, as Google Play distributes it, on a device Google can vouch for. Neither detects a jailbroken iPhone, and a phone the user controls can always be inspected — so what you return to any app must still be safe for that user.
  • Verified users is a proof of the person, checked on every new conversation. An unproven user still gets an answer once you enforce it — anonymously, with no memory, no past conversations, no booked calls and no rings.
  • Websites: app checks apply to app requests. If the same agent also lists websites, a script can pretend to be one of them. For full protection give your app an agent of its own.

Which way should you prove users?

There are three ways, strongest first. Choose the first one that fits your app.

Code on your serverChoose it when
Server tokensYes: it mints a token for each signed-in userThe agent handles sensitive data, such as health or money
Your sign-in's ID tokenNoneYour sign-in already issues ID tokens
Your server signs the user ID (HMAC)Yes: it signs each user's IDNeither of the others works

Server tokens are the strongest, and what we recommend for sensitive data such as health or money. Your server already knows who signed in. It mints a short-lived Graine token for that user with your secret API key, and your app passes it to the SDK (tokenProvider). Nothing your sign-in issued ever leaves your systems, and once you enforce server tokens, the publishable key alone opens nothing. See Server tokens.

Your sign-in's ID token needs no server code. Your app passes the ID token your sign-in already issues, and we check it against the public keys your sign-in publishes: its JWKS URL. One caution: if your own API also accepts that same ID token as a login, use one issued for Graine, with its own audience — a separate client ID in your sign-in, for example. Then the token your app hands us can't be replayed against your API. If your sign-in can't issue one, use server tokens.

Your server signs the user ID (HMAC) only if neither works. It proves users to one agent per app: each agent has its own secret, and your app gives the SDK one identity token for all its agents.

The ID token and the signed user ID are the two choices under Prove who the user is, below. Server tokens have a setting of their own: Embed & Widgets → Install → Server tokens.

Set it up

Open the agent → Embed & Widgets → Install, and press Set up under Your app and your users.

An agent's Embed & Widgets page on the Install tab: the website snippet, the publishable key and Allowed domains, then Your app and your users, still off, with Set up

The setup has six steps, and each says where it stands.

Your app and your users, opened: two apps listed and users proven by sign-in tokens, checking proofs with nothing refused yet, and the six steps, Your apps, What we've seen, Prove it's your app, Prove who the user is, Turn it on and Send setup to your developer, each with where it stands

Your apps

Add your iPhone app and your Android app.

Your apps: an iPhone app with its bundle ID and Team ID, an Android app with its package name and two signing-certificate SHA-256 fingerprints, Add a fingerprint, and Save apps

FieldWhere to find it
iPhone bundle IDXcode → your app target → Signing & Capabilities → Bundle Identifier
Team IDYour Apple developer account → Membership (ten letters and digits)
Android package nameapplicationId in your app's build.gradle
Signing certificate SHA-256 — add every onePlay Console → Test and release → App integrity → App signing key certificate (what users' devices see), plus the Upload key certificate; for debug builds, ./gradlew signingReport

An in-house or ad hoc iPhone build signed by another team (an enterprise account, say) is another app here: add it with that team's Team ID. Up to five apps per platform, five certificates per app.

Saving your apps changes nothing for your users: the agent starts counting what it sees.

What we've seen

The last seven days of real requests, per platform and app ID: Allowed, Not in your list, Verified, Not verified, Development build and Debug token, with the reason for each and, for users, Verified, Unverified and Invalid token. Counts and app IDs only — never a user. Search by app ID; at most 20 rows per platform.

What we've seen: what each switch would refuse, a search by app ID, then each iPhone and Android app's requests from the last 7 days, Allowed, Verified, Not verified, Debug token and Not in your list, with the reasons, and the people your apps named, as counts only

This is where you catch a missing certificate (most often the Play App signing certificate) or an older app version before anything is refused. To follow the checks one by one — each with its result and why — open the Security log from here.

Prove it's your app (optional)

iPhone — App Attest. Nothing to upload. In Xcode, select your app target → Signing & Capabilities → + Capability → App Attest. Xcode adds the entitlement com.apple.developer.devicecheck.appattest-environment: development for builds you run from Xcode; Apple uses production for TestFlight and App Store builds.

Android — Play Integrity.

  1. In Play Console, open your app → Test and release → App integrity → Play Integrity API and link a Google Cloud project.
  2. In that Google Cloud project, enable the Play Integrity API (APIs & Services → Library).
  3. Create a service account in the same project (IAM & Admin → Service accounts; no role is needed) and add a JSON key.
  4. Upload the JSON key in the dashboard. It is checked on save — that the API is enabled and the project linked — without refusing anyone, stored sealed, and never shown again.
  5. Optional: enter the project number (Cloud console → project dashboard). Apps installed from outside Google Play need it.

Then switch on Check app proofs (report only): apps on 0.14.4 start proving themselves, and "What we've seen" shows the verdicts.

Prove it's your app: Check app proofs (report only) switched on, the App Attest steps for iPhone, the Play Integrity steps for Android with the app's uploaded key marked Working, the optional Cloud project number, and Development, closed

Play Integrity requests count against a daily quota (Google's default is 10,000 per app per day). An install proves itself about once a day, then reuses the verdict, so a typical app stays well inside it; Google can raise the quota.

Prove who the user is (optional)

Choose one method. Not sure which? See Which way should you prove users?

Your sign-in's ID token (recommended). If your app already signs users in with a provider that issues ID tokens — your own, or a hosted one — pass that token. Enter its JWKS URL (where the provider publishes its public signing keys — what that is), the issuer, the audience and the claim that holds the user ID (often sub). Use Test a token to paste a token and see pass or fail and why — it never shows the token's contents. Tokens are checked with a minute of clock tolerance; keys the provider rotates are picked up automatically. Of the two methods here, we recommend it because we hold no secret for it: the token is checked against the public keys your provider publishes.

Prove who the user is, with Your sign-in's ID token (JWT) first, marked Recommended and in use: the JWKS URL, issuer, audience and user-ID claim, and Test a token showing that a pasted token passes

Values for common sign-ins

The values for common sign-in providers, with your own project, tenant or client ID in place of the angle brackets:

Your sign-inJWKS URLIssuerAudienceUser-ID claim
Firebase Authenticationhttps://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.comhttps://securetoken.google.com/<project-id><project-id>sub
Auth0https://<tenant>.auth0.com/.well-known/jwks.json (or your custom domain)https://<tenant>.auth0.com/your application's Client IDsub
Amazon Cognitohttps://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/jwks.jsonhttps://cognito-idp.<region>.amazonaws.com/<user-pool-id>your app client IDsub
Oktahttps://<your-org>.okta.com/oauth2/default/v1/keyshttps://<your-org>.okta.com/oauth2/defaultyour app's Client IDsub
Sign in with Googlehttps://www.googleapis.com/oauth2/v3/certshttps://accounts.google.comyour OAuth client IDsub
Sign in with Applehttps://appleid.apple.com/auth/keyshttps://appleid.apple.comyour app's bundle IDsub
Supabase Auth (asymmetric signing keys)https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.jsonhttps://<project-ref>.supabase.co/auth/v1authenticatedsub
Your own sign-in (OpenID Connect)the jwks_uri in its /.well-known/openid-configuration (often /.well-known/jwks.json)its issits audthe claim holding your user ID
  • The claim must hold the user ID your app passes to identify. If your app identifies people by something other than the provider's user ID (a customer number, say), put it in a claim and name that claim here.
  • Copy the issuer exactly as your tokens carry it, trailing slash included. Test a token says which field doesn't match.
  • Does your own API accept the same ID token as a login? Then give Graine one issued for it, with its own audience, so the token your app hands us can't be replayed against your API (why).
  • Public-key signatures only (RS, PS, ES and EdDSA). A sign-in whose tokens are signed with a shared secret (HS256), one with no ID tokens at all (session cookies, a one-time code checked by your own server), or one you would rather not expose: use a signed user ID instead.
  • No sign-in at all: leave this off. People are treated as anonymous: chats and calls work, but there is no memory, booking or past chats for them.
  • The strongest mode works with any sign-in: server tokens. Your server already knows who is signed in, and mints each person a short-lived token.

A signed user ID (the alternative). Create a secret in the dashboard — it is shown once; store it on your server. After your own sign-in, your server computes HMAC-SHA256 of the user's ID with that secret and gives the result to your app, which passes it to identify. During a rotation two secrets work at once: create the new one, update your server, then retire the old one. Each agent has its own secret, but your app gives the SDK one identity token for all its agents: if one app uses several agents, use your sign-in's ID token, which works for all of them.

Prove who the user is, with the alternative, Your server signs the user ID (HMAC), in use: one active secret, Create a second secret to rotate, the server code in Node.js, Python, Java, Kotlin and Go, Test a token, and Check identity tokens (report only) switched on

import { createHmac } from "node:crypto";
 
// After your own sign-in: the token for the signed-in user's ID.
export function graineIdentityToken(userId, secret = process.env.GRAINE_IDENTITY_SECRET) {
  return createHmac("sha256", secret).update(userId).digest("hex");
}

The user ID is exactly the value your app passes to identify for the agent's identity field (Who is the user?). Never put the secret in your app.

Turn it on

Three switches, each with a confirmation that states what it will refuse and how much of last week's traffic that would have been:

  1. Only my apps — any app not under Your apps (or on an SDK older than 0.14.4) is refused with app_not_allowed.
  2. Require a verified app — an app request without a proof is refused with app_not_verified.
  3. Require verified users — an unproven user gets anonymous answers; memory, booking and past conversations answer identity_unverified.

Turn it on: Only my apps, Require a verified app and Require verified users, all off, each with how many requests from the last 7 days it would have refused or treated as anonymous

The confirmation before Only my apps turns on: what it will refuse, and that 92 requests in the last 7 days would have been refused, with Cancel and Turn on

Turning a switch off takes effect within 30 seconds and needs no confirmation.

Send setup to your developer

The last step gives you a short block to copy: update to SDK 0.14.4, add the App Attest capability and the Play Integrity dependency, pass the identity token to identify, and a link to this page.

Send setup to your developer: Copy instructions above the steps for whoever builds the app, the SDK version, App Attest, Play Integrity, the identity token and debug tokens, and a link to this guide

What is a JWKS URL, and where is mine?

A JWKS URL is a public web address where your sign-in publishes the public keys that check its tokens' signatures. JWKS means JSON Web Key Set, a published standard (RFC 7517). Private keys are never in it: your sign-in keeps them, and only it can sign a token.

Each key in the set has an ID, its kid, and each token's header names the kid of the key that signed it. That is how your sign-in rotates keys without breaking anything: it publishes a new key beside the old one and starts signing with it, and we pick up the change by ourselves.

Where yours is: Values for common sign-ins, above, lists it for Firebase, Auth0, Amazon Cognito, Okta, Google, Apple and Supabase. For your own sign-in, open its OpenID Connect discovery document, /.well-known/openid-configuration: the JWKS URL is its jwks_uri.

Check yours: open it in a browser.

  • It shows {"keys": [ … ]}: a list of keys, each with its type (kty, such as RSA or EC) and its kid.
  • No key in it has a d. A key with a d is a private key, there for anyone to sign tokens with: have your sign-in replace that key before you go on.
  • It starts with https:// and answers at that exact address. We read it without signing in and without following redirects, so a sign-in page, an error page or a redirect means it is the wrong address.

In your app

The SDK sends which app it is on every request by itself. Add the identity token, a way to refresh it, and, on first launch, the app's proof.

// After your sign-in: who the user is, and the proof from your server or sign-in provider.
graine.identify(["user_id": user.id], identityToken: tokenFromYourServer)
 
// Called when a token has expired; return a fresh one (or nil to keep the current one).
graine.identityTokenRefresher = { await yourServer.freshGraineToken() }
 
// On first launch, prove the app (App Attest) ahead of the first conversation.
Task { _ = await graine.prepareAppVerification() }
 
// Sign-out drops the token with the identity.
graine.identify(nil)

Fetching the token after sign-in? Identify the user right away and set the refresher. From SDK 0.14.6 on iOS, Android and Kotlin Multiplatform, and website loader 1.6.1, while no token is held, the SDK asks your refresher before the user's first conversation, and before a request for them, and waits for it, at most 10 seconds, so that conversation is verified. On a website the widget's conversation waits at most a second more, so the loader asks as soon as the visitor shows interest in the widget. Answering a call that rings never waits. With Flutter, React Native or an older SDK, fetch the token before you identify, or the first conversation may be answered anonymously.

A token that expires never cuts a live chat or call. Tokens are checked when a conversation starts; a conversation already running carries on, and its reconnects are covered for an hour after the last successful check. Call the refresher hook (or setIdentityToken) to keep tokens fresh.

Signing out and in as someone else never mixes people. identify(null), or another user's traits, drops the previous token and its verdict in the same step.

Test with verification on

Simulators cannot run App Attest; emulators, sideloaded debug builds and CI cannot pass Play Integrity. Development mode keeps them working once you enforce:

  • Debug tokens. A debug build creates a debug token once per install, prints it in the console — Graine debug token for this install: gdt_… — and sends it instead of a proof. A release build never creates, prints or sends one unless your app explicitly opts in (debugTokenMode set to always, for an internal test build). Each SDK detects debug builds the platform's way: iOS from the build configuration or the Simulator, Android from the app's debuggable flag, Flutter from debug mode, React Native from development mode.
  • Register it under Prove it's your app → Development → Debug tokens with a name ("Asha's simulator", "CI"). A registered token counts as a verified app for that agent only, expires after 30 days by default (at most 90), can be revoked at any time, and is never shown again. Up to 20 per agent; each use appears as Debug token in "What we've seen".
  • Allow development builds (off by default) accepts App Attest's development environment and Android builds installed outside Google Play when they are signed with a listed certificate. Its confirmation states the risk: anyone with your development signing key could pass as your app. Turn it off before you release.
  • Test users (up to 20) are user IDs accepted without an identity token — only from an install that sent a registered debug token, never from a real device without one.

Development, under Prove it's your app: two registered debug tokens with when each was added and expires, a new debug token being registered with a name and how long it lasts, Allow development builds switched off, and a test user

Every agent your app uses needs its own lock

Each publishable key is separate, and so are the settings on this page: an agent's switches cover only the requests made with that agent's key. If your app ships keys for two agents — a support agent and a sales agent, say — set up and switch on both. An unlocked second agent is a way in: its key is in the same app for anyone to read, and with nothing switched on, any app can use it and name any user.

The Security page, in the sidebar under Account, shows any agent whose key any app can use. See Secure your workspace, and for several agents in one app, Multiple agents.

Roll it out

  1. Update your app to SDK 0.14.4 and release it.
  2. Add Your apps, and the proofs you want, on every agent your app uses. Leave everything report-only.
  3. Watch What we've seen and the Security log for a few days, until Not in your list and Not verified are only traffic you do not recognise. Run Test it end to end: what a switch would refuse shows as Would be refused.
  4. Turn on Only my apps, then Require a verified app and Require verified users, one at a time, and run the test again.

Test it end to end

Check that each switch does what you expect, with the builds your users get. Run the list first while the switches are report-only: nothing is refused, and the Security log says Would be refused where a switch would refuse. Then run it again with the switches on, and you should see what each step says. Do it for every agent your app uses.

  1. Your key in another app. Put your publishable key into an app that isn't under Your apps — a new, empty app with its own bundle ID or package name — and open a chat. You should see it refused with app_not_allowed (Only my apps).
  2. An install without a proof. Run a debug build of your app on a simulator or emulator, where it can't prove itself, without registering its debug token. The build must be under Your apps (on Android, list its debug certificate); otherwise Only my apps refuses it first. You should see it refused with app_not_verified (Require a verified app).
  3. Your real app. Install it from TestFlight or a Google Play testing track, and open a chat. You should see it as Verified under What we've seen.
  4. A user without an identity token. Sign in, but don't pass an identity token to identify. You should see the agent answer anonymously: no memory, no past conversations, no booked calls (Require verified users).
  5. The Security log. Open it, and read the rows for the tries above: which check ran, the result and why.

Security log: trace every check

Each check an agent's security makes, and each change to its security settings, is in the Security log — what it decided and why, with the app, the platform, a one-way install code and which part of the agent it was for. A refused request is one row, the check that refused it; passes and report-only results are one row per install every 10 minutes (report-only ones counted ×N). Rows are kept for a year, filtered as you choose, a page at a time, and can be downloaded as CSV. Open it under Security → Security log, or for one agent in What we've seen above. Only Admins can open it, and it never holds an IP address, a user ID, a name, a phone number, an email, a token, a proof or a key.

What each row shows, how rows are grouped, and how to filter, page and share: Security log.

If something is refused that should not be

Turn the switch off — it takes effect within 30 seconds — then find the request in the Security log, or look at What we've seen: the reason names what was missing (an unlisted certificate, an older SDK, an expired token, a development build). Fix it, watch the counts, and turn the switch on again.

  • An Android app refused after a Play release: add the Play App signing key certificate (Play Console → App integrity), not only the upload key.
  • Google or Apple briefly unreachable: an install proven in the last seven days keeps working while the service is down; only an install never proven has to wait.
  • A booked call answered right as a verdict lapsed: the verdict the phone had when it rang carries through the answer, so a ring is never refused mid-way.

Sub-accounts

Each sub-account sets up its own agents, from its own workspace. A parent's Admins who have access to a sub-account can open it from the parent and set it up there, as for any other setting; the parent's Security page lists those sub-accounts with a summary of each. A sub-account's own Admins see only their own. See Secure your workspace.

Error codes

CodeWhenWhat to do
app_not_allowedOnly my apps is on and this app is not listed, or its SDK is older than 0.14.4List the app (and every certificate), or update the SDK
app_not_verifiedRequire a verified app is on and this install has no valid proofAdd App Attest / Play Integrity; for a debug build, register its debug token
identity_unverifiedRequire verified users is on and the user is not provenPass a valid identity token to identify, or refresh it