Monetizing Your Custom Apps: A Comprehensive Guide

As a Make.com app developer, you’ve put in the time and effort to create custom apps that solve real-world problems and streamline workflows. Now, it’s time to take your apps to the next level by implementing a monetization strategy.

In this comprehensive guide, I’ll walk you through the process of monetizing your custom Make apps using a powerful combination of technologies: Cloudflare Workers, Supabase, and Lemon Squeezy.

These tools will enable you to create a secure and scalable monetization system that seamlessly integrates with your Make apps.

I’ll cover everything from setting up the necessary infrastructure to implementing key features and integrating the various components.this guide will provide you with the knowledge and steps you need to successfully monetize your custom apps.

Why Cloudflare Workers and Supabase?

Cloudflare

  • Free tier: 100,000 requests/day, 10 ms CPU time/invocation

  • Scalability: Cloudflare Workers can handle a high volume of requests and automatically scale to meet demand.

  • Low Latency: By running your code at the edge, Cloudflare Workers reduces latency and improves performance.

Supabase

  • 500MB storage for the PostgreSQL database.
  • Unlimited API requests.
  • 5GB of bandwidth.

By leveraging Cloudflare Workers and Supabase, you can build a scalable and cost-effective monetization system for your Make apps.

Setting Up the Infrastructure

Supabase Setup

Supabase is an open-source alternative to Firebase that provides a scalable database and authentication services. To get started with Supabase, follow these steps:

  1. Create a Supabase account at https://supabase.com
  2. Once logged in, click on the "New Project" button to create a new project.
  3. Choose a unique name for your project and select a region for hosting.
  4. Wait for the project setup to complete.

Next, let’s create the “Order” table in your Supabase project. The “Order” table will store information about customer orders, including the product ID, email, and license key.

  1. Navigate to the “SQL Editor” section in your Supabase project dashboard.
  2. Click on the “New Query” button to create a new SQL query.
  3. Execute the following SQL statement to create the “Order” table:
