Software License management with Polar.sh
This article would have been incredibly helpful to me just a month ago—so here it is, primarily for other developers who, like me, don't usually write software that's meant to run in non-server environments. I just introduced licenses to one of my apps—DevTUI. There's another app that suffers from my code, PoshTUI, and it might require something similar soon. I suspect all of this will come in handy again in the future. Requirements Starting out, I only knew what needed to be built: a licensing solution for a one-time payment product. A solution should support the following requirements: Ability to issue free licenses to early supporters of PoshTUI Ability to limit each license to three machines (or less) Allow customers to manage their licenses without a need to contact support(e.g. deactivate licenses or view receipts) Should work offline; license validation should not run every time the app starts Licensing Server I'm using Polar.sh as my payment and licensing provider. I'm skipping the part about how I ended up with them—no interest in badmouthing any competitors. Rolling your own solution on top of Stripe is also possible. But that wasn't an option worth pursuing for me. A 4% cut of future revenue seemed like a fair trade-off to avoid dealing with payment handling and license infrastructure. Polar team has been incredibly helpful. When I first contacted them, they didn’t even have a Go SDK, but they published one within two days. They also responded with useful tips and shipped fixes—again, all within 48 hours of my initial email. Polar.sh provides me with all the server side niceties. Offer a way to define a product that can be paid one time There is also Discounts for those early supporters and friends Customer portal Checkout links for a product, even one with a 100% discount But the client-side implementation is up to me. License Activation The app can do three license related actions - active, validate and deactivate. All these action are currently implemented in a CLI interface. Commands for activating, validating, and deactivating licenses Usage: devtui license [command] Available Commands: activate Activate a license deactivate Deactivate a license validate Validate a license Activate But I expect that most users will probably invoke only one command - activate. devtui license activate --key=DEVTUI-2CA57A34-E191-4290-A394-XXXXXX I'm intentionally not using any ENV variables — this would require an additional library, and I don’t want users polluting their .zshrc or .bashrc with extra variables. After activation, the key and related info are stored in a license.json file. During activation, it’s possible to provide "conditions". In our case, we use the MAC address to associate the license with a specific machine. The licensing server enforces a maximum of 3 machines per license. Validate This action uses the license.json file created during activation. While the CLI command simply checks that the license is active, this validation logic is also embedded directly in the app. Here's a rough outline of the validation logic: It's checking that hash sum matches expectations If it doesn't match -> We re-validate license with server If it does match -> We check if it's time to check license with a server. During a license check with server, if MAC address is not similar to one we used during activation - validation will fail. This would prevent people from just moving license.json file to another machine. Software attempts to validate license with a server every week, this time frame is completely arbitrary. Deactivate This also uses the license.json file created during activation. While it's already possible to deactivate a license via the customer portal, having it available in the CLI felt important. Two use cases come to mind: Running the app in CI environments where machines constantly get recycled Switching from one machine to another Identifying a Machine To my knowledge, most software uniquely identify a machine by its MAC address. It's important to try and find the physical, manufacturer-assigned MAC address, use it during license activation, and later rely on it during validation. However, not all MAC addresses are created equal. There are also Locally Administered Addresses (LAAs), these are not the same because they could be: Manually assigned by a network administrator Often used in virtual machines Common in network virtualization scenarios It's possible to identify those by checking the second least significant bit of the first octet: If the bit is 1, it's locally administered If it's 0, it's universally administered Examples: 02:00:00:00:00:00 → locally administered 00:1A:2B:3C:4D:5E → universally administered License File We use the github.com/adrg/xdg library to determine where to store the license file. This lib

