Skip to main content
Migrate Already selling? Move your customers to Keylight without re-issuing a single key.
Keylight
Blog
subscriptions billing licensing

Subscription Licensing for Mac Apps: Renewals, Upgrades, Downgrades

7 min read Nicolas Demanez — Founder

Many Mac app developers hit the same fork when they sit down to set up licensing: one-time purchase or subscription. It is not a pricing preference, it is a product decision that determines how your licensing model behaves at renewal, upgrade, and lapse. Keylight handles both through the same signed-lease infrastructure — what differs is whether expiresAt is set, and what happens at the boundary when it is.

One-time license vs subscription: when each makes sense for a Mac app

A one-time license means the customer pays once and owns that version — or, in the common Mac convention, pays once and receives all updates within the same major version. There is no renewal event. The signed lease has expiresAt: null, and the license remains valid indefinitely unless it is revoked (for example, after a refund). For the customer, this is the clearest value proposition: pay, own, done.

A subscription license means the customer pays periodically for continued access. The lease has an expiresAt tied to the end of the Stripe billing period. When the subscription renews, Keylight re-mints the lease with a new expiresAt. When the customer cancels, the lease expires at period end — no revocation required, just lapse.

One-time fits utilities and tools where the core value is the software itself, standing alone. The customer uses your app offline, on their own schedule, and the payment unlocks it permanently. Subscriptions fit cloud-backed apps, continuously-evolving products, or services where you are delivering ongoing value beyond the software binary — sync, AI features, integrations that need server infrastructure to run.

Neither model is universally correct. Many successful Mac apps — Artstudio Pro, Tot, Reeder — are one-time purchases. Many others that started one-time have added subscription tiers alongside. The right choice is the one that matches what you are actually delivering. Keylight supports both, and you can run them simultaneously if your product warrants it.

How Keylight models a subscription license

Structurally, a subscription license in Keylight is a signed lease with expiresAt set rather than null. The customer receives it through the same flow as a one-time license — how that pipeline works end-to-end — and the SDK verifies it the same way. The difference is purely in what the lease encodes.

Here is the server-side license record Keylight produces when minting a subscription lease. The SDK exposes a smaller Lease struct to your app code — fields like customerId, productId, activationLimit, activationCount, and features are part of the customer-facing API model, not the in-app Lease type. (For the SDK’s actual lease shape and the v3 signed wire payload, see the Ed25519 lease format post.)

{
  "id": "lk_01hx9z4...",
  "customerId": "cus_Qk3mN9...",
  "productId": "prod_macos_pro",
  "tier": "pro",
  "activationLimit": 3,
  "activationCount": 1,
  "issuedAt": "2026-05-01T00:00:00Z",
  "expiresAt": "2026-06-01T00:00:00Z",
  "features": ["pro", "export"],
  "revoked": false,
  "sig": "..."
}

The expiresAt field is set to the end of the Stripe subscription period — the moment Stripe expects the next invoice to be paid. The lease itself does not carry the Stripe subscription ID. That relationship is tracked server-side, not in the lease. The app sees a self-contained signed document with an expiry; it does not need to know anything about Stripe to verify it.

This separation matters for the security model. The lease is Ed25519-signed, which means its fields are tamper-evident. If an attacker extended expiresAt in the stored file, the signature would not match, and the SDK would reject the lease. Keylight’s private key is the only thing that can produce a signature the app accepts — so no customer can extend their own subscription by editing a file.

Keylight’s License Lifecycle v2 design formalised this model, treating subscription-type keys as one of the core lifecycle primitives alongside limited-access fallback and in-place tier upgrades. A subscription key can be renewed into the next period, allowed to lapse, or upgraded to a higher tier — all through the same lease-minting path.

Renewals: what happens at the boundary, online and offline

When a Stripe subscription renews, Stripe fires invoice.payment_succeeded. Keylight handles this event and re-mints the lease with a new expiresAt set to the next period end. No action is required from you — the same pipeline that issues the initial lease handles the renewal, with the same idempotency guarantees.

The customer’s app picks up the renewed lease on the next online revalidation via refreshIfNeeded(). If the app is open and online when the renewal hits, it receives the new lease in the background and currentLicenseExpiresAt updates after the next async await point on refreshIfNeeded() — not synchronously. The license state stays .licensed without any visible interruption.

You can surface the renewal window directly to the customer using the manager’s expiry property:

if case .licensed = manager.state,
   let expiresAt = manager.currentLicenseExpiresAt {
    let daysLeft = Calendar.current.dateComponents(
        [.day], from: .now, to: expiresAt
    ).day ?? 0
    print("Subscription renews in \(daysLeft) days")
}

