<?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: PermitCore</title>
    <description>The latest articles on DEV Community by PermitCore (@permitcore).</description>
    <link>https://dev.to/permitcore</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%2F4119717%2F8a675fea-fa4b-4688-b41b-64247900bc69.png</url>
      <title>DEV Community: PermitCore</title>
      <link>https://dev.to/permitcore</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/permitcore"/>
    <language>en</language>
    <item>
      <title>Open Core in a Public Repo: Gate Paid Features With License Keys, Not Secrecy</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Wed, 11 Nov 2026 09:34:42 +0000</pubDate>
      <link>https://dev.to/permitcore/open-core-in-a-public-repo-gate-paid-features-with-license-keys-not-secrecy-14g3</link>
      <guid>https://dev.to/permitcore/open-core-in-a-public-repo-gate-paid-features-with-license-keys-not-secrecy-14g3</guid>
      <description>&lt;p&gt;Your repository is public. Anyone can read the paid modules, fork them, and build them. So what actually separates the Community edition from the paid one?&lt;/p&gt;

&lt;p&gt;In open core and Fair Source projects the answer is not code secrecy. It is the &lt;strong&gt;license key&lt;/strong&gt;: a signed entitlement that tells one self-hosted installation which paid features it is allowed to use. This post walks through how to design that boundary so it stays fair to operators and practical for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open core vs Fair Source in one paragraph
&lt;/h2&gt;

&lt;p&gt;Classic open core keeps the core under an open source license and puts commercial modules in a separate directory (often &lt;code&gt;ee/&lt;/code&gt;) with their own terms. Fair Source goes a step further: the commercial code is readable, but the license limits certain uses of current versions and converts to a permissive license later. Keygen &lt;a href="https://keygen.sh/blog/keygen-is-now-fair-source/" rel="noopener noreferrer"&gt;moved to FCL&lt;/a&gt;, and Directus &lt;a href="https://directus.com/resources/directus-v12-license-change" rel="noopener noreferrer"&gt;announced an FCL-derived license for v12&lt;/a&gt; together with registration keys for paid users.&lt;/p&gt;

&lt;p&gt;Quick comparison (high level, &lt;strong&gt;not legal advice&lt;/strong&gt;, always read the exact terms you ship):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Separate &lt;code&gt;ee/&lt;/code&gt; terms&lt;/td&gt;
&lt;td&gt;Core and commercial modules carry different licenses; the repo makes the boundary obvious&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://fsl.software/" rel="noopener noreferrer"&gt;FSL&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Limits competing use; becomes Apache 2.0 or MIT after two years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://fcl.dev/" rel="noopener noreferrer"&gt;FCL&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Limits competing use, protects license-key functionality, future license per chosen variant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.elastic.co/licensing/elastic-license" rel="noopener noreferrer"&gt;ELv2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Restricts offering as a managed service, bypassing key functionality, removing notices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The key is the line, and that is fine
&lt;/h2&gt;

&lt;p&gt;A self-hosted operator controls the binary. They can patch it. Accept that, and design for the honest majority:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign the proof.&lt;/strong&gt; A modified config file should never be able to grant access in the unmodified product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check where the paid operation runs&lt;/strong&gt;, on the server side, not only in the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No mandatory telemetry, no remote kill switch.&lt;/strong&gt; Operators of self-hosted software care a lot about this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain state clearly.&lt;/strong&gt; If a feature is unavailable, say why and how to restore it. Never lock people out of their own data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Model features as entitlements, not editions
&lt;/h2&gt;

