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:
.package(url: "https://github.com/AppAttest/appAttest-sdk.git", from: "0.1.0") // or with CocoaPods: pod 'AppAttest'
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"]
npm install @appattest/react-native cd ios && pod install
import { AppAttest, useSecret } from '@appattest/react-native' // start once, at launch AppAttest.start() // in a component — re-renders when secrets land const key = useSecret('OPENAI_API_KEY') // outside components — async const k = await AppAttest.getSecret('OPENAI_API_KEY')
# pubspec.yaml
appattest_flutter: ^0.1.0
flutter pub get && cd ios && pod install import 'package:appattest_flutter/appattest_flutter.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); AppAttest.start(); runApp(const MyApp()); } // read — async final key = await AppAttest.secret('OPENAI_API_KEY');
npm install @appattest/capacitor npx cap sync
import { AppAttest } from '@appattest/capacitor' // start once, at launch AppAttest.start() // read — async const key = await AppAttest.getSecret('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:
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()