# Charging SaaS Tenants based on the number of users

> Let's extend Makerkit to charge Stripe customers based on the number of users in an organization

*Published: 2022-08-28*
*Canonical: https://makerkit.dev/blog/tutorials/per-seat-stripe-subscriptions*

---

Charging a tenant according to the number of users they add is a popular pricing scenario for SaaS.

For example, a SaaS could offer the following subscription plans:

1. A **basic plan** for $9, which allows only one user, and charges an additional 9$ for every 5 users invited
2. A **pro plan** for 29$ per user, which includes 5 users, and charges an additional 29$ for every 5 users invited

In this blog post, we extend [Makerkit](https://makerkit.dev/courses/nextjs-turbo/introduction) to implement per-seat billing with Stripe and Next.js.

## How to implement per-seat subscriptions with Stripe?

Stripe's subscriptions use a field called `quantity` which can be used in different ways. In our case, we can use this field to bill a customer's organization based on how many users they invite.

So, if we billed $9 for each new user, we could simply increment the `quantity` field by 1. Stripe also allows us to transform the `quantity` field: for example, assuming we wanted to allow organizations to have 5 users before we increment their billing price, we would use `transform_quantity[divide_by]=5`.

This means that for up to 5 users, the organization will be billed 9$. Instead, between 6 and 10 users, they will be billed $18, and so on.

To learn more about this, check out the [Stripe documentation](https://stripe.com/docs/billing/subscriptions/quantities).

You can also do the same using Stripe's Dashboard. In fact, we can use `Package pricing` as a pricing model, and set a price for *N* number of units:

<Image src="/assets/images/posts/stripe-create-package-subscription.webp" width="1124" height="1414" />

In the above, we're configuring our subscription to charge customers $10 for every five units: in our scenario, **a unit is a member added to the organization**.

## Extending Makerkit to support per-seat subscriptions

By default, the Makerkit SaaS boilerplate does not handle per-seat Subscriptions, so we're going to go deep into the Makerkit code to show you how you can implement it yourself.

The main parts we're going to change are:

1. Creating a price that supports pricing per unit (as seen above)
2. Setting the correct `quantity` field when creating a `subscription`
3. Increasing the `quantity` field of a `subscription` when **members are added**
4. Decreasing the `quantity` field of a `subscription` when **members are removed**

### Adding plan to Configuration

Makerkit has a very simple built-in plan selector generated automatically from the configuration's plans. To test the integration, I created a new plan with Stripe, and I replaced the existing ones using the `src/configuration.ts` file:

```tsx filename=src/configuration.ts
plans: [
  {
    name: 'Per Seat Subscription',
    description: '$10 for each 5 users',
    price: '$10/5 users',
    stripePriceId: 'price_**********',
  },
],
```

Of course, when you create your plan, remember to replace the `stripePriceId` property with the actual ID.

<Image src="/assets/images/posts/stripe-plan-selector-per-seat-subscription.webp" width="1174" height="562" />

When creating a Checkout Session, the user will see the below:

<Image src="/assets/images/posts/stripe-checkout-per-seat-subscription.webp" width="968" height="578" />

### Creating a Subscription

In the Next.js Makerkit boilerplate, a Stripe subscription is created at `src/lib/stripe/create-checkout.ts` when an organization completes a Stripe checkout session.

Below is a snippet that creates a Stripe checkout session for a `subscription`, with `quantity` set to `1`, as defined in Makerkit: we need to update it so that the `quantity` field reflects the number of users added to the organization.

```tsx filename=src/lib/stripe/create-checkout.ts
const mode: Stripe.Checkout.SessionCreateParams.Mode = 'subscription';

// get stripe instance
const stripe = await getStripeInstance();

const lineItem: Stripe.Checkout.SessionCreateParams.LineItem = {
  quantity: 1,
  price: params.priceId,
};

return stripe.checkout.sessions.create({
  mode,
  customer,
  line_items: [lineItem],
  success_url: successUrl,
  cancel_url: cancelUrl,
  client_reference_id: clientReferenceId,
});
```

First, we want to be able to retrieve the number of users in an organization. To do so, let's add a new function `getOrganizationMembers` to the file `src/lib/server/queries.ts`.

```tsx
export async function getOrganizationMembers(organizationId: string) {
  const ref = await getOrganizationById(organizationId);
  const data = ref?.data();

  if (!data) {
    throw new Error(`Organization with ID ${organizationId} not found`);
  }

  return data.members;
}
```

Going back to `create-checkout.ts`, let's update the quantity with the number of users an organization has:

```tsx filename=src/lib/stripe/create-checkout.ts
const members = await getOrganizationMembers();
const numberOfMembers = Object.values(members).length;

const lineItem: Stripe.Checkout.SessionCreateParams.LineItem = {
  quantity: numberOfMembers,
  price: params.priceId,
};
```

Above, we retrieve the current number of members belonging to the organization and assign it as `quantity`.

### Updating quantity when a member is added

Whenever a user is added to the organization, we want to increment the `quantity` field of the subscription, so that Stripe will be able to automatically set the correct billing amount charged to the customer.

This can be done in two places:
- when the invitation is sent
- when the invitation is accepted

In this case, we'll do it when the invitation is accepted. Therefore, we will update the `POST` handler of the `organizations/members` endpoint.

```tsx filename=/src/lib/server/organizations/memberships.ts
async function increaseSubscriptionQuantity(
  subscriptionId: string
) {
  const stripe = await getStripeInstance();
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);

  // As we add only 1 line item during the subscription creation
  // we can safely select the first in the array
  //
  // NB: if you customize the line items, you have to find a different way
  // to select the correct line item!
  const lineItem = subscription.items.data[0];

  if (!lineItem) {
    throw new Error(
      `Subscription with ID ${subscriptionId} has no line items?`
    );
  }

  const currentQuantity = lineItem.quantity ?? 0;
  const quantity = currentQuantity + 1;

  return stripe.subscriptionItems.update(lineItem.id, {
    quantity,
  });
}
```

Now, we need to call the function defined above in the `members.ts` API controller:

```tsx filename=members.ts
if (method === 'POST') {
  const { code } = getBodySchema().parse(req.body);

  // here we retrieve the subscription ID
  const organization = await getOrganizationById(organizationId);
  const subscriptionId = organization.data()?.subscription?.id;

  // we require a subscription ID to invite a user!
  if (!subscriptionId) {
    return forbiddenException(res);
  }

  // we accept the invitation
  await acceptInviteToOrganization({ code, userId });

  // once successful, we increase the quantity of the line item
  await increaseSubscriptionQuantity(subscriptionId);
}
```

<Alert type={'info'}>
  As you can see above, we require a subscription to have an user accept an
  invite. This is sort of normal, since we need to update the quantity. This
  means you may need to lock invites to paying organizations (or other
  strategies to make sure the above works).
</Alert>

When the number of users exceeds 5, the subscription will automatically
increase the monthly amount to $20:

<Image src='/assets/images/posts/quantity-subscription-20.webp'
       width={"2028"}
       height={"548"} />

### Updating quantity when a member is removed

Instead, when a user is removed from the organization, we want to decrease the `quantity` field. To do so, simply define nearly the same function as `increaseSubscriptionQuantity`, but with a clear difference: instead of summing the quantity, we will subtract it by 1.

```tsx
const currentQuantity = lineItem.quantity ?? 0;
const quantity = currentQuantity - 1;
```

Otherwise, **the body of the function will be identical**.

Now, we want to open the file `pages/api/organizations/[id]/members/[member].ts`. Here, we can see a branch where we handle a `DELETE` request, which we use to remove a member from an organization. When this happens, we can update the subscription:

```tsx filename=pages/api/organizations/[id]/members/[member].ts
if (method === 'DELETE') {
  const organizationId = payload.organizationId;

  // retrieve subscription ID
  const organization = await getOrganizationById(organizationId);
  const subscriptionId = organization.data()?.subscription?.id;

  // remove member from organization
  await removeMemberFromOrganization(payload);

  if (subscriptionId) {
    // once successful, we decrease the quantity of the line item
    await decreaseSubscriptionQuantity(subscriptionId);
  } else {
    logger.warn(payload, `Subscription for organization ${organizationId} was not found`);
  }

  logger.info(payload, `User removed from organization`);

  return res.send({ success: true });
}
```

Assuming the number of users in the organization is now 6, the subscription
amount will go back to $10:

<Image src='/assets/images/posts/quantity-subscription-10.webp'
       width={"2084"}
       height={"614"} />

🎉 And that's it! We have now implemented per-seat billing with Stripe and our Next.js API.

## Previewing Invoices

It can be useful to preview the next payment to your customers after (or before) they add a new member, therefore increasing their spending.

To do so, we can use Stripe's `retrieveUpcoming` method to preview the next invoice given a list of subscription items.

```tsx
function previewNextSubscriptionInvoice(
  params: {
    customerId: string;
    subscriptionId: string;
  }
) {
  const proration_date = Math.floor(Date.now() / 1000);

  const subscription =
    await stripe.subscriptions.retrieve(params.subscriptionId);

  // See what the next invoice would look like with a price switch
  // and proration set:
  const item = subscription.items.data[0];

  const items = [{
    id: item.id,
    price: item.price,
    // use "item.quantity - 1" if the member is removed
    quantity: item.quantity + 1,
  }];

  return stripe.invoices.retrieveUpcoming({
    customer: params.customerId,
    subscription: params.subscriptionId,
    subscription_items: items,
    subscription_proration_date: proration_date,
  });
}
```

The [invoice object](https://stripe.com/docs/api/invoices/object) is rich in information that you can display, but the most important may be `subtotal`, which you could display to customers to be transparent about what they will pay.

## Managing Unsubscriptions

When an organization unsubscribes, or lowers to a more restricted plan, you should think about how to handle the fact that the organization may have invited more users than your plan allows.

First, we need to listen to an unsubscription webhook: we actually already
do. If you visit `pages/api/stripe/webhook.ts`, check out the following branch:

```tsx filename=pages/api/stripe/webhook.ts
case StripeWebhooks.SubscriptionUpdated: {
  const subscription = event.data.object as Stripe.Subscription;

  await onSubscriptionUpdated(subscription);

  // YOUR LOGIC GOES HERE

  break;
}
```

Here, we can write the logic to handle what happens when an organization
unsubscribes from a plan. What happens depends on you and how you prefer to
handle it.

## Summary

In this blog post, we have extended the Next.js SaaS Boilerplate so that our SaaS can charge customers based on the number of users they invite to their projects.

To summarize what we have done:

1. We have created a price using the Stripe Dashboard that allows us to charge customers using `Package pricing`
2. When the subscription gets created, we set the `quantity` field based on the current number of members in the organization. In this case, we're assuming the organization starts without a subscription (as is by default).
3. When a member is added to the organization, we **increase** the `quantity` field
4. When a member is removed from the organization, we **decrease** the `quantity` field

Unfortunately, there is more to do to finalize the logic above, and it
really depends on your preferences.

### Final Considerations

To finalize and improve the logic explained above, you have to make a few
choices.

#### Restrict invitations

Should you restrict invites to organizations
with a valid subscription? Probably. Alternatively, when accepting an
invitation, we can check if the organization is within a free quota (ex. 5
users)

#### What happens when an organization unsubscribes?

You may not be able to prevent a cancellation from the Stripe Portal.

Therefore, you may need a way to gate organizations from using more
resources than the plan allows. One of the ways could be to add some logic
to `withAppProps` and check if the customer is on a plan: if not, force
redirect to the subscription page.

Looking to build an AI-powered SaaS with integrated billing? Check out the [AI SaaS Starter Kit](/ai-saas-starter) for a complete foundation.