CREATE TABLE "Order" (
  id SERIAL PRIMARY KEY,
  "productId" INTEGER NOT NULL,
  "licenseKey" TEXT NULL,
  email TEXT NOT NULL,
  "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

The “Order” table has the following columns:

  • id: The unique identifier for each order (primary key).
  • productId: The ID of the purchased product.
  • email: The email address of the customer.
  • licenseKey: The license key associated with the order.
  • createdAt: The timestamp indicating when the order was created

Cloudflare Workers Setup

Cloudflare Workers is a serverless computing platform that allows you to run code at the edge. It will be used to handle the user info endpoint and process Lemon Squeezy webhooks. In this guide, we’ll set up Cloudflare Workers using the Cloudflare CLI and write our worker code in TypeScript.

Prerequisites:

  • Node.js installed on your machine
  • Cloudflare account

Follow these steps to set up Cloudflare Workers:

1. Install the Cloudflare CLI by running the following command in your terminal:

npm install wrangler --save-dev

2. Log in to your Cloudflare account using the CLI:

npx wrangler login

This will open a browser window where you can log in to your Cloudflare account and grant the necessary permissions.

3. Create a new Cloudflare Workers project:

npx wrangler init monetization-worker

This command creates a new directory named monetization-worker and initializes a new worker project.

4. Navigate to the project directory:

cd monetization-worker
  1. Open the src/index.ts file and replace the default code with the code provided in the next section.

Note: After creating the monetization worker for handling the user info endpoint, you need to create another worker for handling the Lemon Squeezy webhooks. Follow the same steps as before to create a new worker, but make sure to give it a different name (e.g., lemon-squeezy-webhook-handler) and update the code accordingly.

Once you have created both workers, take note of their respective URLs. You will need to provide the URL of the user info worker in your Make app’s connection settings and the URL of the Lemon Squeezy webhook worker in the Lemon Squeezy webhook configuration.

Make sure to keep the worker URLs handy, as you will need them in the subsequent steps of the integration process.

  1. Configure the required environment variables for your Cloudflare workers using the Cloudflare CLI.

Run the following commands:

npx wrangler secret put SUPABASE_URL
npx wrangler secret put SUPABASE_ANON_KEY
npx wrangler secret put LEMON_SQUEEZY_SIGNING_SECRET

Each command will prompt you to enter the corresponding value for the environment variable.

Replace your_supabase_project_url, your_supabase_anon_key, and your_lemon_squeezy_signing_secret with the actual values.

  1. Deploy your worker to Cloudflare by running the following command:
 npx wrangler deploy

This command deploys your worker to Cloudflare and provides you with a unique URL where your worker is accessible.

That’s it! You have now set up Cloudflare Workers using the Cloudflare CLI and TypeScript.

Lemon Squeezy Setup

Lemon Squeezy is a payment processing platform that simplifies the process of selling digital products and managing subscriptions. It will be used to handle the payment flow and generate license keys for your custom apps. To set up Lemon Squeezy, follow these steps:

  1. Create a Lemon Squeezy account at https://lemonsqueezy.com/.
  2. Once logged in, click on the “Products” tab in the Lemon Squeezy dashboard.
  3. Click on the “Create Product” button to create a new product.

  1. Fill in the required details, such as the product name, price, and description.
  2. Customize the product settings according to your requirements.

Note: make sure to enable the option to generate license keys. This will allow Lemon Squeezy to automatically create license keys for each purchase, which will be stored in the Supabase database and used for license verification in your app.

Next, configure the webhook settings in Lemon Squeezy to send order and license key events to your Cloudflare worker.

  1. In the Lemon Squeezy dashboard, navigate to the “Settings” tab.
  2. Click on the “Webhooks” section.
  3. Enter the URL of your Cloudflare worker in the “Webhook URL” field.
  4. Generate a unique signing secret and copy it.
  5. Paste the signing secret into the **LEMON_SQUEEZY_SIGNING_SECRET** environment variable in your Cloudflare worker settings.

Implementing the User Info Endpoint

The user info endpoint is a critical component of the monetization system. It allows your Make app to verify the validity of a user’s license key and retrieve relevant information about the user’s order.

Here’s the Cloudflare worker index.ts code for the user info endpoint:

import { createClient, SupabaseClient } from '@supabase/supabase-js';

interface Env {
  SUPABASE_URL: string;
  SUPABASE_ANON_KEY: string;
}

interface UserInfoRequest {
  email: string;
  productId: number;
}

const createSupabaseClient = (env: Env): SupabaseClient => {
  return createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY);
};

const getUserInfo = async (request: Request, env: Env): Promise<Response> => {
  // Request validation
  if (request.method !== 'POST') {
    return new Response('Method not allowed', { status: 405 });
  }

  try {
    const { email, productId } = await request.json() as UserInfoRequest;
    const token = request.headers.get('Authorization')?.replace('Bearer ', '');

    if (!token) {
      return new Response(JSON.stringify({ error: 'Missing token' }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' },
      });
    }

    const supabase = createSupabaseClient(env);

    // Retrieve the order based on the license key and product ID
    const { data: orderData, error: orderError } = await supabase
      .from('Order')
      .select('*')
      .eq('licenseKey', token)
      .eq('productId', productId)
      .single();

    if (orderError) {
      if (orderError.code === 'PGRST116') {
        // Order not found for the given license key and product ID
        return new Response(JSON.stringify({ error: 'Invalid token for the given product ID' }), {
          status: 400,
          headers: { 'Content-Type': 'application/json' },
        });
      }

      // Handle other errors
      return new Response(JSON.stringify({ error: 'Error retrieving order' }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' },
      });
    }

    const order = orderData;

    // Check if the email matches the order's email
    if (order.email !== email) {
      return new Response(JSON.stringify({ error: 'Invalid email' }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' },
      });
    }

    // Return success response
    return new Response(JSON.stringify({ valid: true }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    });
  } catch (error) {
    return new Response(JSON.stringify({ error: 'Internal server error' }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' },
    });
  }
};

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    return getUserInfo(request, env);
  },
};