currentLicenseExpiresAt is a Date? on LicenseManagernil for lifetime licenses, a specific date for subscription-bound ones. Checking for it being non-nil is the right way to distinguish the two cases when you want to show a renewal countdown.

Offline grace. The cached lease is valid until expiresAt passes, plus the SDK’s clock-skew tolerance (default 300 seconds). This means the cutoff is deterministic: the app does not need a network round-trip to know the subscription has lapsed. If the customer is offline at their renewal date and the payment goes through, the new lease waits on the server. The app continues running on the cached lease; as soon as the network returns before expiresAt elapses, refreshIfNeeded() fetches the renewed lease and the state stays .licensed without interruption.

If the customer stays offline past expiresAt, the SDK resolves to .expired or .limited depending on whether the key type has fallback access enabled. The offline behavior is not “works until network returns” — it is “works until expiresAt”, which is a fixed point in time.

Cancelled subscriptions need no special handling. Stripe stops billing; no invoice.payment_succeeded fires at the next renewal date; no new lease is minted. The existing lease expires naturally at its expiresAt. From the app’s perspective, the lease has lapsed — the same as if the customer never renewed.

For what happens after the lease lapses and the customer asks for a refund on the final period, the mechanics of revocation and the tradeoffs are covered in what happens after a refund in licensed software.

Upgrades and downgrades: the path between tiers

Tier changes mid-subscription are common — a customer starts on Solo, needs team-wide features, and upgrades to Pro. Or they downsize and drop to a lower tier at their next renewal. Both paths go through Stripe for payment processing and through Keylight for lease re-minting.

Stripe handles the billing arithmetic server-side: proration on an upgrade, or a credit applied to the next invoice on a downgrade. Keylight’s role is to mint a new lease reflecting the new tier. An upgrade produces a new lease with the additional entitlement strings in Lease.entitlements and the new activationLimit if the tier carries a higher device allowance. A downgrade produces a new lease with the reduced entitlement set. In both cases, Lease.signature is freshly computed — any entitlements the customer no longer has are simply absent from the new lease.

The app picks up the change on the next online revalidation. If the app is running, the SDK’s background refresh finds the new lease and updates currentEntitlements. Keylight posts keylightEntitlementsDidChange when the entitlement set changes without a state transition, so an upgrade from Solo to Pro — both .licensed — still triggers a notification the app can observe:

NotificationCenter.default.addObserver(
    forName: .keylightEntitlementsDidChange,
    object: manager,
    queue: .main
) { notification in
    let entitlements = notification.userInfo?["entitlements"] as? [String] ?? []
    updateFeatureAccess(entitlements)
}

Downgrades behave identically on the SDK side — the new lease arrives, currentEntitlements shrinks, and the notification fires. The app is responsible for gating the features that are no longer present in the lease; the SDK surfaces what changed, not what to do about it.

One honest caveat on the upgrade flow today: the in-app upgrade UI — rendering the purchase flow inside the app itself without redirecting to a browser — is on the Keylight roadmap, not yet shipped. Right now, customers who want to upgrade go through your browser-hosted Stripe checkout or customer portal. The server-side mechanics work; the embedded UI is coming. If you are designing an upgrade CTA today, a deep link to the customer portal is the right pattern.

Because upgrades and downgrades touch the same lease and the same activationLimit, there is one edge case to plan for: a customer on a three-device Pro plan downgrading to a two-device Solo plan while three devices are active. Keylight enforces the new limit at the next activation attempt on each device, not retroactively — the existing activations are grandfathered through the current period. Setting expectations in your downgrade confirmation copy prevents support tickets.


If you are building a subscription-based Mac app and want to see the renewal and upgrade flows running against a real Stripe test environment, the Keylight dashboard on any plan includes sandbox mode. If something in this post is wrong or out of date, send us your feedback — the model evolves and the docs should keep up.

Frequently asked

Can Keylight issue subscription license keys?+

Yes. A subscription license is a lease with an expiresAt that maps to the Stripe subscription period. Renewals re-mint the lease automatically; cancellations let the lease lapse at the end of the period.

What happens to the app when a subscription lapses offline?+

The app continues to work until the cached lease expires. Keylight uses signed leases with an expiresAt, so the cutoff is deterministic — and the app does not need a network round-trip to know it.

Can a customer upgrade tiers mid-subscription?+

Yes. Stripe handles the proration; Keylight re-mints the lease with the new tier features. The app sees the upgrade on the next online revalidation.

Ready to ship?

Create your account and start licensing your apps in under a minute. Free forever tier included.

Start Free