Offline Ed25519 licensing for Mac apps outside the App Store.

Check Stripe when a Mac activates. Sign an Ed25519 ticket. Verify it offline on the Mac, forever. No customer database, no webhooks, no phone-home.

  • Swift Package (macOS)
  • Stripe Session ID = License Key
  • Cloudflare Worker + SQLite DO
  • No License Checks After Unlock

System Architecture

End-to-end trust boundaries & data flow

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) Browser checkout with chk_ token claim / activate / free signed LK1- ticket GET paid? no dispute weekly GET version.json upload signed vault seal / read Mac App (SeatStamp) 2 embedded Ed25519 public keys Launch: Ed25519 check (0 network) Trial: 4-anchor monotonic clock Verifies updates + Gatekeeper quarantine Inspect component › Local Vault Signed LK1- ticket + cs_live_... key seat code + revokedBefore floor 4-anchor trial timestamps Inspect component › Cloudflare Worker + DO Holds Ed25519 license secret One SQLite record per hashed key Seat cap + yearly move budget Zero emails or IPs stored Inspect component › Static Host / CDN Serves version.json + App.dmg Signed sha256, size & seq Optional revoked_tags list Plain files · zero server runtime Inspect component › Stripe (+ Resend) cs_live_... = license key Pulled on demand (0 webhooks) Checks refund + dispute status Optional 1x backup key email Inspect component › Dev CLI (license-tool) Offline on developer's Mac Holds the update signing key --artifact signs .dmg hash --revoke-key / --rotate-keypair Inspect component ›
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

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

Engineering Honesty

Security guarantees, non-goals & privacy

What SeatStamp enforces, and what no offline desktop software can prevent.

Enforced by crypto & edge

  • Offline Ed25519 ticket and update verification (sha256 + size + manifest_seq + Gatekeeper quarantine).
  • A browser checkout handoff (chk_) is bound to the first Mac that polls for it.
  • Monotonic 4-anchor local trial clock (blocks macOS system clock rollback).
  • Yearly seat-move budget (SEAT_RELEASES_PER_YEAR) and a local revokedBefore floor that rejects tickets pasted back after Deactivate.
  • Stripe errors, pending payments, and won disputes never wipe a valid buyer's seats.

Explicit offline non-goals

  • Patching your compiled app on the buyer's own Mac skips local checks. This is true of every offline Mac app.
  • Deleting all 4 local trial anchors (one of them in the Keychain) at once, or using a fresh macOS user, restarts the trial.
  • Remotely disabling a refunded copy that never checks for updates, because unlocked Macs never phone home.

Accurate privacy posture

  • Worker SQLite stores zero emails and zero IP addresses; records are keyed by a hash of the purchase key.
  • Inactive seat rows auto-purge after 30 days, and --erase-key deletes an order record on request.
  • Stripe (payment processor) and optional Resend (backup email sender) process buyer data under their own policies.
  • Copy an accurate disclosure from docs/privacy.md.