The user info endpoint performs the following steps:

  1. It validates the incoming request method to ensure it’s a POST request.
  2. It extracts the email and productId from the request payload.
  3. It retrieves the token (license key) from the Authorization header.
  4. It creates a Supabase client instance using the provided environment variables.
  5. It retrieves the order data from the Supabase “Order” table based on the provided licenseKey and productId.
  6. It validates the retrieved order data:
  • Checks if the order exists for the given licenseKey and productId. If not, returns an error response.
  • Checks if the provided email matches the order’s email. If not, returns an error response.
  1. If the order is valid, it returns a success response indicating that the license key is valid.

make sure to Configure the required environment variables .

Handling Lemon Squeezy Webhooks

Note: To handle Lemon Squeezy webhooks, you need to create another Cloudflare worker by following the same steps as creating the user info worker. Make sure to give it a different name, such as lemon-squeezy-webhook-handler, and update the code accordingly.

Lemon Squeezy sends webhooks to notify your application about important events, such as order creation and license key generation. The webhook handler in your Cloudflare worker is responsible for processing these events and updating the relevant data in your Supabase database.

Here’s the Cloudflare worker index.ts code for handling Lemon Squeezy webhooks:

import { createClient, SupabaseClient } from '@supabase/supabase-js';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import Hex from 'crypto-js/enc-hex';

interface Env {
  SUPABASE_URL: string;
  SUPABASE_ANON_KEY: string;
  LEMON_SQUEEZY_SIGNING_SECRET: string;
}

const createSupabaseClient = (env: Env): SupabaseClient => {
  return createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY);
};

const handleOrderCreated = async (data: any, env: Env) => {
  const supabase = createSupabaseClient(env);
  const { id, attributes } = data.data;
  const { first_order_item, user_email } = attributes;
  const { product_id } = first_order_item;

  // Insert the order data into the Supabase table
  const { error } = await supabase.from('Order').insert({
    id,
    productId: product_id,
    email: user_email,
  });

  if (error) {
    throw error;
  }
};

const handleLicenseKeyCreated = async (data: any, env: Env) => {
  const supabase = createSupabaseClient(env);
  const { attributes } = data.data;
  const { key, order_id } = attributes;

  // Update the order with the license key
  const { error } = await supabase
    .from('Order')
    .update({ licenseKey: key })
    .eq('id', order_id);

  if (error) {
    throw error;
  }
};

const verifySignature = (payload: string, signature: string | null, signingSecret: string): boolean => {
  if (!signature) {
    return false;
  }

  const expectedSignature = HmacSHA256(payload, signingSecret).toString(Hex);
  return signature === expectedSignature;
};

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }

    try {
      // Verify the webhook signature
      const signature = request.headers.get('X-Signature');
      const payload = await request.text();

      if (!verifySignature(payload, signature, env.LEMON_SQUEEZY_SIGNING_SECRET)) {
        return new Response('Unauthorized', { status: 401 });
      }

      const data = JSON.parse(payload);

      if (data.meta.event_name === 'order_created') {
        await handleOrderCreated(data, env);
      } else if (data.meta.event_name === 'license_key_created') {
        await handleLicenseKeyCreated(data, env);
      }

      return new Response('OK', { status: 200 });
    } catch (error) {
      return new Response('Internal Server Error', { status: 500 });
    }
  },
};

The webhook handler performs the following steps:

  1. It validates the incoming request method to ensure it’s a POST request.
  2. It verifies the webhook signature using the verifySignature function. If the signature is invalid, it returns an unauthorized response.
  3. It parses the webhook payload and determines the event type.
  4. Based on the event type, it calls the appropriate handler function:
  • For the order_created event, it extracts the relevant data from the payload and inserts the order data into the Supabase “Order” table.
  • For the license_key_created event, it extracts the license key and order ID from the payload and updates the corresponding order in the Supabase “Order” table with the license key.
  1. If the event is handled successfully, it returns a success response.

Integrating with Make.com

Setting Up the Connection

To integrate your monetization system with your app, you need to set up a connection. Follow these steps:

  1. Open your Make app and navigate to the “Connections” section.

  2. Click on the “Add Connection” button to create a new OAuth connection.

  3. Add the following code to the connection communication:

