How to sell code with Lemon Squeezy and Github

Sell and monetize your code by giving private access to your Github repositories using Lemon Squeezy

·7 min read
Cover Image for How to sell code with Lemon Squeezy and Github

I'm in the process of migrating from Gumroad to Lemon Squeezy, and as part of it, I'm also migrating the licensing system to be able to redeem an invite to the Makerkit's Github organization.

Since I'm not the only one who wants to do this, I thought I'd write a quick tutorial on how to do it.

In this tutorial we'll learn how to:

  1. Using the Lemon Squeezy API to validate and activate a license
  2. Using the Github Octokit library to invite a user to a Github organization
  3. Creating a Next.js endpoint to handle the request
  4. Using the redirect feature of Lemon Squeezy to include the license key in the redirect URL

The tweet below shows the final result:

Loading Tweet...

Why move from Gumroad to Lemon Squeezy?

I've been using Gumroad for a while now, and I'm happy with it. It's a great platform to sell digital products, and I've been using it for a while now.

Unfortunately, the fees hike to 13% is way too much to justify since I only use the checkout, and not the other features. Lemon Squeezy is a great alternative, which will lower the fees to to 5% (6.5% for international payments).

How we can use Lemon Squeezy to sell code

Makerkit offers access to the premium kits using private GitHub repositories. To allow customers to get an invite after checkout, we redirect customers to a page where they will enter their Github username. We verify that the customer has purchased the product, and has enough seats available, and then we send an invite to the Github organization.

At Makerkit we sell two plans:

  1. a solo-developer plan, which allows access to all the kits, but only for one developer
  2. a team plan, which allows access to all the kits, and allows to add up to 5 developers

The quantity of available activations is set in the Lemon Squeezy dashboard, and we can change it at any time. We will use this quantity to verify that the customer has enough activations available.

The Lemon Squeezy Licensing API

Lemon Squeezy provides a licensing API, which allows us to verify that a customer has purchased a product, and has enough activations available.

Let's write some quick functions that allows us to:

  1. Verify the customer has purchased the product
  2. Verify the customer has enough activations available
  3. Activate the license for a specific Github username

Adding the Lemon Squeezy API types

Below are the TypeScript types for the Lemon Squeezy API. We will use them to type the functions we will write.

export interface LicenseKey { valid: boolean; error: null; license_key: { id: number; status: string; key: string; activation_limit: number; activation_usage: number; created_at: string; expires_at: null; test_mode: boolean; }; instance: null; meta: { store_id: 9810; order_id: 373672; order_item_id: 373912; product_id: 42256; variant_id: 37011; }; } export interface ActivateLicenseResponse { activated: boolean; error: null; license_key: { id: number; status: number; key: string; activation_limit: number; activation_usage: number; created_at: string; expires_at: null | string; }; instance: { id: string; name: string; created_at: string; }; meta: { store_id: number; order_id: number; order_item_id: number; product_id: number; variant_id: number; }; }

The API functions to verify and activate the license

Now, we can use the types defined above to write the functions that will verify and activate the license.

We create two functions:

  1. getLicenseKey which will fetch the license key from Lemon Squeezy. We can use this to verify that the customer has purchased the product and has available activations.
  2. activateLicenseKey which will activate the license for a specific Github username. This will reduce the number of available activations by one.
import { ActivateLicenseResponse, LicenseKey } from '~/lib/ls/types'; const BASE_ENDPOINT = `https://api.lemonsqueezy.com`; const LICENSE_KEYS_ENDPOINT = [BASE_ENDPOINT, `v1/licenses`].join(`/`); const VALIDATE_LICENSE_KEY_ENDPOINT = [LICENSE_KEYS_ENDPOINT, `validate`].join('/'); const ACTIVATE_LICENSE_KEY_ENDPOINT = [LICENSE_KEYS_ENDPOINT, `activate`].join('/'); export async function getLicenseKey(params: { licenseKey: string; }): Promise<LicenseKey> { const { licenseKey } = params; const response = await fetch(VALIDATE_LICENSE_KEY_ENDPOINT, { method: `POST`, headers: getHeaders(), body: new URLSearchParams({ license_key: licenseKey, }).toString(), }); if (!response.ok) { throw response; } return (await response.json()) as LicenseKey; } export async function activateLicenseKey(params: { licenseKey: string; instanceName: string; }): Promise<ActivateLicenseResponse> { const { licenseKey, instanceName } = params; const response = await fetch(ACTIVATE_LICENSE_KEY_ENDPOINT, { method: `POST`, headers: getHeaders(), body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName, }).toString(), }); if (!response.ok) { throw response; } return (await response.json()) as ActivateLicenseResponse; } function getHeaders() { return { 'Content-Type': 'application/x-www-form-urlencoded', }; }

The Github API

We will use the Github API to invite the customer to the Github organization. To do so, we use the octokit library, which is a Javascript wrapper around the Github API.

Below we write a function inviteMemberToRepository that will invite the customer to the Github organization. It takes three parameters:

  1. the Github username owner of the repository
  2. the Github repository name
  3. the Github username to invite
