<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nico</title>
    <description>The latest articles on DEV Community by Nico (@nicodemanez).</description>
    <link>https://dev.to/nicodemanez</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3931195%2Fdab7bca1-ba2a-4552-8cc3-6d01c539278e.jpeg</url>
      <title>DEV Community: Nico</title>
      <link>https://dev.to/nicodemanez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nicodemanez"/>
    <language>en</language>
    <item>
      <title>Adding License Keys to a macOS App Without Building a Licensing Backend</title>
      <dc:creator>Nico</dc:creator>
      <pubDate>Sun, 07 Jun 2026 06:00:49 +0000</pubDate>
      <link>https://dev.to/nicodemanez/adding-license-keys-to-a-macos-app-without-building-a-licensing-backend-2ph4</link>
      <guid>https://dev.to/nicodemanez/adding-license-keys-to-a-macos-app-without-building-a-licensing-backend-2ph4</guid>
      <description>&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://keylight.dev" rel="noopener noreferrer"&gt;Keylight&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;swift&lt;/code&gt; &lt;code&gt;macos&lt;/code&gt; &lt;code&gt;indiedev&lt;/code&gt; &lt;code&gt;licensing&lt;/code&gt; &lt;code&gt;security&lt;/code&gt; &lt;code&gt;saas&lt;/code&gt; &lt;code&gt;programming&lt;/code&gt;&lt;/p&gt;



&lt;p&gt;I build Mac apps, and one thing I kept running into was licensing.&lt;/p&gt;

&lt;p&gt;Not the fun part.&lt;/p&gt;

&lt;p&gt;The annoying part.&lt;/p&gt;

&lt;p&gt;You ship a paid app outside the App Store, then suddenly you need to answer questions like:&lt;/p&gt;

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

&lt;p&gt;At first, licensing looks simple.&lt;/p&gt;

&lt;p&gt;You imagine a table like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;license_key | email | status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then your app sends the key to your server and gets back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works for a prototype.&lt;/p&gt;

&lt;p&gt;But for a real desktop app, it breaks pretty fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Desktop apps are different
&lt;/h2&gt;

&lt;p&gt;A web app can ask the server on every request.&lt;/p&gt;

&lt;p&gt;A Mac app cannot.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That is bad.&lt;/p&gt;

&lt;p&gt;The better model is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Activate online once.&lt;/li&gt;
&lt;li&gt;Store a signed license locally.&lt;/li&gt;
&lt;li&gt;Verify it inside the app.&lt;/li&gt;
&lt;li&gt;Re-check online only when needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That way, normal app launches are fast and offline-safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signed licenses, not checksum keys
&lt;/h2&gt;

&lt;p&gt;Old license keys were often just strings that matched a pattern.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ABCD-1234-WXYZ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app would check if the characters matched a rule.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Modern licensing should use signed licenses.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That means the license can contain real entitlement data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"activationLimit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"features"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sync"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pro_templates"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the user edits any field, the signature fails.&lt;/p&gt;

&lt;p&gt;That is the important part.&lt;/p&gt;

&lt;p&gt;The user can hold the license file, but they cannot change what it says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Device activation is a separate problem
&lt;/h2&gt;

&lt;p&gt;A license key answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this customer entitled to the app?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Device activation answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this Mac one of the devices allowed to use that license?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That matters because a valid license with no activation limit can be shared with a team, a Discord server, or half the internet.&lt;/p&gt;

&lt;p&gt;A good activation system needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a device fingerprint&lt;/li&gt;
&lt;li&gt;an activation limit&lt;/li&gt;
&lt;li&gt;a server-side activation record&lt;/li&gt;
&lt;li&gt;a way to deactivate old devices&lt;/li&gt;
&lt;li&gt;idempotency, so the same Mac does not consume a new slot every time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is easy to miss.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Swift integration should stay boring
&lt;/h2&gt;

&lt;p&gt;The app-side licensing code should not take over your project.&lt;/p&gt;

&lt;p&gt;In a SwiftUI app, I like the shape to be simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SwiftUI&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;KeylightSDK&lt;/span&gt;

