Pick a lifecycle flow to trace each step, or click any component box to inspect its secrets, storage, and failure rules.
Complete Map
Everything left of the dashed line runs offline inside the buyer's Mac. Everything right of it is only touched once at purchase, seat transfer, or weekly update check.
USER'S MAC (OFFLINE TRUST ZONE)
↕ vault seal / read
NETWORK BOUNDARY
↗ Browser checkout with chk_ token
→ claim / activate / free
← signed LK1- ticket
↕ GET paid? / no dispute
↑ weekly GET version.json
↑ upload signed
Real Signatures
Inspect a signed ticket and update manifest
These are real test vectors from fixtures/golden-vectors.json, the same ones the Swift and Worker tests use. Every check below runs in your browser with WebCrypto Ed25519, with no server involved.
Try:
Checking...
-.
// payload, decoded from base64url: four fields joined by "|"
| | |
// checked by this Mac
What it guarantees
Try:
Checking...
{
,
,
,
,
,
,
,
,
}
What it guarantees
Quickstart
5-step developer setup
About 30 minutes · $0 a month on Cloudflare's free plan · only Stripe's standard fee per sale.
1. Create two Ed25519 keypairs
The license private key becomes a Worker secret. The update private key never leaves your Mac. Only the two public keys ship inside your app.
zsh
swift run license-tool --scheme MYAPP-V1 --init-keypair
swift run license-tool --scheme MYAPP-V1 --init-update-keypair
2. Create a Payment Link and a restricted Stripe key
No webhooks. Point the Payment Link's redirect at https://activate.myapp.com/thank-you?session_id={CHECKOUT_SESSION_ID}, then create a restricted key (rk_live_...) with these 4 read-only permissions:
One Worker and one SQLite Durable Object, on the free plan. Set TICKET_SCHEME, APP_NAME and APP_URL_SCHEME in worker/wrangler.jsonc first.
zsh
npx wrangler -c worker/wrangler.jsonc deploy
npx wrangler -c worker/wrangler.jsonc secret put ED25519_PKCS8_B64
npx wrangler -c worker/wrangler.jsonc secret put STRIPE_SECRET_KEY
npx wrangler -c worker/wrangler.jsonc secret put STRIPE_ALLOWED_PAYMENT_LINKS
4. Add SeatStamp to your app and handle every outcome
Paste in the two public keys, gate paid features with verifyGateAccess(), and handle each ActivationOutcome so a pending payment or a wrong clock gets a clear message.
Swift
import SeatStamp
let kit = SeatStampManager(
scheme: "MYAPP-V1",
licensePublicKeyBase64: "<LICENSE_PUBLIC_KEY>",
updatePublicKeyBase64: "<UPDATE_PUBLIC_KEY>",
activationEndpoints: [URL(string: "https://activate.myapp.com")!],
trialLengthDays: 30,
maxSeats: 2,
bundleID: "com.example.myapp"
)
switch await kit.activate(key: pastedKey) {
case .activated: showStatus("Unlocked. Thank you!")
case .paymentPending: showStatus("Your payment is still clearing.")
case .seatLimitReached: showStatus("All Macs are in use. Deactivate one first.")
case .invalidKey: showStatus("That key wasn't recognized, or was refunded.")
case .clockSkew: showStatus("Your Mac's clock is off. Turn on automatic time.")
case .networkError: showStatus("Couldn't reach the server. Try again.")
case .unconfigured: showStatus("This build has no activation server.")
}
5. Sign each release on your own Mac
license-tool signs the version, the download URL, and the file's SHA-256 and size with your offline update key. Upload version.json and the .dmg to any static host.
zsh
swift run license-tool --scheme MYAPP-V1 \
--sign-manifest version.json \
--artifact MyApp-1.2.dmg
Ready to ship.
Engineering Honesty
Security guarantees, non-goals & privacy
What SeatStamp enforces, and what no offline desktop software can prevent.