giancarlo b8f00f0a2d Refactor E2E tests, update utils, and modify plan-picker validation
Several changes have been made to optimize the testing and validation workflows. The end-to-end tests for user billing, invitations, account, team billing, and team accounts have been retouched for improved performance and better accuracy. There has been a minor modification in the validation rules for plan-picker in the billing component. Furthermore, modifications are made to several utility functions to fine-tune their operations.
2024-05-05 18:15:11 +07:00
2024-04-16 15:03:41 +08:00

Makerkit - Supabase SaaS Starter Kit - Turbo Edition

This is a Starter Kit for building SaaS applications using Supabase, Next.js, and Tailwind CSS.

This version uses Turborepo to manage multiple packages in a single repository.

This project is currently under development. Please wait for the stable release before using it in production. It will undergo big changes and improvements.

A demo version of this project can be found at makerkit/next-supabase-saas-kit-turbo-demo. This version contains a tasks functionality that is not present in the original version, multiple languages, and other various modifications.

Roadmap

The roadmap for the project is as follows:

  • - March 31: Alpha release - authentication, personal accounts, team accounts (memberships)
  • - April 7: Beta release - billing, Stripe, Lemon Squeezy, and more
  • - April 14: Release candidate - admin dashboard, translations, and more
  • - April 21: Final Release candidate - in-app notifications, final features and improvements
  • - April 28: Stable release - final features and improvements
  • - May 4: Post-release - documentation, tutorials, and more

Features

  • Authentication: Sign up, sign in, sign out, forgot password, update profile, and more.
  • Billing: Subscription management, one-off payments, flat subscriptions, per-seat subscriptions, and more.
  • Personal Account: Manage your account, profile picture, and more.
  • Team Accounts: Invite members, manage roles, and more. Manage resources within a team.
  • RBAC: Simple-to-use role-based access control. Customize roles and permissions (coming soon).
  • Admin Dashboard: Manage users, subscriptions, and more.
  • Pluggable: Easily add new features and packages to your SaaS application.
  • Super UI: Beautiful UI using Shadcn UI and Tailwind CSS.

The most notable difference between this version and the original version is the use of Turborepo to manage multiple packages in a single repository.

Thanks to Turborepo, we can manage and isolate different parts of the application in separate packages. This makes it easier to manage and scale the application as it grows.

Additionally, we can extend the codebase without it impacting your web application.

Let's get started!

Quick Start

0. Prerequisites

  • Node.js 18.x or later
  • Docker
  • Pnpm
  • Supabase account (optional for local development)
  • Payment Gateway account (Stripe/Lemon Squeezy)
  • Email Service account (optional for local development)

Clone this repository with the command:

git clone git@github.com:makerkit/next-supabase-saas-kit-turbo

If your SSH key isn't set - then use the HTTPS.

git clone https://github.com/makerkit/next-supabase-saas-kit-turbo

Now, remove the original origin:

git remote rm origin

Add upstream pointing to this repository so you can pull updates

git remote add upstream git@github.com:makerkit/next-supabase-saas-kit-turbo

Once you have your own repository, do the same but use origin instead of upstream

To pull updates (please do this daily with your morning coffee):

git pull upstream main

This will keep your repository up to date.

0.1. Install Pnpm

# Install pnpm
npm i -g pnpm

1. Setup dependencies

# Install dependencies
pnpm i

2. Start the development server

# Start the development server
pnpm dev

This command will run the web application.

Please refer to apps/web/README.md for more information about the web application.

To get started right away, use the credentials below:

  • Email: test@makerkit.dev
  • Password: testingpassword

Turbopack

Makerkit uses Turbopack by default. Turbopack is the new bundler for Next.js - and it is still experimental. It is faster, but it may have some issues.

If you encounter any issues, you can switch to the default Next.js by removing the flag --turbo from the dev command in apps/web/package.json.

3. Start the Supabase server

To start the Supabase server, you can use the following command:

# Start the Supabase server
pnpm run supabase:web:start

This command runs the Supabase server locally for the app web.

Should you add more apps, you can run the following command:

# Start the Supabase server for the app `app-name`
pnpm run supabase:app-name:start

Instead, to stop the Supabase server, you can use the following command:

# Stop the Supabase server
pnpm run supabase:web:stop

To generate the Supabase schema, you can use the following command:

# Generate the Supabase schema
pnpm run supabase:web:typegen

Architecture

This project uses Turborepo to manage multiple packages in a single repository.

Apps

The core web application can be found in the apps/web package.

Here is where we add the skeleton of the application, including the routing, layout, and global styles.

The main application defines the following:

  1. Configuration: Environment variables, feature flags, paths, and more. The configuration gets passed down to other packages
  2. Routing: The main routing of the application. Since this is file-based routing, we define the routes here.
  3. Local components: Shared components that are used across the application but not necessarily shared with other apps/packages.
  4. Global styles: Global styles that are used across the application.

Packages

