Where the macOS SDK stores data
The SDK stores four small values per product, in the Keychain, and mirrors them in files in case the keychain is unavailable or loses them.
| Value | What it is | Written |
|---|---|---|
licenseKey | The key the customer activated. | When setLicenseKey(_:) succeeds. |
licenseToken | The last license token. | At each successful validation. Deleted when LicenseManager refuses the license. |
installId | The random part of the fingerprint. | The first time the fingerprint is computed. Never changed afterwards. |
lastSeen | The latest time seen by the clock check, lightly obscured in base64. | At each clock check. |
Nothing else is stored: no customer data, no history.
The entries and files are the same as the Electron SDK’s and the Tauri SDK’s: a Swift build of your app reads what an Electron or Tauri build wrote, and the other way around. See migrating from Electron or Tauri.
The keychain
Values are generic passwords in the user’s login keychain, the one the Keychain Access app shows, like those of the Electron and Tauri SDKs. Each has:
- the service
keychainService, which defaults to your app’s bundle id, likecom.acme.photo, ordev.licensemanager.sdkwhen the app has none. It’s the name macOS shows when it asks for permission. - an account made of your product id and the value’s name, like
x7Kp2QaZ:licenseKey. Two apps, or two products, never read or overwrite each other’s entries.
.init(keychainService: "com.acme.photo") // default: the bundle id
.init(useKeychain: false) // files onlyDevelopment builds
useKeychain defaults to false for unsigned and ad-hoc signed builds, like swift run, or Xcode’s Sign to Run Locally: their signature changes on every build, and the Keychain would ask for permission each time. They use the files only. Builds signed with a certificate, Apple Development or Developer ID, use the Keychain. Pass useKeychain: true or false to choose yourself.
When the Keychain can’t be reached, or refuses access, the SDK uses the files only, without any error. When a value is found in a file but not in the Keychain, like after keychainService changed, it’s written back to the Keychain.
Files
Each value is also written to two folders, even when the keychain works:
| App | Folders |
|---|---|
| Not sandboxed | ~/Library/Application Support/.<namespace> and ~/Library/Caches/.<namespace> |
| Sandboxed | The same folders, inside the app’s container: ~/Library/Containers/<bundle id>/Data/Library/… |
<namespace>is the first 32 hex characters of the SHA-256 ofdev.licensemanager.sdk:<productId>. It looks like a random id, and doesn’t mention licenses or your app.- Each value is in a file named after the first 24 hex characters of the SHA-256 of its name, like
3f1a…c9.dat. - Files are created readable by the user only (
600). They aren’t encrypted. - Writing to one folder can fail without affecting the other.
For apps that aren’t sandboxed, these folders are outside your app’s own folders, on purpose: see uninstalling.
Reading and writing
| Operation | What happens |
|---|---|
| Read | The keychain first. When it has no value, or fails, each folder in order. The first value found wins. |
| Write | The keychain, then both folders. Each one can fail on its own: the others are still written. |
| Delete | The keychain entry and both files. |
None of them ever fails: storage is best effort, and a failure never crashes your app. They run on the SDK’s actor, never on the main thread.
Uninstalling
The stored data is kept when your app is moved to the Trash, on purpose: the Keychain and the folders are outside the app. Cleaner apps that delete an app’s container remove the files of a sandboxed app, but not its Keychain items. A customer who reinstalls your app is still activated, with the same fingerprint, and doesn’t take a new place on their license. A trial can’t be restarted by reinstalling either.
Resetting during development
To start over as a new customer, delete the entries of your product in the keychain, and the two folders: when only one of them is deleted, the SDK reads the values from the other.
Compute your product’s namespace:
printf 'dev.licensemanager.sdk:x7Kp2QaZ' | shasum -a 256 | cut -c1-32for name in licenseKey licenseToken installId lastSeen; do
security delete-generic-password -s com.acme.photo -a "x7Kp2QaZ:$name" # your keychainService
done
rm -rf ~/Library/Application\ Support/.<namespace> ~/Library/Caches/.<namespace>
# Sandboxed app:
rm -rf ~/Library/Containers/com.acme.photo/Data/Library/{Application\ Support,Caches}/.<namespace>Deleting the install id changes the fingerprint: your computer becomes a new machine on the license. Use a test product, where it doesn’t matter.
Never tell customers to delete these files to fix a problem: they lose their activation, and their computer becomes a new machine, which can be refused with machine_limit_reached.
Edge cases
The Keychain asks for permission. A build signed with another certificate than the one that created an entry, like a development build after a release build, or a Swift build after an Electron build, makes the Keychain ask the customer to allow access, once. When access is denied, the SDK reads the values from the files.
The Keychain is locked or unavailable, like over SSH. The SDK uses the files.
The keychain and the files disagree, like after restoring a backup. The keychain wins when it has a value.
A read-only home folder, and no keychain. Nothing can be stored: the customer must activate on every launch, and each launch is a new machine. Very unlikely on a customer’s computer.