This article would have been incredibly helpful to me just a month ago—so here it is, primarily for other developers who, like me, don't usually write software that's meant to run in non-server environments.
I just introduced licenses to one of my apps—DevTUI. There's another app that suffers from my code, PoshTUI, and it might require something similar soon. I suspect all of this will come in handy again in the future.
Requirements
Starting out, I only knew what needed to be built: a licensing solution for a one-time payment product. A solution should support the following requirements:
- Ability to issue free licenses to early supporters of PoshTUI
- Ability to limit each license to three machines (or less)
- Allow customers to manage their licenses without a need to contact support(e.g. deactivate licenses or view receipts)
- Should work offline; license validation should not run every time the app starts
Licensing Server
I'm using Polar.sh as my payment and licensing provider. I'm skipping the part about how I ended up with them—no interest in badmouthing any competitors.
Rolling your own solution on top of Stripe is also possible. But that wasn't an option worth pursuing for me. A 4% cut of future revenue seemed like a fair trade-off to avoid dealing with payment handling and license infrastructure.
Polar team has been incredibly helpful. When I first contacted them, they didn’t even have a Go SDK, but they published one within two days. They also responded with useful tips and shipped fixes—again, all within 48 hours of my initial email.
Polar.sh provides me with all the server side niceties.
- Offer a way to define a product that can be paid one time
- There is also Discounts for those early supporters and friends
- Customer portal
- Checkout links for a product, even one with a 100% discount
But the client-side implementation is up to me.
License Activation
The app can do three license related actions - active, validate and deactivate. All these action are currently implemented in a CLI interface.
Commands for activating, validating, and deactivating licenses
Usage:
devtui license [command]
Available Commands:
activate Activate a license
deactivate Deactivate a license
validate Validate a license
Activate
But I expect that most users will probably invoke only one command - activate
.
devtui license activate --key=DEVTUI-2CA57A34-E191-4290-A394-XXXXXX
I'm intentionally not using any ENV variables — this would require an additional library, and I don’t want users polluting their .zshrc
or .bashrc
with extra variables.
After activation, the key and related info are stored in a license.json
file.
During activation, it’s possible to provide "conditions". In our case, we use the MAC address to associate the license with a specific machine. The licensing server enforces a maximum of 3 machines per license.
Validate
This action uses the license.json
file created during activation. While the CLI command simply checks that the license is active, this validation logic is also embedded directly in the app.
Here's a rough outline of the validation logic:
- It's checking that hash sum matches expectations
- If it doesn't match -> We re-validate license with server
- If it does match -> We check if it's time to check license with a server.
- During a license check with server, if MAC address is not similar to one we used during activation - validation will fail. This would prevent people from just moving
license.json
file to another machine.
Software attempts to validate license with a server every week, this time frame is completely arbitrary.
Deactivate
This also uses the license.json
file created during activation.
While it's already possible to deactivate a license via the customer portal, having it available in the CLI felt important. Two use cases come to mind:
- Running the app in CI environments where machines constantly get recycled
- Switching from one machine to another
Identifying a Machine
To my knowledge, most software uniquely identify a machine by its MAC address. It's important to try and find the physical, manufacturer-assigned MAC address, use it during license activation, and later rely on it during validation.
However, not all MAC addresses are created equal. There are also Locally Administered Addresses (LAAs), these are not the same because they could be:
- Manually assigned by a network administrator
- Often used in virtual machines
- Common in network virtualization scenarios
It's possible to identify those by checking the second least significant bit of the first octet:
- If the bit is 1, it's locally administered
- If it's 0, it's universally administered
Examples:
- 02:00:00:00:00:00 → locally administered
- 00:1A:2B:3C:4D:5E → universally administered
License File
We use the github.com/adrg/xdg library to determine where to store the license file.
This library implements the XDG Base Directory and XDG User Directory specifications, offering a standard mechanism for storing application state, data, or configuration across multiple OSes—Windows, Linux, Plan 9, and macOS.
In the app’s case, we rely on XDG_DATA_HOME
, or fallbacks as per OS:
Unix | macOS | Plan 9 | Windows |
---|---|---|---|
~/.local/share | ~/Library/Application Support | $home/lib | LocalAppData |
Example 'license.json':
{
"hash": "7394991a704a054096f4484d8a19f9ac66e3e8c98b68603652feadc785a364f2",
"license_key_id": "DEVTUI-2CA57A34-E191-4290-A394-XXXXXX",
"activation_id": "2ee71107-2ecb-4172-aff7-ceaa6b2f7cef",
"next_check_time": "2025-05-12T23:18:12.7724912+02:00",
"last_verified_at": "2025-05-05T23:18:12.772488825+02:00"
}
The hash is generated from multiple values and is used to verify that the license data hasn't been tampered with. If the hash doesn't match, the app treats the license as invalid.
I won’t go into the full details of what goes into the hash, but in general:
- MAC address
- License key
- Activation time
- Next check timestamp
- A salt value (random string)
This mechanism isn’t hacker-proof, but the effort required to crack it should outweigh the $40 one-time price tag.
Final notes
This is my first time implementing client-side license logic for an app, so I’ve probably missed some edge cases. But if anyone has some feedback or recommendations, please reach out.