How do you tie a purchase to your own user with appAccountToken, and why derive the token instead of storing a random one?

28.7.5 · L5 · Interview

Predict first

What does a name-based (version 5) token give for the same and different users?

import Foundation
import CryptoKit

// UUID version 5 (name-based, SHA-1): the same user id always maps to the same token.
func accountToken(for userID: String, namespace: UUID = UUID(uuidString: "6ba7b810-9dad-11d1-80b4-00c04fd430c8")!) -> UUID {
    var data = withUnsafeBytes(of: namespace.uuid) { Data($0) }
    data.append(Data(userID.utf8))
    var h = Array(Insecure.SHA1.hash(data: data))[0..<16].map { $0 }
    h[6] = (h[6] & 0x0F) | 0x50
    h[8] = (h[8] & 0x3F) | 0x80
    return UUID(uuid: (h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], h[8], h[9], h[10], h[11], h[12], h[13], h[14], h[15]))
}
let a = accountToken(for: "user-42"), b = accountToken(for: "user-42"), c = accountToken(for: "user-43")
print("same user, same token:", a == b)
print("different user, different token:", a != c)
print("version nibble:", a.uuidString.split(separator: "-")[2].first!)
print("known vector (DNS namespace, python.org):", accountToken(for: "python.org", namespace: UUID(uuidString: "6ba7b810-9dad-11d1-80b4-00c04fd430c8")!).uuidString.lowercased())
  • same user, same token: true / different user, different token: true / version nibble: 5 / known vector: 886313e1-3b8a-5372-9b90-0c9aee199e5d
  • same user, same token: false
  • version nibble: 4

Mental model. A coat-check ticket that you can reproduce from the owner’s name: you never lose it, and the cloakroom can match coats to owners without keeping a list.

An appAccountToken is a UUID you pass in the purchase options (product.purchase(options: [.appAccountToken(token)])); Apple stores it with the transaction and returns it in Transaction.appAccountToken, in server notifications and in the Server API. It is how your backend knows which of your users a transaction belongs to without trusting the device. Use a UUID, never an email or a name: it is visible to Apple and stored with the transaction, so it must not be personal data.

The design question is where the token comes from. A random UUID per purchase forces you to store the mapping and loses it if the write fails after the purchase. A better pattern is to derive it deterministically from your user id with a name-based UUID (version 5, SHA-1 over a namespace plus the id): the same user always yields the same token, the backend can recompute it, and nothing has to be stored. The check shows the properties (same user gives the same token, a different user a different one, version nibble 5) and a known vector: the namespace UUID for DNS and the name “python.org” gives 886313e1-3b8a-5372-9b90-0c9aee199e5d, which matches Python’s uuid.uuid5 (compared separately), so a server in any language can derive the same value. Use your own fixed namespace UUID for the app, not the DNS one. Caveats: the token is set at purchase time (restore and old transactions may carry none), a user who purchases while logged out cannot be linked until they sign in (so ask them to sign in first, or link on login from the signed transactions), and one Apple ID may be used by several of your users: decide whether the entitlement follows the Apple ID (the default) or your account (use the token). Validate it server-side from the signed payload, never from a client claim.

A version-5 UUID is a hash of namespace and name, so it is stable per user and recomputable anywhere.

Common traps

  • Putting an email or user id directly in the token field. It must be a UUID and must not be personal data.
  • Trusting a token the client sends to your server. Read it from the signed transaction (Q1005).

They might follow up with

  • What if the user buys before logging in? The transaction has no token for your account; link it after login using the signed transaction and the original transaction id.
  • Is a random token ever fine? Yes, if you persist the mapping atomically before presenting the purchase sheet; derivation just removes that failure mode.

Related questions

  • 28.7.4 How do you show a subscriber their status (renewing, in grace, will expire) and let them change or cancel the…
  • 28.7.6 How do you unit-test the purchase and entitlement logic when Product and Transaction cannot be created in a…
  • 28.4.1 How is a StoreKit JWS built, and what must a server check before trusting signedTransactionInfo?
  • 28.4.3 How do App Store Server Notifications V2 behave, and how do you process them idempotently and out of order?
  • 28.6.2 Design the billing and entitlement platform for an app with iOS, Android and web subscriptions and a backend…

← How do you show a subscriber their status (renewing, in grace, will…   How do you unit-test the purchase and entitlement logic when Product… →