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.

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`);
  }

  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.


Stay informed with our latest resources for building a SaaS

Subscribe to our newsletter to receive updatesor

Read more about

Cover Image for Writing clean React

Writing clean React

·9 min read
Learn how to write clean React code using Typescript with this guide.
Cover Image for How to use MeiliSearch with React

How to use MeiliSearch with React

·12 min read
Learn how to use MeiliSearch in your React application with this guide. We will use Meiliseach to add a search engine for our blog posts
Cover Image for Setting environment variables in Remix

Setting environment variables in Remix

·3 min read
Learn how to set environment variables in Remix and how to ensure that they are available in the client-side code.
Cover Image for Programmatic Authentication with Supabase and Cypress

Programmatic Authentication with Supabase and Cypress

·3 min read
Testing code that requires users to be signed in can be tricky. In this post, we show you how to sign in programmatically with Supabase Authentication to improve the speed of your Cypress tests and increase their reliability.
Cover Image for Reset the Supabase Database in Cypress

Reset the Supabase Database in Cypress

·4 min read
Resetting your database during E2E tests is important to prevent flakiness. In this tutorial, we'll show you how to reset the Supabase database in Cypress E2E tests.
Cover Image for Authenticating users with Remix and Supabase

Authenticating users with Remix and Supabase

·16 min read
Learn how to use Remix and Supabase to authenticate users in your application.