&lt;p&gt;Use stable entitlement identifiers instead of hard-coding "is this Pro?":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Community&lt;/strong&gt;: read and edit data (no key needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro&lt;/strong&gt;: adds &lt;code&gt;exports&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise&lt;/strong&gt;: adds &lt;code&gt;sso&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each paid operation checks its own entitlement. If the proof is invalid or scoped to another product, the paid operation is denied and Community keeps working.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nc"&gt;PermitCore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nc"&gt;Exports&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nc"&gt;Sso&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;CheckFeatures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;PermitCoreClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PERMITCORE_LICENSE_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ValidateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedProductId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsValid&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HasFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsValid&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HasFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sso"&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;h2&gt;
  
  
  Air-gapped and flaky-network installs
&lt;/h2&gt;

&lt;p&gt;Self-hosted often means "no internet". Two patterns cover most cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Air-gapped&lt;/strong&gt;: ship a signed token file, ship trusted public keys through your release channel, and validate offline (signature, deadline, device, expected product).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normally connected&lt;/strong&gt;: configure an offline grace period so signed cached proof keeps working through a network outage.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nc"&gt;AcceptOffline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PermitCoreClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PERMITCORE_OFFLINE_FILE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ActivateOffline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReadAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trim&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="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsValid&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;h2&gt;
  
  
  Let customers keep what they paid for
&lt;/h2&gt;

&lt;p&gt;If your terms promise a perpetual fallback, keep that decision separate from license validation: compare the chosen build's release date with a trusted maintenance end date.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nc"&gt;CanUseRelease&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LicenseResult&lt;/span&gt; &lt;span class="n"&gt;entitlement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;releasedAtUtc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entitlement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsValid&lt;/span&gt;
       &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entitlement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaintenanceExpiresAt&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;releasedAtUtc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;end&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;h2&gt;
  
  
  Docker, Kubernetes and CI
&lt;/h2&gt;

&lt;p&gt;Never bake the key into an image layer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker: &lt;code&gt;docker run --env PERMITCORE_LICENSE_KEY ...&lt;/code&gt; (inherits from the exported variable)&lt;/li&gt;
&lt;li&gt;Kubernetes: &lt;code&gt;env.valueFrom.secretKeyRef&lt;/code&gt; on the workload&lt;/li&gt;
&lt;li&gt;CI: map the protected secret to the same variable for integration tests&lt;/li&gt;
&lt;li&gt;Offline installs: mount the token file read-only and point &lt;code&gt;PERMITCORE_OFFLINE_FILE&lt;/code&gt; at it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep keys and token files out of build logs and public CI output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The examples use the PermitCore .NET SDK, which handles key issuing, entitlements and online/offline validation. The full guide, with links to the offline, grace-period and key-rotation deep dives, is on our blog: &lt;a href="https://permitcore.dev/blog/open-core-license-key" rel="noopener noreferrer"&gt;Open Core and Fair Source: gating paid features with license keys&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you ship open core or Fair Source: where do you draw the line between Community and paid, and do you check entitlements in the UI, the server, or both? 🙂&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>selfhosted</category>
      <category>licensing</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Selling Your Game Outside Steam: Keys, Downloads and a Direct Sale That Delivers Itself</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Mon, 09 Nov 2026 09:36:38 +0000</pubDate>
      <link>https://dev.to/permitcore/selling-your-game-outside-steam-keys-downloads-and-a-direct-sale-that-delivers-itself-3mip</link>
      <guid>https://dev.to/permitcore/selling-your-game-outside-steam-keys-downloads-and-a-direct-sale-that-delivers-itself-3mip</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://permitcore.dev/blog/sell-game-outside-steam" rel="noopener noreferrer"&gt;PermitCore blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Selling your game directly, next to or instead of a big store, starts with one question: what exactly does the buyer get after paying? A Steam activation code, an itch.io download link, a DRM-free build and a build that checks its own license are four different things. They come from different systems and need different delivery and support. Naming the right one on your sales page is the first step to a smooth direct sale.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pick the kind of access you sell
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Steam keys.&lt;/strong&gt; A Steam key activates the game inside Steam. You request keys through Steamworks, and selling them on your own site still gives the buyer a Steam entitlement. Read the current &lt;a href="https://partner.steamgames.com/doc/features/keys" rel="noopener noreferrer"&gt;Steam Key rules and guidelines&lt;/a&gt; before you plan bundles, backer rewards or discounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;itch.io download keys.&lt;/strong&gt; An itch.io &lt;a href="https://itch.io/docs/creators/download-keys" rel="noopener noreferrer"&gt;download key&lt;/a&gt; is a private URL to your project's files on itch.io. It is a great fit when itch.io hosts your builds and you want to hand out access to backers or press.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A DRM-free build.&lt;/strong&gt; You sell a download and trust the buyer. Here the important parts are clear usage terms, a reliable download page and an easy way to get updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your own licensed build.&lt;/strong&gt; Your game checks an entitlement you issued: the right product, a valid key, and the device policy you chose. This is the option that gives you full control over editions, trials, device limits and offline play, and it is the one the rest of this post focuses on.&lt;/p&gt;

&lt;p&gt;Selling more than one kind? That works well as long as each listing says clearly which one it delivers.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Write the promise next to the price
&lt;/h2&gt;

&lt;p&gt;Before the buyer pays, tell them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;supported operating systems and the edition they are buying&lt;/li&gt;
&lt;li&gt;where the download comes from, and whether an account or launcher is needed&lt;/li&gt;
&lt;li&gt;how many devices they can activate&lt;/li&gt;
&lt;li&gt;how the game behaves offline&lt;/li&gt;
&lt;li&gt;which updates are included&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the same wording on the checkout page, in the game UI, on the support page and in your refund policy. If the build depends on platform features (cloud saves, achievements, workshop content, multiplayer identity), test the direct build on its own before launch so the listing matches what players get.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use editions for demos, backers and the full game
&lt;/h2&gt;

&lt;p&gt;With your own license you can model editions as features on one product: for example &lt;code&gt;full_campaign&lt;/code&gt; or &lt;code&gt;level_editor&lt;/code&gt;. The game checks the feature before unlocking that content.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo:&lt;/strong&gt; playable without a key; the full game unlocks after a valid license check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trial:&lt;/strong&gt; a time-limited license with a clear end date and an obvious upgrade button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backers / early access:&lt;/strong&gt; say which builds and which updates are included instead of promising a vague "lifetime key".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More on this: &lt;a href="https://permitcore.dev/blog/feature-based-licensing" rel="noopener noreferrer"&gt;feature-based licensing&lt;/a&gt; and &lt;a href="https://permitcore.dev/blog/software-free-trial-license" rel="noopener noreferrer"&gt;free-trial licenses&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Design offline play deliberately
&lt;/h2&gt;

&lt;p&gt;Players expect a single-player game to run on a train. A good pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Activate once online and bind the license to a stable device identity.&lt;/li&gt;
&lt;li&gt;Store a signed offline token and verify it locally against a public key that ships with your game.&lt;/li&gt;
&lt;li&gt;Check the product, the device and the token's expiry, not only the signature.&lt;/li&gt;
&lt;li&gt;Refresh the token in the background whenever the player is online.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then test the real situations on every platform you ship: clean install, restart, suspend and resume, lost connection, and an expired token. The &lt;a href="https://permitcore.dev/docs/offline-licensing" rel="noopener noreferrer"&gt;offline licensing docs&lt;/a&gt; walk through the setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Connect payment and delivery into one flow
&lt;/h2&gt;

&lt;p&gt;The smoothest direct sale is one where the payment itself triggers delivery:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The buyer pays through your payment provider (for example your own Stripe, Paddle or Lemon Squeezy account).&lt;/li&gt;
&lt;li&gt;A verified payment webhook, not the success page, confirms the purchase.&lt;/li&gt;
&lt;li&gt;The license is issued for the right product and features.&lt;/li&gt;
&lt;li&gt;The buyer receives an email with the key and the download link, and can find both again in a customer account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Renewals, cancellations and refunds should run through the same flow, so the license always matches the payment state. If you are deciding between a merchant of record and your own Stripe account, this &lt;a href="https://permitcore.dev/blog/merchant-of-record-vs-stripe" rel="noopener noreferrer"&gt;comparison&lt;/a&gt; covers VAT and fees.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Example: a Unity game sold from your own store
&lt;/h2&gt;

&lt;p&gt;Here is how this looks with PermitCore, which connects licensing to a branded Store on your own payment account with 0% sales commission:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the product and choose the edition, validity and device policy.&lt;/li&gt;
&lt;li&gt;Add the &lt;a href="https://permitcore.dev/tutorials/sell-unity-game-asset-licenses" rel="noopener noreferrer"&gt;Unity SDK&lt;/a&gt; and validate the expected product before the protected content runs; activate the device where registration is needed.&lt;/li&gt;
&lt;li&gt;Create the Store listing, describe the build clearly and add the download URL of your release host.&lt;/li&gt;
&lt;li&gt;Connect your payment account, matching prices and signed webhooks.&lt;/li&gt;
&lt;li&gt;Run a sandbox purchase and check the whole chain: payment, correct product and features, delivery email and the download.&lt;/li&gt;
&lt;li&gt;In the game, test the valid, expired, revoked and wrong-product outcomes, plus your device and offline rules.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://permitcore.dev/docs/store" rel="noopener noreferrer"&gt;Store docs&lt;/a&gt; and the &lt;a href="https://permitcore.dev/docs/customer-portal" rel="noopener noreferrer"&gt;customer portal docs&lt;/a&gt; cover setup and self-service.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Make support boring
&lt;/h2&gt;

&lt;p&gt;Tell buyers where to find their purchase, key and download, how to move to a new PC and whom to contact if an email does not arrive. Self-service device deactivation and re-download from a customer account remove most support email. Pair a fair device limit with that self-service and honest buyers stay happy while casual key sharing gets awkward; &lt;a href="https://permitcore.dev/blog/prevent-license-key-sharing" rel="noopener noreferrer"&gt;this guide&lt;/a&gt; goes deeper.&lt;/p&gt;

&lt;p&gt;Before launch, check this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The checkout says which build or platform key is sold.&lt;/li&gt;
&lt;li&gt;Download and update hosting work on their own.&lt;/li&gt;
&lt;li&gt;Payment, license issuance and the delivery email have each been tested.&lt;/li&gt;
&lt;li&gt;The game handles wrong product, device limit, expiry and no network without unlocking paid content.&lt;/li&gt;
&lt;li&gt;Device transfer, offline time, refunds and support match what the page promises.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you sell (or plan to sell) a game outside the big stores: which part took the most work for you, payments, delivery or keeping keys from being shared?&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>indiedev</category>
      <category>unity3d</category>
      <category>licensing</category>
    </item>
    <item>
      <title>Sell software through your own Paddle or Lemon Squeezy account—with licensing connected</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Sat, 07 Nov 2026 19:31:56 +0000</pubDate>
      <link>https://dev.to/permitcore/sell-software-through-your-own-paddle-or-lemon-squeezy-account-with-licensing-connected-1jbn</link>
      <guid>https://dev.to/permitcore/sell-software-through-your-own-paddle-or-lemon-squeezy-account-with-licensing-connected-1jbn</guid>
      <description>&lt;p&gt;You want to sell your app, not spend every release maintaining a second product made of checkout pages, tax settings, invoice links and license-delivery scripts.&lt;/p&gt;

&lt;p&gt;A merchant of record can take on the payment, tax and receipt side of eligible software sales under your agreement with it. But a successful checkout still needs to become something your customer can actually use: the right license, for the right product, in an account they can return to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PermitCore Store now connects those steps with Stripe, Paddle and Lemon Squeezy.&lt;/strong&gt; Choose one payment provider for your branded Store, connect your own account, and link verified payments to automatic software license delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose your payment relationship. Keep your own brand.
&lt;/h2&gt;

&lt;p&gt;Your payment account should fit how you sell.&lt;/p&gt;

&lt;p&gt;If you want to use Paddle or Lemon Squeezy as merchant of record, you can connect your own provider account to PermitCore Store. The selected provider handles checkout, tax and its receipts under your merchant-of-record agreement. PermitCore handles the licensing and fulfillment side.&lt;/p&gt;

&lt;p&gt;Stripe remains supported for your Store. If you sell directly through your own Stripe account, you remain responsible for your applicable tax obligations; the Store setup guide explains the available tax configuration.&lt;/p&gt;

&lt;p&gt;The important distinction is ownership of the relationship: this is &lt;strong&gt;your provider account for your customers' purchases&lt;/strong&gt;, not the account used to pay for your own PermitCore subscription.&lt;/p&gt;

&lt;p&gt;PermitCore takes &lt;strong&gt;0% sales commission&lt;/strong&gt;. Your selected provider's fees, payout terms and account requirements still apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  A storefront that looks like your software belongs there
&lt;/h2&gt;

&lt;p&gt;The Store is more than a payment link.&lt;/p&gt;

&lt;p&gt;Use your logo, colors and product imagery. Organize products into collections, add a featured spotlight, and build product pages with descriptions, feature lists and price choices. Offer one-time purchases or recurring products according to your sales model.&lt;/p&gt;

&lt;p&gt;The same product and license settings connect the storefront to what your app enforces: features, device limits and access rules.&lt;/p&gt;

&lt;p&gt;That gives the sale a continuous path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create&lt;/strong&gt; the product and the license settings your customer should receive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement&lt;/strong&gt; licensing in your app through the REST API or an official SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sell&lt;/strong&gt; through your branded Store and selected payment account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliver&lt;/strong&gt; access after payment is verified, with purchase history available in the customer's account.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Connect payment to access—not just a success page
&lt;/h2&gt;

&lt;p&gt;A browser landing on a thank-you page is useful feedback. It is not the authority for issuing a paid license.&lt;/p&gt;

&lt;p&gt;PermitCore verifies provider payment notifications and payment evidence before fulfillment. A completed purchase creates the appropriate order and license; the customer receives an email linking to their account rather than a license key exposed in the message.&lt;/p&gt;

&lt;p&gt;For recurring products, paid renewals extend the existing key. Failed payments follow the Store's configured payment-grace policy. Cancellation and refunds follow their respective access rules, while customers retain their purchase history and provider documents.&lt;/p&gt;

&lt;p&gt;That connection matters after the first sale as much as during checkout. You should not need a separate manual process to reconcile a renewed subscription with a customer's software access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paddle and Lemon Squeezy: same goal, different setup
&lt;/h2&gt;

&lt;p&gt;With &lt;strong&gt;Paddle&lt;/strong&gt;, the Store can create or update the connected provider catalog from your Store products and prices. Review the resulting mappings and provider tax categories as part of setup.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;Lemon Squeezy&lt;/strong&gt;, create products and variants in its dashboard, then connect the matching variants in PermitCore. A multi-product cart uses an aggregate provider line while PermitCore preserves the individual purchase shares. Different recurring intervals use separate checkouts.&lt;/p&gt;

&lt;p&gt;Both routes connect your provider account to the same branded storefront and licensing workflow. The &lt;a href="https://permitcore.dev/docs/store" rel="noopener noreferrer"&gt;Store setup guide&lt;/a&gt; walks through account configuration, product mappings, payment notifications and going live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Less glue between selling and shipping
&lt;/h2&gt;

&lt;p&gt;For an indie developer, reducing tax and invoicing administration is only part of the benefit. The other part is keeping the sale connected to the software you ship.&lt;/p&gt;

&lt;p&gt;Your Store presents the product. Your selected provider handles the payment relationship. PermitCore delivers and manages the license.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://permitcore.dev/solutions/sell-license-keys" rel="noopener noreferrer"&gt;Explore PermitCore Store&lt;/a&gt;, or follow the setup guide to connect your account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which part of selling software takes more of your time today: tax and invoices, storefront setup, or keeping paid access synchronized?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>saas</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>License Checks at App Startup: Open the Window First, Verify in the Background</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Sat, 07 Nov 2026 09:41:08 +0000</pubDate>
      <link>https://dev.to/permitcore/license-checks-at-app-startup-open-the-window-first-verify-in-the-background-39kl</link>
      <guid>https://dev.to/permitcore/license-checks-at-app-startup-open-the-window-first-verify-in-the-background-39kl</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://permitcore.dev/blog/license-check-at-app-startup" rel="noopener noreferrer"&gt;PermitCore blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A license check at startup has two jobs: get the app on screen quickly, and decide which features the customer can use. Those jobs do not have to happen in the same moment. Render the first window from local proof, then confirm the entitlement with the server in the background and update the UI when the answer arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Open the window and verify local proof
&lt;/h2&gt;

&lt;p&gt;Show the main shell right away and keep license settings and the user's own files reachable. If your licensing policy allows offline use, verify the cached signed token against your trusted public key, and check that it belongs to the expected device and product. Check its expiry every time it is used.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No token, or a token that fails verification: keep paid features restricted until the server confirms access.&lt;/li&gt;
&lt;li&gt;A revocation the app has already seen: persist it, so restarting with an old token on disk cannot bring access back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In PermitCore, the offline flow accepts a server-issued signed token through &lt;code&gt;ActivateOffline&lt;/code&gt; / &lt;code&gt;activate_offline&lt;/code&gt; and checks it locally with &lt;code&gt;ValidateOffline&lt;/code&gt; / &lt;code&gt;validate_offline&lt;/code&gt;. Use the same trusted public key, device and product context at both steps. The &lt;a href="https://permitcore.dev/docs/offline-licensing" rel="noopener noreferrer"&gt;offline licensing setup&lt;/a&gt; walks through the configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Run the online check after the window renders
&lt;/h2&gt;

&lt;p&gt;Run one validation per license at a time, with a short timeout. Retry transient failures with a small random delay, and respect the server's retry window when you are rate limited. Validation goes through the &lt;a href="https://permitcore.dev/docs/api/validate" rel="noopener noreferrer"&gt;validate endpoint&lt;/a&gt;; device activation is its own step.&lt;/p&gt;

&lt;p&gt;C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LicenseResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CheckAfterLaunchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PermitCoreClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;windowClosing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateLinkedTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;windowClosing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedProductId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&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;Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_after_launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_product_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Construct the Python client with &lt;code&gt;timeout=2&lt;/code&gt;. If your desktop toolkit has its own worker mechanism instead of asyncio, run the call there and hand the result back to the UI thread. Either way, ignore results that arrive for a window that has closed or a key that has been replaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Treat valid, denied and unknown as three different outcomes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verified online:&lt;/strong&gt; apply the entitlement for that product. When the cached signed token gets close to expiry, fetch a freshly issued one from your trusted issuer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit denial&lt;/strong&gt; (revoked, expired, wrong product): persist the decision for that key, device and product, invalidate the offline authorization, and keep the recovery screen available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown&lt;/strong&gt; (timeout, network failure, HTTP 503): the entitlement is unresolved. Keep exactly the access backed by current, verified local proof and its existing expiry. Authentication or response-format errors are something to diagnose; they should never create or extend offline access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://permitcore.dev/docs/api/errors" rel="noopener noreferrer"&gt;API error reference&lt;/a&gt; lists which responses are authoritative denials. Show a quiet status line during outages and a specific recovery action for denials. If paid access expires while offline, keep the user's documents safe and let them reconnect or enter another key.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Test the transitions and measure first paint
&lt;/h2&gt;

&lt;p&gt;In an integration fixture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Delay the validation response past the timeout.&lt;/li&gt;
&lt;li&gt;Return HTTP 503.&lt;/li&gt;
&lt;li&gt;Return an authoritative revocation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two must keep an unexpired offline grant working and keep an expired or missing grant restricted. The revocation must stop paid access and still apply after a restart with the old token on disk. Repeat with a wrong product, a changed device, a corrupted token and a rolled-back clock; treat unexpected clock changes as a reason to verify online.&lt;/p&gt;

&lt;p&gt;Measure two numbers separately: time to the first interactive window, and time to the online entitlement. Compare p95 cold starts under normal latency, offline, and with a slow backend. Network time should never show up in the first number.&lt;/p&gt;

&lt;p&gt;For longer interruptions, see &lt;a href="https://permitcore.dev/blog/what-happens-if-a-license-server-goes-down" rel="noopener noreferrer"&gt;what happens if a license server goes down&lt;/a&gt;, and for wording recovery screens, the &lt;a href="https://permitcore.dev/blog/license-error-messages" rel="noopener noreferrer"&gt;license error messages guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How does your app handle startup today: does it wait for the license server, or open first and check in the background?&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>desktop</category>
      <category>csharp</category>
      <category>python</category>
    </item>
    <item>
      <title>Moving to a New Licensing Provider Without Replacing Customer Keys</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Thu, 05 Nov 2026 09:32:28 +0000</pubDate>
      <link>https://dev.to/permitcore/moving-to-a-new-licensing-provider-without-replacing-customer-keys-3fba</link>
      <guid>https://dev.to/permitcore/moving-to-a-new-licensing-provider-without-replacing-customer-keys-3fba</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://permitcore.dev/blog/switch-software-licensing-provider" rel="noopener noreferrer"&gt;PermitCore blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Moving to a new licensing provider does not have to mean sending every customer a new key. The key string can stay the same. What changes is the plan around it: how the application talks to the new system, how entitlement data is mapped, and how verification is trusted from the first release that points at the new endpoint.&lt;/p&gt;

&lt;p&gt;Start with a sample you own and a rollback plan. Treat key lookup, device registrations, offline proofs, payments and customer identity as separate pieces, and move each one on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the key when the lookup can be reproduced
&lt;/h2&gt;

&lt;p&gt;The PermitCore CSV import wizard works from your existing key strings. It hashes each key in the browser, and the server then applies its own HMAC lookup protection, so the plaintext never needs to be stored for lookup to work. If you prefer to script it, the API importer accepts the documented SHA-256 input.&lt;/p&gt;

&lt;p&gt;Keep the exact original value and its encoding. Case, whitespace and separators matter, because the server hashes the plaintext bytes you supply instead of inventing a new key format. Before you roll out a client update, check a few real old keys against a migrated record that you own.&lt;/p&gt;

&lt;p&gt;If your source system stored keys in a form you cannot read back, such as a vendor-specific salted hash or an encrypted blob, work out which verifiable lookup material you actually hold. Server-side migration is the right place for that step. Keep any privileged import credential on the server and out of customer software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map products and policy before importing
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;Licenses → Import&lt;/strong&gt;, choose the source preset that matches your export, and map the columns explicitly. Match each row to the correct PermitCore product, then check expiry, trial state, features, device cap, node-locking and customer association one by one rather than assuming every source field means the same thing.&lt;/p&gt;

&lt;p&gt;The API takes up to 500 rows per batch and reports a per-row outcome: Created, Duplicate or ValidationFailed. Rows commit independently, so a large import can be done in stages, and you can keep the results to reconcile any duplicate or failed rows before you retry them.&lt;/p&gt;

&lt;p&gt;Version limits use dotted-numeric versions, and dates deserve a deliberate conversion, including what timezone they mean. The &lt;a href="https://permitcore.dev/docs/migrating-from-other-systems" rel="noopener noreferrer"&gt;import reference&lt;/a&gt; lists the current column mapping and API fields. Anything outside that mapping can be applied afterwards through the normal administration workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan device activations on purpose
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;InitialActivations&lt;/code&gt; seeds the used count for a key, anywhere from zero up to its activation limit. Pick that number together with your re-enrollment policy: an installation that registers against the new system enrolls as a fresh device, so test three cases before the real cutover: a partly occupied key, an existing installation, and a genuinely new device.&lt;/p&gt;

&lt;p&gt;Floating licenses start fresh in the new system, with a new checkout and a returned deadline for each seat. The &lt;a href="https://permitcore.dev/blog/team-license-for-software" rel="noopener noreferrer"&gt;capacity guide&lt;/a&gt; and the &lt;a href="https://permitcore.dev/docs/floating-licenses" rel="noopener noreferrer"&gt;floating lifecycle docs&lt;/a&gt; walk through that flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose how keys are recovered later
&lt;/h2&gt;

&lt;p&gt;The import wizard offers optional passphrase encryption. If you turn it on, you keep ciphertext that an authorised person can reveal later with that passphrase. Lookup and validation work either way. Keep your authorised source export somewhere safe if you want a recovery path for the original keys.&lt;/p&gt;

&lt;p&gt;Licenses issued by the PermitCore Store have their own server-managed delivery and recovery path. The &lt;a href="https://permitcore.dev/exit-plan" rel="noopener noreferrer"&gt;exit and recovery plan&lt;/a&gt; shows which artifacts matter for which situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the client update deliberately
&lt;/h2&gt;

&lt;p&gt;Importing data prepares the new system; your next application release is what points users at it. Ship a version that calls the new endpoint and checks the new product scope. If your app already keeps the customer's key in its settings, reuse that stored value so people are not asked to retype it. Implement and test that storage step, since it lives in your app.&lt;/p&gt;

&lt;p&gt;For offline use, plan new signed proofs from the new system. The official SDKs verify them against trusted public keys that you configure yourself, so include that configuration in the same release, and keep private signing and Admin keys out of the client. &lt;a href="https://permitcore.dev/blog/license-signing-key-rotation" rel="noopener noreferrer"&gt;Signing and trust rollout&lt;/a&gt; covers how to stage a change of trusted keys.&lt;/p&gt;

&lt;p&gt;Run an owned pilot where you can watch old and new decisions side by side. Write down the cutover and rollback conditions, avoid running both systems for the same customer by accident, and check what an older client does once the original endpoint is retired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep payments on their own track
&lt;/h2&gt;

&lt;p&gt;License import is about entitlements. Card tokens, mandates, provider subscriptions and store order history are a payment-provider topic, so handle them as a separate, approved process with that provider and coordinate access dates so nobody is charged twice. If you use the PermitCore Store, changing the vendor's selected provider applies to new checkouts, and existing purchases keep the provider they were bought with. The &lt;a href="https://permitcore.dev/blog/paddle-license-keys" rel="noopener noreferrer"&gt;payment lifecycle guide&lt;/a&gt; explains how fulfillment follows each purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-cutover checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reconcile row counts, duplicates, failures and product mapping between the source and the import.&lt;/li&gt;
&lt;li&gt;Test existing key strings, wrong-product rejection, expiry and your device re-enrollment policy.&lt;/li&gt;
&lt;li&gt;Test the app update, the retained key setting, the new trust configuration and offline recovery.&lt;/li&gt;
&lt;li&gt;Keep billing changes, customer communication and rollback under separate control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have you moved a licensing system before? Which step took the most care: the key lookup, the client rollout, or payments?&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>saas</category>
      <category>migration</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Charging for an MCP Server: Check the License Before the Paid Tool Runs</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Tue, 03 Nov 2026 09:36:08 +0000</pubDate>
      <link>https://dev.to/permitcore/charging-for-an-mcp-server-check-the-license-before-the-paid-tool-runs-59c4</link>
      <guid>https://dev.to/permitcore/charging-for-an-mcp-server-check-the-license-before-the-paid-tool-runs-59c4</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://permitcore.dev/blog/monetize-mcp-server" rel="noopener noreferrer"&gt;PermitCore blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you ship an MCP server people actually use, sooner or later someone asks "is there a paid version?". The pattern is simple: decide which operations are paid, tie the customer to an entitlement, and check that entitlement &lt;strong&gt;before&lt;/strong&gt; the protected work runs. A local license key suits a distributed tool; a hosted service also needs its own user authentication.&lt;/p&gt;

&lt;p&gt;This guide covers local Node.js and Python servers, plus the boundaries of a hosted design. PermitCore supplies the license check and the entitlement data; you bring your MCP server library and transport. The code below is a licensing-policy helper that you call from your own tool handler, not a complete MCP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose where the paid value runs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local server with online checks.&lt;/strong&gt; Ship the program and check a customer key when a paid operation begins. Keep the free tools usable even if the license service is unreachable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local server with signed offline proof.&lt;/strong&gt; Verify a previously issued proof against trusted keys and its actual constraints. Revocation visibility while offline is bounded by reconnect time and token expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosted server.&lt;/strong&gt; Authenticate the caller, map their account to its entitlement and authorise each protected operation. You also keep control of remote computation, storage and API costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A customer controls a local installation and can modify its code, so a local gate supports your commercial policy rather than guaranteeing anything. Put especially valuable shared data or costly computation behind an authenticated service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the key out of the conversation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MCP_LICENSE_KEY&lt;/code&gt; here is just a convention of this guide, not an MCP-standard field. Supply it to your own local process through private configuration or a trusted launcher. Don't ask the model to paste it into a prompt or pass it as a tool argument: those paths can end up in conversation history and traces. Your PermitCore administrative API key never belongs in the customer program.&lt;/p&gt;

&lt;p&gt;Cursor supports environment interpolation in &lt;code&gt;mcp.json&lt;/code&gt;, so the secret can stay in your environment:&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;"mcpServers"&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;span class="nl"&gt;"my-local-server"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"/absolute/path/to/your/server.mjs"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;span class="nl"&gt;"MCP_LICENSE_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${env:MCP_LICENSE_KEY}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MCP_PRODUCT_ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_PRODUCT_GUID"&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;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;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;Replace the product GUID and point &lt;code&gt;args&lt;/code&gt; at your real server entry point. Keep real keys out of a committed project config. For Claude Desktop, use Settings → Developer → Edit Config and the official local-server guide; don't assume Cursor's interpolation syntax works in another client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the paid operation before any side effect
&lt;/h2&gt;

&lt;p&gt;Keep installation, discovery and genuinely free tools available without a key. A tool description saying "Pro" is only a UI hint: enforce the entitlement inside the execution handler, before exporting files, calling a billable service or changing data. Hiding a tool in the client is not access control.&lt;/p&gt;

&lt;p&gt;For the examples, create a product in PermitCore, use its GUID as &lt;code&gt;MCP_PRODUCT_ID&lt;/code&gt;, and add a &lt;code&gt;pro_export&lt;/code&gt; feature to the licenses that should unlock your export tool. A valid key for a &lt;em&gt;different&lt;/em&gt; product must not grant access, so the helpers compare the product id as well as the feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node.js
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PermitCoreClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@permitcore/permitcore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkProLicense&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MCP_LICENSE_KEY&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MCP_PRODUCT_ID&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Configure MCP_LICENSE_KEY and MCP_PRODUCT_ID privately.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PermitCoreClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MCP_API_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.permitcore.dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;enableOfflineCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pro access could not be confirmed. Check the key, product and service.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;PermitCoreClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro_export&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This license does not include Pro export.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pro access could not be confirmed. Check the configuration and service.&lt;/span&gt;&lt;span class="dl"&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;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;permitcore&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PermitCoreClient&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_pro_license&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MCP_LICENSE_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MCP_PRODUCT_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Configure MCP_LICENSE_KEY and MCP_PRODUCT_ID privately.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PermitCoreClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MCP_API_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.permitcore.dev&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;enable_offline_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_product_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Pro access could not be confirmed. Check the key, product and service.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pro_export&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;This license does not include Pro export.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Pro access could not be confirmed. Check the configuration and service.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call the helper from the protected handler and stop before doing the work when &lt;code&gt;allowed&lt;/code&gt; is false. Free handlers never call it. These online-only helpers disable offline caching and retries and use a five-second timeout per attempt. The Python SDK call is synchronous, so an async server should run it on a worker thread. &lt;code&gt;MCP_API_URL&lt;/code&gt; is trusted process configuration (self-hosting or a local test fixture); never derive it from a tool argument or model output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Return a useful refusal
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/server/tools" rel="noopener noreferrer"&gt;MCP tools specification&lt;/a&gt; separates tool execution failures from malformed protocol requests. For a denied business operation, return a tool error (&lt;code&gt;isError: true&lt;/code&gt;) with a short, safe explanation, using the result type your MCP library expects.&lt;/p&gt;

&lt;p&gt;Say what to do next: configure the key outside the conversation, check that it belongs to this product, or get a license that includes the feature. A temporary verification failure shouldn't tell a paying customer their key is definitely invalid, and the message must never contain the key or account details.&lt;/p&gt;

&lt;p&gt;One stdio detail: stdout carries protocol messages, so a licensing banner printed there can break the connection. Keep diagnostics on stderr and redact secrets there too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design offline use as its own policy
&lt;/h2&gt;

&lt;p&gt;A local server may need to work on a train or an isolated workstation. Use the signed offline path from the &lt;a href="https://permitcore.dev/docs/offline-licensing" rel="noopener noreferrer"&gt;offline licensing docs&lt;/a&gt;: a successful online check can provide a signed grace-cache token when configured. Verifying a saved unsigned JSON response, or caching a &lt;code&gt;true&lt;/code&gt; forever, is not safe. Test expiry, tampering and key rotation separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seats and usage
&lt;/h2&gt;

&lt;p&gt;One key copied into several client configs doesn't define a team-seat policy. Pick &lt;a href="https://permitcore.dev/blog/node-locked-vs-floating-licenses" rel="noopener noreferrer"&gt;node-locked or floating controls&lt;/a&gt; for what you sell, and don't activate on every tool call: activation is separate from validation. If you charge by usage, define the billable unit (messages, tokens, successful exports are all different) and use &lt;a href="https://permitcore.dev/blog/metered-software-licensing-explained" rel="noopener noreferrer"&gt;metering&lt;/a&gt; explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosted access needs authentication too
&lt;/h2&gt;

&lt;p&gt;For an HTTP-based server, follow the &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization" rel="noopener noreferrer"&gt;MCP authorization specification&lt;/a&gt;. Authentication says who is calling; your commercial policy says what their account may use, so map the authenticated account to its entitlement instead of sharing one global key. Don't treat a model-supplied email or license string as proof of identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sell it and test the whole path
&lt;/h2&gt;

&lt;p&gt;A PermitCore Store can use your own Stripe or Paddle account and deliver a key after verified payment, with 0% PermitCore sales commission (provider fees apply). Configure the product, features and seat policy, then test a purchase together with the protected tool.&lt;/p&gt;

&lt;p&gt;Before release, run through the real client and transport: free tools with no key; Pro with the right feature; a key for another product; an expired or revoked key; a service failure; and confirm no side effect happens after a denial and that license material never reaches protocol output, logs or the model.&lt;/p&gt;

&lt;p&gt;Full original with all the links: &lt;a href="https://permitcore.dev/blog/monetize-mcp-server" rel="noopener noreferrer"&gt;permitcore.dev/blog/monetize-mcp-server&lt;/a&gt;. Docs: &lt;a href="https://permitcore.dev/docs/getting-started" rel="noopener noreferrer"&gt;permitcore.dev/docs/getting-started&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you're building MCP servers, how are you planning to charge for them: a license key in the client config, a hosted account, or something else? I'd love to hear what's working. 🙂&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>node</category>
      <category>licensing</category>
    </item>
    <item>
      <title>Perpetual License vs Subscription License: Which Should You Sell?</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Sun, 01 Nov 2026 09:27:06 +0000</pubDate>
      <link>https://dev.to/permitcore/perpetual-license-vs-subscription-license-which-should-you-sell-16gl</link>
      <guid>https://dev.to/permitcore/perpetual-license-vs-subscription-license-which-should-you-sell-16gl</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://permitcore.dev/blog/perpetual-vs-subscription-license" rel="noopener noreferrer"&gt;PermitCore blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Perpetual and subscription aren't really about the license technology at all. A license key doesn't care whether it's "supposed to" last forever. The real difference is a business model decision: does the customer own access forever after one payment, or do they keep paying to keep access? Most licensing platforms, including ours, can issue either from the exact same product.&lt;/p&gt;

&lt;h2&gt;
  
  
  How purchase type determines access
&lt;/h2&gt;

&lt;p&gt;For a one-time purchase in PermitCore Store, &lt;code&gt;LicenseExpiryDays&lt;/code&gt; sets the number of days of access; leaving it blank creates a perpetual key. Monthly and annual purchases follow Stripe's paid billing period instead: each paid invoice extends the same key. Failed renewals use the configured payment grace window, 14 days by default, and access expires when it ends.&lt;/p&gt;

&lt;p&gt;A product can offer one-time, monthly, and annual options side by side; the selected purchase type determines which expiry rule applies. Activations, node locking, and feature flags remain independent controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perpetual: what you're actually trading
&lt;/h2&gt;

&lt;p&gt;A perpetual license (one payment, access forever) is simple to explain to a customer and creates no ongoing billing relationship to maintain: no dunning emails, no cancellation flow, no "why was I charged again" support tickets.&lt;/p&gt;

&lt;p&gt;The tradeoff is revenue shape. Each sale is a one-time event, so growth depends entirely on new customers or paid upgrades, not a compounding base of renewals.&lt;/p&gt;

&lt;p&gt;It also creates a real, ongoing support obligation most vendors underestimate. A customer who paid once five years ago may still expect their software to run, even as the OS and platform change underneath them. Perpetual access does not itself promise endless feature upgrades or commercial support. Applicable security-support duties need separate assessment; they are not erased by an expired upgrade contract. Our &lt;a href="https://permitcore.dev/blog/cyber-resilience-act-software-developers" rel="noopener noreferrer"&gt;CRA support and licensing guide&lt;/a&gt; goes deeper. In short: state the purchased execution rights, the upgrade policy and the security-support period separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscription: what you're actually trading
&lt;/h2&gt;

&lt;p&gt;A subscription (recurring charge, access requires staying current) creates a predictable, compounding revenue base and a natural way to fund ongoing development. The customer relationship doesn't end at the sale.&lt;/p&gt;

&lt;p&gt;The tradeoff is operational surface:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Failed payments need handling (grace window, then expiry).&lt;/li&gt;
&lt;li&gt;Cancellations need a clean flow.&lt;/li&gt;
&lt;li&gt;Every renewal is a moment a customer can decide to leave, so churn becomes a metric you actively manage.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Selling both isn't unusual
&lt;/h2&gt;

&lt;p&gt;A common pattern: subscription as the default (lower upfront price, steady revenue) with a perpetual option at a higher price point for customers who specifically want to avoid recurring billing. Enterprise procurement teams in particular often prefer a one-time purchase over an operating subscription for budgeting reasons that have nothing to do with your product.&lt;/p&gt;

&lt;p&gt;Since both are just a price plus an expiry setting on the same product, offering both isn't extra engineering work, just a second price point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't change either way
&lt;/h2&gt;

&lt;p&gt;Node-locking, floating seats, feature gating, and offline support are all independent of whether the license is perpetual or subscription. The perpetual-vs-subscription decision is purely about the business model; how the license actually behaves on a customer's machine is a separate, orthogonal choice.&lt;/p&gt;




&lt;p&gt;Which model are you selling right now, and what made you pick it? I'd love to hear how other indie devs decided. 🙂&lt;/p&gt;

</description>
      <category>saas</category>
      <category>licensing</category>
      <category>indiehackers</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>How to Implement Feature-Based Licensing</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Fri, 30 Oct 2026 09:33:17 +0000</pubDate>
      <link>https://dev.to/permitcore/how-to-implement-feature-based-licensing-1f5d</link>
      <guid>https://dev.to/permitcore/how-to-implement-feature-based-licensing-1f5d</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://permitcore.dev/blog/feature-based-licensing" rel="noopener noreferrer"&gt;PermitCore blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Is this license valid?" is often the wrong question. The more useful one is usually "what does &lt;em&gt;this specific&lt;/em&gt; license unlock?" A Pro customer and a Team customer might both hold a genuinely valid key, but your app needs to know which of them gets the export button, SSO, or the API integration. That's feature-based licensing, and it's a field on the license key itself, not a separate system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where features live
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;LicenseKey&lt;/code&gt; has a &lt;code&gt;FeaturesJson&lt;/code&gt; field: a plain JSON array of feature-flag strings, e.g. &lt;code&gt;["export","api","sso"]&lt;/code&gt;. You set this when you create a key (or define it once on a License Template so every key generated from that template inherits the same list), and it's returned in full on every &lt;code&gt;validate()&lt;/code&gt;/&lt;code&gt;activate()&lt;/code&gt; call, no separate endpoint required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading it in your app
&lt;/h2&gt;

&lt;p&gt;The response's &lt;code&gt;features&lt;/code&gt; field is a plain string array. PermitCore's .NET SDK adds one convenience method on top of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// Returns true if this feature flag is in the license's feature list.&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;HasFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;Features&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your own code, that's a one-line gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;licenseKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"export"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Visible&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sso"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;EnableSsoLogin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check is case-insensitive on purpose: a casing difference between how a feature was set on the key and how you check it in code is exactly the kind of bug that only shows up for one customer, weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to assign features
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Per-key.&lt;/strong&gt; Set &lt;code&gt;FeaturesJson&lt;/code&gt; directly when creating or editing an individual license. Useful for a one-off custom deal or an enterprise customer with a negotiated feature set that doesn't match any of your standard tiers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Via a License Template.&lt;/strong&gt; Define a template once (e.g. "Pro Template" with &lt;code&gt;["export","api"]&lt;/code&gt;, "Team Template" with &lt;code&gt;["export","api","sso","seats"]&lt;/code&gt;) and every key generated from it, whether created manually, through the Store, or via the Stripe checkout flow, inherits that feature list automatically. This is the right approach if you're selling more than one tier: define the tiers as templates once, and every future key just picks one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits versus other licensing controls
&lt;/h2&gt;

&lt;p&gt;Feature flags answer "what can this customer do," which is a different question from what &lt;a href="https://permitcore.dev/blog/node-locked-vs-floating-licenses" rel="noopener noreferrer"&gt;node-locked vs. floating&lt;/a&gt; answers ("how many devices or seats can use it") or what an expiry date answers ("for how long"). Most real products combine all three: a Pro-tier key might be node-locked to 2 devices, expire in 12 months, and unlock a specific feature set, all on the same license.&lt;/p&gt;

&lt;p&gt;How do you split features between tiers in your own product: flags on the license, or separate builds? 🙂&lt;/p&gt;

</description>
      <category>saas</category>
      <category>licensing</category>
      <category>gamedev</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>How to Prevent License Key Sharing (Three Layers That Work Together)</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Wed, 28 Oct 2026 09:31:54 +0000</pubDate>
      <link>https://dev.to/permitcore/how-to-prevent-license-key-sharing-three-layers-that-work-together-bca</link>
      <guid>https://dev.to/permitcore/how-to-prevent-license-key-sharing-three-layers-that-work-together-bca</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://permitcore.dev/blog/prevent-license-key-sharing" rel="noopener noreferrer"&gt;PermitCore blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A license key on its own is just a string. Nothing stops someone from pasting a valid one into a forum thread or a Discord server unless the system that checks the key actually does something about it. Here are three complementary techniques we use in PermitCore. Each one covers a different sharing pattern, and together they make casual key-sharing genuinely inconvenient.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Node-locking: tie a key to a device
&lt;/h2&gt;

&lt;p&gt;Every license has a &lt;strong&gt;Max Activations&lt;/strong&gt; limit: how many distinct devices it can be active on at once. With node-locking on, the first &lt;code&gt;activate()&lt;/code&gt; call binds the key to a hardware fingerprint (a hash derived from stable machine identifiers, nothing personally identifying). Reactivating on the same device doesn't use a new seat, but a genuinely different machine does, and once the limit is reached the next machine is rejected.&lt;/p&gt;

&lt;p&gt;That's the first line of defense: sharing a key with a friend costs one of your own seats the moment they use it, and once the limit is hit, both of you find out immediately. The &lt;a href="https://permitcore.dev/blog/device-activation-hwid" rel="noopener noreferrer"&gt;device activation guide&lt;/a&gt; covers how the fingerprinting works.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Floating licenses: shared seats, not shared copies
&lt;/h2&gt;

&lt;p&gt;For team products where node-locking doesn't fit, for example a license that should float to whoever on the team is using the app right now, floating (concurrent) licenses work better. A fixed number of sessions are checked out and released as people start and stop the app. When every seat is taken, the next activation waits until someone checks theirs back in.&lt;/p&gt;

&lt;p&gt;Simultaneous use stays bounded by what was paid for; it's just counted per concurrent session instead of per permanent device.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Abuse detection: the pattern seat limits can't see
&lt;/h2&gt;

&lt;p&gt;A key with a generous seat count (say a 50-seat team license) can still be posted publicly and used by far more than 50 people over time, as long as no more than 50 are active at once. Every individual request looks legitimate.&lt;/p&gt;

&lt;p&gt;So we watch for that pattern directly: per license key, how many distinct caller IPs hit &lt;code&gt;validate()&lt;/code&gt; / &lt;code&gt;activate()&lt;/code&gt; within a rolling one-hour window.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;RequestSuspiciousThreshold&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// requests/hour&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;RequestBlockThreshold&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// requests/hour&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;IpFanoutSuspiciousMultiplier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// × MaxActivations&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;IpFanoutSuspiciousFloor&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// distinct IPs/hour, minimum&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;IpFanoutBlockMultiplier&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// × MaxActivations&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;IpFanoutBlockFloor&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// distinct IPs/hour, minimum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threshold scales with the license's own seat count. A 1-seat license reaching 20 distinct IPs in an hour is already abnormal, while a 500-seat license needs proportionally more before it's flagged. There are two tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Suspicious:&lt;/strong&gt; the request is still served, so a real customer is never blocked over a false positive, but an audit entry is written and a &lt;code&gt;license.abuse_suspected&lt;/code&gt; webhook fires (at most once per key per hour) so you can decide what to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block:&lt;/strong&gt; well past the suspicious threshold, the request is rejected outright before any database work, which protects your infrastructure from a key that's clearly being hammered from everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why layering matters
&lt;/h2&gt;

&lt;p&gt;Node-locking and floating limits bound how much simultaneous use one key can support. Abuse detection catches the slower leak, where a key spreads more widely than its seat count alone would reveal. Combined with revocation, you can shut a leaked key down the moment the webhook tells you about it, without touching paying customers.&lt;/p&gt;

&lt;p&gt;How do you handle leaked keys in your own products today: hard limits, monitoring, or just revoking when someone reports it? 🙂&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>saas</category>
      <category>gamedev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Node-Locked vs. Floating Licenses: Which One Do You Need?</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Mon, 26 Oct 2026 09:39:52 +0000</pubDate>
      <link>https://dev.to/permitcore/node-locked-vs-floating-licenses-which-one-do-you-need-iia</link>
      <guid>https://dev.to/permitcore/node-locked-vs-floating-licenses-which-one-do-you-need-iia</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://permitcore.dev/blog/node-locked-vs-floating-licenses" rel="noopener noreferrer"&gt;PermitCore blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Both options sit right next to each other on a typical Create License form, and it's not always obvious which one fits your product. The short version: node-locked ties a key to specific machines; floating ties a key to a &lt;em&gt;pool of concurrent seats&lt;/em&gt; that any machine can borrow from. Here's when each one actually makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node-Locked: One Key, Specific Machines
&lt;/h2&gt;

&lt;p&gt;A node-locked license binds to a device's hardware fingerprint the first time it's activated, and stays bound until it's explicitly deactivated. &lt;code&gt;MaxActivations&lt;/code&gt; is a hard ceiling on how many distinct devices can ever hold that key at once.&lt;/p&gt;

&lt;p&gt;This is the right default for most desktop software: a customer buys one seat, installs it on their one work laptop, and that's that. It's simple to reason about, simple to support, and matches how most customers already think about "a license."&lt;/p&gt;

&lt;h2&gt;
  
  
  Floating: A Shared Pool of Seats
&lt;/h2&gt;

&lt;p&gt;A floating license doesn't permanently tie itself to any machine. Instead, your app &lt;strong&gt;checks out&lt;/strong&gt; a seat when it starts, and &lt;strong&gt;checks in&lt;/strong&gt; (releases it) when it closes, or after a heartbeat timeout if it crashes without checking in cleanly. &lt;code&gt;MaxActivations&lt;/code&gt; here means "how many machines can be actively using the software &lt;em&gt;right now&lt;/em&gt;," not "how many machines have ever run it."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0758rpzwmnc3gpnx6ka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0758rpzwmnc3gpnx6ka.png" alt="Floating license configuration showing seat pool and heartbeat settings" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 10-seat floating license lets any number of employees install the software, but only 10 can have it open at the same time.&lt;/p&gt;

&lt;p&gt;This is the right fit for office software used by a team that's never all logged in simultaneously: CAD tools, analytics dashboards, anything where you'd rather sell "10 concurrent users" than "10 named installs."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On app start&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CheckoutFloatingSeatAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// result.ErrorCode == "SeatsExhausted" means every seat is in use right now&lt;/span&gt;
    &lt;span class="nf"&gt;ShowNoSeatsAvailable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Periodically, while the app is running&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HeartbeatAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// On clean app shutdown&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CheckinFloatingSeatAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Side by Side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Node-Locked&lt;/th&gt;
&lt;th&gt;Floating&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ties to&lt;/td&gt;
&lt;td&gt;a specific device&lt;/td&gt;
&lt;td&gt;a seat pool, any device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Seats" means&lt;/td&gt;
&lt;td&gt;total installs ever&lt;/td&gt;
&lt;td&gt;concurrent active users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs&lt;/td&gt;
&lt;td&gt;activate/deactivate&lt;/td&gt;
&lt;td&gt;checkout/heartbeat/checkin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;1 user, 1 machine&lt;/td&gt;
&lt;td&gt;shared office tools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Happens If a Floating Session Never Checks In?
&lt;/h2&gt;

&lt;p&gt;If a customer's machine crashes, loses power, or their app process is killed rather than closed cleanly, the seat isn't lost forever: a missed heartbeat past the configured timeout automatically frees the seat back into the pool for the next machine that needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Both models assume you're selling directly through your own app. The other distribution path is a built-in license store, for teams who want a real checkout page and automatic key delivery without building any of that themselves: &lt;a href="https://permitcore.dev/blog/sell-license-keys-store" rel="noopener noreferrer"&gt;Setting Up a License Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which model do you use for your own desktop app, and did you ever have to switch? 🙂&lt;/p&gt;

</description>
      <category>saas</category>
      <category>licensing</category>
      <category>webdev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>How to Build a License Key System</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:28:35 +0000</pubDate>
      <link>https://dev.to/permitcore/how-to-build-a-license-key-system-922</link>
      <guid>https://dev.to/permitcore/how-to-build-a-license-key-system-922</guid>
      <description>&lt;p&gt;Whether you build one yourself or use a service, every license key system is answering the same three questions: what does a key look like, how do you check one without leaking what a valid one looks like, and how do you stop a valid key from working everywhere at once. Here's the real architecture, independent of any particular vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the key format
&lt;/h2&gt;

&lt;p&gt;A license key is a string a human might need to type, read over the phone, or paste from an email — that constrains the design more than it first seems. A sensible format groups characters for readability (&lt;code&gt;PERMIT-XXXX-XXXX-XXXX-XXXX&lt;/code&gt; rather than one long block), and a good charset avoids characters that are easy to confuse when hand-typed or read aloud — no &lt;code&gt;0&lt;/code&gt;/&lt;code&gt;O&lt;/code&gt;, no &lt;code&gt;1&lt;/code&gt;/&lt;code&gt;I&lt;/code&gt;/&lt;code&gt;l&lt;/code&gt;. A real example: &lt;code&gt;ABCDEFGHJKLMNPQRSTUVWXYZ23456789&lt;/code&gt; — deliberately missing the ambiguous characters, 32 symbols so each character carries 5 bits of entropy, and a 20-character key (the four &lt;code&gt;XXXX&lt;/code&gt; groups) gives 100 bits — comfortably beyond brute-force range.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never store the plaintext key server-side
&lt;/h2&gt;

&lt;p&gt;This is the part naive implementations get wrong most often: storing the key as plaintext (or as a reversible encryption of it) means a stolen database dump hands an attacker every valid key directly. The correct approach is a one-way transform — hash the key, and look it up by hash, not by the key itself. A plain hash isn't quite enough either: a fast hash function (SHA-256 alone) lets an attacker with a stolen dump brute-force short/predictable keys offline at billions of guesses per second. The fix is a &lt;strong&gt;keyed&lt;/strong&gt; hash — HMAC with a secret that lives only on your server, never in the database — so even a full database theft is useless without also stealing the separate secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lookup path
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Customer sends the plaintext key in a POST body (never a URL — keeps it out of&lt;/span&gt;
&lt;span class="c1"&gt;// access logs, browser history, and CDN caches)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;HMAC_SHA256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plaintextKey&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Look up by the keyed hash, never by the plaintext key itself&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KeyHash&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the property that matters most: your server can validate a key it has never seen the plaintext form of stored anywhere, and a database breach alone — without the separate HMAC secret — is not enough to forge or verify keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Activation limits need a real lock, not just a count check
&lt;/h2&gt;

&lt;p&gt;A naive "read the count, check it's under the limit, then increment" has a race window: two devices activating a nearly-full license within milliseconds of each other can both read the same under-limit count and both succeed, overselling a seat. The fix is an atomic conditional update (&lt;code&gt;UPDATE ... SET count = count + 1 WHERE count &amp;lt; max&lt;/code&gt;, checking the database's own row affected count) or a database-level lock scoped to that one license — tested against genuinely concurrent requests, not just sequential ones, since the race only shows up under real concurrency.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tends to get skipped, and shouldn't
&lt;/h2&gt;

&lt;p&gt;A system that only handles the happy path is missing: per-key failed-lookup rate limiting (so a script can't enumerate keys by brute force), abuse detection for a valid key shared across too many distinct callers (a different problem than a rate limit catches — see &lt;a href="https://permitcore.dev/blog/prevent-license-key-sharing" rel="noopener noreferrer"&gt;how PermitCore detects this&lt;/a&gt;), and an offline story if your app needs to keep working without a network call (see &lt;a href="https://permitcore.dev/blog/why-mac-addresses-are-bad-hardware-ids" rel="noopener noreferrer"&gt;device fingerprinting&lt;/a&gt; and &lt;a href="https://permitcore.dev/blog/offline-license-validation-csharp" rel="noopener noreferrer"&gt;offline validation&lt;/a&gt;). None of these are exotic — they're just easy to underestimate until a real customer hits the gap.&lt;/p&gt;

&lt;p&gt;If building all of this yourself sounds like more than you want to maintain long-term, that's the actual tradeoff a licensing API like PermitCore is solving — see our honest &lt;a href="https://permitcore.dev/blog/build-vs-buy-software-licensing" rel="noopener noreferrer"&gt;build vs. buy breakdown&lt;/a&gt; for the fuller picture either way.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://permitcore.dev/blog/how-to-build-a-license-key-system" rel="noopener noreferrer"&gt;PermitCore blog&lt;/a&gt;. How do you handle license keys in your own apps? I'd love to hear what's worked for you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>saas</category>
      <category>webdev</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>PermitCore Store 2.0: a branded software storefront built into licensing</title>
      <dc:creator>PermitCore</dc:creator>
      <pubDate>Mon, 12 Oct 2026 15:50:23 +0000</pubDate>
      <link>https://dev.to/permitcore/permitcore-store-20-a-branded-software-storefront-built-into-licensing-405e</link>
      <guid>https://dev.to/permitcore/permitcore-store-20-a-branded-software-storefront-built-into-licensing-405e</guid>
      <description>&lt;p&gt;A licensing API solves key creation and validation. It does not automatically give a software vendor a polished way to present products, run promotions, take payment, deliver licenses, and support customers after checkout.&lt;/p&gt;

&lt;p&gt;That is why we rebuilt the PermitCore Store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store 2.0 is a responsive, vendor-branded software storefront connected directly to the same products, prices, entitlements, orders, subscriptions, and licenses managed in PermitCore.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0u2cjnydd9bsqx4qlq1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0u2cjnydd9bsqx4qlq1.webp" alt="PermitCore Store 2.0 branded storefront" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Merchandising without building another application
&lt;/h2&gt;

&lt;p&gt;Vendors can now create a storefront with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hero campaigns and featured releases&lt;/li&gt;
&lt;li&gt;curated product collections&lt;/li&gt;
&lt;li&gt;category and workflow tiles&lt;/li&gt;
&lt;li&gt;horizontal product shelves&lt;/li&gt;
&lt;li&gt;custom colors, backgrounds, images, spacing, and labels&lt;/li&gt;
&lt;li&gt;search and responsive desktop/mobile navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every section can be configured from the Shop Designer instead of being hard-coded into a separate storefront project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fen4rr4ziu9dcwcrnx4cy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fen4rr4ziu9dcwcrnx4cy.webp" alt="Store 2.0 visual merchandising" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Trials, bundles, and scheduled discounts
&lt;/h2&gt;

&lt;p&gt;Store 2.0 supports one-time and subscription products, free trials, bundles, scheduled promotions, percentage or fixed-value discounts, and campaign labels.&lt;/p&gt;

&lt;p&gt;For discounted offers, vendors can also show the lowest applied price from the previous 30 days. This gives EU-facing stores the information needed for transparent discount presentation while keeping the display configurable for stores operating elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvn8kcgt3lyr1owl9a4m9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvn8kcgt3lyr1owl9a4m9.webp" alt="Store 2.0 product page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Checkout and licensing stay connected
&lt;/h2&gt;

&lt;p&gt;A buyer can discover a product, add it to the cart, pay through the vendor's connected Stripe account, and receive the correct license automatically.&lt;/p&gt;

&lt;p&gt;The complete workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a product and define its license rules.&lt;/li&gt;
&lt;li&gt;Implement validation through the REST API or one of eight official SDKs.&lt;/li&gt;
&lt;li&gt;Merchandise the product in a branded Store 2.0 storefront.&lt;/li&gt;
&lt;li&gt;Accept payment through the vendor's own Stripe account.&lt;/li&gt;
&lt;li&gt;Generate and deliver the correct license automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PermitCore takes &lt;strong&gt;0% commission on software sales&lt;/strong&gt;. The vendor keeps the Stripe relationship while PermitCore connects checkout to customers, orders, invoices, subscriptions, entitlements, and license delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built for desktop and mobile
&lt;/h2&gt;

&lt;p&gt;The storefront, product pages, cart, checkout flow, and customer account are responsive. Storefront content is cached for fast delivery, while checkout, account, and license operations remain protected by the platform's authorization, rate-limiting, and tenant-isolation controls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F48dsjhq2qxsla7o9452f.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F48dsjhq2qxsla7o9452f.webp" alt="Store 2.0 on mobile" width="430" height="932"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the presentation store
&lt;/h2&gt;

&lt;p&gt;We turned Nexova into a complete demo storefront so the entire flow can be explored with realistic products, imagery, bundles, trials, and discounts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://permitcore.dev/store/nexova" rel="noopener noreferrer"&gt;Open the live Nexova demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://permitcore.dev/solutions/sell-license-keys" rel="noopener noreferrer"&gt;Explore every Store 2.0 capability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://permitcore.dev/docs/store" rel="noopener noreferrer"&gt;Read the Shop Designer tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://permitcore.dev/blog/permitcore-store-2-0" rel="noopener noreferrer"&gt;Read the full announcement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How are you currently connecting product discovery, checkout, and license delivery?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>saas</category>
      <category>webdev</category>
      <category>marketing</category>
    </item>
  </channel>
</rss>
