appAttest Start in sandbox

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:

  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.

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:

  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/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/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.succeeded event.
  • One sync.succeeded event with the count of delivered secrets.

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