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
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 inInfo.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 returns400
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).environment—developmentwhile testing on a development-signed build,productionfor the App Store / TestFlight build. This selects which Apple App Attest root the gateway verifies against, so a mismatch fails attestation.
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
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
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 formdevice:<hash> — a stable
per-device identity minted from the hardware key. Per-device free limits apply automatically.
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.
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.