Integrating the Freemius JS SDK into your Application
Most SaaS applications have unique stacks and architectures. However, after analyzing common integration patterns, we have developed a straightforward guide that should work for the majority of applications.
In this integration guide, we will:
- Create a local database table to store Freemius purchase information (in an entitlement table).
- Demonstrate how to create Checkout Options in the backend to easily generate checkouts in the frontend.
- Process a purchase and store the relevant information in your local database.
- Synchronize license updates via webhooks.
Refer to the installation guide if you have not set up the SDK yet.
This guide does not assume any specific framework or architecture.
- We use TypeScript, but you can easily adapt the examples to JavaScript.
- We use a generic
db
object to represent database operations. Replace it with your actual database library or ORM, such as Prisma or Drizzle. We provide examples for popular ORMs. - We use a generic
fetch
-based API to illustrate how to create API endpoints. You can substitute this with your framework's routing and request handling mechanism, such as Express, Next.js, or Fastify.
Using Next.js? See our Next.js integration guide for a more tailored approach.
Creating the Entitlement Table
A license is the primary entitlement in Freemius. It represents a purchase made by a customer for a specific product, plan and pricing. A license may be associated with a subscription, which defines the billing cycle and renewal terms. For one-time purchases, the license will not be linked to any subscription.
We recommend creating a local user_fs_entitlement
table in your database to store relevant information about each license. Below is an example schema:
- SQL
- Prisma
- Drizzle
-- First create the enum type
CREATE TYPE fs_entitlement_type AS ENUM ('subscription', 'lifetime');
-- Then create the table
CREATE TABLE user_fs_entitlement (
id TEXT PRIMARY KEY,
"userId" TEXT NOT NULL,
"fsLicenseId" TEXT NOT NULL UNIQUE,
"fsPlanId" TEXT NOT NULL,
"fsPricingId" TEXT NOT NULL,
"fsUserId" TEXT NOT NULL,
type fs_entitlement_type NOT NULL,
expiration TIMESTAMP(3) WITHOUT TIME ZONE,
"isCanceled" BOOLEAN NOT NULL,
"createdAt" TIMESTAMP(3) WITHOUT TIME ZONE NOT NULL,
CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES "User"(id) ON DELETE CASCADE
);
-- Index on type for faster filtering
CREATE INDEX idx_user_fs_entitlement_type ON user_fs_entitlement (type);
// For the sake of this example, we are using a single active subscription license per user.
// But the schema can handle multiple licenses per user.
model UserFsEntitlement {
id String @id @default(cuid())
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
// The following matches the PurchaseDBData from Freemius Node SDK.
// You can also use these fields as BigInt if you prefer, and in that case you will need to convert the String to BigInt in your application.
fsLicenseId String @unique
fsPlanId String
fsPricingId String
fsUserId String
type FsEntitlementType
expiration DateTime?
isCanceled Boolean
createdAt DateTime
// Add the index to optimize queries filtering by type (as needed for the JS SDK).
@@index(type)
@@map("user_fs_entitlement")
}
import {
pgTable,
text,
boolean,
timestamp,
index,
pgEnum,
} from 'drizzle-orm/pg-core';
import { users } from './user'; // your User table
// Enum type
export const fsEntitlementType = pgEnum('fs_entitlement_type', [
'subscription',
'lifetime',
]);
// Table
export const userFsEntitlements = pgTable(
'user_fs_entitlement',
{
id: text('id').primaryKey(),
userId: text('userId')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
fsLicenseId: text('fsLicenseId').notNull().unique(),
fsPlanId: text('fsPlanId').notNull(),
fsPricingId: text('fsPricingId').notNull(),
fsUserId: text('fsUserId').notNull(),
type: fsEntitlementType('type').notNull(),
expiration: timestamp('expiration', { withTimezone: false, precision: 3 }),
isCanceled: boolean('isCanceled').notNull(),
createdAt: timestamp('createdAt', {
withTimezone: false,
precision: 3,
}).notNull(),
},
(table) => ({
typeIdx: index('idx_user_fs_entitlement_type').on(table.type),
})
);
Creating Checkouts
We recommend using the backend to create Checkouts. This lets you easily scope the Checkout to a specific user, pre-fill customer information, and apply discounts or trials.
const checkout = await freemius.checkout.create({
user: session?.user,
isSandbox: process.env.NODE_ENV !== 'production',
});
Here we assume you have a session
object that contains the authenticated user's information. The user
property should include at least the email
, and name
or firstName
and lastName
fields.
With the isSandbox
flag set to true
, the checkout will be created in sandbox mode, which is useful for testing purposes. In production, you should set it to false
. For more information please refer to the checkout documentation.
How you pass this information from the backend to the frontend depends on your framework. You will want to use the following method to generate the checkout options:
const options = checkout.getOptions();
const link = checkout.getLink();
We also have a handy serialize
method that combines both:
const { options, link } = checkout.serialize();
For the sake of this example, we will assume that the options
object is now available in the frontend as window.__FS_CHECKOUT_OPTIONS__
.
You can now use it to create a checkout in the frontend:
// Front-end code
import { Checkout } from '@freemius/checkout';
const checkout = new Checkout(window.__FS_CHECKOUT_OPTIONS__);
checkout.open({
success: (data) => {
console.log('Purchase completed:', data);
},
});
Please note that the example above uses the Freemius Checkout JS SDK, which is not to be confused with the Freemius JS SDK.
The Checkout SDK is specifically designed to handle the checkout process in the frontend, while the Freemius JS SDK is used for backend operations such as creating checkout options, managing licenses, and handling subscriptions.
Sending Purchase Data to the Backend
Next, we want to send purchase data back to the backend to store it in our local database. To do this, we will use the success
callback of the checkout.
checkout.open({
success: async (data) => {
console.log('Purchase completed:', data);
await fetch('/api/purchase', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
},
});
Now, on the backend, we can create an API endpoint to handle this request.
First, we will create a convenient function, processPurchaseInfo
, to process the purchase data and store it in our local database.
As a convention, let's create all licensing related functions in a file named user-entitlement.ts
.
import { type PurchaseInfo } from '@freemius/sdk';
import { freemius } from './freemius'; // our freemius instance
import { db } from './db'; // our database instance
export async function findUserByEmail(email: string) {
// Replace with your actual user lookup logic
return db.user.findUnique({ where: { email } });
}
export async function processPurchaseInfo(purchase: PurchaseInfo) {
const user = await findUserByEmail(purchase.email);
// We exit if the user hasn't registered to our application yet.
// Alternatively, you can register the user automatically here if desired.
if (!user) {
return;
}
// Insert or update the purchase in our local database
await db.userFsEntitlement.upsert({
where: {
fsLicenseId: fsPurchase.licenseId,
},
update: fsPurchase.toEntitlementRecord(),
create: fsPurchase.toEntitlementRecord({ userId: user.id }),
});
}
Finally, we can create the API endpoint to handle the purchase data.
import { freemius } from './lib/freemius'; // our freemius instance
import { db } from './lib/db'; // our database instance
import { processPurchaseInfo } from './lib/licensing';
export default {
async fetch(request: Request) {
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
const purchaseData = await request.json();
// Validate the purchase data
if (!purchaseData.purchase?.license_id) {
return new Response('Bad Request: Missing licenseId', { status: 400 });
}
try {
// Retrieve the full purchase details from Freemius
const licenseId = purchaseData.purchase.license_id;
const purchase = await freemius.purchase.retrievePurchase(licenseId);
await processPurchaseInfo(purchase);
return new Response('Purchase recorded', { status: 200 });
} catch (error) {
console.error('Error processing purchase:', error);
return new Response('Internal Server Error', { status: 500 });
}
},
};
The example above shows a serverless function that uses the Fetch API. You can adapt it to your framework's routing and request handling mechanism.
You can just implement the needed endpoints and use the pre-built UI components. See the Starter Kit API Endpoints guide for more details.
Using Entitlement Logic
Finally, we can create a function to check if a user has an active entitlement.
import { freemius } from './lib/freemius'; // our freemius instance
import { db, type UserFsEntitlement } from './lib/db'; // our database instance
// ... Existing code ...
/**
* Get the user's entitlement.
*
* @returns The user's active entitlement or null if the user does not have an active entitlement.
*/
export async function getUserEntitlement(
userId: string
): Promise<UserFsEntitlement | null> {
const entitlements = await db.userFsEntitlement.findMany({
where: { userId, type: 'subscription' },
});
return freemius.entitlement.getActive(entitlements);
}
The entitlement.getActive
method will check if the license is still valid, not expired, and not canceled. If the license is valid, it will return the license data; otherwise, it will return null
. You can read more about it in the purchase documentation.
Now you can use this function to check if a user has an active entitlement and grant or restrict access to your product accordingly. We recommend using the fsPricingId
field to determine the level of access, as it is unique for each pricing plan.
export function hasAccessToFeatureX(
entitlement: UserFsEntitlement | null
): boolean {
if (!entitlement) {
return false;
}
const requiredPricingId = 'your_required_pricing_id_here';
return entitlement.fsPricingId === requiredPricingId;
}
Handling License Updates via Webhooks
We need to handle license updates that may occur outside of our application, such as cancellations, renewals, or expirations. For this, we will use webhooks.
If you do not set up webhooks, your application will not be able to update the license's expiration date or cancellation status when they change in Freemius. This may lead to users losing access to your product even though they have an active subscription.
We will set up a webhook listener at /api/webhook
to process incoming webhook events from Freemius.
import { freemius } from './lib/freemius'; // our freemius instance
import { type LicenseEntity, type WebhookEventType } from '@freemius/sdk';
import { processPurchaseInfo } from './lib/licensing';
async function syncEntitlementFromWebhook(licenseId: string) {
const purchaseInfo = await freemius.purchase.retrievePurchase(licenseId);
if (purchaseInfo) {
await processPurchaseInfo(purchaseInfo);
}
}
export default {
async fetch(request: Request) {
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
const listener = freemius.webhook.createListener();
const licenseEvents: WebhookEventType[] = [
'license.created',
'license.extended',
'license.shortened',
'license.updated',
'license.cancelled',
'license.expired',
'license.plan.changed',
];
listener.on(licenseEvents, async ({ objects: { license } }) => {
if (license && license.id) {
await syncEntitlementFromWebhook(license.id);
}
});
return await freemius.webhook.processFetch(listener, request);
},
};
This will automatically synchronize the license information in your local database whenever a relevant event occurs in Freemius.
From the Freemius Developer Dashboard, you can also delete licenses, which will trigger the license.deleted
event. To listen for this event, you can add the following code to the licensing file and webhook listener:
// ... Existing code ...
export async function deleteEntitlement(fsLicenseId: string) {
await db.userFsEntitlement.delete({
where: { fsLicenseId },
});
}
And update the webhook listener:
// ... Existing code ...
export default {
async fetch(request: Request) {
// ...
const listener = freemius.webhook.createListener();
// ... Existing event listeners ...
listener.on('license.deleted', async ({ data} }) => {
await deleteEntitlement(data.license_id);
});
return await freemius.webhook.processFetch(listener, request);
},
};
Notice that the data structure for the license.deleted
event is different from the other events, so we need to handle it separately.
Finally, configure the webhook URL in the Freemius Developer Dashboard.
What's next?
You have now fully integrated Freemius into your SaaS application. You can explore different features of the SDK to create even richer experiences for your users.
If you are looking for pre-made UI components for Checkout, Paywall, Customer Portal, Pricing Table, and more, please take a look at our React Starter Kit.
The starter kit requires setting up two API endpoints, which you can implement using the examples provided in this guide.