Recently I needed to license a non-web digital product, lighthouse. Licensing logic is easy in theory but in practice it requires a lot of UI work, integrations to be able to support an active customer base, and some degree of expertise in cryptography to be able to produce secure keys. Writing one from scratch in less than a day felt like a considerable feat once I'd identified all the basic pieces I needed to work through to get one working. I decided to shop around instead to try to find a team that could help me for a reasonable price. I wound up choosing whop.
What you need
To license an app, you essentially need a cloud service that will at least produce and validate license keys for you. To make it full-service though, you also want to be able to regenerate license keys on-demand, expire license keys, and restore license keys. All of this logic needs to be tied to a billing service regardless of whether the charge to maintain the license is recurring or not because the license key also needs to hold metadata for cases where you may want to limit the versions a particular license key has access to.
How it works
Whop has an api that is very straight-forward to follow. To make things work, you essentially need to create a product through their website and attach a License Key to it. This will make it so that anyone that purchases a license can also manage it through whop. Once you've created that, you'll want to create a backend service and an integration from your app to your backend to securely validate licenses.
An optimal low cost back-end
The best path towards building a scalable low-cost back-end is as of this writing a Lambda. Whether you're using Azure, AWS, or GCP, there's no need to pay the monthly cost of a server when you can just as easily execute validations at scale for $0 upfront. There are many ways to do so from Vercel, to CDK/Pulumi(my favorite IaC), to just dropping into their UI's and writing it in their code-editors.
For smaller projects, I like to pack everything up into Vercel to leverage all their lambda optimizations and host the marketing site as well. I opted to create a NextJS application and attach Apollo GraphQL to it.
All this server is doing is exposing a validation function:
const resolvers = { Mutation: { async validateLicense(root, props, context) { const { license, machineId } = props.form; const { whop } = context; // NOTE: Passes if unused, fails if reused, passes if reset via whop.com await whop.post(`/memberships/${license}/validate_license`, { metadata: { machineId }, // For debugging }); return true; }, }, }
Notice the server requires whop
in its context which is easily created by leveraging axios
:
// whop.js import axios from 'axios'; export default axios.create({ baseURL: 'https://api.whop.com/api/v2', headers: { Authorization: 'Bearer <your-very-secret-token>', }, });
The integration with your app
For the app, there are a million ways to integrate depending on how you built it. When I build electron apps, I like to write them in ReactJS and leverage common tooling like CRA, React Router, and TanStack Query. With this it's pretty quick and easy to write application logic to allow and deny different parts of the application based on any of the attributes of the license. I decided to keep it simple and simply allow a trial then disallow access after the trial.
The logic looks like this:
// LicensePage.js export default function LicensePage() { // ...otherCode const validateLicense = useMutation( async () => { const license = _license.current.value; if (isEmpty(license)) { notify({ headline: 'License is required!', type: 'error' }); return; } const machineId = await window.electron.machineId(); const isValid = await request( ` mutation validateLicense($form: validateLicenseForm!) { validateLicense(form: $form) } `, { form: { machineId, license }, }, ) .then(() => true) .catch(() => false); if (!isValid) throw new Error('Invalid License!'); secureLicenseKeyStore({ license, lastChecked: Date.now() }); return isValid; }, { onMutate() { notify({ headline: 'Validating License', type: 'loading', }); }, onSuccess(newIdx) { notify({ headline: 'License is Valid!' }); navigate('/'); }, onError(error) { notify({ headline: 'License validation failed!', description: error.response?.data?.error?.message || error?.description || error?.message, type: 'error', }); }, }, ); // ...otherCode }
Important to notice that I am also storing a lastChecked
variable. It's also important to have your application check for the validity of the installed license on a regular basis in case the user fails to make payment or expires their license for whatever reason. You can essentially build the same logic into a separate component that will trigger as soon as the application opens and once every few hours or minutes. If the application is unable to reach the internet, the application should be lenient and provide some amount of time before expiring itself.
Whop is Awesome
I really enjoyed integrating with the Whop API and couldn't recommend them enough for application licensing. The additional tooling for exposing your app to their marketplace users is icing on the cake.