This tutorial replaces the static dev token from the image-enhance tutorial with anonymous, per-device authentication. Apple App Attest proves a request comes from a genuine, unmodified build of your app on real hardware — no login screen, no account, and no secret key in the binary. The gateway turns that proof into a per-device end user (device:<hash>) and mints a short-lived client token the app uses to call the image endpoints. RoadieKit’s AppAttestTokenProvider performs the whole handshake for you. Most of this tutorial is project configuration; the app change is a one-line provider swap.
App Attest is unavailable in the simulator — you need a real device and a paid Apple Developer account. Keep the StaticTokenProvider path for the simulator (the client selects between them at runtime, as in step 4).

How it works

Only the publishable key (rd_pk_…) is ever sent from the device, and it can do nothing but mint device tokens — a leaked publishable key alone mints nothing without a genuine attestation.

Prerequisites

You need the image-enhance tutorial working with a static token first (RoadieKit installed, an OpenAI BYOK credential, a publishable key in Info.plist). Then:
  • A real iOS device and your Apple Developer Team ID and app Bundle ID.
  • The App Attest capability — Apple enables it for your app id automatically; no extra entitlement is required for DCAppAttestService.

1. Enable device attestation on the project

Device auth is off by default. Until you enable it, every device route returns 400 invalid_request_error (device_attest_not_configured). In the dashboard, enable device attestation for the project (device_attest_config) with:
  • teamId — your Apple Developer Team ID.
  • bundleId — the app’s bundle identifier (must match the running app).
  • environmentdevelopment while testing on a development-signed build, production for the App Store / TestFlight build. This selects which Apple App Attest root the gateway verifies against, so a mismatch fails attestation.
See Mobile device auth for the field-by-field reference.

2. (Optional) Add a DeviceCheck key for reinstall anti-farming

App Attest proves genuine app + device, not identity permanence — reinstalling the app resets the App Attest key (Apple behavior), producing a new device identity. If you want to stop a device from farming a fresh free tier by reinstalling, add your Apple DeviceCheck .p8 key to the project. Apple persists two bits per device that survive reinstalls and erase, and Roadie uses them to remember that a device already consumed its free allotment. DeviceCheck is best-effort and fail-open: RoadieKit attaches the token when the device supports it and omits it otherwise, and a device that omits it simply gets the normal free tier. It is never a bearer credential on its own.

3. Set the publishable key

AppAttestTokenProvider needs the publishable key to authorize the device routes. You already put it in Info.plist and read it into RoadieConfig in the image-enhance tutorial:
Info.plist
If the config’s publishableKey is missing, minting throws tokenUnavailable with a clear message.

4. Swap in the App Attest token provider

This is the entire app-side change. Keep the static-token branch for the simulator; use App Attest everywhere else. Nothing at the call site (roadie.images.edit(…)) changes.
RoadieClient.swift
Under the hood, AppAttestTokenProvider:
  • First run — generates a Secure Enclave key, fetches a challenge, attests the key over SHA256(challenge), exchanges it at /v1/device/attest, and caches the returned client token. It persists only the App Attest key id in the Keychain — never the token.
  • Later runs — loads the key id, fetches a fresh challenge, produces an assertion (a signature plus a strictly increasing counter that defeats replay), and mints again at /v1/device/assert — cheaper, no re-attestation.
  • Caches the token in memory and re-mints before expiry (and once after a 401). Tokens live at most an hour.

5. Run on device and verify

Build to a real device (not the simulator) and trigger an image edit. Then open the dashboard’s request logs: the request is attributed to an end user id of the form device:<hash> — a stable per-device identity minted from the hardware key. Per-device free limits apply automatically.
Every attestation/assertion verification failure — a bad chain, a wrong nonce, a reused challenge, a replayed counter — collapses to one opaque 401 authentication_error (invalid_device_attestation). The specific reason is logged server-side, never returned, so a probe learns nothing. If you get a 400 device_attest_not_configured, revisit step 1; a 403 publishable_key_required means a non-publishable credential reached a device route.

Appendix — the raw handshake (non-RoadieKit clients)

If you are not using RoadieKit, implement the same three publishable-key-only calls. clientDataHash is SHA256(challenge) in every step.
All three routes are documented in the device auth API reference. The base URL is . Store the key id in the Keychain; the client token is short-lived and re-minted on demand.

What you built

Your app now authenticates every device silently with App Attest — no login, no account, and no secret in the binary. Each device becomes a metered end user (device:<hash>) with its own free limits, and returning devices re-mint cheaply with assertions. With a DeviceCheck key added, a reinstall can’t farm a fresh free tier. The same roadie.images.edit(…) call powers it all; only the token provider changed.

Next steps

Free → Pro with StoreKit

Grant the device a pro entitlement after an in-app purchase.

Per-user limits

Set the free and pro per-device limit presets these tokens enforce.

Client tokens

How minting, scopes, and short TTLs keep device auth safe.

Mobile device auth guide

The configuration reference behind this flow.