Below are the reusable packages that can be shared across multiple applications (or packages).

  • @kit/ui: Shared UI components and styles (using Shadcn UI and some custom components)
  • @kit/shared: Shared code and utilities
  • @kit/supabase: Supabase package that defines the schema and logic for managing Supabase
  • @kit/i18n: Internationalization package that defines utilities for managing translations
  • @kit/billing: Billing package that defines the schema and logic for managing subscriptions
  • @kit/billing-gateway: Billing gateway package that defines the schema and logic for managing payment gateways
  • @kit/email-templates: Here we define the email templates using the react.email package.
  • @kit/mailers: Mailer package that abstracts the email service provider (e.g., Resend, Cloudflare, SendGrid, Mailgun, etc.)
  • @kit/monitoring: A unified monitoring package that defines the schema and logic for monitoring the application with third party services (e.g., Sentry, Baselime, etc.)
  • @kit/database-webhooks: Database webhooks package that defines the actions following database changes (e.g., sending an email, updating a record, etc.)
  • @kit/cms: CMS package that defines the schema and logic for managing content
  • @kit/next: Next.js specific utilities

And features that can be added to the application:

  • @kit/auth: Authentication package (using Supabase)
  • @kit/accounts: Package that defines components and logic for managing personal accounts
  • @kit/team-accounts: Package that defines components and logic for managing team
  • @kit/admin: Admin package that defines the schema and logic for managing users, subscriptions, and more.

And billing packages that can be added to the application:

  • @kit/stripe: Stripe package that defines the schema and logic for managing Stripe. This is used by the @kit/billing-gateway package and abstracts the Stripe API.
  • @kit/lemon-squeezy: Lemon Squeezy package that defines the schema and logic for managing Lemon Squeezy. This is used by the @kit/billing-gateway package and abstracts the Lemon Squeezy API. (Coming soon)
  • @kit/paddle: Paddle package that defines the schema and logic for managing Paddle. This is used by the @kit/billing-gateway package and abstracts the Paddle API. (Coming soon

The CMSs that can be added to the application:

  • @kit/wordpress: WordPress package that defines the schema and logic for managing WordPress. This is used by the @kit/cms package and abstracts the WordPress API.
  • @kit/contentlayer: Contentlayer package that defines the schema and logic for managing Contentlayer. This is used by the @kit/cms package and abstracts the Contentlayer API. Set to be replaced.

Also planned (post-release):

  • @kit/notifications: Notifications package that defines the schema and logic for managing notifications
  • @kit/plugins: Move the existing plugins to a separate package here
  • @kit/analytics: A unified analytics package to track user behavior

Application Configuration

The configuration is defined in the apps/web/config folder. Here you can find the following configuration files:

  • app.config.ts: Application configuration (e.g., name, description, etc.)
  • auth.config.ts: Authentication configuration
  • billing.config.ts: Billing configuration
  • feature-flags.config.ts: Feature flags configuration
  • paths.config.ts: Paths configuration (e.g., routes, API paths, etc.)
  • personal-account-sidebar.config.ts: Personal account sidebar configuration (e.g., links, icons, etc.)
  • team-account-sidebar.config.ts: Team account sidebar configuration (e.g., links, icons, etc.)

Conventions

1. Packages vs Apps

In this project, we use packages to define reusable code that can be shared across multiple applications.

Apps are used to define the main application, including the routing, layout, and global styles.

Apps pass down the configuration to the packages - and the packages provide the corresponding logic and components.

Imports and Paths

When importing something from a package or an app, you will use the following paths:

  • When you import something from a package, you will use @kit/package-name (e.g., @kit/ui, @kit/shared, etc.).
  • When you import something from an app, you will use ~/ (e.g., ~/lib/components, ~/config, etc.).

Non-Route Folders

Non-route folders within app will be prefixed with an underscore (e.g., _components, _lib, etc.). This makes it obvious that these folders are not routes and are used for shared components, utilities, etc.

Server Code

The files localed in server folders are to be assumed as server-side code. They are not meant to be used in the client-side code. This helps everyone understanding where the code is meant to be run, since the lines are very blurry in Next.js.

2. Environment Variables

Environment variables are defined in the .env file in the root of the apps/web package.

  1. Shared Environment Variables: Shared environment variables are defined in the .env file. These are the env variables shared between environments (e.g., development, staging, production).
  2. Environment Variables: Environment variables for a specific environment are defined in the .env.development and .env.production files. These are the env variables specific to the development and production environments.
  3. Secret Keys: Secret keys and sensitive information are not stored in the .env file. Instead, they are stored in the environment variables of the CI/CD system.
  4. Secret keys to be used locally: If you need to use secret keys locally, you can store them in the .env.local file. This file is not committed to Git, therefore it is safe to store sensitive information in it.

3. Your app

Your application will be defined in the apps/web package. This is where you will define the main application, including the routing, layout, and global styles.

If you want - create a package for it - but it's not necessary. The apps/web package is the main application and you can place all your logic and files there.

4. Updating "packages"

The app is designed so that you should focus on what's in apps/ and that's it. The packages are managed by the core team and are updated regularly.

So - should you update the packages? If you need to, then you need to.

Makerkit makes assumptions and as such - they may not apply to your specific use case. If you need to update the packages, you can and should do so - but it's worth noting that this will cause conflicts. That's fine, but you need to resolve them.

Installing a Shadcn UI component

To install a Shadcn UI component, you can use the following commands:

cd packages/ui
npx shadcn-ui@latest add <component> --path=src/shadcn

For example, to install the Button component, you can use the following command:

cd packages/ui
npx shadcn-ui@latest add button --path=src/shadcn

We pass the --path flag to specify the path where the component should be installed. You may need to adjust the path based on your project structure.

NB: you may need to update the imports to the cn utility function to use the relative imports because it somehow breaks. Please do that.

Writing Server Actions

In the large majority of cases - you will be writing React Server Actions to update data in your DB. Server Actions are used to perform mutations on the server-side - while being called like normal functions.

To create a server action, it's enough to add use server at the top of the file and export the function.

'use server';

// I am now a server action!
export const myServerAction = async function () {
  // ... your code here
  return {
    success: true,
  };
};

The above is a plain POST request that basically does nothing. Let's see how we can make it more useful.

Makerkit ships with a utility to help you write these actions. The utility is called enhanceAction and we import it from @kit/next/actions.

import { enhanceAction } from '@kit/next/actions';

This utility helps us with three main things:

  1. checks the user state (if the user is authenticated)
  2. given a Zod schema, it validates the request body
  3. given a captcha site key, it validates the captcha token

Fantastic, let's see how we can use it.

'use server';

import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';

const ZodSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6),
});