{
  "token": {
    "condition": "{{if(data.expires, data.expires < addMinutes(now, 1), true)}}",
    "url": "https://whoami.add-your-api.com",
    "method": "POST",
    "body": {
      "email": "{{parameters.email}}",
      "productId": "{{common.productId}}"
    },
    "headers": {
      "Authorization": "Bearer {{parameters.token}}",
      "Content-Type": "application/json"
    },
    "response": {
      "error": {
        "message": "[{{statusCode}}]: {{body.error}}"
      }
    }
  },
  "info": {
    "url": "https://add-yourapi.dev",
    "headers": {
      "Authorization": "Bearer {{parameters.apiKey}}"
    },
    "response": {
      "error": {
        "message": "[{{statusCode}}] {{body.error}}"
      },
      "metadata": {
        "type": "email",
        "value": "{{parameters.email}}"
      }
    },
    "log": {
      "sanitize": ["request.headers.Authorization"]
    }
  }
}
  1. Update the url fields in the connection communication with your actual API endpoints:
  • Replace https://whoami.add-your-api.com with the URL of your user info endpoint hosted on Cloudflare Workers.

  • Replace https://add-yourapi.dev with the URL of your API endpoint for retrieving additional user information.

  1. Update the common data with your product ID:
{
  "productId": "YOUR_PRODUCT_ID"
}

Note: You can find your product ID in the Lemon Squeezy dashboard. Navigate to your product’s settings and copy the product ID.

That’s it! You have now set up the connection in your app to integrate with your monetization system.

The connection communication consists of two main parts:

  • The token object handles the user info endpoint request. It sends a POST request to your Cloudflare worker with the user’s email and product ID, along with the license token in the Authorization header.

  • The info object handles retrieving additional user information from your API endpoint. It sends a request to your API with the user’s API key in the Authorization header.

The common data object allows you to store and access common data across your app. In this case, you’ll store your product ID obtained from Lemon Squeezy.

With the connection set up, your Make app can now communicate with your monetization system, verifying user licenses and retrieving user information based on the provided credentials

Configuring Mappable Parameters

Mappable parameters allow you to define input fields in your app that users can fill in when setting up the connection .

To configure mappable parameters, follow these steps:

In your Make app, navigate to the “Parameters” section.
Click on the “Add Parameter” button to create a new parameter. Define the following parameters:

Here’s how the parameters.jsonc file should look:

[
  {
    "name": "apiKey",
    "type": "text",
    "label": "API Key",
    "required": true,
    "help": "Go to example.com and get your API key"
  },
  {
    "name": "email",
    "type": "text",
    "label": "User Email",
    "required": true,
    "help": "The email address you used to sign up for the app"
  },
  {
    "name": "token",
    "type": "text",
    "label": "License Token",
    "required": true,
    "help": "The license key received in the email"
  }
]

Testing the Integration

To ensure that your monetization system is working correctly, it’s important to test the integration thoroughly. Follow these steps to create a test scenario:

Purchase a product on Lemon Squeezy:

Go to your Lemon Squeezy product page.

Complete the purchase process by providing the necessary information and making a test payment.

Receive the license key via email:

After a successful purchase, Lemon Squeezy will generate a license key and send it to the email address you provided.

Check your email inbox and locate the email containing the license key.

Use the license key in your Make app:

Open your Make app and navigate to the connection settings.

Enter the required information, including the API key, user email, and license token (license key).

Save the connection settings.

Verify the data flow:

Check the Supabase “Order” table:

Open your Supabase project and navigate to the “Order” table.

Verify that a new row has been created with the correct order information, including the product ID, email, and license key.

Check the Make.com app’s behavior:

Interact with your Make app and ensure that it behaves as expected based on the provided license key.

Verify that the app recognizes the license key as valid and allows access to the monetized features.

Common Errors

When integrating your monetization system with your app, you (end-user)may encounter some common errors. In this section, we’ll discuss these errors and provide troubleshooting steps to resolve them.