&lt;span class="kd"&gt;@main&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;licensing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try!&lt;/span&gt; &lt;span class="kt"&gt;Keylight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;sdkKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"sdk_live_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"acme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"myapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"ACME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;trustedPublicKeyBase64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"lk_pub_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;trialDurationDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;branding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"My App"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;purchaseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://example.com/buy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;supportEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"support@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;tintColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blue&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;Scene&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;WindowGroup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;licensing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then on launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;licensing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkOnLaunch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your UI reacts to state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;licensing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;licensed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;enablePaidFeatures&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;enablePaidFeatures&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;showTrialBanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;showRenewalPrompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;invalid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;showActivationSheet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the level of complexity I want in the app.&lt;/p&gt;

&lt;p&gt;The app should not care about webhook retries, signature formats, device records, revocation delays, refunds, or subscription state machines.&lt;/p&gt;

&lt;p&gt;It should ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is the current license state?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then render the right UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trials should be part of licensing
&lt;/h2&gt;

&lt;p&gt;A trial is not a separate system.&lt;/p&gt;

&lt;p&gt;It is just another license state.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The mistake is treating trial logic, paid logic, and subscription logic as three different systems.&lt;/p&gt;

&lt;p&gt;They are all entitlement states.&lt;/p&gt;

&lt;p&gt;One app. One source of truth. One license state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built &lt;a href="https://keylight.dev" rel="noopener noreferrer"&gt;Keylight&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;I got tired of rebuilding this every time.&lt;/p&gt;

&lt;p&gt;License keys, trials, activations, offline grace, refunds, revoked keys, customer portals, migration from old systems.&lt;/p&gt;

&lt;p&gt;It is never the main product, but if it is wrong, users feel it immediately.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://keylight.dev" rel="noopener noreferrer"&gt;Keylight&lt;/a&gt; as a licensing layer for Mac and Apple apps.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

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

&lt;p&gt;Payment tools help you sell.&lt;/p&gt;

&lt;p&gt;A licensing layer decides who can use what, on which device, and for how long.&lt;/p&gt;

&lt;p&gt;That separation is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you are shipping a paid Mac app outside the App Store, do not treat licensing as a tiny afterthought.&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signed licenses&lt;/li&gt;
&lt;li&gt;offline validation&lt;/li&gt;
&lt;li&gt;device activation&lt;/li&gt;
&lt;li&gt;deactivation&lt;/li&gt;
&lt;li&gt;refund and revocation handling&lt;/li&gt;
&lt;li&gt;trial state&lt;/li&gt;
&lt;li&gt;a clean UI state machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can build it yourself.&lt;/p&gt;

&lt;p&gt;But be honest about what you are signing up for.&lt;/p&gt;

&lt;p&gt;Sometimes the best infrastructure is the infrastructure you do not have to maintain.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>programming</category>
      <category>security</category>
      <category>saas</category>
    </item>
    <item>
      <title>Selling a macOS app outside the App Store is easy. Licensing is the hard part.</title>
      <dc:creator>Nico</dc:creator>
      <pubDate>Sun, 31 May 2026 14:03:24 +0000</pubDate>
      <link>https://dev.to/nicodemanez/selling-a-macos-app-outside-the-app-store-is-easy-licensing-is-the-hard-part-347o</link>
      <guid>https://dev.to/nicodemanez/selling-a-macos-app-outside-the-app-store-is-easy-licensing-is-the-hard-part-347o</guid>
      <description>&lt;p&gt;A lot of indie macOS developers eventually ask the same question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Should I sell through the Mac App Store, or sell directly?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Selling directly sounds simple at first.&lt;/p&gt;

&lt;p&gt;You add Stripe, Paddle, Lemon Squeezy, Gumroad, Polar, or another payment provider.&lt;br&gt;
You create a checkout page.&lt;br&gt;
Someone pays.&lt;br&gt;
You send them a license key.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;Except not really.&lt;/p&gt;

&lt;p&gt;The payment is usually the easiest part.&lt;/p&gt;