export const myServerAction = enhanceAction(
  async function (data, user) {
    // 1. "data" has been validated against the Zod schema, and it's safe to use
    // 2. "user" is the authenticated user
    
    // ... your code here
    return {
      success: true,
    };
  },
  {
    schema: ZodSchema,
  },
);

Using a Captcha token protection

If you want to protect your server actions with a captcha token, you can do so by passing the captcha site token to the enhanceAction function and setting the captcha flag to true.

'use server';

import { enhanceAction } from '@kit/next/actions';

export const myServerAction = enhanceAction(
  async function (data, user) {
    // ... your code here
    return {
      success: true,
    };
  },
  {
    captcha: true,
    schema: ZodSchema,
  },
);

When calling the server action, we must supply the captcha token in the request body.

The captcha token can be retrieved from the useCaptchaToken hook in the package @kit/auth/captcha/client.

import { useCaptchaToken } from '@kit/auth/captcha/client';

function Component() {
  const captchaToken = useCaptchaToken();
  
  // ... your code here
}

Now, when calling the server action, we can pass the captcha

import { useCaptchaToken } from '@kit/auth/captcha/client';

function Component() {
  const captchaToken = useCaptchaToken();
  
  const onSubmit = async (params: {
    email: string;
    password: string;
  }) => {
    const response = await myServerAction({
      ...params,
      captchaToken,
    });
    
    // ... your code here
  };
}

NB: to use Captcha protection, you need to set the captcha token in the environment variables.

CAPTCHA_SECRET_TOKEN=
NEXT_PUBLIC_CAPTCHA_SITE_KEY=

As a secret environment variable, please do not add it to the .env file. Instead, add it to the environment variables of your CI/CD system.

The only captcha provider supported is Cloudflare Turnstile.

Writing API Route Handlers

API Route handlers are added using the convention route.ts and exporting one or many HTTP methods (e.g., GET, POST, PUT, DELETE).

While you won't be writing too many API route handlers (prefer Server Actions for mutations) - you can use the enhanceRouteHandler utility to help you with the following:

  1. checks the user state (if the user is authenticated)
  2. given a Zod schema, it validates the request body
  3. given a captcha site key, it validates the captcha token

Fantastic, let's see how we can use it.

import { z } from 'zod';

import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server';

const ZodSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6),
});

export const POST = enhanceRouteHandler(
  async function({ body, user, request }) {
    // 1. "body" has been validated against the Zod schema, and it's safe to use
    // 2. "user" is the authenticated user
    // 3. "request" is the request object that contains the headers, query, etc.

    // ... your code here
    return NextResponse.json({
      success: true,
    });
  },
  {
    schema: ZodSchema,
  },
);

Using a Captcha token protection

If you want to protect your API route handlers with a captcha token, you can do so by passing the captcha site token to the enhanceRouteHandler function and setting the captcha flag to true.

import { enhanceRouteHandler } from '@kit/next/routes';

export const POST = enhanceRouteHandler(
  async function({ body, user, request }) {
    // ... your code here
    return NextResponse.json({
      success: true,
    });
  },
  {
    captcha: true,
    schema: ZodSchema,
  },
);

When calling the API route handler, we must supply the captcha token in the request body.

The captcha token can be retrieved from the useCaptchaToken hook in the package @kit/auth/captcha/client.

import { useCaptchaToken } from '@kit/auth/captcha/client';

function Component() {
  const captchaToken = useCaptchaToken();
  
  // ... your code here
}

Now, when calling the API route handler, we can pass the captcha and the CSRF token.