Invalid Token for the Given Product ID

If you encounter the error message “Invalid token for the given product ID,” it indicates that the provided license token is not valid for the specified product ID. Here are a few things to check:

  1. Make sure that the license token provided in the parameters.token field of the connection is correct and matches the token assigned to the user.

  2. Verify that the common.productId value in your Make app matches the actual product ID for which the license token was generated.

  3. Check the Supabase “Order” table to ensure that there is a valid order record with the corresponding licenseKey and productId.

Invalid Email

If you see the error message “Invalid email,” it means that the email provided in the parameters.email field of the connection does not match the email associated with the license token. To resolve this error:

  1. Ensure that the user is providing the correct email address associated with their license.
  2. Check the Supabase “Order” table to verify that the email in the order record matches the email provided in the connection.

Other Errors

If you encounter any other errors, such as “Missing token” or “Error retrieving order,” here are some general troubleshooting steps:

  1. Double-check that all the required fields in the connection (e.g., parameters.email, parameters.token, common.productId) are properly populated.

  2. Verify that the Cloudflare worker is deployed and accessible at the specified URL.

  3. Check the Cloudflare worker logs for any error messages or stack traces that can help identify the issue.

  4. Ensure that the Supabase connection details (URL and anon key) are correctly configured in the Cloudflare worker.

By following these best practices and prioritizing security, you can ensure that your monetization system is robust, reliable, and protected against potential threats.

Conclusion

Monetizing custom Make apps using Cloudflare, Supabase, and Lemon Squeezy provides a powerful and flexible solution for app developers. By leveraging these technologies, you can create a seamless monetization experience for your users while ensuring the security and scalability of your system.

If you need any assistance implementing this monetization system or have further questions, feel free to reach out to me at bilalmansouri.com.

6 Likes

Hi @Bilal_Mansouri,

Thank you for the elaborate and comprehensive explanation. It is clear that you put time, effort and thought into this, it is amazing.

I am trying to replicate the complete guide with Xano (for backend) and Mollie (a PSP from The Netherlands). I have built several Custom Apps for third party cloudsoftware.

What I don’t understand in the ‘Integrating with Make.com’ part (step 3) of the guide is; if I add that code to the connection communication, how is the communication with the third party’s API (Oauth2) taken care of?

Is this setup also meant for monetizing Custom Apps that need to connect with their own API?

Cheers,
Henk

Hi @Henk-Operative, Thank you for your kind words .

so if the API requires OAuth2 authentication instead of an API key, you will need to modify the connection communication code to include the necessary authorization and scope handling. However, keep in mind that if you want to handle the access control flow, you would need to submit your app in the apps marketplace instead of getting it published as a “verified” app, as “verified” apps do not permit access control flow.

The connection communication code consists of two main parts:

  1. The “token” object: This part handles the user info endpoint request. It sends a POST request to your Cloudflare worker (or in your case, the Xano backend) with the user’s email and product ID, along with the license token in the Authorization header. The purpose of this request is to verify the validity of the user’s license token and retrieve relevant user information from your monetization system.

  2. The “info” object: This part handles retrieving additional user information from 3rd party API endpoint. It sends a request to the API with the user’s API key in the Authorization header. This is where you can include any necessary logic to communicate with the third-party API using the authentication mechanism required by the third-party service.

Regarding your question about monetizing Custom Apps that need to connect with their own API, yes, this setup can be adapted for that purpose as well.

I hope this helps you,

Thank you for the explanation! It is very good to know about the submission of community apps. It makes sense to have the Token object handle the user info request for validation of the user’s license with the monetization system. The Info object can then be used for communication with the third party API.

This is straightforward if the third party API has basic authentication and only demands an API key that can be pulled from {{parameters.apiKey}}. But what if the third party API only accepts the OAuth2 Authorization Code Flow? In that case an authorization code must be requested, exchanged for tokens and kept alive with the refresh token. (this flow is normally handled with the authorize, token and refresh directives, of which ‘token’ is now used for the monetization request)

I don’t suppose that flow can be handled within the Info object?

Curious to hear your thoughts!

