Web SDK
Fetch, verify, cache, and serve Ed25519-signed string bundles in the browser or Node.js with a framework-agnostic TypeScript library.
The AirStrings Web SDK fetches remotely managed localized strings, verifies every bundle's Ed25519 signature before exposing a single string, and caches verified bundles for offline use, in the browser or Node.js. Framework-agnostic.
Requirements: Node.js 18+ (uses global fetch) or any modern browser supporting ES2020.
Install
npm install @airstrings/webpnpm add @airstrings/webyarn add @airstrings/webSetup
Construct an AirStrings instance. Only organizationId, projectId, environmentId, publicKeys, and locale are required.
import { AirStrings } from '@airstrings/web'
const airstrings = new AirStrings({
organizationId: 'org_a1b2c3d4e5f6',
projectId: 'proj_a1b2c3d4e5f6',
environmentId: 'env_a1b2c3d4e5f6',
publicKeys: ['BASE64_ED25519_PUBLIC_KEY'],
locale: 'en',
})| Field | Type | Required | Description |
|---|---|---|---|
organizationId | string | yes | Your AirStrings organization ID. |
projectId | string | yes | Project ID. |
environmentId | string | yes | Environment ID (e.g. production, staging). |
publicKeys | readonly string[] | yes | One or more base64-encoded Ed25519 public keys. Multiple keys supported for rotation. |
locale | string | yes | Initial BCP-47 locale (e.g. "en", "fr-CA"). |
apiBaseURL | string | no | API base URL. Defaults to https://api.airstrings.com. |
logger | (level, msg, ctx) => void | no | Optional logger. Default: no-op. |
store | BundleStore | no | Inject a custom cache backend. |
seed | readonly unknown[] | no | Bundled fallback contents supplied at build time (parsed objects or raw JSON strings). |
seedDir | string | false | no | Node only. Seed directory override, or false to disable seeding. Default: probe <cwd>/airstrings/bundles/. |
Find your IDs and public key in the dashboard under Project Settings > SDK Configuration, or in the setup snippet shown after you publish a bundle.
Usage
Browser
import { AirStrings } from '@airstrings/web'
const airstrings = new AirStrings({
organizationId: 'org_a1b2c3d4e5f6',
projectId: 'proj_a1b2c3d4e5f6',
environmentId: 'env_a1b2c3d4e5f6',
publicKeys: ['BASE64_ED25519_PUBLIC_KEY'],
locale: navigator.language,
})
airstrings.on('strings:updated', ({ locale, revision }) => {
document.getElementById('greeting')!.textContent = airstrings.t('greeting')
})In the browser, bundles are cached in IndexedDB and the SDK auto-refreshes them when the document becomes visible.
Node.js (server-side)
whenReady() resolves once the first load + bootstrap + refresh cycle completes. Await it before reading strings on the server:
import { AirStrings } from '@airstrings/web'
const airstrings = new AirStrings({
organizationId: 'org_a1b2c3d4e5f6',
projectId: 'proj_a1b2c3d4e5f6',
environmentId: 'env_a1b2c3d4e5f6',
publicKeys: ['BASE64_ED25519_PUBLIC_KEY'],
locale: 'en',
})
await airstrings.whenReady()
console.log(airstrings.t('greeting'))In Node, AirStrings falls back to an in-memory cache (no IndexedDB). The cache lives for the process lifetime (gone on restart).
Locale
Switching locale loads from cache immediately if available, then refreshes:
await airstrings.setLocale('fr-CA')Methods & properties
t(key): string: raw localized string forkey, orkeyas fallback.format(key, args?): string: formats an ICU MessageFormat string.refresh(): Promise<void>: forces a bundle refresh from the CDN. Honors ETag/304.setLocale(bcp47): Promise<void>: switches locale.whenReady(): Promise<void>: resolves once the initial load cycle completes.destroy(): void: removes browser visibility listeners. Call on unmount in long-lived UIs.on(event, handler): () => void: subscribe to events; returns an unsubscribe function.strings,locale,revision,isReady: read-only snapshots of current state.
Events
type AirStringsEvents = {
'strings:updated': { locale: string; revision: number }
'strings:error': { error: AirStringsError }
}Framework integration
The SDK exposes data and events, not UI bindings.
React, via useSyncExternalStore:
const useStrings = (a: AirStrings) =>
useSyncExternalStore(
cb => a.on('strings:updated', cb),
() => a.strings,
() => a.strings,
)Vue:
const strings = shallowRef(airstrings.strings)
airstrings.on('strings:updated', () => { strings.value = airstrings.strings })String variants
String variants are A/B experiments: each variant is selected deterministically from a stable assignment id, entirely client-side, with no server round-trip. Set an assignment id once and forward exposures to your own analytics:
// A stable per-user id so each user always sees the same variant.
airstrings.setAssignmentId(currentUser.id)
// Forward every exposure to your own analytics.
airstrings.on('experiment:exposure', ({ key, experimentId, variant, locale, assignmentId }) => {
analytics.track('experiment_exposure', { key, experimentId, variant, locale, assignmentId })
})Reads are unchanged: airstrings.t('greeting') returns the assigned variant's value.
Experiment content is Ed25519-verified via a separate experiments_signature and soft-fails to base values if verification fails: variants are never served unverified. Passing null to setAssignmentId() clears the assignment and returns to base values. Available since Web SDK 1.1.0.
See String Variants for how selection, bucketing, and exposure work, and managing experiments from the CLI to create one.
ICU formatting
Every string has a format: "text" (plain) or "icu" (ICU MessageFormat). Use format(key, args) to format ICU strings via intl-messageformat:
airstrings.format('items.count', { count: 3 })text strings are returned as-is (arguments ignored). Pattern syntax: ICU by example.
Caching & offline
- On construction, the SDK loads the cached bundle (if any), then fetches the latest bundle from the CDN.
- Every bundle is Ed25519-signed. Verification is mandatory: an invalid signature is rejected and never exposed.
- Cached bundles are re-verified on every load (defense in depth). Browser cache uses IndexedDB; Node/SSR uses memory.
- Anti-downgrade protection: a newer revision is never replaced by an older one for the same locale.
- The SDK refreshes when the document becomes visible, using
ETag/If-None-Matchto avoid re-downloading unchanged bundles. - Signature failures never throw from the public API. The SDK keeps the previous good bundle and emits
strings:error. With no cache and no network, key names are returned as a fallback.
Multiple publicKeys support rotation; see Key rotation.
To ship published, signed bundles inside your build for offline-safe cold starts, see Offline & fallback.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
npm install can't find the package | Wrong package name | The package is @airstrings/web. |
t('key') returns the key name | No bundle has loaded yet, or no bundle is published for this locale | Call await airstrings.whenReady() (or refresh()); confirm organizationId, projectId, and environmentId are correct. |
| Strings never update after publishing | Wrong environmentId, or the bundle was published to a different environment | Verify the environment ID matches the one you published from. |
strings:error emitted, strings don't change | publicKeys does not contain the key that signed the bundle | Copy the environment's public key from Project Settings > SDK Configuration. After rotation, include both old and new keys. |