Skip to content
The mobile SDKs are available on request. Email support@grona.ai to get access.

Security model

This page sets out what the Grona SDK trusts, what it never trusts, and what your app and your servers must do to keep an integration safe. The same contract binds the SDK, Grona’s servers and every host integration. Use it for your security review, together with Data, privacy and security.

  1. The device user may be an attacker. Anything the app decides alone, the user can change: cached files, the clock, deep links, a repackaged build.
  2. The configuration is public. The workspace key ships in your app, so anyone can fetch the configuration. It holds no personal data, no customer lists and only opaque segment ids.
  3. Network data is untrusted, even over TLS. Integrity comes from signatures, not from the channel.
  4. Money never rests on a client assertion. A treatment the device reports is display state until your backend verifies a Grona-signed grant.
  5. Fail closed for money and baskets, fail safe for the app. A failed check removes the treatment or the campaign. It never blocks the app or the order.
Input Trusted when Otherwise
Configuration Its signature verifies with a config key you pinned, and the workspace, version, time and SDK version checks pass The SDK keeps the last configuration it accepted. With none, nothing renders
Grant Its signature verifies with a grant key you pinned, and it names the identified shopper, your workspace, and the offer’s campaign, variant, treatment type and value The offer does not render
Preview pin Its signature verifies with a config key you pinned, it names this install’s id and your workspace, and it lasts at most 24 hours The pin is ignored
Product names, prices, stock and basket Always, because they come from your app’s own code
Activation answers Only to switch campaigns off, never on

See Signed configuration and Offers and grants for each check.

  • The network channel on its own. The SDK does not pin certificates. Configurations, grants and pins are trusted for their signatures, not for the connection they came over.
  • Configuration text as markup. Text is plain text, with no HTML and no links. The SDK strips control characters other than newline and tab, and the bidirectional override characters that could reorder the digits of a price.
  • Images. Image bytes are not signed. The SDK loads only asset:// paths under img/ and https URLs on hosts the configuration lists, bounds every download and decode, and drops any image it refuses. The component still renders without it.
  • Links. A preview link must match an exact form: <scheme>://grona-preview?code=... or ?clear=1, and on iOS also https://<your domain>/grona-preview?.... Any other parameter, host or path is rejected.
  • Values the app reports at checkout. Your backend applies only what a verified grant says.

Grona components can add products to the basket and navigate through your app’s own code. The SDK bounds both.

  • Actions run only from a user tap on an SDK-rendered call to action, or from a call your app makes on a decision, which your app makes from its own tap.
  • An add asks for a quantity from 1 up to the maximum add quantity option, and covers at most the maximum add lines option. Both default to 5 and accept 1 to 20.
  • Every product in an add must resolve through your app’s product lookup and be in stock. Your add handler returns accepted, partial or rejected.
  • Navigation uses typed routes (product, category, cart, checkout, deals and home) that your own router resolves. There are no raw links.
  • Prices, totals and product names in copy come only from your app’s data, never from the configuration.
  • On Android, SDK buttons ignore a tap that passes through another app’s overlay, which blocks tapjacking.

See Handle actions for Android or iOS.

  • The SDK owns a random install id, a UUID kept in app-private storage that the SDK excludes from backups (on Android also from device transfer; iOS treats backup exclusion as a hint). A reset replaces it.
  • The SDK never reads advertising identifiers or platform device ids. On Android that covers the advertising ID, OAID, ANDROID_ID and App Set ID.
  • identify takes an opaque internal customer id, never a phone number, email address or national id. The SDK cannot tell an opaque number from a phone number, so it accepts an all-digit id of 9 or more characters but logs a warning. Do not pass one.
  • Events carry product ids and prices only for products that Grona showed or added in this session. View, add and remove events for other products are still counted, without the id or price. A purchase carries the order total, treatment ids and only those session products. The full basket, browsing history and search text never leave the device. Exception: on keyed placements such as cart_line, campaign events carry the line’s product id as their key, in every arm. So while a cart_line campaign is live, the product ids of the cart lines bound to it leave the device.
  • Profiles reach the SDK only from your app, out of your own authenticated session. Grona never serves profiles keyed by customer id plus the public workspace key.
  • The configuration fetch carries no identifiers in any consent state.

