Quickstart
This page gets AppAttest running in an existing iOS app. End-to-end, it takes about five minutes on a real device. The rest of the docs go deeper on each step.
Before you start
- An iOS app you can run on a real device. App Attest does not run in the simulator. For SwiftUI previews, simulator runs, and tests, set
AppAttest.debugMode = .local(stubs: [...])in a#if DEBUGblock — release builds always run real attestation. - Apple Developer Program membership. App Attest is an Apple capability and requires signed builds.
- An appAttest account. Sign up at https://app.appattest.build/signup.
1. Create the app in AppAttest
In the dashboard at https://app.appattest.build/dashboard:
- Click New app.
- Enter your bundle identifier. Must match your Xcode project exactly. Example:
com.yourcompany.yourapp. - Pick a team. The default team is fine for a solo setup.
The dashboard now has one app with two environments: development and production.
2. Add secrets
In your new app:
- Select the
developmentenvironment. - Click Add secret.
- Give it a name, for example
openai. - Paste the value.
Secret values are write-only. Once saved, the dashboard never shows a value back to you — the platform can’t decrypt stored values, so there is no reveal or copy-out path. If you lose a value, overwrite the secret with a new one. Editing a secret means entering a new value to replace the old.
Repeat for production with the production-grade key when you’re ready.
3. Turn on App Attest in Apple Developer Console
Full walkthrough: see Apple Developer Console setup.
Short version:
- https://developer.apple.com/account → Certificates, Identifiers & Profiles → Identifiers.
- Open your app identifier.
- Scroll to App Services. Enable App Attest.
- Save.
You don’t need to regenerate provisioning profiles for this change.
4. Xcode configuration
Full walkthrough: see Xcode setup.
Short version:
- Open the project.
- Select your app target.
- Signing & Capabilities → + Capability → App Attest.
- Xcode adds
App Attestto your entitlements file. That’s it.
Reference: entitlements.
5. Add the Swift SDK
In Xcode:
- File → Add Package Dependencies.
- URL:
https://github.com/AppAttest/appAttest-sdk. - Pick Up to Next Major Version from the latest tagged release.
- Add the
AppAttestproduct to your app target.
Or in Package.swift:
dependencies: [
.package(url: "https://github.com/AppAttest/appAttest-sdk", from: "0.1.0")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "AppAttest", package: "appattest-sdk")
]
)
]
Platform floor: iOS 17 / macOS 14 / tvOS 17 / watchOS 10 (locked by the @Observable macro).
Using React Native, Capacitor, or Flutter? See Other platforms for the equivalent install and read APIs — the rest of this guide (account, secrets, Apple Developer Console, entitlements) applies unchanged.
6. Initialize and use
The SDK has no configuration. Bundle ID and Team ID come from the signed app at runtime — there’s no key to paste in. One synchronous call in your App init does the whole bootstrap:
import SwiftUI
import AppAttest
@main
struct MyApp: App {
init() { AppAttest.start() }
var body: some Scene {
WindowGroup { ContentView() }
}
}
AppAttest.start() is idempotent and returns in microseconds. Internally it hydrates any previously-synced secrets from Keychain (so cold-start reads feel synchronous), spawns the background attest + sync, and registers a foreground observer so the next foreground re-syncs.
Read a secret anywhere in your app via the synchronous subscript:
struct ContentView: View {
var body: some View {
if let key = AppAttest.secrets["OPENAI_API_KEY"] {
Text("Ready")
} else {
ProgressView("Loading…")
}
}
}
AppAttest.secrets is an in-memory dict observed by SwiftUI through AppAttestClient.shared (which is @Observable @MainActor). When a value lands, any view reading it re-renders. The subscript returns String? — nil while the first sync is still running, the value once it lands.
For testability or explicit injection, prefer @Environment(AppAttestClient.self):
@main
struct MyApp: App {
init() { AppAttest.start() }
var body: some Scene {
WindowGroup {
ContentView()
.environment(AppAttestClient.shared)
}
}
}
struct ContentView: View {
@Environment(AppAttestClient.self) private var attest
var body: some View {
Text(attest.secrets["OPENAI_API_KEY"] ?? "Loading…")
}
}
If you need a bootstrap step that absolutely cannot run before secrets are present, use the one-time async helper:
.task {
try? await AppAttest.waitForReady()
APIClient.configure(token: AppAttest.secrets["BACKEND_KEY"]!)
}
waitForReady() throws on .subscriptionRequired, .creditsRequired, or .unavailable — let your error UI react accordingly.
7. Observe state for unhappy paths
Errors that aren’t recoverable per-call live on AppAttest.state rather than thrown from the read path. Switch on it where you want to gate UI:
struct RootView: View {
@Environment(AppAttestClient.self) private var attest
var body: some View {
switch attest.state {
case .initializing, .attesting, .syncing:
SplashView()
case .ready:
MainView()
case .subscriptionRequired(let err):
UnavailableView(title: "Service paused", error: err)
case .creditsRequired(let err):
UnavailableView(title: "Service paused", error: err)
case .unavailable(let err):
switch err {
case .attestationRejected:
AttestationFailedView(error: err)
default:
RetryView(error: err) { attest.retry() }
}
}
}
}
retry() re-runs the sync without re-attesting — useful for a Retry button on the failure state. .attestationRejected is terminal for this install; the other .unavailable cases (.serviceUnavailable, .network) retry automatically on next foreground.
For end-user-facing apps: show a generic “service temporarily unavailable” view when state isn’t .ready. Don’t surface our specific reasons or the embedded subscribeUrl / topupUrl to end users — they’re for your developer-mode diagnostics or admin flows.
8. Verify
Run on a real device. In the AppAttest dashboard, open Recent activity. After your device attests and syncs, you’ll see a request row recorded for your app, marked as delivered, with its environment (sandbox or production). Rejections show up here too — not subscribed, no credits, rate limited, or attestation invalid — so a failed run is visible, not silent.
If you don’t see activity within 30 seconds, see errors and debugging.
What’s next
- Apple Developer Console setup — the Identifier and App Attest toggle in depth.
- Xcode setup — capability, entitlements, signing.
- Entitlements reference — what goes in the file and why.
- For AI agents — how an agent should read these docs on a developer’s behalf.