App Attest
App Attest verifies requests from your Apple apps to your user backend. Its policy is a server-side decision, scoped to one Environment. The SDK handles enrollment and retries; your app handles errors when device verification cannot complete.
Configure Authentication → Settings → Device trust in Studio, or use the same environment policy through the CLI:
palbase auth settings get
palbase auth settings set --json '{"app_attest_mode":"monitor","app_attest_apple_apps":["ABCDE12345.com.example.app"]}'
The administration endpoint is PUT /admin/auth-settings. Read GET /admin/auth-settings first and include its ETag in the write's If-Match header. A missing revision returns 428; settings changed by another administrator return 412. Review the current settings before retrying. Studio and palbase auth settings set manage the revision for you. Omitted fields retain their current values. The modes are off, monitor, and enforce. Monitor records observations without rejecting requests. Enforce requires a non-empty Apple app list and a configured challenge secret; the API rejects an incomplete enforcement configuration.
The deployed backend runtime must include the App Attest gate for this policy to protect user backend calls. A saved policy alone does not verify the deployed runtime or a physical device. Check the app in Monitor before requiring attestation.
What your code does
Handle the two error cases below. There is no client-side switch to enable the server policy:
import Palbe
do {
let todos = try await pb.todos.list()
} catch let error as BackendError {
switch error {
case .attestationUnavailable(let failure):
// This device can't attest (most commonly: the Simulator).
// The server refused the call and the SDK won't retry.
// failure.reason explains why.
showBlockingNotice(failure.reason)
case .appAttestRequired:
// The server demanded attestation and the SDK's automatic
// enrollment + retry didn't satisfy it.
showBlockingNotice("Device verification failed. Please try again.")
default:
handle(error)
}
}
The flow the SDK already implements
- A user backend call goes out plain first. No attestation headers on the first attempt, so a call to a backend that does not demand attestation pays zero attestation cost.
- The backend demands attestation. It answers
401with the error codeapp_attest_required. - Enrollment on demand. The SDK generates a hardware-backed key in the device's Secure Enclave, has Apple attest it, and registers the device over
POST /auth/devices/challengeandPOST /auth/devices/attest/ios. Both identifiers — the Secure EnclavekeyIdand thedev_-prefixed device id — are persisted in the Keychain, so this happens once per install, not per call. The private key never leaves the Secure Enclave. - Retry, exactly once. The SDK generates a fresh assertion, bound to a server challenge and to a hash of this exact request so it cannot be replayed or moved to another request, attaches it, and retries the original call with the same idempotency key — so a non-GET call is never double-executed.
Enforcement scope
| Surface | Attested |
|---|---|
Generated user backend calls (pb.todos.list(), …) | Yes |
pb.call(...) to your backend | Yes |
| Auth and device enrollment | No — bootstrap must stay reachable |
pb.upload(...) / a generated @Upload call | No — uploads go straight to Storage |
| Storage's own write of the file bytes | No — Storage is exempt |
| Palbase module routes (Messaging, Flags, Realtime, …) | No — module routes are exempt |
pb.analytics.* event ingestion | No — Analytics is exempt |
The gate lives in your backend's runtime, and the edge decides what reaches it:
platform prefixes (/auth, /v1, /admin, /realtime, …) are routed to the
platform, and everything else to your runtime. That routing IS the exemption
list — there is no second list to keep in step with it.
pb.upload(...) is on the platform side for a concrete reason: an upload goes
to /v1/storage, and the whole point of a signed upload is that a large file
does not stream through your runtime. This row said "Yes" until
2026-09-01; it was wrong, and it contradicted the row directly below it.
See Calling Your Backend and Uploads for the call surfaces themselves.
On the wire
The SDK attaches the device identity, server challenge, attestation payload, and signature to the retry. You do not construct these headers yourself.
The two errors
Both arrive as BackendError cases:
| Case | Payload | When |
|---|---|---|
.attestationUnavailable(AttestationFailure) | reason: String | The device cannot attest at all, or enrollment failed — the SDK refuses to retry |
.appAttestRequired(AuthFailure) | requestId: String? | The server demanded attestation and the automatic enroll-and-retry still came back 401 |
.attestationUnavailable is deterministic for a given device: retrying will not help, so treat it as a blocking state rather than a transient failure.
Note:
AppAttestErroritself is a separate public type and does not conform toPalbaseError— it has nocode,statusCodeorrequestId. What a caller sees isBackendError.attestationUnavailable, whosereasoncomes fromAppAttestError.backendReason.error.code,error.statusCodeanderror.requestIdwork uniformly on bothBackendErrorcases.
It never runs off a physical iOS device
Warning: App Attest requires a supported physical iPhone or iPad with a Secure Enclave. It does not work in the Simulator, and it does not work on macOS or Mac Catalyst — the SDK does not even construct an attestor there. This is not a Simulator quirk; it is "anything that is not a physical iOS device".
The reason string the SDK ships is explicit about it:
App Attest is unavailable on this device — it requires a supported physical iPhone/iPad with a Secure Enclave and does not work on the Simulator or macOS. Either run on a physical device, or disable App Attest enforcement for this app/environment binding in Studio.
Environment policy
offbypasses the gate;monitorobserves;enforcerequires valid attestation for user backend calls.- An Environment has one policy and an allowed Apple app list. The policy is not a client flag.
- Changes are read by the runtime on its next policy poll. No code deploy is required to write settings.
- Use a separate Environment with enforcement off for Simulator work, and a physical device to validate enforcement.
Related
- Error Handling — the full
BackendErrorreference. - Calling Your Backend — the request pipeline covered by attestation.
- Uploads — the multipart call, and what Storage does with the bytes.
- Introduction — the isolated runtimes an app binds to.