feat: whyrating - initial project from turbostarter boilerplate
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
---
|
||||
title: Configuration
|
||||
description: Configure billing for your application.
|
||||
url: /docs/web/billing/configuration
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
The billing configuration schema replicates your billing provider's schema, so that:
|
||||
|
||||
* we can display the data in the UI (pricing table, billing section, etc.)
|
||||
* create the correct checkout session
|
||||
* make some features work correctly - such as feature-based access
|
||||
|
||||
It is common to all billing providers and placed in `packages/billing/src/config/index.ts`. Some billing providers have some differences in what you can or cannot do. In these cases, the schema will try to validate and enforce the rules - but it's up to you to make sure the data is correct.
|
||||
|
||||
The schema is based on few entities:
|
||||
|
||||
* **Plans:** The main product you are selling (e.g., "Pro Plan", "Starter Plan", etc.)
|
||||
* **Prices:** The pricing plan for the product (e.g., "Monthly", "Yearly", etc.)
|
||||
* **Discounts:** The discount for the price (e.g., "10% off", "20% off", etc.)
|
||||
|
||||
```ts title="index.ts"
|
||||
type BillingConfig = {
|
||||
plans: PlanWithPrices[];
|
||||
discounts: Discount[];
|
||||
};
|
||||
```
|
||||
|
||||
<Callout title="Getting the schema right is important!" type="error">
|
||||
Getting the IDs of your plans is **extremely important** - as these are used to:
|
||||
|
||||
* create the correct checkout
|
||||
* manage your customers billing data
|
||||
|
||||
Please take it easy while you configure this, do one step at a time, and test it thoroughly.
|
||||
</Callout>
|
||||
|
||||
## Billing provider
|
||||
|
||||
To set the billing provider, you need to modify the exports in the `packages/billing/src/providers` directory. It defaults to [Stripe](/docs/web/billing/stripe).
|
||||
|
||||
<Tabs items={["index.ts", "env.ts"]}>
|
||||
<Tab value="index.ts">
|
||||
```ts
|
||||
// [!code word:stripe]
|
||||
export * from "./stripe";
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="env.ts">
|
||||
```ts
|
||||
// [!code word:stripe]
|
||||
export * from "./stripe/env";
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
It's important to set it correctly, as this is used to determine the correct API calls and environment variables used during the communication with the billing provider.
|
||||
|
||||
## Billing model
|
||||
|
||||
To set the billing model, you need to modify the `BILLING_MODEL` environment variable. It defaults to `recurring` as it's the most common model for SaaS apps.
|
||||
|
||||
```dotenv
|
||||
BILLING_MODEL="recurring"
|
||||
```
|
||||
|
||||
This field will be used to display corresponding data in the UI (e.g. in pricing tables) and to create the correct checkout session.
|
||||
|
||||
<Callout title="Available billing models">
|
||||
For now, TurboStarter supports two billing models:
|
||||
|
||||
* `recurring` - for subscription-based models
|
||||
* `one-time` - for one-time payments
|
||||
|
||||
When changing it, make sure to also update corresponding data on the provider side to match it with the correct billing model.
|
||||
</Callout>
|
||||
|
||||
## Plans
|
||||
|
||||
Plans are the main products you are selling. They are defined by the following fields:
|
||||
|
||||
```ts title="index.ts"
|
||||
export const config = billingConfigSchema.parse({
|
||||
...
|
||||
plans: [
|
||||
{
|
||||
id: PricingPlanType.PREMIUM,
|
||||
name: "Premium",
|
||||
description: "Become a power user and gain benefits",
|
||||
badge: "Bestseller",
|
||||
prices: [],
|
||||
},
|
||||
],
|
||||
...
|
||||
}) satisfies BillingConfig;
|
||||
```
|
||||
|
||||
Let's break down the fields:
|
||||
|
||||
* `id`: The unique identifier for the plan (e.g., `free`, `pro`, `enterprise`, etc.). **This is chosen by you, it doesn't need to be the same one as the one in the provider.** It's also used to determine the access level of the plan.
|
||||
* `name`: The name of the plan
|
||||
* `description`: The description of the plan
|
||||
* `badge`: A badge to display on the product (e.g., "Bestseller", "Popular", etc.)
|
||||
|
||||
The majority of these fields are going to populate the pricing table in the UI.
|
||||
|
||||
### Prices
|
||||
|
||||
Prices are the pricing plans for the plan. They are defined by the following fields:
|
||||
|
||||
```ts title="index.ts"
|
||||
export const config = billingConfigSchema.parse({
|
||||
...
|
||||
plans: [
|
||||
{
|
||||
id: PricingPlanType.PREMIUM,
|
||||
name: "Premium",
|
||||
description: "Become a power user and gain benefits",
|
||||
badge: "Bestseller",
|
||||
prices: [
|
||||
{
|
||||
/* 👇 This is the `priceId` from the provider (e.g. Stripe), `variantId` (e.g. Lemon Squeezy) or `productId` (e.g. Polar) */
|
||||
id: "price_1PpZAAFQH4McJDTlig6Fxsyy",
|
||||
amount: 1900,
|
||||
currency: "usd",
|
||||
interval: RecurringInterval.MONTH,
|
||||
trialDays: 7,
|
||||
type: BillingModel.RECURRING,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
...
|
||||
}) satisfies BillingConfig;
|
||||
```
|
||||
|
||||
Let's break down the fields:
|
||||
|
||||
* `id`: The unique identifier for the price. **This must match the price ID in the billing provider**
|
||||
* `amount`: The amount of the price (displayed values will be divided by 100)
|
||||
* `currency`: The currency of the price (only currencies from the [current locale](/docs/web/internationalization/overview) will be displayed - defaults to `usd`)
|
||||
|
||||
<Callout title="Set the correct currency on your billing provider">
|
||||
Make sure to have the same currency set on your third-party billing provider (e.g. as a [store currency](https://docs.lemonsqueezy.com/help/payments/currencies) on Lemon Squeezy)
|
||||
</Callout>
|
||||
|
||||
* `interval`: The interval of the price (e.g., `month`, `year`, etc.)
|
||||
* `trialDays`: The number of trial days for the price
|
||||
* `type`: The type of the price (e.g., `recurring`, `one-time`, etc.)
|
||||
|
||||
The amount is set for UI purposes. The billing provider will handle the actual billing - therefore, please make sure the amount is correctly set in the billing provider.
|
||||
|
||||
<Callout title="Set the correct price ID!" type="error">
|
||||
Make sure to set the correct price ID that corresponds to the price in the billing provider. This is very important - as this is used to identify the correct price when creating a checkout session.
|
||||
</Callout>
|
||||
|
||||
### One-off payments
|
||||
|
||||
One-off payments are a type of price that is used to create a checkout session for a one-time payment. They are defined by the following fields:
|
||||
|
||||
```ts title="index.ts"
|
||||
export const config = billingConfigSchema.parse({
|
||||
...
|
||||
plans: [
|
||||
{
|
||||
id: PricingPlanType.PREMIUM,
|
||||
name: "Premium",
|
||||
description: "Become a power user and gain benefits",
|
||||
badge: "Bestseller",
|
||||
prices: [
|
||||
{
|
||||
/* 👇 This is the `priceId` from the provider (e.g. Stripe), `variantId` (e.g. Lemon Squeezy) or `productId` (e.g. Polar) */
|
||||
id: "price_1PpUagFQH4McJDTlHCzOmyT6",
|
||||
amount: 29900,
|
||||
currency: "usd",
|
||||
type: BillingModel.ONE_TIME,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
...
|
||||
}) satisfies BillingConfig;
|
||||
```
|
||||
|
||||
Let's break down the fields:
|
||||
|
||||
* `id`: The unique identifier for the price. **This must match the price ID in the billing provider**
|
||||
* `amount`: The amount of the price (displayed values will be divided by 100)
|
||||
* `currency`: The currency of the price (only currencies from the [current locale](/docs/web/internationalization/overview) will be displayed - defaults to `usd`)
|
||||
* `type`: The type of the price (e.g. `recurring`, `one-time`, etc.). In this case it's `one-time` as it's a one-off payment.
|
||||
|
||||
Please remember that the cost is set for UI purposes. **The billing provider will handle the actual billing - therefore, please make sure the cost is correctly set in the billing provider.**
|
||||
|
||||
### Custom prices
|
||||
|
||||
Sometimes - you want to display a price in the pricing table - but not actually have it in the billing provider. This is common for custom plans, free plans that don't require the billing provider subscription, or plans that are not yet available.
|
||||
|
||||
To do so, let's add the `custom` flag to the price:
|
||||
|
||||
```ts title="index.ts"
|
||||
{
|
||||
id: "enterprise-monthly",
|
||||
label: "Contact us!",
|
||||
href: "/contact",
|
||||
interval: RecurringInterval.MONTH,
|
||||
custom: true,
|
||||
type: BillingModel.RECURRING,
|
||||
}
|
||||
```
|
||||
|
||||
Here's the full example:
|
||||
|
||||
```ts title="index.ts"
|
||||
export const config = billingConfigSchema.parse({
|
||||
...
|
||||
plans: [
|
||||
{
|
||||
id: PricingPlanType.PREMIUM,
|
||||
name: "Premium",
|
||||
description: "Become a power user and gain benefits",
|
||||
badge: "Bestseller",
|
||||
prices: [
|
||||
{
|
||||
id: "premium-monthly",
|
||||
label: "Contact us!",
|
||||
href: "/contact",
|
||||
interval: RecurringInterval.MONTH,
|
||||
custom: true,
|
||||
type: BillingModel.RECURRING,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
...
|
||||
}) satisfies BillingConfig;
|
||||
```
|
||||
|
||||
As you can see, the plan is now a custom plan. The UI will display the plan in the pricing table, but it won't be available for purchase.
|
||||
|
||||
We do this by adding the following fields:
|
||||
|
||||
* `custom`: A flag to indicate that the plan is custom. This will prevent the plan from being available for purchase. It's set to `false` by default.
|
||||
* `label`: This is used to display the label in the pricing table instead of the price.
|
||||
* `href`: The link to the page where the user can contact you. This is used in the pricing table.
|
||||
|
||||
<Callout title="Translations supported!">
|
||||
All labels and descriptions can be translated using the [internationalization](/docs/web/internationalization/overview) feature. The UI will display the correct translation based on the user's locale.
|
||||
|
||||
```ts title="index.ts"
|
||||
label: "common:contactUs",
|
||||
```
|
||||
|
||||
To make strings translatable, make sure to provide the translation key in the config.
|
||||
</Callout>
|
||||
|
||||
### Discounts
|
||||
|
||||
Sometimes, you want to offer a discount to your users. This is done by adding a discount to the price in `discounts` field.
|
||||
|
||||
```ts title="index.ts"
|
||||
export const config = billingConfigSchema.parse({
|
||||
...
|
||||
discounts: [
|
||||
{
|
||||
code: "50OFF",
|
||||
type: BillingDiscountType.PERCENT,
|
||||
off: 50,
|
||||
appliesTo: [
|
||||
"price_1PpUagFQH4McJDTlHwsCzOmyT6",
|
||||
],
|
||||
},
|
||||
],
|
||||
...
|
||||
}) satisfies BillingConfig;
|
||||
```
|
||||
|
||||
Let's break down the fields:
|
||||
|
||||
* `code`: The code of the discount (e.g., "50OFF", "10% off", etc.) **This must match the code configured in the billing provider**
|
||||
* `type`: The type of the discount (e.g., `percent`, `amount`, etc.)
|
||||
* `off`: The amount of the discount (e.g., 50 for 50% off)
|
||||
* `appliesTo`: The list of prices that the discount applies to. This is the price ID that you've configured above for the price.
|
||||
|
||||
This data will allow to display the correct banner in the UI e.g. "10% off for the first 100 customers!" and to apply the discount to the correct price at checkout.
|
||||
|
||||
## Adding more products, plans and discounts
|
||||
|
||||
Simply add more plans, prices and discounts to the arrays. The UI **should** be able to handle it in most traditional cases. If you have a more complex billing schema, you may need to adjust the UI accordingly.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Creem
|
||||
description: Manage your customers data and subscriptions using Creem.
|
||||
url: /docs/web/billing/creem
|
||||
---
|
||||
|
||||
# Creem
|
||||
|
||||
<Callout title="Creem integration is coming soon!">
|
||||
We are working on adding [Creem](https://www.creem.io/) integration to our platform. As soon as it's ready, we will update this page with the necessary information.
|
||||
|
||||
[See roadmap](https://github.com/orgs/turbostarter/projects/1)
|
||||
</Callout>
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: Lemon Squeezy
|
||||
description: Manage your customers data and subscriptions using Lemon Squeezy.
|
||||
url: /docs/web/billing/lemon-squeezy
|
||||
---
|
||||
|
||||
# Lemon Squeezy
|
||||
|
||||
[Lemon Squeezy](https://lemonsqueezy.com/) is another billing provider available within TurboStarter. Here we'll go through the configuration and how to set it up as a provider for your app.
|
||||
|
||||
To switch to Lemon Squeezy, you need to update the exports in:
|
||||
|
||||
<Tabs items={["index.ts", "env.ts"]}>
|
||||
<Tab value="index.ts">
|
||||
```ts
|
||||
// [!code word:lemon-squeezy]
|
||||
export * from "./lemon-squeezy";
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="env.ts">
|
||||
```ts
|
||||
// [!code word:lemon-squeezy]
|
||||
export * from "./lemon-squeezy/env";
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Then, let's configure the integration:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
## Get API keys
|
||||
|
||||
After you have created your account and a store for [Lemon Squeezy](https://lemonsqueezy.com/), you will need to create a new API key. You can do this by going to the [API page](https://app.lemonsqueezy.com/settings/api) in the settings and clicking on the plus button. You will need to give your API key a name and then click on the *Create* button. Once you have created your API key, you will need to copy the API key to use it in the setup of the integration.
|
||||
|
||||
For local development, make sure to use [Test Mode](https://docs.lemonsqueezy.com/help/getting-started/test-mode) to not mess with the real transactions.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Set environment variables
|
||||
|
||||
You need to set the following environment variables:
|
||||
|
||||
```dotenv title="apps/web/.env.local"
|
||||
LEMONSQUEEZY_API_KEY="" # Your Lemon Squeezy API key
|
||||
LEMONSQUEEZY_SIGNING_SECRET="" # Your Lemon Squeezy webhook signing secret
|
||||
LEMONSQUEEZY_STORE_ID="" # Your Lemon Squeezy store ID (can be found under Settings > Stores next to your store url, e.g #12345)
|
||||
```
|
||||
|
||||
**Please do not add the secret keys to the .env file in production.** During development, you can place them in `.env.local` as it's not committed to the repository. In production, you can set them in the environment variables of your hosting provider.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Create products
|
||||
|
||||
For your users to choose from the available subscription plans, you need to create those Products first on the [Products page](https://app.lemonsqueezy.com/products). You can create as many products as you want.
|
||||
|
||||
Create one product per plan you want to offer. You can add multiple variant within the product to offer multiple models or different billing intervals.
|
||||
|
||||

|
||||
|
||||
To offer multiple intervals for each plan, you can use the [Variant](https://docs.lemonsqueezy.com/help/products/variants) feature of Lemon Squeezy. Just create one variant for each interval/model you want to offer.
|
||||
|
||||

|
||||
|
||||
<Callout type="warn" title="Match the variant id with configuration">
|
||||
You need to make sure that the price ID you set in the configuration matches the ID of the variant you created in Lemon Squeezy.
|
||||
|
||||
[See configuration](/docs/web/billing/configuration#prices) for more information.
|
||||
</Callout>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Create a webhook
|
||||
|
||||
To sync the current subscription status or checkout conclusion and other information to your database, you need to set up a webhook.
|
||||
|
||||
The webhook handling code comes ready to use with TurboStarter, you just have to create the webhook in the Lemon Squeezy dashboard and insert the URL for your project.
|
||||
|
||||
To configure a new webhook, go to the [Webhooks page](https://app.lemonsqueezy.com/settings/webhooks) in the Lemon Squeezy settings and click the *Plus* button.
|
||||
|
||||

|
||||
|
||||
Select the following events:
|
||||
|
||||
* For subscriptions:
|
||||
* `subscription_created`
|
||||
* `subscription_updated`
|
||||
* `subscription_cancelled`
|
||||
* For one-off payments:
|
||||
* `order_created`
|
||||
|
||||
You will also have to enter a *Signing secret* which you can get by running the following command in your terminal:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Copy the generated string and paste it into the *Signing secret* field.
|
||||
|
||||
You also need to add this secret to your environment variables:
|
||||
|
||||
```dotenv title="apps/web/.env.local"
|
||||
LEMONSQUEEZY_WEBHOOK_SECRET=your-signing-secret
|
||||
```
|
||||
|
||||
To get the callback URL for the webhook, you can either use a local development URL or the URL of your deployed app:
|
||||
|
||||
### Local development
|
||||
|
||||
If you want to test the webhook locally, you can use [ngrok](https://ngrok.com) to create a tunnel to your local machine. Ngrok will then give you a URL that you can use to test the webhook locally.
|
||||
|
||||
To do so, install ngrok and run it with the following command (while your TurboStarter web development server is running):
|
||||
|
||||
```bash
|
||||
ngrok http 3000
|
||||
```
|
||||
|
||||

|
||||
|
||||
This will give you a URL (see the *Forwarding* output) that you can use to create a webhook in Lemon Squeezy. Just use that url and add `/api/billing/webhook` to it.
|
||||
|
||||
<Card title="Lemon Squeezy Webhooks" description="docs.lemonsqueezy.com" href="https://docs.lemonsqueezy.com/api/webhooks" />
|
||||
|
||||
### Production deployment
|
||||
|
||||
When going to production, you will need to set the webhook URL and the events you want to listen to in Lemon Squeezy.
|
||||
|
||||
The webhook path is `/api/billing/webhook`. If your app is hosted at `https://myapp.com` then you need to enter `https://myapp.com/api/billing/webhook` as the URL.
|
||||
|
||||
All the relevant events are automatically handled by TurboStarter, so you don't need to do anything else. If you want to handle more events please check [Webhooks](/docs/web/billing/webhooks) for more information.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Add discount
|
||||
|
||||
You can add a discount for your customers that will apply on a specific price.
|
||||
|
||||
You can create the discount on [Discounts page](https://app.lemonsqueezy.com/discounts).
|
||||
|
||||

|
||||
|
||||
You can set there a details of discount such as products that it should apply to, amount off, duration, max redemptions and more.
|
||||
|
||||
<Card title="Lemon Squeezy Discounts" description="lemonsqueezy.com" href="https://www.lemonsqueezy.com/marketing/discount-codes" />
|
||||
|
||||
You need to add also the discount code and details to TurboStarter billing configuration to enable displaying it in the UI, creating checkout sessions with it and calculate prices.
|
||||
|
||||
[See discounts configuration](/docs/web/billing/configuration#discounts) for more details.
|
||||
|
||||
That's it! 🎉 You have now set up Lemon Squeezy as a billing provider for your app.
|
||||
|
||||
Feel free to add more products, prices, discounts and manage your customers data and subscriptions using Lemon Squeezy.
|
||||
|
||||
<Callout type="warn" title="Ensure configuration matches">
|
||||
Make sure that the data you set in the configuration matches the details of things you created in Lemon Squeezy.
|
||||
|
||||
[See configuration](/docs/web/billing/configuration) for more information.
|
||||
</Callout>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Overview
|
||||
description: Get started with billing in TurboStarter.
|
||||
url: /docs/web/billing/overview
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
The `@turbostarter/billing` package is used to manage subscriptions, one-off payments, and more.
|
||||
|
||||
Inside, we're making an abstraction layer that allows us to use different billing providers without breaking our code nor changing the API calls.
|
||||
|
||||

|
||||
|
||||
## Providers
|
||||
|
||||
TurboStarter implements multiple providers for managing billing:
|
||||
|
||||
* [Stripe](/docs/web/billing/stripe)
|
||||
* [Lemon Squeezy](/docs/web/billing/lemon-squeezy)
|
||||
* [Polar](/docs/web/billing/polar)
|
||||
* [Creem](/docs/web/billing/creem) (coming soon)
|
||||
|
||||
All configuration and setup is built-in with a unified API, so you can switch between providers by simply changing the exports and even introduce your own provider without breaking any billing-related logic.
|
||||
|
||||
## Subscriptions vs. One-off payments
|
||||
|
||||
TurboStarter supports both one-off payments and subscriptions. You have the choice to use one or both. What TurboStarter cannot assume with certainty is the billing mode you want to use. By default, we assume you want to use subscriptions, as this is the most common billing mode for SaaS applications.
|
||||
|
||||
This means that - by default - TurboStarter will be looking for a subscription plan when visiting the billing section or pricing page.
|
||||
|
||||
**It's easily customizable** - [take a look at configuration](/docs/web/billing/configuration).
|
||||
|
||||
### But I want to use both
|
||||
|
||||
Perfect - you can, but you need to customize the pages to display the correct data.
|
||||
|
||||
Depending on the service you use, you will need to set the environment variables accordingly. By default - the billing package uses [Stripe](/docs/web/billing/stripe). Alternatively, you can use [Lemon Squeezy](/docs/web/billing/lemon-squeezy) or [Polar](/docs/web/billing/polar). In the future, we will also add [Creem](/docs/web/billing/creem).
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
title: Polar
|
||||
description: Manage your customers data and subscriptions using Polar.
|
||||
url: /docs/web/billing/polar
|
||||
---
|
||||
|
||||
# Polar
|
||||
|
||||
[Polar](https://www.polar.com/) is another billing provider available within TurboStarter. Here we'll go through the configuration and how to set it up as a provider for your app.
|
||||
|
||||
To switch to Polar, you need to update the exports in:
|
||||
|
||||
<Tabs items={["index.ts", "env.ts"]}>
|
||||
<Tab value="index.ts">
|
||||
```ts
|
||||
// [!code word:polar]
|
||||
export * from "./polar";
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="env.ts">
|
||||
```ts
|
||||
// [!code word:polar]
|
||||
export * from "./polar/env";
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Then, let's configure the integration:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
## Get the access token
|
||||
|
||||
After you have created your account for [Polar](https://www.polar.com/) and created your store, you will need to get the API key.
|
||||
|
||||
Under the *Settings*, scroll to *Developers* and click "New token". Enter a name for the token, set the expiration duration and select the scopes you want the token to have.
|
||||
|
||||
To keep it simple, you can select all scopes.
|
||||
|
||||

|
||||
|
||||
For local development, make sure to use [Sandbox Mode](https://docs.polar.sh/integrate/sandbox) to not mess with the real transactions.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Set environment variables
|
||||
|
||||
You need to set the following environment variables:
|
||||
|
||||
```dotenv title="apps/web/.env.local"
|
||||
POLAR_ACCESS_TOKEN="" # Your Polar access token
|
||||
POLAR_WEBHOOK_SECRET="" # Your Polar webhook secret
|
||||
POLAR_ORGANIZATION_SLUG="" # Your Polar organization slug (can be found under Settings > Organization)
|
||||
```
|
||||
|
||||
**Please do not add the secret keys to the .env file in production.** During development, you can place them in `.env.local` as it's not committed to the repository. In production, you can set them in the environment variables of your hosting provider.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Create products
|
||||
|
||||
For your users to choose from the available subscription plans, you need to create those Products first on the [Products page](https://docs.polar.sh/features/products). You can create as many products as you want.
|
||||
|
||||

|
||||
|
||||
Polar takes a different approach to product variants. Instead of having one product with multiple pricing options, Polar treats each pricing option as a separate product. This simplifies the user experience and API while giving you full flexibility.
|
||||
|
||||
At checkout, customers can choose between different products (like monthly or yearly plans), each with its own pricing and benefits.
|
||||
|
||||

|
||||
|
||||
<Callout type="warn" title="Match the product id with configuration">
|
||||
You need to make sure that the price ID you set in the configuration matches the ID of the product you created in Polar.
|
||||
|
||||
[See configuration](/docs/web/billing/configuration#prices) for more information.
|
||||
</Callout>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Create a webhook
|
||||
|
||||
To sync the current subscription status or checkout conclusion and other information to your database, you need to set up a webhook.
|
||||
|
||||
The webhook handling code comes ready to use with TurboStarter, you just have to create the webhook in the Polar dashboard and insert the URL for your project.
|
||||
|
||||
To configure a new webhook, go to the [Webhooks page](https://docs.polar.sh/integrate/webhooks/endpoints) in the Polar settings and click the *Add endpoint* button.
|
||||
|
||||

|
||||
|
||||
Select the following events:
|
||||
|
||||
* For subscriptions:
|
||||
* `subscription.created`
|
||||
* `subscription.updated`
|
||||
* `subscription.canceled`
|
||||
* `subscription.revoked`
|
||||
* For one-off payments:
|
||||
* `order.created`
|
||||
|
||||
You will also have to enter a *Secret* which you can get by running the following command in your terminal:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Copy the generated string and paste it into the *Secret* field.
|
||||
|
||||
You also need to add this secret to your environment variables:
|
||||
|
||||
```dotenv title="apps/web/.env.local"
|
||||
POLAR_WEBHOOK_SECRET=your-generated-secret
|
||||
```
|
||||
|
||||
To get the URL for the webhook, you can either use a local development URL or the URL of your deployed app:
|
||||
|
||||
### Local development
|
||||
|
||||
If you want to test the webhook locally, you can use [ngrok](https://ngrok.com) to create a tunnel to your local machine. Ngrok will then give you a URL that you can use to test the webhook locally.
|
||||
|
||||
To do so, install ngrok and run it with the following command (while your TurboStarter web development server is running):
|
||||
|
||||
```bash
|
||||
ngrok http 3000
|
||||
```
|
||||
|
||||

|
||||
|
||||
This will give you a URL (see the *Forwarding* output) that you can use to create a webhook in Polar. Just use that url and add `/api/billing/webhook` to it.
|
||||
|
||||
<Card title="Polar Webhooks" description="docs.polar.sh" href="https://docs.polar.sh/integrate/webhooks/delivery" />
|
||||
|
||||
### Production deployment
|
||||
|
||||
When going to production, you will need to set the webhook URL and the events you want to listen to in Polar.
|
||||
|
||||
The webhook path is `/api/billing/webhook`. If your app is hosted at `https://myapp.com` then you need to enter `https://myapp.com/api/billing/webhook` as the URL.
|
||||
|
||||
All the relevant events are automatically handled by TurboStarter, so you don't need to do anything else. If you want to handle more events please check [Webhooks](/docs/web/billing/webhooks) for more information.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Add discount
|
||||
|
||||
You can add a discount for your customers that will apply on a specific price.
|
||||
|
||||
You can create the discount under the *Products* page on *Discounts* tab in the Polar dashboard.
|
||||
|
||||

|
||||
|
||||
You can set there a details of discount such as products that it should apply to, amount off, duration, max redemptions and more.
|
||||
|
||||
<Card title="Polar Discounts" description="docs.polar.sh" href="https://docs.polar.sh/features/discounts" />
|
||||
|
||||
You need to add also the discount code and details to TurboStarter billing configuration to enable displaying it in the UI, creating checkout sessions with it and calculate prices.
|
||||
|
||||
[See discounts configuration](/docs/web/billing/configuration#discounts) for more details.
|
||||
|
||||
That's it! 🎉 You have now set up Polar as a billing provider for your app.
|
||||
|
||||
Feel free to add more products, prices, discounts and manage your customers data and subscriptions using Polar.
|
||||
|
||||
<Callout type="warn" title="Ensure configuration matches">
|
||||
Make sure that the data you set in the configuration matches the details of things you created in Polar.
|
||||
|
||||
[See configuration](/docs/web/billing/configuration) for more information.
|
||||
</Callout>
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: Stripe
|
||||
description: Manage your customers data and subscriptions using Stripe.
|
||||
url: /docs/web/billing/stripe
|
||||
---
|
||||
|
||||
# Stripe
|
||||
|
||||
[Stripe](https://stripe.com) is the default billing provider for TurboStarter. Here we'll go through the configuration and how to set it up as a provider for your app.
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
## Get API keys
|
||||
|
||||
After you have created your account for [Stripe](https://stripe.com), you will need to get the API key. You can do this by going to the [API page](https://dashboard.stripe.com/apikeys) in the dashboard. Here you will find the *Secret key* and the *Publishable key*. You will need the *Secret key* for the integration to work.
|
||||
|
||||
For local development, make sure to use [Test Mode](https://docs.stripe.com/test-mode) to not mess with the real transactions.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Set environment variables
|
||||
|
||||
You need to set the following environment variables:
|
||||
|
||||
```dotenv title="apps/web/.env.local"
|
||||
STRIPE_SECRET_KEY="" # Your Stripe secret key
|
||||
STRIPE_WEBHOOK_SECRET="" # The secret key of the webhook you created (see below)
|
||||
```
|
||||
|
||||
**Please do not add the secret keys to the .env file in production.** During development, you can place them in `.env.local` as it's not committed to the repository. In production, you can set them in the environment variables of your hosting provider.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Create products
|
||||
|
||||
For your users to choose from the available subscription plans, you need to create those Products first on the [Products page](https://dashboard.stripe.com/products). You can create as many products as you want.
|
||||
|
||||
Create one product per plan you want to offer. You can add multiple prices within this product to offer multiple models or different billing intervals.
|
||||
|
||||

|
||||
|
||||
<Callout type="warn" title="Match the price id with configuration">
|
||||
You need to make sure that the price ID you set in the configuration matches the ID of the price you created in Stripe.
|
||||
|
||||
[See configuration](/docs/web/billing/configuration) for more information.
|
||||
</Callout>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Create a webhook
|
||||
|
||||
To sync the current subscription status or checkout conclusion and other information to your database, you need to set up a webhook.
|
||||
|
||||
The webhook code comes ready to use with TurboStarter, you just have to create the webhook in the Stripe dashboard and insert the URL for your project.
|
||||
|
||||
To configure a new webhook, go to the [Webhooks page](https://dashboard.stripe.com/webhooks) in the Stripe settings and click the Add endpoint button.
|
||||
|
||||

|
||||
|
||||
Select the following events:
|
||||
|
||||
* For subscriptions:
|
||||
* `customer.subscription.created`
|
||||
* `customer.subscription.updated`
|
||||
* `customer.subscription.deleted`
|
||||
* For one-off payments:
|
||||
* `checkout.session.completed`
|
||||
|
||||
To get the URL for the webhook, you can either use a local development URL or the URL of your deployed app:
|
||||
|
||||
### Local development
|
||||
|
||||
There are two ways to test the webhook during local development:
|
||||
|
||||
<Tabs items={["Stripe CLI", "Tunnel"]}>
|
||||
<Tab value="Stripe CLI">
|
||||
The Stripe CLI which allows you to listen to Stripe events straight to your own localhost. You can install and use the CLI using a variety of methods, but we recommend using official way to do it.
|
||||
|
||||
[Install the Stripe CLI](https://docs.stripe.com/stripe-cli)
|
||||
|
||||
Then - login to your Stripe account using the project you want to run:
|
||||
|
||||
```bash
|
||||
stripe login
|
||||
```
|
||||
|
||||
Copy the webhook secret displayed in the terminal and set it as the `STRIPE_WEBHOOK_SECRET` environment variable in your `apps/web/.env.local` file:
|
||||
|
||||
```dotenv title="apps/web/.env.local"
|
||||
STRIPE_WEBHOOK_SECRET=*your-secret-key*
|
||||
```
|
||||
|
||||
Now, you can listen to Stripe events running the following command:
|
||||
|
||||
```bash
|
||||
stripe listen --forward-to localhost:3000/api/billing/webhook
|
||||
```
|
||||
|
||||
This will forward all the Stripe events to your local endpoint.
|
||||
|
||||
<Callout type="warn" title="Not receiving events?">
|
||||
**If you have not logged in** - the first time you set it up, you are required to sign in. This is a one-time process. Once you sign in, you can use the CLI to listen to Stripe events.
|
||||
|
||||
**Please sign in and then re-run the command.** Now, you can listen to Stripe events.
|
||||
|
||||
If you're not receiving events, please make sure that:
|
||||
|
||||
* the webhook secret is correct
|
||||
* the account you signed in is the same as the one you're using in your app
|
||||
</Callout>
|
||||
|
||||
You can even trigger the event manually for testing purposes:
|
||||
|
||||
```bash
|
||||
stripe trigger customer.subscription.created
|
||||
```
|
||||
|
||||
<Card title="Stripe CLI" description="docs.stripe.com" href="https://docs.stripe.com/stripe-cli" />
|
||||
</Tab>
|
||||
|
||||
<Tab value="Tunnel">
|
||||
If you want to test the webhook locally, you can use [ngrok](https://ngrok.com) to create a tunnel to your local machine. Ngrok will then give you a URL that you can use to test the webhook locally.
|
||||
|
||||
To do so, install ngrok and run it with the following command (while your TurboStarter web development server is running):
|
||||
|
||||
```bash
|
||||
ngrok http 3000
|
||||
```
|
||||
|
||||

|
||||
|
||||
This will give you a URL (see the *Forwarding* output) that you can use to create a webhook in Stripe. Just use that url and add `/api/billing/webhook` to it.
|
||||
|
||||
<Card title="Stripe Webhooks" description="docs.stripe.com" href="https://docs.stripe.com/webhooks" />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Production deployment
|
||||
|
||||
When going to production, you will need to set the webhook URL and the events you want to listen to in Stripe.
|
||||
|
||||
The webhook path is `/api/billing/webhook`. If your app is hosted at `https://myapp.com` then you need to enter `https://myapp.com/api/billing/webhook` as the URL.
|
||||
|
||||
All the relevant events are automatically handled by TurboStarter, so you don't need to do anything else. If you want to handle more events please check [Webhooks](/docs/web/billing/webhooks) for more information.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
## Configure Stripe Customer Portal
|
||||
|
||||
Stripe requires you to set up the Customer Portal so that users can manage their billing information, invoices and plan settings from there.
|
||||
|
||||
You can do it [under the following link.](https://dashboard.stripe.com/settings/billing/portal)
|
||||
|
||||

|
||||
|
||||
1. Please make sure to enable the setting that lets users switch plans
|
||||
2. Configure the behavior of the cancellation according to your needs
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Add discount
|
||||
|
||||
You can add a discount for your customers that will apply on a specific price.
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Create coupon
|
||||
|
||||
First, you'd need to create a coupon on the [Coupons page](https://dashboard.stripe.com/coupons).
|
||||
|
||||

|
||||
|
||||
You can set there a details of discount such as prices that it should apply to, amount off, duration, max redemptions and more.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add promotion code
|
||||
|
||||
To enable using code during checkout you need to get a promotion code. You can define it on the same page as the coupon and give some user-friendly name to it.
|
||||
|
||||

|
||||
|
||||
This code will be auto-applied at new checkout sessions.
|
||||
|
||||
<Card title="Stripe Discounts" description="docs.stripe.com" href="https://docs.stripe.com/checkout/custom-checkout/add-discounts" />
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Configure discount
|
||||
|
||||
You need to add also the discount code and details to TurboStarter billing configuration to enable displaying it in the UI, creating checkout sessions with it and calculate prices.
|
||||
|
||||
[See discounts configuration](/docs/web/billing/configuration#discounts) for more details.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
That's it! 🎉 You have now set up Stripe as a billing provider for your app.
|
||||
|
||||
Feel free to add more products, prices, discounts and manage your customers data and subscriptions using Stripe.
|
||||
|
||||
<Callout type="warn" title="Ensure configuration matches">
|
||||
Make sure that the data you set in the configuration matches the details of things you created in Stripe.
|
||||
|
||||
[See configuration](/docs/web/billing/configuration) for more information.
|
||||
</Callout>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Webhooks
|
||||
description: Handle webhooks from your billing provider.
|
||||
url: /docs/web/billing/webhooks
|
||||
---
|
||||
|
||||
# Webhooks
|
||||
|
||||
TurboStarter handles billing webhooks to update customer data based on events received from the billing provider.
|
||||
|
||||
Occasionally, you may need to set up additional webhooks or perform custom actions with webhooks.
|
||||
|
||||
In such cases, you can customize the billing webhook handler in the billing router at `packages/api/src/modules/billing/router.ts`.
|
||||
|
||||
By default, the webhook handler is configured to be as straightforward as possible:
|
||||
|
||||
```ts title="router.ts"
|
||||
import { webhookHandler } from "@turbostarter/billing/server";
|
||||
|
||||
export const billingRouter = new Hono().post("/webhook", (c) =>
|
||||
webhookHandler(c.req.raw),
|
||||
);
|
||||
```
|
||||
|
||||
However, you can extend it using the callbacks provided from `@turbostarter/billing` package:
|
||||
|
||||
```ts title="router.ts"
|
||||
import { webhookHandler } from "@turbostarter/billing/server";
|
||||
|
||||
export const billingRouter = new Hono().post("/webhook", (c) =>
|
||||
webhookHandler(c.req.raw, {
|
||||
onCheckoutSessionCompleted: (sessionId) => {},
|
||||
onSubscriptionCreated: (subscriptionId) => {},
|
||||
onSubscriptionUpdated: (subscriptionId) => {},
|
||||
onSubscriptionDeleted: (subscriptionId) => {},
|
||||
onEvent: (rawEvent) => {},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
You can provide one or more of the callbacks to handle the events you are interested in.
|
||||
Reference in New Issue
Block a user