Browse API 1 documentation
GroveAPI 1preview

Storage and settings

Persistent web data, Keychain secrets, and configuration pages.

Mango 3.1+View Markdown

Grove offers two storage layers with different security properties:

  • standard Web storage for ordinary extension state;
  • a native Keychain bridge for credentials and other secrets.

Request only the layer the extension needs.

Persistent Web storage

The storage capability gives the extension a persistent WebKit data store. Use standard browser APIs:

js
localStorage.setItem('tracked-cities', JSON.stringify(['Prague']))
const tracked = JSON.parse(localStorage.getItem('tracked-cities') || '[]')

IndexedDB, cookies, and cache data use the same isolated store. Without storage, the instance uses an ephemeral data store and must expect its browser state to disappear.

Web storage is isolated from other extension identifiers and from Mango’s own web views. In API 1, instances of the same extension identifier share their WebKit store, including instances bound to different channels. Include a network or scope key in stored records when the state should remain separate:

js
const stateKey = `settings:${grove.info.networkID}:${grove.info.scope.kind}:${grove.info.scope.target || ''}`

Do not put passwords, bearer tokens, API keys, or private keys in localStorage.

Keychain-backed secrets

The storage.secure capability unlocks asynchronous secret operations:

js
await grove.secrets.set('channel-key', secret)
const value = await grove.secrets.get('channel-key')
await grove.secrets.delete('channel-key')

The default namespace is isolated by extension identifier and Mango’s opaque network identifier. It is suitable for credentials that differ between IRC networks.

Use the explicit application namespace for a service credential shared by every network instance of the same extension:

js
await grove.secrets.application.set('service-token', token)
const token = await grove.secrets.application.get('service-token')
await grove.secrets.application.delete('service-token')

Application-scoped Roots have an empty network identifier and should use this explicit namespace for service credentials. It documents the intended lifetime and remains stable while Mango mirrors the Root into connected sessions.

Secret keys are bounded to 200 characters and values to 16,384 characters. Missing keys resolve to null. Handle rejected promises and report a safe error without logging the value.

Removing an extension removes its associated web data and secret namespaces.

OAuth access and refresh tokens belong in the same Keychain-backed storage. A common application-wide pattern is:

js
const authorization = await grove.oauth.authorize(authorizationURL)
const token = await exchangeCode(authorization)
await grove.secrets.application.set('oauth-session', JSON.stringify(token))

Do not persist the one-time authorization code or PKCE verifier after a successful exchange. See JavaScript bridge for the native authorization contract.

Settings pages

Declare a package-relative settings document:

json
{
  "entry": "index.html",
  "settings": "settings.html",
  "capabilities": ["storage", "storage.secure"]
}

Mango opens the page inside Grove Manager. It uses the same extension origin, persistent WebKit store, Keychain namespaces, and host theme tokens as the main runtime, so configuration written there is immediately visible to a running instance.

The settings page is deliberately not an IRC instance:

  • its scope is application-wide and its network identifier is empty;
  • it receives no IRC messages, commands, member actions, or lifecycle stream;
  • IRC send and persistent native-UI bridge commands are rejected;
  • its network allow-list is empty even when the main manifest declares hosts.

A settings page has no network allow-list and cannot use oauth.authorize or network.request. Perform sign-in and service calls in the visible or running extension; use settings only for local configuration.

The settings page should edit and validate configuration locally. The running extension performs service connectivity checks through its granted network destinations.

Notify running instances after saving

After committing a settings change, call grove.settings.changed() from the settings page. Mango delivers a settingsChanged event to running instances of the same extension identifier, across networks and scopes, including hidden Roots. The native delivery wakes their WebView without opening a pane, restarting the extension, or changing keyboard focus.

js
// settings.html — notify only after all writes have completed.
localStorage.setItem('tracked-cities', JSON.stringify(cities))
grove.settings?.changed()

// index.html — register during startup and also load settings initially.
grove.on('settingsChanged', () => {
  reloadSettings().catch(error => console.error('Could not reload settings', error))
})

The signal carries no settings values and cannot target another extension. Changes may coalesce while the document loads or a delivery is in flight: re-read current storage rather than counting events. Stopped extensions are not started. Continue reading settings on startup; this is a live notification, not a durable job queue. Network operations still require the running instance’s existing grants and host allow-list. Calls from a normal runtime, rather than the host-owned settings page, are rejected.

This is an additive API 1 feature in Mango 3.1 builds from 18 September 2026. Optional chaining keeps settings pages compatible with older hosts, where settings take effect on the next normal reload.

A safe settings pattern

Store public configuration in Web storage and secrets in Keychain:

js
const ENDPOINT_KEY = 'endpoint'
const TOKEN_KEY = 'bearer-token'

async function save(endpointValue, tokenValue) {
  const endpoint = new URL(endpointValue)
  if (endpoint.protocol !== 'https:' || endpoint.username || endpoint.password || endpoint.hash) {
    throw new Error('The endpoint must be HTTPS without embedded credentials or a fragment.')
  }

  localStorage.setItem(ENDPOINT_KEY, endpoint.toString())
  if (tokenValue) await grove.secrets.application.set(TOKEN_KEY, tokenValue)
  else await grove.secrets.application.delete(TOKEN_KEY)
  grove.settings?.changed()
}

Validate again in the runtime before using stored data. Web storage is extension-owned mutable input, not a trust boundary.