How to make a Nuxt module protected by a licensing system delivered by Lemonsqueezy?
Bloggrify is an open source project I recently developed. It's a static blog template, built on top of Nuxt and Nuxt-Content, allowing you to start a new, full-featured blog in just a few minutes, the time it takes to write npm run dev
.
Distribute a paid nuxt module
I quickly added a theme system that makes it easy to add your own layout, and came up with two themes:
And then I wondered what my options were for distributing this last theme. I looked at :
- themeforest: a marketplace for selling pieces of code
- buymeacoffee, a site I'm already using that may be similar to gumroad, utip etc...
- a private github repo with a private token mechanism to access it
- npm, which I don't think I need to describe, but which raises an important question: how do you monetize?
The first two solutions have the same problem. You have to manually upload an archive, put it online and repeat the operation for each new version. That's okay if you have to do it once or twice, but not very satisfying in the long term. Especially for the buyer, who doesn't benefit from any updates. On the other hand, monetization is included by default, it's meant to be.
The 3rd option is often used at the moment, for example by shipfa.st, which is a NextJs boilerplate for starting up a new SAAS application quickly.
If you buy Shipfa.st, you receive an invitation to a private repo. The main advantage is that you have access to GitHub issues. But you need to manage access to the repo automatically after purchasing a license. And you need to manage the cancellation of a license (refund), and therefore the deletion of the invitation. I haven't explored this possibility, but I imagine the author has automated this part of the process, otherwise it can be a real pain to manage.
For my part, I was inspired by the solution used by Nuxt UI Pro:
- The Nuxt UI Pro package is free to use in dev environment.
- Buying the pro package gives you a license key.
- This license key allows you to build your application for production.
I like the idea of being able to play with the product in dev, and having a key only for production. Even if, by design, that means you can tinker with the code to remove the protection.
Now let's look at the implementation.
Lemonsqueezy
I chose Lemonsqueezy on the advice of Sebastien Chopin, creator of Nuxt. I don't like advertising, but I'm obliged to talk about it, since it's their API that we'll be using next.
Think of Lemonsqueezy as a sort of Stripe (online payment solution). Except that it can also handle taxes, affiliation, recurring payments and, above all, license key creation.
And that's not bad at all, since it allows me to create a sales page that issues a license number. Then I just have to check that the number is valid (and active) when people use it.
And this is the part we're going to detail.
License key validation
Broadly speaking, validation looks like this:
- during build, retrieve the license from environment variables
- check the license with an API
- if license is ok, continue build
Use Nuxt hook to check license
Here, we'll take advantage of the Nuxt Module system and add a hook to build:before
that triggers validation.
export default defineNuxtModule({
setup (options, nuxt) {
const theme = pkg.theme || { env: 'BLOGGRIFY_EPOXIA_LICENSE', link: CHECKOUT_URL_LEMONSQUEEZY }
const key = process.env[theme.env] || ''
nuxt.hook('build:before', async () => {
await validateLicense({ key, theme, dir: nuxt.options.rootDir })
})
}
})
The validateLicense method looks like this:
export async function validateLicense (opts: { key: string, dir: string, theme: { env: string, link: string } }) {
try {
await ofetch('https://MY_CHECK_SERVICE/api/check', {
headers: {
Accept: 'application/json',
},
params: {
license_key: opts.key
}
})
} catch (error) {
// manage error and break build
}
}
You'll notice that we call https://MY_CHECK_SERVICE/api/check and not lemonsqueezy directly, because otherwise I'd have to share my lemonsqueezy API key with everyone, which doesn't seem like a great idea ^^.
So that means we need to deploy this API. Let's take a look at it together.
API key validation service
To deploy this service, I chose to use a Nuxt SSR application, deployed in edge, on Netlify.
If this sentence is incomprehensible, I'll say for simplicity's sake that it allows you to deploy a dynamic application (not just static pages), but without using a server. The application runs as close to the user as possible, just like on a CDN, but with the added benefit of being able to run on Node.js.
This Nuxt application is very simple, just a server/api/check.ts
file.
In this file, we declare a handler that listens on /api/check
and validates the license key.
This handler takes a license_key
parameter and calls the Lemonsqueezy API to check whether the key exists and is valid.
const MY_PRODUCT = id_of_the_product_on_lemonsqueezy
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const licenseKey = query?.license_key as string
if (!licenseKey) {
throw createError({
statusCode: 400,
statusMessage: 'license_key is required',
message: 'license_key is required',
stack: '',
})
}
const lemonsqueezyApiKey = process.env.LEMONSQUEEZY_API_KEY
if (!lemonsqueezyApiKey) {
throw createError({
statusCode: 400,
statusMessage: 'LEMONSQUEEZY_API_KEY is not set',
message: 'LEMONSQUEEZY_API_KEY is not set',
stack: '',
})
}
const response = await fetch(`https://api.lemonsqueezy.com/v1/licenses/validate?license_key=${licenseKey}`, {
method: 'POST',
headers: {
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json',
Authorization: `Bearer ${lemonsqueezyApiKey}`,
}
})
if (!response.ok) {
throw createError({
statusCode: response.status,
statusMessage: response.statusText,
message: 'Invalid license key',
stack: '',
})
}
const data = await response.json()
const isValid = data.valid
const productId = data.meta.product_id
if (!isValid) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid license key',
message: 'Invalid license key',
stack: '',
})
}
if (productId !== MY_PRODUCT) {
throw createError({
statusCode: 400,
statusMessage: 'This license is not valid for this product',
message: 'This license is not valid for this product',
stack: '',
})
}
return {
status: 'ok',
message: 'The license is valid',
}
})
And that's it. We now have a licensing system for our Nuxt module.
Don't hesitate to ask me questions in the comments, or let me know if you have any feedback on this implementation.