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 without a debug stub (see the SDK reference).
- Apple Developer Program membership. App Attest is an Apple capability and requires signed builds.
- An appAttest account. Sign up at https://app.staging.appattest.dev/signup.
1. Create the app in AppAttest
In the dashboard at https://app.staging.appattest.dev/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.
Values are shown once. Copy it now if you need it elsewhere. AppAttest cannot show it again.
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/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/sdk", from: "0.1.0")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "AppAttest", package: "sdk")
]
)
]
Platform floor: iOS 17 / macOS 14 / tvOS 17 / watchOS 10 (locked by the @Observable macro).
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 your app and watch Recent activity. You should see:
- One
attestation.succeededevent. - One
sync.succeededevent with the count of delivered secrets.
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.