&lt;p&gt;The hard part is everything that happens after the payment.&lt;/p&gt;

&lt;p&gt;You need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this license active?&lt;/li&gt;
&lt;li&gt;Is it a lifetime license or a subscription?&lt;/li&gt;
&lt;li&gt;Has the subscription renewed?&lt;/li&gt;
&lt;li&gt;Was the payment refunded?&lt;/li&gt;
&lt;li&gt;How many devices can this license activate?&lt;/li&gt;
&lt;li&gt;Can the app work offline?&lt;/li&gt;
&lt;li&gt;What happens if the customer upgrades?&lt;/li&gt;
&lt;li&gt;What happens if they lose their key?&lt;/li&gt;
&lt;li&gt;Can they manage their own license?&lt;/li&gt;
&lt;li&gt;Can you see which licenses are actually being used?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most payment platforms are payment-first.&lt;/p&gt;

&lt;p&gt;That makes sense. Their main job is checkout, tax, invoices, subscriptions, merchant of record workflows, and payment operations.&lt;/p&gt;

&lt;p&gt;But for a desktop app, especially a macOS app sold outside the App Store, licensing becomes its own product surface.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A basic license key field is not always enough.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a serious direct-sale macOS app, the licensing layer usually needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;License key generation&lt;/li&gt;
&lt;li&gt;In-app validation&lt;/li&gt;
&lt;li&gt;Device activation&lt;/li&gt;
&lt;li&gt;Device limits&lt;/li&gt;
&lt;li&gt;Offline access&lt;/li&gt;
&lt;li&gt;Subscription-based access&lt;/li&gt;
&lt;li&gt;Lifetime access&lt;/li&gt;
&lt;li&gt;Trials&lt;/li&gt;
&lt;li&gt;Free tiers&lt;/li&gt;
&lt;li&gt;Upgrade flows&lt;/li&gt;
&lt;li&gt;Renewal handling&lt;/li&gt;
&lt;li&gt;Refund handling&lt;/li&gt;
&lt;li&gt;Customer self-service&lt;/li&gt;
&lt;li&gt;License analytics&lt;/li&gt;
&lt;li&gt;A native Swift SDK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of developers solve this by building their own license server.&lt;/p&gt;

&lt;p&gt;That works, but it also means maintaining webhook logic, access states, activation rules, customer support tools, edge cases, and SDK code forever.&lt;/p&gt;

&lt;p&gt;The other option is to depend fully on your payment provider’s licensing features.&lt;/p&gt;

&lt;p&gt;That can also work, but licensing is usually not the main product. It is often an extra feature attached to the payment platform.&lt;/p&gt;

&lt;p&gt;This is the gap I’m working on with Keylight.&lt;/p&gt;

&lt;p&gt;Keylight is a license-first backend for macOS apps sold outside the App Store.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;p&gt;Use the payment provider you want.&lt;br&gt;
Use Keylight for the licensing layer.&lt;/p&gt;

&lt;p&gt;Keylight handles license issuing, activations, access validation, subscription and lifetime licenses, renewals, upgrades, customer portal flows, offline fallback, analytics, and Swift SDK integration.&lt;/p&gt;

&lt;p&gt;The goal is not to replace Stripe, Paddle, Lemon Squeezy, Gumroad, or Polar.&lt;/p&gt;

&lt;p&gt;The goal is to handle what happens after checkout, inside the app.&lt;/p&gt;

&lt;p&gt;I’m building it mainly for indie macOS developers who want to sell directly, keep more control, and avoid spending weeks building licensing infrastructure from scratch.&lt;/p&gt;

&lt;p&gt;Curious how other macOS devs are handling this today.&lt;/p&gt;

&lt;p&gt;Are you using Paddle, Lemon Squeezy, Stripe, Gumroad, Polar, or your own custom license server?&lt;/p&gt;

&lt;p&gt;Keylight: &lt;a href="https://keylight.dev" rel="noopener noreferrer"&gt;https://keylight.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>developers</category>
      <category>saas</category>
      <category>softwaredevelopment</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