Cheers,
Henk

Hi @Henk-Operative ,

Thank you for your follow-up question! You raise an important point about handling the OAuth2 Authorization Code Flow for third-party APIs that require it.

To handle the OAuth2 Authorization Code Flow within the connection communication, you can utilize the preauthorize, authorize, token, and info directives as specified in the Make documentation. Here’s an example of how you can structure the communication:

{
  "preauthorize": {
    "condition": "{{if(data.expires, data.expires < addMinutes(now, 1), true)}}",
    "url": "https://whoami.add-your-api.com",
    "method": "POST",
    "body": {
      "email": "{{parameters.email}}",
      "productId": "{{common.productId}}"
    },
    "headers": {
      "Authorization": "Bearer {{parameters.token}}",
      "Content-Type": "application/json"
    },
    "response": {
      "error": {
        "message": "[{{statusCode}}]: {{body.error}}"
      }
    }
  },
  "authorize": {
    "url": "https://www.example.cloud/api/oauth2/authorize",
    "qs": {
      "scope": "openid",
      "client_id": "{{ifempty(parameters.clientId, common.clientId)}}",
      "redirect_uri": "{{ifempty(parameters.redirectUri, common.redirectUri)}}",
      "response_type": "code"
    },
    "response": {
      "temp": {
        "code": "{{query.code}}"
      }
    }
  },
  "token": {
    "url": "https://www.example.cloud/api/oauth2/token",
    "method": "POST",
    "body": {
      "code": "{{temp.code}}",
      "client_id": "{{ifempty(parameters.clientId, common.clientId)}}",
      "grant_type": "authorization_code",
      "redirect_uri": "{{ifempty(parameters.redirectUri, common.redirectUri)}}",
      "client_secret": "{{ifempty(parameters.clientSecret, common.clientSecret)}}"
    },
    "type": "urlencoded",
    "response": {
      "data": {
        "accessToken": "{{body.access_token}}"
      }
    },
    "log": {
      "sanitize": [
        "request.body.code",
        "request.body.client_secret",
        "response.body.access_token"
      ]
    }
  },
  "info": {
    "url": "https://www.example.cloud/api/oauth2/userinfo",
    "headers": {
      "authorization": "Bearer {{connection.accessToken}}"
    },
    "response": {
      "uid": "{{body.uid}}",
      "metadata": {
        "type": "email",
        "value": "{{body.metadata.value}}"
      }
    },
    "log": {
      "sanitize": ["request.headers.authorization"]
    }
  }
}

You need as well to add common data:

{
  "clientId": "*********************",
  "clientSecret": "*********************",
  "redirectUri": "https://www.make.com/oauth/cb/app",
  "productId": "******"
}

in this example:

The preauthorize directive is used to check the user’s license key against the monetization system. It sends a POST request to the specified URL with the user’s email and product ID, along with the license token in the Authorization header. This step for validating the user’s license before proceeding with the OAuth2 flow.

The authorize directive initiates the OAuth2 authorization request. It redirects the user to the third-party API’s authorization endpoint with the necessary query parameters, including the client ID and redirect URI.
After the user grants permission, the third-party API redirects back to the specified redirect URI with an authorization code.

The token directive exchanges the authorization code for an access token. It sends a POST request to the third-party API’s token endpoint with the authorization code, client ID, client secret, and other required parameters.

The info directive retrieves the authorized user’s information from the third-party API using the obtained access token. It sends a request to the specified user info endpoint with the access token in the Authorization header. This step is optional but commonly used to validate the connection and store account metadata.

By structuring the connection communication in this way, you can handle both the license key validation against the monetization system and the OAuth2 Authorization Code Flow for the third-party API.

The preauthorize directive takes care of the license key validation, while the authorize, token, and info directives handle the OAuth2 flow.

The initial OAuth2 flow can be described as:

preauthorize => authorize => token => info

I hope this answers your questions . Best of luck with your app development!

1 Like

This is amazing! Incredibly useful information for anyone who wants to monetize a Custom App.

Thank you so much for sharing.

Cheers,
Henk

3 Likes