NB: The CSRF token must be added for all API routes making mutations in routes that are outside /api/*. Routes inside /api/* are not protected by default as they're meant to be used externally.

import { useCaptchaToken } from '@kit/auth/captcha/client';
import { useCsrfToken } from '@kit/shared/hooks';

function Component() {
  const captchaToken = useCaptchaToken();
  const csrfToken = useCsrfToken();
  
  const onSubmit = async (params: {
    email: string;
    password: string;
  }) => {
    const response = await fetch('/my-api-route', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-csrf-token': csrfToken,
        'x-captcha-token': captchaToken,
      },
      body: JSON.stringify(params),
    });
    
    // ... your code here
  };
  
  // ... your code here
}

You can improve the above using React Query:

import { useMutation } from '@tanstack/react-query';
import { useCaptchaToken } from '@kit/auth/captcha/client';

function Component() {
  const captchaToken = useCaptchaToken();
  const mutation = useMutateData();

  const onSubmit = async (params: {
    email: string;
    password: string;
  }) => {
    await mutation.mutateAsync(params);
  };
  
  // ... your code here
}

function useMutateData() {
  return useMutation({
    mutationKey: 'my-mutation',
    mutationFn: async (params: { email: string; password: string }) => {
      const response = await fetch('/my-api-route', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-captcha-token': captchaToken,
        },
        body: JSON.stringify(params),
      });
      
      if (!response.ok) {
        throw new Error('An error occurred');
      }

      return response.json();
    },
  });
}

NB: to use Captcha protection, you need to set the captcha token in the environment variables.

CAPTCHA_SECRET_TOKEN=
NEXT_PUBLIC_CAPTCHA_SITE_KEY=

As a secret environment variable, please do not add it to the .env file. Instead, add it to the environment variables of your CI/CD system.

The only captcha provider supported is Cloudflare Turnstile.

Environment Variables

The majority of the environment variables are defined in the apps/web/.env file. These are the env variables shared between environments (eg. they will be the same for development, staging, and production).

NB: You will not add any secret keys or sensitive information here. Only configuration, paths, feature flags, etc.

# SHARED ENVIRONMENT VARIABLES
# HERE YOU CAN ADD ALL THE **PUBLIC** ENVIRONMENT VARIABLES THAT ARE SHARED ACROSS ALL THE ENVIRONMENTS
# PLEASE DO NOT ADD ANY CONFIDENTIAL KEYS OR SENSITIVE INFORMATION HERE
# ONLY CONFIGURATION, PATH, FEATURE FLAGS, ETC.
# TO OVERRIDE THESE VARIABLES IN A SPECIFIC ENVIRONMENT, PLEASE ADD THEM TO THE SPECIFIC ENVIRONMENT FILE (e.g. .env.development, .env.production)

# SITE
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_PRODUCT_NAME=Makerkit
NEXT_PUBLIC_SITE_TITLE="Makerkit - The easiest way to build and manage your SaaS"
NEXT_PUBLIC_SITE_DESCRIPTION="Makerkit is the easiest way to build and manage your SaaS. It provides you with the tools you need to build your SaaS, without the hassle of building it from scratch."
NEXT_PUBLIC_DEFAULT_THEME_MODE=light
NEXT_PUBLIC_THEME_COLOR="#ffffff"
NEXT_PUBLIC_THEME_COLOR_DARK="#0a0a0a"

# AUTH
NEXT_PUBLIC_AUTH_PASSWORD=true
NEXT_PUBLIC_AUTH_MAGIC_LINK=false
NEXT_PUBLIC_CAPTCHA_SITE_KEY=

# BILLING
NEXT_PUBLIC_BILLING_PROVIDER=stripe

# CMS
CMS_CLIENT=keystatic

# KEYSTATIC
NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH=./content

# LOCALES PATH
NEXT_PUBLIC_LOCALES_PATH=apps/web/public/locales

# FEATURE FLAGS
NEXT_PUBLIC_ENABLE_THEME_TOGGLE=true
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION=true
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true

Please update the apps/web/.env file with the appropriate values.

This is complemented by the environment variables defined in the apps/web/.env.development and apps/web/.env.production files.

Feature Flags

To enable or disable certain application features, please toggle the values below:

NEXT_PUBLIC_ENABLE_THEME_TOGGLE=true
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION=true
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
  1. NEXT_PUBLIC_ENABLE_THEME_TOGGLE - you can hide the theme toggle (if you want to force a single theme)
  2. NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION - to prevent users from self-deleting their personal account
  3. NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING - to enable/disable billing for personal accounts
  4. NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION - to prevent team accounts from self-deleting the account
  5. NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING - to enable/disable billing for team accounts
  6. NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS - to disable team accounts
  7. NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION - to enable/disable the ability to create a team account

Personal Accounts vs Team Accounts

Personal accounts are accounts that are owned by a single user. Team accounts are accounts that are owned by multiple users.

This allows you to:

  1. Serve B2C customers (personal accounts)
  2. Serve B2B customers (team accounts)
  3. Allow both (for example, like GitHub)

In the vast majority of cases, you will want to enable billing for personal OR team accounts. I have not seen many cases where billing both was required.

To do so, please set the following variables accordingly.

For enabling personal accounts billing only:

NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=false

You may also want to fully disable team accounts:

NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=false

To enable team accounts billing only:

NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING=false
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true

To enable both, leave them both as true.

Please remember that to ensure DB consistency, you need to also set them at DB level by adjusting the table config.

To enable personal account billing:

update config set enable_account_billing = true;

To enable team account billing:

update config set enable_team_account_billing = true;

To disable team accounts:

update config set enable_team_accounts = false;

To leave them both enabled, just leave them as they are.

Cloudflare Turnstile protection

To use Cloudflare's Turnstile Captcha, you need to set the following environment variables:

CAPTCHA_SECRET_TOKEN=
NEXT_PUBLIC_CAPTCHA_SITE_KEY=

Of course, please make sure Turnstile it's set up correctly.

This token will be used to validate:

  1. Sign ups (automatically) by passing the token to Supabase. Please set the captcha token in Supabase too.
  2. API routes and server actions that are protected by the captcha token.

Production Environment Variables

When going to production, you will need to define the environment variables in the apps/web/.env.production file.

# SITE
NEXT_PUBLIC_SITE_URL=

If you use Stripe, also add:

# STRIPE
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

Exclusively from your CI or hosting platform, please add the following environment variables:

# SUPABASE
SUPABASE_SERVICE_ROLE_KEY=

If you use Stripe, also add:

# STRIPE
STRIPE_WEBHOOK_SECRET=
STRIPE_SECRET_KEY=

Database Webhooks

Finally, you need to set a secret SUPABASE_DB_WEBHOOK_SECRET that your server and your Supabase instance will share in order to authenticate the requests.

SUPABASE_DB_WEBHOOK_SECRET=**************************************************

Make it a strong secret key - and make sure to keep it secret!

Now, you need to deploy the Supabase DB webhooks to your Supabase instance.

Please copy the webhooks (written with Postgres SQL) from apps/web/supabase/seed.sql and make sure to replicate them to the Supabase instance.

Make sure to add the following header X-Supabase-Event-Signature with the value of the SUPABASE_DB_WEBHOOK_SECRET to the request.

In this way - you server will be able to authenticate the request and be sure it's coming from your Supabase instance.

As the endpoint, remember to use the /api/db/webhook endpoint. If your APP url is https://myapp.vercel.app, the endpoint will be https://myapp.vercel.app/api/db/webhook.

Adding Database Webhooks from Supabase Studio

The below is only needed when going to production. The local development seed.sql script will add the webhooks for you.

While you can create a migration to add the database webhooks, you can also add them from the Supabase Studio.

  1. Go to the Supabase Studio
  2. Go to Database->Webhooks
  3. Click on "Enable Webhooks"
  4. Click on "Create a new hook"

Now, replicate the webhooks at apps/web/supabase/seed.sql using the UI:

  1. Please remember to set the X-Supabase-Event-Signature header with the value of the SUPABASE_DB_WEBHOOK_SECRET to the request.
  2. Please remember to set the endpoint to /api/db/webhook using your real APP URL. If your APP URL is https://myapp.vercel.app, the endpoint will be https://myapp.vercel.app/api/db/webhook.
  3. Use 5000 as the timeout.

Authentication

From your Supabase dashboard, please visit Authentication->URL Configuration and set the following:

  • Site URL: The URL of your application (e.g., http://mypp.com)
  • Redirect URLs: The URL to redirect the user after signing in (e.g., http://myapp.com/auth/callback)

Remember to update the mailing sender in Supabase too, as the default sender is most likely going to spam and has very limited quota.

You can do so from Settings->Authentication->SMTP Settings.

Development Gotchas

When you update the repository - I found it best to clear the workspaces and reinstall the dependencies.

pnpm run clean:workspaces
pnpm run clean

Then, reinstall the dependencies:

pnpm i

PNPM is so fast this won't take long.

Sometimes - you will see errors when running the Dev Server (sometimes it's Turbopack, and sometimes pnpm uses a different version of React).

While I figure this stuff out, in these cases, please re-run the Dev Server:

pnpm dev

If necessary, repeat the process above.

Billing

The billing package is used to manage subscriptions, one-off payments, and more.

The billing package is abstracted from the billing gateway package, which is used to manage the payment gateway (e.g., Stripe, Lemon Squeezy, etc.).

To set up the billing package, you need to set the following environment variables:

NEXT_PUBLIC_BILLING_PROVIDER=stripe # or lemon-squeezy

Makerkit supports both one-off payments and subscriptions. You have the choice to use one or both. What Makerkit 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 - Makerkit will be looking for a subscription plan when visiting the billing section of the personal or team account. This means we fetch data from the tables subscriptions and subscription_items.

If you want to use one-off payments, you need to set the billing mode to one-time:

BILLING_MODE=one-time

By doing so, Makerkit will be looking for one-off payments when visiting the billing section of the personal or team account. This means we fetch data from the tables orders and order_items.

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. Alternatively, you can use Lemon Squeezy. In the future, we will also add Paddle.

Stripe

For Stripe, you'll need to set the following environment variables:

STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

To run the Stripe CLI - which allows you to listen to Stripe events straight to your own localhost - you can use the following command:

pnpm run stripe:listen

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 re-run the command. Now, you can listen to Stripe events.

Lemon Squeezy

For Lemon Squeezy, you'll need to set the following environment variables:

LEMON_SQUEEZY_SECRET_KEY=
LEMON_SQUEEZY_SIGNING_SECRET=

I am aware you know this, but never add these variables to the .env file. Instead, add them to the environment variables of your CI/CD system.

To test locally, you can add them to the .env.local file. This file is not committed to Git, therefore it is safe to store sensitive information in it.

Billing Schema

The billing schema replicates your billing provider's schema, so that:

  1. we can display the data in the UI (pricing table, billing section, etc.)
  2. create the correct checkout session
  3. make some features work correctly - such as per-seat billing

The billing schema is common to all billing providers. 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 three main entities:

  1. Products: The main product you are selling (e.g., "Pro Plan", "Starter Plan", etc.)
  2. Plans: The pricing plan for the product (e.g., "Monthly", "Yearly", etc.)
  3. Line Items: The line items for the plan (e.g., "flat subscription", "metered usage", "per seat", etc.)

Setting the Billing Provider

The billing provider is already set as process.env.NEXT_PUBLIC_BILLING_PROVIDER and defaults to stripe.

For clarity - this is set in the apps/web/config/billing.config.ts file:

export default createBillingSchema({
  // also update config.billing_provider in the DB to match the selected
  provider,
  // products configuration
  products: []
});

We will now add the products to the configuration.

Products

Products are the main product you are selling. They are defined by the following fields:

export default createBillingSchema({
  provider,
  products: [
    {
      id: 'starter',
      name: 'Starter',
      description: 'The perfect plan to get started',
      currency: 'USD',
      badge: `Value`,
      plans: [],
    }
  ]
});

Let's break down the fields:

  1. id: The unique identifier for the product. This is chosen by you, it doesn't need to be the same one as the one in the provider.
  2. name: The name of the product
  3. description: The description of the product
  4. currency: The currency of the product
  5. badge: A badge to display on the product (e.g., "Value", "Popular", etc.)

The majority of these fields are going to populate the pricing table in the UI.

Plans

Plans are the pricing plans for the product. They are defined by the following fields:

export default createBillingSchema({
  provider,
  products: [
    {
      id: 'starter',
      name: 'Starter',
      description: 'The perfect plan to get started',
      currency: 'USD',
      badge: `Value`,
      plans: [
        {
          name: 'Starter Monthly',
          id: 'starter-monthly',
          trialDays: 7,
          paymentType: 'recurring',
          interval: 'month',
          lineItems: [],
        }
      ],
    }
  ]
});

Let's break down the fields:

  • name: The name of the plan
  • id: The unique identifier for the plan. This is chosen by you, it doesn't need to be the same one as the one in the provider.
  • trialDays: The number of days for the trial period
  • paymentType: The payment type (e.g., recurring, one-time)
  • interval: The interval of the payment (e.g., month, year)
  • lineItems: The line items for the plan

Now, we will be looking at the line items. The line items are the items that make up the plan, and can be of different types:

  1. Flat Subscription: A flat subscription (e.g., $10/month) - specified as flat
  2. Metered Billing: Metered billing (e.g., $0.10 per 1,000 requests) - specified as metered
  3. Per-Seat Billing: Per-seat billing (e.g., $10 per seat) - specified as per-seat

You can add one or more line items to the plan when using Stripe. When using Lemon Squeezy, you can only add one line item - but you can decorate it with the necessary metadata to achieve a similar result.

Flat Subscriptions

Flat subscriptions are defined by the following fields:


export default createBillingSchema({
  provider,
  products: [
    {
      id: 'starter',
      name: 'Starter',
      description: 'The perfect plan to get started',
      currency: 'USD',
      badge: `Value`,
      plans: [
        {
          name: 'Starter Monthly',
          id: 'starter-monthly',
          trialDays: 7,
          paymentType: 'recurring',
          interval: 'month',
          lineItems: [
            {
              id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
              name: 'Addon 2',
              cost: 9.99,
              type: 'flat',
            },
          ],
        }
      ],
    }
  ]
});

Let's break down the fields:

  • id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
  • name: The name of the line item
  • cost: The cost of the line item
  • type: The type of the line item (e.g., flat, metered, per-seat). In this case, it's flat.

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.

Metered Billing

Metered billing is defined by the following fields:


export default createBillingSchema({
  provider,
  products: [
    {
      id: 'starter',
      name: 'Starter',
      description: 'The perfect plan to get started',
      currency: 'USD',
      badge: `Value`,
      plans: [
        {
          name: 'Starter Monthly',
          id: 'starter-monthly',
          trialDays: 7,
          paymentType: 'recurring',
          interval: 'month',
          lineItems: [
            {
              id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
              name: 'Addon 2',
              cost: 0,
              type: 'metered',
              unit: 'GBs',
              tiers: [
                {
                    upTo: 10,
                    cost: 0.1,
                },
                {
                    upTo: 100,
                    cost: 0.05,
                },
                {
                    upTo: 'unlimited',
                    cost: 0.01,
                }
              ]
            },
          ],
        }
      ],
    }
  ]
});

Let's break down the fields:

  • id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
  • name: The name of the line item
  • cost: The cost of the line item. This can be set to 0 as the cost is calculated based on the tiers.
  • type: The type of the line item (e.g., flat, metered, per-seat). In this case, it's metered.
  • unit: The unit of the line item (e.g., GBs, requests, etc.). You can use a translation key here.
  • tiers: The tiers of the line item. Each tier is defined by the following fields:
    • upTo: The upper limit of the tier. If the usage is below this limit, the cost is calculated based on this tier.
    • cost: The cost of the tier. This is the cost per unit.

The tiers data is used exclusively for UI purposes. The billing provider will handle the actual billing - therefore, please make sure the tiers are correctly set in the billing provider.

Per-Seat Billing

Per-seat billing is defined by the following fields:

export default createBillingSchema({
  provider,
  products: [
    {
      id: 'starter',
      name: 'Starter',
      description: 'The perfect plan to get started',
      currency: 'USD',
      badge: `Value`,
      plans: [
        {
          name: 'Starter Monthly',
          id: 'starter-monthly',
          trialDays: 7,
          paymentType: 'recurring',
          interval: 'month',
          lineItems: [
            {
              id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
              name: 'Addon 2',
              cost: 0,
              type: 'per_seat',
              tiers: [
                {
                    upTo: 3,
                    cost: 0,
                },
                {
                    upTo: 5,
                    cost: 7.99,
                },
                {
                    upTo: 'unlimited',
                    cost: 5.99,
                }
              ]
            },
          ],
        }
      ],
    }
  ]
});

Let's break down the fields:

  • id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
  • name: The name of the line item
  • cost: The cost of the line item. This can be set to 0 as the cost is calculated based on the tiers.
  • type: The type of the line item (e.g., flat, metered, per-seat). In this case, it's per-seat.
  • tiers: The tiers of the line item. Each tier is defined by the following fields:
    • upTo: The upper limit of the tier. If the usage is below this limit, the cost is calculated based on this tier.
    • cost: The cost of the tier. This is the cost per unit.

If you set the first tier to 0, it basically means that the first n seats are free. This is a common practice in per-seat billing.

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.

One-Off Payments

One-off payments are defined by the following fields:

export default createBillingSchema({
  provider,
  products: [
    {
      id: 'starter',
      name: 'Starter',
      description: 'The perfect plan to get started',
      currency: 'USD',
      badge: `Value`,
      plans: [
        {
          name: 'Starter Monthly',
          id: 'starter-monthly',
          paymentType: 'one-time',
          lineItems: [
            {
              id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
              name: 'Addon 2',
              cost: 9.99,
              type: 'flat',
            },
          ],
        }
      ],
    }
  ]
});

Let's break down the fields:

  • name: The name of the plan
  • id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
  • paymentType: The payment type (e.g., recurring, one-time). In this case, it's one-time.
  • lineItems: The line items for the plan
    • id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
    • name: The name of the line item
    • cost: The cost of the line item
    • type: The type of the line item (e.g., flat). It can only be flat for one-off payments.

Adding more Products, Plans, and Line Items

Simply add more products, plans, and line items 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.

Deploying to Vercel

Deploying to Vercel is straightforward. You can deploy the application using the Vercel CLI or the Vercel dashboard.

No additional configuration is needed to deploy the application to Vercel. If you want to opt-in to the Edge Runtime, please follow the instructions below (except for the Cloudflare CLI installation).

Since Vercel Edge runtime uses Cloudflare, the steps are similar to deploying to Cloudflare.

Deploying to Cloudflare 🔥

To deploy the application to Cloudflare, you need to do the following:

  1. Opt-in to the Edge runtime
  2. Using the Cloudflare Mailer
  3. Install the Cloudflare CLI
  4. Switching CMS
  5. Setting Node.js Compatibility Flags

0. Limitations

Before you continue, please evaluate the limitations of the Edge runtime. The Edge runtime does not support all Node.js features, so you may need to adjust your application accordingly.

Cloudflare is cheaper and faster than many other providers, but running your application on Cloudflare Workers means not having access to the vast Node.js ecosystem.

Makerkit uses Cloudflare as a baseline, so you can deploy it to Cloudflare Workers without any issues. However, you will need to keep in mind the limitations of the Edge runtime when adding new features.

One more thing to consider is that the Edge runtime does run close to your users, but may run far from your database. Consider read replicas or other strategies to reduce latency in all situations.

If your mind is set on using Cloudflare, please follow the instructions below.

1. Opting into the Edge runtime

To opt-in to the Edge runtime, you need to do the following: open the root layout file of your app apps/web/app/layout.tsx and export the const runtime as edge:

export const runtime = 'edge';

This will enable the Edge runtime for your application.

2. Using the Cloudflare Mailer

Since the default library nodemailer relies on Node.js, we cannot use it in the Edge runtime. Instead, we will use the Cloudflare Mailer or the Resend Mailer.

To use the Cloudflare Mailer, you need to do the following. Set the MAILER_PROVIDER environment variable to cloudflare in the apps/web/.env file:

MAILER_PROVIDER=cloudflare

Setup SPF and DKIM records in your DNS settings.

Please follow the Vercel Email documentation to set up the SPF and DKIM records.

Alternatively, you can use the Resend Mailer. Set the MAILER_PROVIDER environment variable to resend in the apps/web/.env file:

MAILER_PROVIDER=resend

And provide the Resend API key:

RESEND_API_KEY=your-api-key

3. Installing the Cloudflare CLI

Please follow the instructions on the Cloudflare documentation to install the Cloudflare CLI.

4. Switching CMS

By default, Makerkit uses Keystatic as a CMS. Keystatic's local mode (which relies on the file system) is not supported in the Edge runtime. Therefore, you will need to switch to another CMS.

At this time, the other CMS supported is WordPress. Set CMS_CLIENT to wordpress in the apps/web/.env file:

CMS_CLIENT=wordpress

More alternative CMS implementations will be added in the future.

If you leave Keystatic (or any unsupported CMSs) - it'll deploy, but it won't be able to fetch the content so you'll see a 500 error.

5. Setting Node.js Compatibility Flags

Cloudflare requires you to set the Node.js compatibility flags. Please follow the instructions on the Cloudflare documentation.

Please don't miss this step, as it's crucial for the application to work in the Edge runtime.

Super Admin

The Super Admin panel allows you to manage users and accounts.

To access the super admin panel at /admin, you will need to assign a user as a super admin.

To do so, pick the user ID of the user you want to assign as a super admin and run the following SQL query:

UPDATE auth.users SET raw_app_meta_data = raw_app_meta_data || '{"role": "super-admin"}' WHERE id='<user_id>';

Please replace <user_id> with the user ID you want to assign as a super admin.

How to approach Local Development

Supabase's hosted Studio is pretty great - but I don't think it should be used to perform schema changes. Instead, I recommend using your local Supabase Studio to make the changes and then generate the migration file. Then, you can push the migration to the remote Supabase instance.

At this point, you have two options:

  1. create a migration with pnpm --filter web supabase migration new <name> and update the code manually
  2. or, use the local Supabase Studio to make the changes and then run pnpm --filter web supabase db diff -f <name> which will generate the migration file for you. DOUBLY CHECK THE FILE!

Once you've tested it all and are happy with your local changes, push the migration to the remote Supabase instance with pnpm --filter web supabase db push.

Doing the opposite is also okay - but:

  1. You're making changes against the production database - which is risky
  2. You're not testing the changes locally - which is risky
  3. You need to pull the changes from the remote Supabase instance to your local instance so they are in sync

My two cents - but you do you - anything that works for you is good.

Monitoring

Monitoring is crucial for any application. Makerkit uses Sentry or Baselime for error tracking. Additionally, Makerkit makes use of Next.js experimental instrumentation for performance monitoring.

Sentry

To use Sentry, you need to set the following environment variables:

NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_MONITORING_PROVIDER=sentry

Baselime

To use Baselime, you need to set the following environment variables:

NEXT_PUBLIC_BASELIME_KEY=your_key
NEXT_PUBLIC_MONITORING_PROVIDER=baselime

Next.js Instrumentation

To enable instrumentation, you need to set the following environment variables:

MONITORING_INSTRUMENTATION_ENABLED=true

That's it! Makerkit will take care of the rest.

Capturing Errors

By default, Makerkit captures errors that are caught when rendering the upper error.tsx component. This is a good place to catch errors that are not caught by the error boundary.

If you want to capture errors manually, you have two ways

Capturing Errors using a hook

import { useMonitoring } from '@kit/monitoring/client';

function Component() {
  const { captureException } = useMonitoring();

  const onClick = () => {
    try {
      throw new Error('An error occurred');
    } catch (error) {
      captureException(error);
    }
  };

  return (
    <button onClick={onClick}>Throw an error</button>
  );
}

Alternatively, directly use useCaptureException:

import { useCaptureException } from '@kit/monitoring/client';

function Component() {
  const captureException = useCaptureException();

  const onClick = () => {
    try {
      throw new Error('An error occurred');
    } catch (error) {
      captureException(error);
    }
  };

  return (
    <button onClick={onClick}>Throw an error</button>
  );
}

Server Errors

To capture server errors, you can use the captureException function from the monitoring package:

import { getMonitoringService } from '@kit/monitoring/server';

async function serverSideFunction() {
  try {
    await someAsyncFunction();
  } catch (error) {
    const monitoring = await getMonitoringService();
    
    await monitoring.captureException(error);
  }
}

In the future - this will be automatically captured by the enhanceAction and enhanceRouteHandler functions - so you won't need to do this manually unless you're swallowing the errors in the inner function (in which case, you should rethrow the error or capture manually).

Merging Conflicts

It is very likely that you will encounter merging conflicts when working with the repository.

Generally speaking, when encountering conflicts on the following files, you need to accept any of them but make sure to re-generate them:

  1. pnpm-lock.yaml
  2. database.types.ts

pnpm-lock.yaml

To fix the conflicts, you can run the following command:

pnpm i
  1. The lock file will be regenerated according to the changes in the repository and your local changes.

Database Types

For the database types, reset the supabase local state and run the following command:

pnpm run supabase:web:typegen

This will regenerate the types according to the changes in the repository and your local changes.

For the rest of the files, you need to resolve the conflicts manually.

Going to Production

When you are ready to go to production, please follow the checklist below. This is an overview, a more detailed guide will be provided in the future.

This could take a couple of hours, so buckle up!

  1. Create a Supabase project. Link the project locally using the Supabase CLI (supabase link).
  2. Migrations: Push the migrations to the remote Supabase project (supabase db push).
  3. Auth: Set your APP URL in the Supabase project settings. This is required for the OAuth flow. Make sure to add the path /auth/callback to the allowed URLs. If you don't have it yet, wait.
  4. Auth Providers: Set the OAuth providers in the Supabase project settings. If you use Google Auth, make sure to set it up. This requires creating a Google Cloud project and setting up the OAuth credentials.
  5. Auth Emails: It is very much recommended to update the auth emails using the following documentation. The kit already implements the confirm route, but you need to update the emails in your Supabase settings.
  6. Deploy Next.js: Deploy the Next.js app to Vercel or any another hosting provider. Copy the URL and set it in the Supabase project settings.
  7. Environment Variables: The initial deploy will likely fail because you may not yet have a URL to set in your environment variables. This is normal. Once you have the URL, set the URL in the environment variables and redeploy.
  8. Webhooks: Set the DB Webhooks in Supabase pointing against your Next.js app at /api/db/webhooks.
  9. Emails: Get some SMTP details from an email service provider like SendGrid or Mailgun or Resend and configure the emails in both the Environment Variables and the Supabase project settings.
  10. Billing: Create a Stripe/Lemon Squeezy account, make sure to update the environment variables with the correct values. Point webhooks from these to /api/billing/webhook.
Description
MyEasyCMS v2 — German Club/VHS/Hotel SaaS (Next.js 16 + Supabase + Puck Site Builder)
Readme 76 MiB
Languages
TypeScript 88.4%
PLpgSQL 9.2%
HTML 1.2%
Roff 0.4%
CSS 0.3%
Other 0.3%