import { Octokit } from 'octokit'; interface Params { owner: string; repo: string; username: string; } export function inviteMemberToRepository(params: Params) { const octokit = new Octokit({ auth: process.env.GITHUB_API_TOKEN, }); return octokit.request('PUT /repos/{owner}/{repo}/collaborators/{username}', { owner: params.owner, repo: params.repo, username: params.username, permission: 'pull', }); }

The Next.js API route

We will use a Next.js API route to handle the Github invitation. This is easily replicated in any other framework.

Storing the Variant ID in an environment variable

First, we need to store the variant ID of the product we want to sell in an environment variable. We can find the variant ID in the Lemon Squeezy dashboard.

We will use this variant ID to verify that the license key is valid.

LEMON_SQUEEZY_VARIANT_ID=12345

Creating the API route

Let's create an API handler at pages/api/licenses/activate.ts that will handle the Github invitation.

async function activateLicenseHandler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== `POST`) { res.status(405).json({ error: `Method not allowed` }); return; } const { licenseKey, githubUsername } = req.body; try { // validate key await validateLicenseKey(licenseKey); // invite user to github await inviteUserToRepositories(githubUsername); // activate license key const activation = await activateLicenseKey({ licenseKey, instanceName: githubUsername, }); if (activation.activated) { return res.json({ success: true, }); } return res.status(500).json({ success: false, error: `Could not activate license`, }); } catch (error) { console.error(error, `Error activating license`); return res.status((error as { status: number }).status).json(error); } }

Let's fill the blanks in the activateLicenseHandler function.

First, we validate the license key using the validateLicenseKey function we wrote above.

async function validateLicenseKey(licenseKey: string) { const response = await getLicenseKey({ licenseKey, }); if (!response || !response.valid) { return throwForbiddenException(`Invalid license key`); } if (response.meta.variant_id !== process.env.LEMON_SQUEEZY_VARIANT_ID) { return throwForbiddenException(`Invalid license key`); } const limit = response.license_key.activation_limit; const count = response.license_key.activation_usage; if (count >= limit) { throwForbiddenException(`License key has reached its activation limit`); } }

Then, we invite the customer to the Github organization using the inviteUserToRepositories function we wrote above.

async function inviteUserToRepositories(username: string) { const repositories = [ // add the list of repositories you want to invite the user to ] as string[]; // add the Github username of the owner of the repositories to your .env file const owner = process.env.GITHUB_OWNER; const requests = repositories.filter(Boolean).map((repository) => { return inviteMemberToRepository({ owner, repo: repository, username, }); }); return Promise.allSettled(requests); }

Now, you can send POST requests to your /api/licenses/activate endpoint with the following body:

{ "licenseKey": "your-license-key", "githubUsername": "your-github-username" }

Et voilà! You can now invite your customers to your Github organization and activate their Lemon Squeezy license keys.

You can use a client-side form to fill this information and make a POST request to the API route.

One little trick to prefill the license key from your users is to use a magic interpolation in your redirect route that you will be setting up in your Lemon Squeezy dashboard.

For example, assuming you redirect your customers to https://myapp.com/licenses/activate, you can append the following query parameter to include the license key as a query parameter:

license_key=[license_key]

For example, you can use the following redirect URL: http://localhost:3000/activate-license?action=license&license_key=[license_key]

Conclusion

In this tutorial, we saw how to use the Lemon Squeezy API to validate and activate license keys, and how to invite customers to your Github organization. This can be helpful if you're selling access to your private Github repositories and want to automate the process of inviting your customers to your Github organization.



Read more about Tutorials

Cover Image for Building an AI Writer SaaS with Next.js and Supabase

Building an AI Writer SaaS with Next.js and Supabase

·57 min read
Learn how to build an AI Writer SaaS with Next.js and Supabase - from writing SEO optimized blog posts to managing subscriptions and billing.
Cover Image for Announcing the Data Loader SDK for Supabase

Announcing the Data Loader SDK for Supabase

·8 min read
We're excited to announce the Data Loader SDK for Supabase. It's a declarative, type-safe set of utilities to load data into your Supabase database that you can use in your Next.js or Remix apps.
Cover Image for Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

·20 min read
In this tutorial, we will learn how to use add AI capabilities to your SaaS using Supabase Vector, HuggingFace models and Next.js Server Components.
Cover Image for Building an AI-powered Blog with Next.js and WordPress

Building an AI-powered Blog with Next.js and WordPress

·17 min read
Learn how to build a blog with Next.js 13 and WordPress and how to leverage AI to generate content.
Cover Image for Using Supabase Vault to store secrets

Using Supabase Vault to store secrets

·6 min read
Supabase Vault is a Postgres extension that allows you to store secrets in your database. This is a great way to store API keys, tokens, and other sensitive information. In this tutorial, we'll use Supabase Vault to store our API keys
Cover Image for Introduction to Next.js Server Actions

Introduction to Next.js Server Actions

·9 min read
Next.js Server Actions are a new feature introduced in Next.js 13 that allows you to run server code without having to create an API endpoint. In this article, we'll learn how to use them.