DEV Community

Cover image for Adding License Keys to a macOS App Without Building a Licensing Backend
Nico
Nico

Posted on

Adding License Keys to a macOS App Without Building a Licensing Backend

Description:

A practical look at how Mac developers can add license keys, device activation, and offline validation without building the whole licensing system from scratch.

Keylight

Tags:
swift macos indiedev licensing security saas programming


I build Mac apps, and one thing I kept running into was licensing.

Not the fun part.

The annoying part.

You ship a paid app outside the App Store, then suddenly you need to answer questions like:

  • How do I issue license keys?
  • How do I know if a key is valid?
  • How do I stop one key from being shared everywhere?
  • What happens when the user is offline?
  • What happens after a refund?
  • Where do I store the license on macOS?
  • How do I support trials without building a second system?

At first, licensing looks simple.

You imagine a table like this:

license_key | email | status
Enter fullscreen mode Exit fullscreen mode

Then your app sends the key to your server and gets back:

{ "valid": true }
Enter fullscreen mode Exit fullscreen mode

That works for a prototype.

But for a real desktop app, it breaks pretty fast.

Desktop apps are different

A web app can ask the server on every request.

A Mac app cannot.

Your customer might open the app on a plane, in a train, in a hotel with bad Wi-Fi, or behind a company firewall. If every launch depends on your licensing server being available, then your licensing system becomes a point of failure for the entire product.

That is bad.

The better model is:

  1. Activate online once.
  2. Store a signed license locally.
  3. Verify it inside the app.
  4. Re-check online only when needed.

That way, normal app launches are fast and offline-safe.

Signed licenses, not checksum keys

Old license keys were often just strings that matched a pattern.

Something like:

ABCD-1234-WXYZ
Enter fullscreen mode Exit fullscreen mode

The app would check if the characters matched a rule.

That is not real security anymore. If the rule is inside the app, someone can reverse-engineer it. If one valid key leaks, it can be shared forever.

Modern licensing should use signed licenses.

The server signs the license with a private key. The app ships with a public key. The app can verify that the license was really issued by your system, but it cannot create new licenses.

That means the license can contain real entitlement data:

{
  "productId": "myapp",
  "plan": "pro",
  "activationLimit": 3,
  "expiresAt": null,
  "features": ["export", "sync", "pro_templates"]
}
Enter fullscreen mode Exit fullscreen mode

If the user edits any field, the signature fails.

That is the important part.

The user can hold the license file, but they cannot change what it says.

Device activation is a separate problem

A license key answers:

Is this customer entitled to the app?

Device activation answers:

Is this Mac one of the devices allowed to use that license?

That matters because a valid license with no activation limit can be shared with a team, a Discord server, or half the internet.

A good activation system needs:

  • a device fingerprint
  • an activation limit
  • a server-side activation record
  • a way to deactivate old devices
  • idempotency, so the same Mac does not consume a new slot every time

That last one is easy to miss.

If a customer reinstalls your app on the same Mac, it should not count as a new device. If your system gets that wrong, honest users hit their limit and your support inbox becomes the unlock button.

The Swift integration should stay boring

The app-side licensing code should not take over your project.

In a SwiftUI app, I like the shape to be simple:

import SwiftUI
import KeylightSDK

@main
struct MyApp: App {
    let licensing = try! Keylight.manager(
        sdkKey: "sdk_live_...",
        tenantId: "acme",
        productId: "myapp",
        keyPrefix: "ACME",
        trustedPublicKeyBase64: "lk_pub_...",
        trialDurationDays: 14,
        branding: .init(
            appName: "My App",
            purchaseURL: URL(string: "https://example.com/buy")!,
            supportEmail: "support@example.com",
            tintColor: .blue
        )
    )

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(licensing)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Then on launch:

await licensing.checkOnLaunch()
Enter fullscreen mode Exit fullscreen mode

And your UI reacts to state:

switch licensing.state {
case .licensed:
    enablePaidFeatures()

case .trial(let daysLeft):
    enablePaidFeatures()
    showTrialBanner(daysLeft: daysLeft)

case .expired:
    showRenewalPrompt()

case .invalid:
    showActivationSheet()
}
Enter fullscreen mode Exit fullscreen mode

That is the level of complexity I want in the app.

The app should not care about webhook retries, signature formats, device records, revocation delays, refunds, or subscription state machines.

It should ask:

What is the current license state?

Then render the right UI.

Trials should be part of licensing

A trial is not a separate system.

It is just another license state.

During the trial, the user should experience the real product. When the trial ends, the state changes. The app can then show a purchase screen, activation sheet, or limited free mode.

The mistake is treating trial logic, paid logic, and subscription logic as three different systems.

They are all entitlement states.

One app. One source of truth. One license state.

Why I built Keylight

I got tired of rebuilding this every time.

License keys, trials, activations, offline grace, refunds, revoked keys, customer portals, migration from old systems.

It is never the main product, but if it is wrong, users feel it immediately.

So I built Keylight as a licensing layer for Mac and Apple apps.

The idea is simple:

  • use Stripe, Paddle, Lemon Squeezy, Gumroad, Polar, or your own payment setup
  • keep licensing separate from payments
  • drop in the Swift SDK
  • verify signed licenses locally
  • support trials, device limits, feature flags, renewals, and revocation
  • avoid running your own licensing backend

Payment tools help you sell.

A licensing layer decides who can use what, on which device, and for how long.

That separation is the whole point.

The takeaway

If you are shipping a paid Mac app outside the App Store, do not treat licensing as a tiny afterthought.

You need:

  • signed licenses
  • offline validation
  • device activation
  • deactivation
  • refund and revocation handling
  • trial state
  • a clean UI state machine

You can build it yourself.

But be honest about what you are signing up for.

Sometimes the best infrastructure is the infrastructure you do not have to maintain.

Top comments (1)

Collapse
 
nicodemanez profile image
Nico

Let me know what you think, and how do you currently license your apps! Always curious to hear other dev stories. šŸ¤—