appAttest
Docs

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 DEBUG block — 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.dev/signup.

1. Create the app in AppAttest

In the dashboard at https://app.appattest.dev/dashboard:

  1. Click New app.
  2. Enter your bundle identifier. Must match your Xcode project exactly. Example: com.yourcompany.yourapp.
  3. 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:

  1. Select the development environment.
  2. Click Add secret.
  3. Give it a name, for example openai.
  4. 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:

  1. https://developer.apple.com/accountCertificates, Identifiers & ProfilesIdentifiers.
  2. Open your app identifier.
  3. Scroll to App Services. Enable App Attest.
  4. Save.

You don’t need to regenerate provisioning profiles for this change.

4. Xcode configuration

Full walkthrough: see Xcode setup.

Short version:

  1. Open the project.
  2. Select your app target.
  3. Signing & Capabilities+ CapabilityApp Attest.
  4. Xcode adds App Attest to your entitlements file. That’s it.

Reference: entitlements.

5. Add the Swift SDK

In Xcode:

  1. FileAdd Package Dependencies.
  2. URL: https://github.com/AppAttest/appAttest-sdk.
  3. Pick Up to Next Major Version from the latest tagged release.
  4. Add the AppAttest product 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

For AI agents — this page is available as markdown:
View markdown