See Shoppers and identity and Consent and data minimisation.

The SDK keeps its files in app-private storage that the SDK excludes from backups, and writes them atomically from its own background thread. It adds no encryption of its own: the operating system’s app sandbox, file-based encryption, data minimisation and retention caps protect the files.

Stored Notes
Install id Replaced on reset
Configuration state The cached signed configuration, and the rollback and version-guard state
Event queue Written only after consent is granted. At most 1,000 events or 7 days, applied at every start
Preview pins Each one verified again whenever it is loaded

Grants live in memory only and are never written to disk.

  • When consent is denied, the SDK deletes the event queue and the preview pins. It keeps the install id and the configuration state, which never leave the device in that state. Deleting them would re-bucket shoppers and weaken rollback protection.
  • A reset deletes the queue and the pins, replaces the install id, and clears the customer, profile, grants and basket snapshot.
  • HTTPS only. The SDK refuses plain http, even when your app allows cleartext traffic, and it does not follow redirects.
  • Deadlines. Each request has an overall deadline of 30 seconds, enforced by ending the connection, so a stalled response cannot hold a request open. On Android the connect timeout is 10 seconds and the read timeout 15 seconds; on iOS a request that stalls for 15 seconds fails. Image downloads have an overall deadline of 20 seconds.
  • Size caps. Past a byte cap, the SDK disconnects and stops reading instead of draining the rest of the response.
  • Retries. The SDK sends If-None-Match for the configuration, backs off exponentially with jitter, and honours Retry-After on 429 and 5xx responses.

For the hosts the SDK contacts, see Domains and network calls.

The debug view ships as a separate package. On Android it is grona-debug, added with debugImplementation. On iOS it is the GronaDebug product, which Swift Package Manager links into every build, so guard its import and calls with #if DEBUG.

  • Opening the debug view does nothing in release builds: on Android when the package is missing or the app is not debuggable, on iOS when GronaDebug was compiled without DEBUG.
  • On Android the debug snapshot is empty in apps that are not debuggable.
  • The record of recent events exists only in debug builds, so release builds keep no copy of order ids, totals or product ids for it.
  • Local preview pins exist only in debug builds.

On Android a repackaged build can be made debuggable; that exposes only the device user’s own data on their own device. Signed configurations, grants and pins still hold. See Test and debug for Android or iOS.

The SDK’s logger is silent by default. When you pass your own logger, it never receives customer ids, basket contents, tokens or keys.

  • Render decision strings, such as titles, subtitles, badges, call-to-action labels and product labels, as plain text: no HTML, no automatic link detection and no markdown.
  • Treat extras on a decision as untrusted layout hints. Check each value against what your layout accepts, and ignore anything else.
  • Make your logger, activation source and grant source thread-safe. Grona calls them from its background threads, sometimes at the same time as your own code, and they must never touch views.
  • Expect your host callbacks, such as product lookup and add to basket, on the main thread only.
  • Pass identify an opaque internal id, never a phone number, email address or national id.
  • Pin two config keys, the current and the next, and two grant keys in the same way if you use offers.
  • Never turn on demo mode in a production build.
  • Send grant tokens, not treatment values, with each order.
  • Serve your app’s grant source only inside the shopper’s authenticated session. Take the customer id from the session, and call Grona with your workspace secret key, which never ships in the app.
  • Verify every grant at order time: the signature with a pinned key, the expiry, the customer, the workspace and the preview flag. Use each jti once, inside the order transaction, re-check the campaign’s basket conditions against the real order, and take values only from the verified claims. A failed check means no treatment, and the order still succeeds.
  • Report revenue from your order system, server to server.
  • Pin Grona’s grant keys by kid, and add the next key before Grona signs with it.

Backend integration overview covers each job.

  • Sign configurations and preview pins with a key held in a key management service, kept apart from the credentials that publish configurations.
  • Mint grants only after checking eligibility on the server.
  • Treat preview pairing codes as bearer secrets: single use, expiring within minutes, and rate limited per install id and per IP.
  • Deduplicate events on their event_id.