appAttest
Docs

Get started with AppAttest

Deliver your API keys to genuine iOS installs at runtime. Add the SDK, store your secrets in the dashboard, and read them in code — nothing sensitive ships in your binary.

Overview

Every platform exposes the same shape: start → optionally waitForReady → read a secret → observe lifecycle state. There's no configuration: the SDK derives your app identity on-device, and sandbox vs production is selected automatically from Apple's attestation.

Requirements

  • ·iOS 17 or later.
  • ·A real device — App Attest needs hardware, so there's no simulator path. Use debug mode for previews and CI.
  • ·Swift natively, or React Native, Flutter, or Capacitor via the bridges. The bridges deliver on the iOS side; there's no Android or web attestation.

Install & quickstart

The same shape on every platform — start once at launch, then read a secret. Pick yours:

Install · Swift Package Manager
.package(url: "https://github.com/AppAttest/appAttest-sdk.git", from: "0.1.0")
// or with CocoaPods:  pod 'AppAttest'
Quickstart
import SwiftUI
import AppAttest

@main struct MyApp: App {
  init() { AppAttest.start() }
  var body: some Scene { WindowGroup { ContentView() } }
}

// read — synchronous, nil until the first sync lands
let key = AppAttest.secrets["OPENAI_API_KEY"]

Reading secrets

Swift reads are a synchronous subscript. The cross-platform bridges read asynchronously — that difference is intentional per runtime.

Swift          let key = AppAttest.secrets["KEY"]
RN / Capacitor await AppAttest.getSecret('KEY')
Flutter        await AppAttest.secret('KEY')

For a required key, await waitForReady() before reading so the first sync has landed.

Lifecycle state

Gate your UI on state rather than try/catch. The states are:

ready initializing attesting syncing subscriptionRequired creditsRequired unavailable

In SwiftUI the client is observable, so views re-render as the state changes. Branch on it to decide what to show:

switch AppAttestClient.shared.state {
case .ready:
  // secrets are available — use them
case .subscriptionRequired:
  // send the developer to subscribe
case .creditsRequired:
  // prompt a top-up
case .unavailable:
  // transient — show a retry and call retry()
case .initializing, .attesting, .syncing:
  // loading — show a spinner
}

On the bridges the same states arrive via useAppAttestState() (React Native), stateStream (Flutter), and getState() + listeners (Capacitor).

Testing without a device

App Attest needs real hardware, so it can't run in the simulator, in SwiftUI previews, or in CI. Rather than special-casing the attestation path for those builds, inject local stubs in debug — your code reads secrets exactly as it will in production, and the whole app runs normally. The stubs are stripped from release.

#if DEBUG
AppAttest.debugMode = .local(stubs: ["OPENAI_API_KEY": "sk-test-stub"])
#endif
AppAttest.start()
For AI agents — this page is available as markdown:
View markdown