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

Signed configuration

Your app gets its campaigns from one configuration document per workspace, and Grona signs it. The SDK applies a signed copy bundled with your app at launch, then refreshes it from the network, and it checks the signature and a set of rules every time. This page explains the envelope, the checks, and what your app team and Grona each do. Read it before you ship the SDK and before you plan a key rotation.

Your app team Grona
Pins Grona’s config public keys in the SDK options: the current key and the next one Signs every configuration with a key held in a key management service, kept apart from the credentials that publish it
Bundles a signed copy of the configuration with the app and names it in the SDK options Signs the bundled copy too
Points the SDK at the https config URL, for example https://api.grona.ai/v1/mobile/workspaces/acme-market/config Publishes each new signed version at that URL
Ships an app release when the pinned keys change Gives you the next public key before it signs with it

The configuration is public. The workspace key ships in your app, so anyone can fetch the configuration, and it holds no personal data, no customer lists and only opaque segment ids. The signature protects its integrity, not its secrecy.

To set the keys, the config URL and the bundled copy, see Initialise the SDK for Android or iOS.

The configuration travels as a JWS in flattened JSON serialization (RFC 7515, section 7.2.2):

{
"protected": "<base64url of the protected header>",
"payload": "<base64url of the payload JSON>",
"signature": "<base64url of the 64-byte signature>"
}

The protected header holds exactly three fields:

{"alg": "ES256", "kid": "acme-market-2026-10", "typ": "grona-config+jws"}
  • alg is ES256: ECDSA on the P-256 curve with SHA-256.
  • typ is grona-config+jws.
  • kid names one of the config keys your app pins.
  • Any other header field, another alg or typ, or an unknown kid rejects the configuration.

The signature is the raw 64-byte pair of R and S values, as JOSE specifies, over the ASCII bytes of protected + "." + payload exactly as they appear in the envelope. Base64url values carry no padding.

The SDK applies a configuration only when every check passes. Otherwise it keeps the last configuration it accepted. If it has none that is still valid, nothing renders.

Check Rule
Signature Valid for the key that kid names
schema 1
workspace Equals the workspace key your app passes to the SDK, for example acme-market
version At least the highest version this install has accepted
issued_at Unix seconds between 2020-01-01 and 2100-01-01
expires_at After issued_at, at most 400 days after it, and still in the future
min_sdk At most the SDK’s own version

The SDK also rejects an envelope over 768 KB, a decoded payload over 512 KB, or JSON nested deeper than 32 levels.

The SDK checks the bundled copy the same way, and it verifies a cached copy again every time it loads it.

Your app passes Grona’s config public keys to the SDK when it initialises. Each key is a P-256 public key as base64 DER SubjectPublicKeyInfo, with its key id. Pass two, the current key and the next one. The SDK picks the key by the kid in the header and rejects every other kid, so a configuration signed with any key you did not pin never applies.

With the next key already pinned, Grona can start signing with it and installed apps keep working. Rotate signing keys gives the procedure. The same config keys verify preview pins, which Grona signs with the config key.

The payload names its workspace, and the SDK rejects a configuration for any other workspace even when the signature is valid. A valid signature alone is not enough to run in your app.

Each install remembers the highest version it has accepted and never applies a lower one. An attacker who replays an older signed configuration, for example one in which a campaign was still switched on, gets nothing. The remembered version survives restarts. It also stays on the device when consent is denied, because deleting it would weaken this protection.

Past expires_at, every campaign in the configuration is inactive, with no fetch needed. A device that cannot reach Grona, or a network that keeps serving an old copy, cannot hold a configuration in place forever. A 304 Not Modified response does not extend expires_at, so Grona signs and serves a new version before the current one expires.

A configuration can need a newer SDK, because its schema is unknown or its top-level min_sdk is above the SDK’s version. When such a configuration is validly signed, names your workspace and has a higher version, the SDK marks the configuration it is running as superseded. It then keeps only content that moves no money and no basket:

  • Offers, banners whose call to action adds to the basket, and host-rendered decisions stop at once, including on screen.
  • Banners without an add action, progress bars and messages continue.

This fails closed: an old SDK cannot keep running an offer that the newer configuration may have switched off. The mark survives restarts and clears once the SDK accepts a configuration at that version or higher. To avoid stranding older installs, Grona sets min_sdk on individual campaigns instead of raising the top-level value, so an older SDK switches off only the campaigns it cannot run.

Every time in the configuration is in Unix seconds (UTC). The SDK bounds each value when it parses it:

  • issued_at falls between 2020-01-01 and 2100-01-01 (Unix seconds 1577836800 to 4102444800).
  • expires_at is after issued_at and at most 400 days later.
  • The same 2020 to 2100 bounds apply to the iat and exp claims in grants and preview pins.

Timers and delays use saturating arithmetic, so no signed value can overflow them.

ttl_seconds in the payload is a refresh hint. The SDK clamps it to 300 to 86,400 seconds, never refreshes more often than every 300 seconds, and rejects a configuration with a negative value.

  • While your app is in the foreground, the SDK fetches the configuration every ttl_seconds. A backgrounded app fetches nothing.
  • The SDK sends If-None-Match with the ETag of its remote copy. A 304 Not Modified keeps the current copy.
  • When the endpoint fails, the SDK backs off exponentially with jitter and honours Retry-After on 429 and 5xx responses. Neither holds refetches back by more than 1 hour.
  • A remote configuration with a higher version replaces the bundled one. At equal version, the remote copy wins.

To ask for a refresh at a moment you choose, see Campaign activation and kill switch, which also covers how fast a change reaches a device.

The bundled copy lets campaigns render at first launch and offline. It is a signed configuration like any other and passes the same checks.

  • Grona never signs a bundled copy with a version above the one the server serves, so a bundle cannot block remote configurations through the rollback check.
  • The bundled copy carries its own short expiry. Once it expires, it renders nothing until a remote configuration arrives.
  • Grona keeps offers out of bundled copies or gives the bundle a short expiry, because a bundle that carries an offer can show it offline until the bundle expires.
  • Without a config URL, the SDK runs on the bundled copy alone.

Demo mode lets offers render without a grant, so a demo can run without a backend. It needs two switches:

  • the demo mode option in your app’s SDK options;
  • settings.demo: true in the signed configuration.

Only when both are on do ungranted offers render. Their treatments then carry no token and are flagged as demo, and every event carries demo: true, which keeps it out of analysis. When only one switch is on, the SDK behaves as in production and logs a warning. Production configurations never set settings.demo, so a demo flag copied into a production app does nothing.