---
title: FAQ
description: Frequently asked questions about next-forge.
type: troubleshooting
summary: Answers to commonly asked questions about next-forge.
---

# FAQ



## Why don't you use trpc?

While tRPC is a great tool for building type-safe APIs, Next.js Server Actions provide similar benefits with tighter framework integration. This native solution reduces complexity while maintaining the type safety that makes tRPC attractive. Since Server Actions are part of the framework itself, they're also likely to receive continued optimization and feature improvements directly from the Next.js team.

## Why did you pick X as the default tool instead of Y?

The default next-forge tooling was chosen by [myself](https://x.com/haydenbleasel) after having used them in numerous production applications. It doesn't mean they're the best tools, it means they're the ones that helped me launch quickly. Tools and tastes change over time and it's inevitable that the defaults will change at some point.

That being said, if you really believe tool Y is a much better choice than tool X, feel free to start up a conversation with me on X and we can hash it out!

## Why is there a `suppressHydrationWarning` on every `html` tag?

This is the recommendation by `next-themes` to supress the warning stemming from determining theme on the client side.

## Why are there unused dependencies like `import-in-the-middle`?

> Without these packages, Turbopack throws warnings highlighting cases where packages are being used but are not installed so it can fail when running in production.
>
> This was already an issue when we were using Webpack, it just never warned us that it was missing. This can be fixed by installing the external packages into the project itself.

— [@timneutkens](https://github.com/vercel/next-forge/pull/170#issuecomment-2459255583)

## Why are certain folders ignored by the linting configuration?

There are three types of files that are ignored by the linting configuration:

1. **shadcn/ui components, libraries, and hooks** - shadcn/ui has its own linting configuration that is less strict than the one used in this project. As such, we ignore these files to avoid modifying them. This makes it easier to update shadcn/ui in the future.
2. **Collaboration package configuration** - This is a package that is used to configure the collaboration package. The types are stubs and fail some of the linting rules, but it's not important unless you're using them.
3. **Internal documentation files** - These are the documentation files for this project. They're deleted when you initialize the project, so they're not important to lint.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Overview
description: What is next-forge and how do I get started?
type: overview
summary: An introduction to next-forge and how to get started.
related:
  - /docs/setup/quickstart
  - /docs/philosophy
  - /docs/structure
---

# Overview



next-forge is a production-grade [Turborepo](https://turborepo.com) template for [Next.js](https://nextjs.org/) apps. It is designed to be a comprehensive starting point for new apps, providing a solid, opinionated foundation with a minimal amount of configuration.

It is a culmination of my experience building web apps over the last decade and is designed to help you build your new SaaS app as thoroughly as possible, balancing speed and quality.

## Demo

We have a demo version of next-forge that you can use to get a feel for the project!

Here are the URLs:

* [Web](https://demo.next-forge.com)
* [App](https://app.demo.next-forge.com)
* [Storybook](https://storybook.demo.next-forge.com)
* [API](https://api.demo.next-forge.com/health)

## Contributing

We welcome contributions from the community! Please see the [contributing guide](https://github.com/vercel/next-forge/blob/main/.github/CONTRIBUTING.md) for more information. You can also check out these links to join the conversation.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Philosophy
description: Guiding principles for the development of next-forge.
type: conceptual
summary: The guiding principles behind next-forge's design decisions.
related:
  - /docs/structure
---

# Philosophy



### Fast

Your project should be **fast**. This doesn't just mean fast to build, run and deploy. It also means it should be fast to validate ideas, iterate and scale. This is critical for front-loading the important parts of starting a startup: finding product-market fit, iterating on the core concept and scaling to customers.

### Cheap

Your project should be **cheap** and preferably free to start. You shouldn't be spending money on tools and infrastructure if you don't have customers yet. It should avoid a flat cost, or have a generous free tier. You should aim to make your project self-sustaining, with the goal of avoiding any recurring costs upfront and finding services that scale with you.

### Opinionated

Your project should be **opinionated**. This means that the tooling should be designed to work together, and the project should be designed to work with the tooling. This is important for reducing friction and increasing productivity.

### Modern

Your project should be **modern**. This means that the tooling should be actively maintained, and the project should be designed to take advantage of the latest features. This is important for reducing technical debt and increasing longevity. This doesn't mean you should use bleeding edge or experimental tooling, but instead should use the latest stable versions of modern tools with healthy communities and long-term support.

### Safe

Your project should be **safe**. Practically, this means end-to-end type safety, securely handling secrets and using platforms that offer robust security posture.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Structure
description: Learn how next-forge apps and packages are structured.
type: conceptual
summary: How the monorepo, apps, and packages are organized.
related:
  - /docs/philosophy
---

# Structure



next-forge is a monorepo, which means it contains multiple packages in a single repository. This is a common pattern for modern web applications, as it allows you to share code between different parts of the application, and manage them all together.

The monorepo is managed by [Turborepo](https://turborepo.com), which is a tool for managing monorepos. It provides a simple way to manage multiple packages in a single repository, and is designed to work with modern web applications.

## Apps

next-forge contains a number of [apps](/docs/apps/api) that make up your project. Each app is a self-contained application that can be deployed independently.

While you can choose to run these apps on the subdomain of your choice, the recommended subdomains are listed on each page. Remember to add them to your [environment variables](/docs/setup/env) under `NEXT_PUBLIC_APP_URL`, `NEXT_PUBLIC_WEB_URL`, and `NEXT_PUBLIC_DOCS_URL`.

Each app should be self-contained and not depend on other apps. They should have an `env.ts` file at the root of the app that composes the environment variables from the packages it depends on.

## Packages

next-forge contains a number of shared packages that are used across the monorepo. The purpose of these packages is to isolate shared code from the main app, making it easier to manage and update.

Additionally, it makes it easier to swap out parts of the app for different implementations. For example, the `database` package contains everything related to the database, including the schema and migrations. This allows us to easily swap out the database provider or ORM without impacting other parts of the app.

Each package should be self-contained and not depend on other packages. They should export everything that is needed by the app — middleware, hooks, components and even the [environment variables](/docs/setup/env).

## Boundaries

next-forge uses [Turborepo's boundaries](https://turborepo.com/docs/reference/boundaries) to ensure that Turborepo features work correctly by checking for package manager Workspace violations.

You can run `bun run boundaries` to check for any violations.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Updates
description: Built-in helpers to help you keep your project up to date.
type: guide
summary: How to keep your next-forge project up to date.
prerequisites:
  - /docs/setup/installation
---

# Updates



## Upgrading next-forge

As next-forge evolves, you may want to stay up to date with the latest changes. This can be difficult to do manually, so we've created a script to help you.

```sh title="Terminal"
npx next-forge@latest update
```

This will run our update script, which will guide you through the process of updating your project.

```
┌  Let's update your next-forge project!
│
│
◆  Select a version to update to:
│  ● v3.2.15
│  ○ v3.2.14
│  ○ v3.2.13
│  ○ v3.2.12
│  ○ v3.2.11
└
```

This will clone the latest version of next-forge into a temporary directory, apply the updates, and then copy the files over to your project. From here, you can commit the changes and push them to your repository.

<Tip>
  Because next-forge is a boilerplate and not a library, you'll likely need to manually merge the changes you've made with the changes from the update.
</Tip>

## Upgrading dependencies

You can upgrade all the dependencies in all your `package.json` files and installs the new versions with the `bump-deps` command:

```sh title="Terminal"
bun run bump-deps
```

This will update all the dependencies in your `package.json` files and install the new versions.

<Tip>
  You should run a 

  `bun run build`

   after running 

  `bump-deps`

   to ensure the project builds correctly. You should also run 

  `bun dev`

   and ensure the project runs correctly in runtime.
</Tip>

## Upgrading shadcn/ui components

You can upgrade all the shadcn/ui components in the [Design System](/docs/packages/design-system/components) package with the `bump-ui` command:

```sh title="Terminal"
bun run bump-ui
```

This will update all the shadcn/ui components, as well as the relevant dependencies in the Design System's `package.json` file.

<Warning>
  This will override all customization you've made to the components. To avoid this happening, we recommend proxying the components into a new folder, such as 

  `@repo/design-system/components`

  .
</Warning>

<Warning>
  The 

  `shadcn`

   CLI will likely make some unwanted changes to your shared Tailwind config file and global CSS. Make sure you review changes before committing them.
</Warning>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: API
description: How the "API" application works in next-forge.
product: API
type: reference
summary: How the API application works in next-forge.
related:
  - /docs/packages/security/rate-limiting
  - /docs/packages/webhooks/inbound
---

# API



<Tip>
  The 

  `api`

   application runs on port 3002. We recommend deploying it to 

  `api.{yourdomain}.com`

  .
</Tip>

next-forge exports the API from the `apps/api` directory. It is designed to be run separately from the main app, and is used to run isolate functions that are not part of the main user-facing application e.g. webhooks, cron jobs, etc.

## Overview

The API is designed to run serverless functions, and is not intended to be used as a traditional Node.js server. However, it is designed to be as flexible as possible, and you can switch to running a traditional server if you need to.

Functionally speaking, splitting the API from the main app doesn't matter if you're running these projects on Vercel. Serverless functions are all independent pieces of infrastructure and can scale independently. However, having it run independently provides a dedicated endpoint for non-web applications e.g. mobile apps, smart home devices, etc.

## Features

* **Cron jobs**: The API is used to run [cron jobs](/docs/packages/cron). These are defined in the `apps/api/app/cron` directory. Each cron job is a `.ts` file that exports a route handler.
* **Webhooks**: The API is used to run [inbound webhooks](/docs/packages/webhooks/inbound). These are defined in the `apps/api/app/webhooks` directory. Each webhook is a `.ts` file that exports a route handler.

## Connecting to the API

By default, the API app only handles webhooks and cron jobs. However, you can add your own endpoints for use by the `app`, `web`, or external clients like mobile apps.

### When you need the API

In many cases, you don't need to call the API app at all. Since `app` and `web` are both Next.js applications, they can use [Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components), [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations), and [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) to access the database and other services directly through shared packages like `@repo/database`.

The API app is useful when you need:

* A dedicated endpoint for non-web clients (mobile apps, IoT devices, third-party integrations)
* A public REST API for your platform
* Long-running or resource-intensive operations isolated from your user-facing apps

### Adding an endpoint

Create a new route handler in `apps/api/app`. For example, to create a `/users` endpoint:

```ts title="apps/api/app/users/route.ts"
import { database } from '@repo/database';

export const GET = async () => {
  const users = await database.user.findMany();

  return Response.json(users);
};
```

### Calling the API from another app

Each app has a `NEXT_PUBLIC_API_URL` environment variable pre-configured in its `.env.example` file, pointing to `http://localhost:3002` for local development. Use this variable when making requests to the API.

From a Server Component or Server Action:

```ts title="apps/app/app/actions/users.ts"
'use server';

import { env } from '@/env';

export const getUsers = async () => {
  const response = await fetch(`${env.NEXT_PUBLIC_API_URL}/users`);

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

From a client component:

```tsx title="apps/app/components/users.tsx"
'use client';

const Users = () => {
  const fetchUsers = async () => {
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_API_URL}/users`
    );

    return response.json();
  };

  // ...
};
```

### Preview deployments

In local development, the API URL defaults to `http://localhost:3002`. In production, you set `NEXT_PUBLIC_API_URL` to your API's production URL (e.g. `https://api.yourdomain.com`).

For preview deployments on Vercel, each project gets a unique URL. Since the `app` or `web` preview can't automatically discover the `api` preview URL, you have a few options:

1. **Point previews at the production API.** Set `NEXT_PUBLIC_API_URL` to your production API URL in Vercel's environment variable settings for "Preview" environments. This is the simplest approach and works well if your API is stable.
2. **Use Vercel's branch-based URLs.** Vercel generates deterministic URLs based on the branch name (e.g. `api-git-my-branch-yourteam.vercel.app`). You can construct the API URL from the `VERCEL_GIT_COMMIT_REF` environment variable if all apps share the same repository and branch.
3. **Set the URL manually per preview.** For full isolation, override `NEXT_PUBLIC_API_URL` in the Vercel deployment settings for each preview deployment.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: App
description: How the main application works in next-forge.
product: App
type: reference
summary: How the main user-facing application works in next-forge.
related:
  - /docs/packages/authentication
  - /docs/packages/database
  - /docs/packages/design-system/components
---

# App



<Tip>
  The 

  `app`

   application runs on port 3000. We recommend deploying it to 

  `app.{yourdomain}.com`

  .
</Tip>

next-forge exports the main app from the `apps/app` directory. It is designed to be run on a subdomain of your choice, and is used to run the main user-facing application.

## Overview

The `app` application is the main user-facing application built on [Next.js](https://nextjs.org). It is designed to be a starting point for your own unique projects, containing all the core functionality you need.

## Features

* **Design System**: The app is connected to the [Design System](/docs/packages/design-system/components) and includes a variety of components, hooks, and utilities to help you get started.
* **Authentication**: The app includes a fully-featured [authentication system](/docs/packages/authentication) with support for email login. You can easily extend it to support other providers and authentication methods. The app is also broken into authenticated and unauthenticated route groups.
* **Database**: The app is connected to the [Database](/docs/packages/database) and can fetch data in React Server Components.
* **Collaboration**: The app is connected to the [Collaboration](/docs/packages/collaboration) and contains Avatar Stack and Live Cursor components.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Documentation
description: How the documentation is configured in next-forge.
product: Docs
type: reference
summary: How the documentation application is configured.
related:
  - /docs/packages/cms/overview
---

# Documentation



<Tip>
  The 

  `docs`

   application runs on port 3004. We recommend deploying it to 

  `docs.{yourdomain}.com`

  .
</Tip>

next-forge uses [Mintlify](https://mintlify.com) to generate beautiful docs. Each page is a `.mdx` file, written in Markdown, with built-in UI components and API playground.

## Creating a new page

To create a new documentation page, add a new MDX file to the `apps/docs` directory. The file name will be used as the slug for the page and the frontmatter will be used to generate the docs page. For example:

```mdx title="apps/docs/hello-world.mdx"
---
title: 'Quickstart'
description: 'Start building modern documentation in under five minutes.'
---
```

Learn more supported [meta tags](https://mintlify.com/docs/page).

## Adding a page to the navigation

To add a page to the sidebar, you'll need to define it in the `mint.json` file in the `apps/docs` directory. From the previous example, here's how you can add it to the sidebar:

```mdx title="mint.json {2-5}"
"navigation": [
  {
    "group": "Getting Started",
    "pages": ["hello-world"]
  },
  {
    // ...
  }
]
```

## Advanced

You can build the docs you want with advanced features.

<Card title="Global Settings" icon="wrench" href="https://mintlify.com/docs/settings/global" horizontal>
  Customize your documentation using the mint.json file
</Card>

<Card title="Components" icon="shapes" href="https://mintlify.com/docs/content/components" horizontal>
  Explore the variety of components available
</Card>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Email
description: How email templates work in next-forge
product: Email
type: reference
summary: How the email preview application works.
related:
  - /docs/packages/email
---

# Email



<Tip>
  The 

  `email`

   application runs on port 3003.
</Tip>

next-forge comes with [`react.email`](https://react.email/) built in, allowing you to create and send beautiful emails using React and TypeScript.

`react.email` has a preview server, so you can preview the emails templates in the browser.

To preview the emails templates, simply run the `email` app:

```sh title="Terminal"
bun dev --filter email
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Storybook
description: Frontend workshop for the design system
product: Storybook
type: reference
summary: How the Storybook design system workshop works.
related:
  - /docs/packages/design-system/components
---

# Storybook



<Tip>
  The 

  `storybook`

   application runs on port 6006.
</Tip>

next-forge uses [Storybook](https://storybook.js.org/) as a frontend workshop for the design system. It allows you to interact with the components in the design system, and see how they behave in different states.

## Configuration

By default, Storybook is configured with every component from [shadcn/ui](https://ui.shadcn.com/), and allows you to interact with them. It is also configured with the relevant fonts and higher-order components to ensure a consistent experience between your application and Storybook.

## Running the workshop

Storybook will start automatically when you run `bun dev`. You can also start it independently with `bun dev --filter storybook`. The preview will be available at [localhost:6006](http://localhost:6006).

## Adding stories

You can add your own components to the workshop by adding them to the `apps/storybook/stories` directory. Each component should have its own `.stories.tsx` file.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Studio
description: Visualize and edit your database in a UI.
product: Studio
type: reference
summary: How the database studio application works.
related:
  - /docs/packages/database
---

# Studio



<Tip>
  The 

  `studio`

   application runs on port 3005.
</Tip>

next-forge includes Prisma Studio, which is a visual editor for your database. To start it, run the following command:

```sh title="Terminal"
bun dev --filter studio
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Web
description: How the website application works in next-forge.
product: Web
type: reference
summary: How the marketing website application works.
related:
  - /docs/packages/seo/metadata
  - /docs/packages/cms/overview
---

# Web



<Tip>
  The 

  `web`

   application runs on port 3001. We recommend deploying it to 

  `www.{yourdomain}.com`

  .
</Tip>

next-forge comes with a default website application, which is located in the `apps/web` folder.

## Overview

It's built on Next.js and Tailwind CSS, with some example pages scaffolded using [TWBlocks](https://www.twblocks.com/). It's designed for you to customize and extend to your needs, whether that means keeping the default pages or replacing them with your own.

## Features

* **Design System**: The app is connected to the [Design System](/docs/packages/design-system/components) and includes a variety of components, hooks, and utilities to help you get started.
* **CMS**: The app is connected to the [CMS](/docs/packages/cms/overview) package to power your type-safe blog.
* **SEO**: The app is connected to the [SEO](/docs/packages/seo/metadata) package which optimizes the site for search engines.
* **Analytics**: The app is connected to the [Analytics](/docs/packages/analytics/product) package to track visitor behavior.
* **Observability**: The app is connected to the [Observability](/docs/packages/observability/error-capture) package to track errors and performance.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: c15t
description: How to add privacy consent management to your app with c15t.
type: integration
summary: How to add consent management with c15t.
---

# c15t



## Overview

c15t is an open-source consent management platform that transforms privacy consent from a compliance checkbox into a fully observable system. It provides a TypeScript-first SDK with automatic jurisdiction detection, a customizable consent banner, and support for managing third-party scripts based on user consent.

## Quickstart

```
npx @c15t/cli generate
```

## Installation

Install the c15t Next.js package in the app(s) that need consent management:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @c15t/nextjs
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @c15t/nextjs
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @c15t/nextjs
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @c15t/nextjs
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Setup

### 1. Create the provider

```tsx title="components/consent-manager/provider.tsx"
'use client';

import { type ReactNode } from 'react';
import {
  ConsentManagerProvider,
  CookieBanner,
  ConsentManagerDialog,
} from '@c15t/nextjs/client';

export default function ConsentManager({
  children,
}: { children: React.ReactNode }) {
  return (
    <ConsentManagerProvider
      options={{
        mode: 'c15t',
        backendURL: '/api/c15t',
        consentCategories: ['necessary', 'measurement', 'marketing'],
      }}
    >
      <CookieBanner />
      <ConsentManagerDialog />
      {children}
    </ConsentManagerProvider>
  );
}
```

<Tip>
  For local development or prototyping, you can use `mode: 'offline'` instead of `mode: 'c15t'` to store consent in cookies without a backend.
</Tip>

### 2. Add to your root layout

Wrap your app with the `ConsentManager` in your root layout:

```tsx title="app/layout.tsx"
import { ConsentManager } from '@/components/consent-manager';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <ConsentManager>
          {children}
        </ConsentManager>
      </body>
    </html>
  );
}
```

### 3. Configure Next.js rewrites (optional)

To optimize c15t further you can proxy c15t API requests through your Next.js server, which improves latency and reduces risk of blocking via an ad-blocker.

```ts title="next.config.ts"
import type { NextConfig } from 'next';

const config: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/c15t/:path*',
        destination: `${process.env.NEXT_PUBLIC_C15T_URL}/:path*`,
      },
    ];
  },
};

export default config;
```

## Managing scripts

c15t can conditionally load third-party scripts based on user consent. Pass a `scripts` array to the provider options:

```tsx
import { googleTagManager } from "@c15t/scripts/google-tag-manager"
import { metaPixel } from "@c15t/scripts/meta-pixel"
 
<ConsentManagerProvider
  options={{
    mode: 'c15t',
    backendURL: '/api/c15t',
    scripts: [
      googleTagManager({ id: 'GTM-XXXXXXX' }),
      metaPixel({ pixelId: '123456789012345' }),
      {
        id: 'example',
        src: 'https://analytics.example.com/script.js',
        category: 'measurement',
      },
    ],
  }}
>
```

<Tip>
  Check the [c15t integrations docs](https://c15t.com/docs/integrations/overview) for pre-built helpers for popular services like Google Tag Manager, PostHog, and more.
</Tip>

For more information and detailed documentation, visit the [c15t docs](https://c15t.com/docs/frameworks/next/quickstart).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Dub
description: How to add link tracking to your app with Dub.
type: integration
summary: How to add link tracking and analytics with Dub.
---

# Dub







While next-forge does not come with link tracking and analytics out of the box, you can easily add it to your app with [Dub](https://dub.co/).

## Overview

Dub is an open-source link tracking and analytics platform that allows you to track the performance of your links and see how they're performing. It comes with a suite of features that make it a great choice for marketing teams, including link shortening, custom domains, branded QR codes, and more.

## Signing up

You can sign up for a Dub account [on their website](https://app.dub.co/register).

<img alt="/images/dub-register.png" src={__img0} placeholder="blur" />

## Creating a link

Once you've signed up, you can create a link by clicking the "Create Link" button in the top right corner.

<img alt="/images/dub-create.png" src={__img1} placeholder="blur" />

## Adding link tracking to your app

From here, simply replace all `href` values with the Dub link!

```tsx
<a href="https://dub.co/example">Example</a>
<Link href="https://dub.co/example">Example</Link>
```

## Interfacing programmatically

Dub provides a simple SDK for creating links, managing customers, tracking leads and more. You can install it with:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install dub
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add dub
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add dub
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add dub
    ```
  </CodeBlockTab>
</CodeBlockTabs>

For more information on the SDK, you can refer to the [official documentation](https://dub.co/docs/api-reference/introduction).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Fuse.js
description: A powerful, lightweight fuzzy-search library, with zero dependencies.
type: integration
summary: How to add fuzzy search with Fuse.js.
---

# Fuse.js



### Installation

To install `fuse.js`, simply run the following command:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install fuse.js
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add fuse.js
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add fuse.js
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add fuse.js
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Usage

Here is an example of how to use `fuse.js` for searching through an array of objects:

```tsx title="search.ts"
import Fuse from 'fuse.js';

const data = [
  { id: 1, name: 'John Doe', email: 'john.doe@example.com' },
  { id: 2, name: 'Jane Doe', email: 'jane.doe@example.com' },
];

const fuse = new Fuse(data, {
  keys: ['name', 'email'],
  minMatchCharLength: 1,
  threshold: 0.3,
});

const results = fuse.search('john');

console.log(results);
```

### Benefits

* `fuse.js` is easy to use and has a simple API.
* **Performant**: `fuse.js` is performant and has zero dependencies.

For more information and detailed documentation, visit the [`fuse.js` GitHub repo](https://github.com/krisk/fuse).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Joyful
description: Generate delightful, random word combinations for your app — perfect for project names, usernames, or unique identifiers.
type: integration
summary: How to generate friendly random words for project names.
---

# Joyful



### Installation

To install `joyful`, simply run the following command:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install joyful
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add joyful
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add joyful
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add joyful
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Usage

Here is an example of how to use `joyful` for generating friendly words:

```tsx title="get-project-name.ts"
import { joyful } from "joyful";

const words = joyful(); // "amber-fox"
const words = joyful({ segments: 3 }); // "golden-marble-cathedral"
const words = joyful({ segments: 3, separator: "_" }); // "swift_northern_lights"
const words = joyful({ maxLength: 8 }); // "tan-elk"
```

### Benefits

* **Easy to Use**: `joyful` is easy to use and generates friendly words with a simple API.
* **Customizable**: You can customize the number of segments and the separator.

For more information and detailed documentation, visit the [`joyful` GitHub repo](https://github.com/haydenbleasel/joyful).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Metabase
description: How to add business intelligence and analytics to your app with Metabase.
type: integration
summary: How to add embedded business intelligence with Metabase.
---

# Metabase





While next-forge doesn't include BI tooling out of the box, you can easily add business intelligence and analytics to your app with [Metabase](https://www.metabase.com).

Try it locally or in the cloud:

<div className="block -mt-6">
  <a href="https://www.metabase.com/start/oss" className="block -mb-6">
    <img src="https://img.shields.io/badge/Self--host-Metabase-blue?logo=metabase" alt="Self-host Metabase" />
  </a>

  <a href="https://metabase.com/start" className="block -mt-6">
    <img src="https://img.shields.io/badge/Try%20Cloud-Metabase-brightgreen?logo=metabase" alt="Try Metabase Cloud" />
  </a>
</div>

## Overview

Metabase is an open-source business intelligence platform. You can use Metabase to ask questions about your data, or embed Metabase in your app to let your customers explore their data on their own.

## Installing Metabase

Metabase provides an official Docker image via Docker Hub that can be used for deployments on any system that is running Docker. Here's a one-liner that will start a container running Metabase:

```sh
docker run -d --name metabase -p 3000:3000 metabase/metabase
```

For full installation instructions:

* [Docker Documentation](https://www.metabase.com/docs/latest/installation-and-operation/running-metabase-on-docker)
* [Jar File Documentation](https://www.metabase.com/docs/latest/installation-and-operation/running-the-metabase-jar-file)

## Database Connection

By default, next-forge uses Neon as its database provider. Metabase works seamlessly with Postgres. To connect, you'll need:

* The `hostname` of the server where your database lives
* The `port` the database server uses
* The `database name`
* The `username` you use for the database
* The `password` you use for the database

You can find these details in your `DATABASE_URL`:

```js
DATABASE_URL="postgresql://[username]:[password]@[hostname]:[port]/[database_name]?sslmode=require"
```

Then plug your database connection credentials into Metabase:

<img alt="/images/metabase-add-database.png" src={__img0} placeholder="blur" />

Metabase supports over 20 databases. For other database options, see [Metabase Database Documentation](https://www.metabase.com/docs/latest/databases/connecting).

## Asking Questions and Building Dashboards

Once connected, you can start asking [Questions](https://www.metabase.com/docs/latest/questions/query-builder/introduction) and building [Dashboards](https://www.metabase.com/docs/latest/dashboards/introduction).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Motion
description: A library for animating React components with ease.
type: integration
summary: How to add animations with the Motion library.
---

# Motion



<Tip>
  Motion was formerly known as Framer Motion.
</Tip>

### Installation

To install Motion, simply run the following command:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install motion
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add motion
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add motion
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add motion
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Usage

Here is an example of how to use Motion to animate a component:

```tsx title="my-component.tsx"
import { motion } from 'motion';

function MyComponent() {
  return (
    <motion.div animate={{ x: 100 }}>This is a component that is animated.</motion.div>
  );
}
```

### Benefits

* **Easy Animation**: Motion makes it easy to animate components with a simple and intuitive API.
* **Customization**: Motion allows you to customize animations to your needs, providing a high degree of control over the animation process.
* **Performance**: Motion is performant and has minimal impact on your application's performance.

For more information and detailed documentation, visit the [Motion website](https://motion.dev/).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Next Safe Action
description: A powerful library for managing and securing your Next.js Server Actions.
type: integration
summary: How to add type-safe server actions with next-safe-action.
---

# Next Safe Action



## Installation

To install Next Safe Action, simply run the following command:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install next-safe-action zod --filter app
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add next-safe-action zod --filter app
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add next-safe-action zod --filter app
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add next-safe-action zod --filter app
    ```
  </CodeBlockTab>
</CodeBlockTabs>

By default, Next Safe Action uses Zod to validate inputs, but it also supports adapters for Valibot, Yup, and Typebox.

## Basic Usage

Here is a basic example of how to use Next Safe Action to call your Server Actions:

### Server Action

```ts title="action.ts"
"use server"

import { createSafeActionClient } from "next-safe-action";
import { z } from "zod";

export const serverAction = createSafeActionClient()
  .schema(
    z.object({
      name: z.string(),
      id: z.string()
    })
  )
  .action(async ({ parsedInput: { name, id } }) => {
    // Fetch data in server
    const data = await fetchData(name, id);
    
    // Write server logic here ...
    
    // Return here the value to the client
    return data;
  });
```

### Client Component

```tsx title="my-component.tsx"
"use client"

import { serverAction } from "./action"
import { useAction } from "next-safe-action/hooks";
import { toast } from "@repo/design-system/components/ui/sonner";

function MyComponent() {
  const { execute, isPending } = useAction(serverAction, {
    onSuccess() {
      // Display success message to client
      toast.success("Action Success");
    },
    onError({ error }) {
      // Display error message to client
      toast.error("Action Failed");
    },
  });

  const onClick = () => {
    execute({ name: "next-forge", id: "example" });
  };

  return (
    <div>
      <Button disabled={isPending} onClick={onClick}>
        Click to call action
      </Button>
    </div>
  );
}
```

In this example, we create an action with input validation on the server, and call it on the client to with type-safe inputs and convinient callback utilities to simplify state management and error handling.

## Benefits

* **Simplified State Management**: Next Safe Action simplifies server action state management by providing callbacks and status utilities.
* **Type-safe**: By using Zod or other validation libraries, your inputs are type-safe and validated end-to-end.
* **Easy Integration**: Next Safe Action is extremely easy to integrate, and you can incrementally use more of its feature like optimistic updates and middlewares.

For more information and detailed documentation, visit the [Next Safe Action website](https://next-safe-action.dev).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: NUQS
description: A powerful library for managing URL search parameters in your application. It provides a simple and efficient way to handle state management through URL search parameters.
type: integration
summary: How to add type-safe URL search parameter management with nuqs.
---

# NUQS



### Installation

To install NUQS, simply run the following command:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install nuqs
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add nuqs
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add nuqs
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add nuqs
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Usage

Here is an example of how to use NUQS for URL search parameter state management:

```tsx title="my-component.tsx"
import { useQueryState, parseAsString } from 'nuqs';

function MyComponent() {
  const [query, setQuery] = useQueryState('query', parseAsString.withDefault(''));

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <p>Search Query: {query}</p>
    </div>
  );
}
```

In this example, the `useQueryState` hook from nuqs is used to manage a single URL search parameter with type-safe parsing. The `setQuery` function updates the `query` URL parameter whenever the input value changes.

### Benefits

* **Simplified State Management**: NUQS simplifies state management by using URL search parameters, making it easy to share and persist state across different parts of your application.
* **SEO-Friendly**: By using URL search parameters, NUQS helps improve the SEO of your application by making the state accessible through the URL.
* **Easy Integration**: NUQS is easy to integrate into your existing React application, providing a seamless experience for managing URL search parameters.

For more information and detailed documentation, visit the [NUQS website](https://nuqs.47ng.com/).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: React Wrap Balancer
description: A simple React component that makes titles more readable
type: integration
summary: How to add balanced text wrapping with React Wrap Balancer.
---

# React Wrap Balancer



### Installation

To install `react-wrap-balancer`, simply run the following command:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install react-wrap-balancer
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add react-wrap-balancer
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add react-wrap-balancer
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add react-wrap-balancer
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Usage

Here is an example of how to use `react-wrap-balancer` to make titles more readable:

```tsx title="my-component.tsx"
import { Balancer } from 'react-wrap-balancer';

function MyComponent() {
  return (
    <div>
      <Balancer>This is a title that is too long to fit in one line.</Balancer>
    </div>
  );
}
```

### Benefits

* **Improved Readability**: `react-wrap-balancer` makes titles more readable by automatically wrapping them at the appropriate breakpoints.
* **Easy Integration**: `react-wrap-balancer` is easy to integrate into your existing React application, providing a seamless installation experience.

For more information and detailed documentation, visit the [react-wrap-balancer website](https://react-wrap-balancer.vercel.app/).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Trunk
description: How to add Trunk's merge queue and flaky test detection to your next-forge project.
type: integration
summary: How to add a merge queue and flaky test detection with Trunk.
---

# Trunk



[Trunk](https://trunk.io) provides a merge queue and flaky test detection for GitHub repositories. This guide covers setting up both features with your next-forge project.

## Setup

Create a Trunk account at [app.trunk.io](https://app.trunk.io) and connect your GitHub repository.

## Merge Queue

[Trunk Merge Queue](https://trunk.io/merge-queue) tests PRs against the predicted state of the target branch before merging, including PRs ahead of yours in the queue.

### Update CI workflow triggers

The merge queue creates `trunk-merge/**` branches to test PR combinations. Your CI workflows need to run on these branches:

```yaml title=".github/workflows/test.yml"
on:
  pull_request:
    branches: [main]
  push:
    branches:
      - main
      - 'trunk-merge/**'
```

Apply the same change to any other workflows that must pass before merging.

### Configure branch protection

In your GitHub repository settings under **Branches > Branch protection rules** for `main`:

* Allow the `trunk-io` bot to push to your protected branch
* **Disable** "Require branches to be up to date before merging"
* Ensure `trunk-temp/*` and `trunk-merge/*` branches are **not** blocked by wildcard protection rules

### Guard main-only steps

Steps that should only run on actual merges to `main` (not queue test branches) need a condition:

```yaml
- name: Create Release
  if: github.ref == 'refs/heads/main'
  run: npx auto shipit
```

### Usage

Submit PRs to the queue by either:

* Checking the box in the Trunk bot's PR comment
* Commenting `/trunk merge` on the PR

For more information, visit the [Trunk Merge Queue documentation](https://docs.trunk.io/merge-queue).

## Flaky Tests

[Trunk Flaky Tests](https://trunk.io/flaky-tests) tracks your test results over time and identifies tests with inconsistent pass/fail behavior. It ingests JUnit XML reports uploaded from CI.

### Adding JUnit reporters

Add the JUnit reporter to each Vitest config. In `apps/app/vitest.config.mts` and `apps/api/vitest.config.mts`:

```ts title="apps/app/vitest.config.mts"
export default defineConfig({
  // ...existing config
  test: {
    environment: "jsdom",
    reporters: [
      "default",
      ["junit", { outputFile: "./junit.xml", addFileAttribute: true }],
    ],
  },
});
```

<Tip>
  Disable automatic test retries in Vitest, as retries compromise flaky test detection accuracy.
</Tip>

### Uploading results from CI

Add the upload step after your test command. Use `if: always()` so results are uploaded even when tests fail:

```yaml title=".github/workflows/test.yml"
- name: Run tests
  run: bun run test
  continue-on-error: true

- name: Upload test results to Trunk
  if: always()
  uses: trunk-io/analytics-uploader@v1
  with:
    junit-paths: '**/junit.xml'
    org-slug: $\{{ vars.TRUNK_ORG_SLUG }}
    token: $\{{ secrets.TRUNK_API_TOKEN }}
```

## Required secrets

Add the following to your GitHub repository:

* **`TRUNK_API_TOKEN`** — API token from [Trunk organization settings](https://app.trunk.io)
* **`TRUNK_ORG_SLUG`** — Your Trunk organization slug (can be a repository variable)

For more information, visit the [Trunk Flaky Tests documentation](https://docs.trunk.io/flaky-tests).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Zustand
description: A small, fast, and scalable bearbones state-management solution for React applications. It provides a simple and efficient way to manage state in your application.
type: integration
summary: How to add client-side state management with Zustand.
---

# Zustand



### Installation

To install Zustand, run the following command:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install zustand
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add zustand
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add zustand
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add zustand
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Usage

Here is an example of how to use Zustand for state management:

```tsx title="counter.tsx"
import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

function Counter() {
  const { count, increment, decrement } = useStore();

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}
```

In this example, the `create` function from Zustand is used to create a store with a `count` state and `increment` and `decrement` actions. The `useStore` hook is then used in the `Counter` component to access the state and actions.

### Benefits

* **Simple API**: Zustand provides a simple and intuitive API for managing state in your React application.
* **Performance**: Zustand is optimized for performance, making it suitable for large-scale applications.
* **Scalability**: Zustand is scalable and can be used to manage state in applications of any size.

For more information and detailed documentation, visit the [Zustand website](https://zustand.docs.pmnd.rs/guides/nextjs).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Deploying with Docker
description: How to deploy next-forge with Docker for self-hosting.
type: guide
summary: How to deploy next-forge with Docker for self-hosting.
prerequisites:
  - /docs/setup/env
related:
  - /docs/deployment/vercel
  - /docs/deployment/netlify
---

# Deploying with Docker



Docker lets you self-host next-forge on any platform that supports containers — such as [Railway](https://railway.app), [Fly.io](https://fly.io), [Coolify](https://coolify.io), [DigitalOcean App Platform](https://www.digitalocean.com/products/app-platform), or your own server.

## Enable standalone output

First, you'll need to enable Next.js [standalone output](https://nextjs.org/docs/app/api-reference/config/next-config-js/output#automatically-copying-traced-files) in your shared config. This creates a self-contained build that includes only the files needed to run your app, significantly reducing the final image size.

In `packages/next-config/index.ts`, add the `output` property:

```ts title="packages/next-config/index.ts"
export const config: NextConfig = {
  output: "standalone",

  // ... rest of your config
};
```

## Create a Dockerfile

Create a `Dockerfile` in the root of your repository. This uses a multi-stage build to keep the final image small:

```dockerfile title="Dockerfile"
FROM oven/bun:1 AS base

# Stage 1: Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json bun.lock turbo.json ./
COPY apps/ ./apps/
COPY packages/ ./packages/
RUN bun install --frozen-lockfile

# Stage 2: Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/ ./

# Set the app to build: app, web, or api
ARG APP_NAME=app
ENV APP_NAME=$APP_NAME

# Add build-time environment variables here
# ARG DATABASE_URL
# ENV DATABASE_URL=$DATABASE_URL

RUN bun run build --filter=@repo/${APP_NAME}

# Stage 3: Production runner
FROM node:20-slim AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

# Set the app to run
ARG APP_NAME=app
ENV APP_NAME=$APP_NAME

COPY --from=builder /app/apps/${APP_NAME}/public ./apps/${APP_NAME}/public
COPY --from=builder --chown=nextjs:nodejs /app/apps/${APP_NAME}/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/${APP_NAME}/.next/static ./apps/${APP_NAME}/.next/static

USER nextjs

EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "apps/${APP_NAME}/server.js"]
```

<Callout type="info">
  The `CMD` above uses a shell-interpolated variable. If your container runtime doesn't support this, replace `${APP_NAME}` with the actual app name e.g. `node apps/app/server.js`.
</Callout>

## Create a .dockerignore

Add a `.dockerignore` to speed up builds and keep secrets out of the image:

```txt title=".dockerignore"
node_modules
.next
.git
.env
.env.*
```

## Build and run

Build and run the image for a specific app by passing the `APP_NAME` build argument:

```bash
# Build the app
docker build --build-arg APP_NAME=app -t next-forge-app .

# Run it
docker run -p 3000:3000 --env-file .env.local next-forge-app
```

Repeat for `web` and `api` if you want to deploy all three.

## Using Docker Compose

If you'd prefer to run all apps together, create a `docker-compose.yml`:

```yaml title="docker-compose.yml"
services:
  app:
    build:
      context: .
      args:
        APP_NAME: app
    ports:
      - "3000:3000"
    env_file:
      - .env.local

  web:
    build:
      context: .
      args:
        APP_NAME: web
    ports:
      - "3001:3000"
    env_file:
      - .env.local

  api:
    build:
      context: .
      args:
        APP_NAME: api
    ports:
      - "3002:3000"
    env_file:
      - .env.local
```

Then run everything with:

```bash
docker compose up --build
```

## Environment variables

When deploying with Docker, pass your environment variables at runtime using `--env-file` or `-e` flags. Do not bake secrets into the image. Learn more about how [environment variables](/docs/setup/env) work in next-forge.

If certain variables are needed at build time (e.g. `DATABASE_URL` for Prisma), uncomment the relevant `ARG` and `ENV` lines in the builder stage.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Deploying to Netlify
description: How to deploy next-forge to Netlify.
type: guide
summary: How to deploy next-forge to Netlify.
prerequisites:
  - /docs/setup/env
related:
  - /docs/deployment/vercel
---

# Deploying to Netlify



To deploy next-forge on Netlify, you need to create 3 new projects for the `app`, `api` and `web` apps. After selecting your repository, change the "Site to deploy" selection to the app of choice e.g. `apps/app`. This should automatically detect the Next.js setup and as such, the build command and output directory.

Then, add all your environment variables to the project.

Finally, just hit "Deploy" and Netlify will take care of the rest!

## Environment variables

If you're deploying on Netlify, we recommend making use of the Shared Environment Variables feature. Variables used by libraries need to exist in all packages and duplicating them can be a headache. Learn more about how [environment variables](/docs/setup/env) work in next-forge.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Deploying to Vercel
description: How to deploy next-forge to Vercel.
type: guide
summary: How to deploy next-forge to Vercel.
prerequisites:
  - /docs/setup/env
related:
  - /docs/deployment/netlify
---

# Deploying to Vercel



To deploy next-forge on Vercel, you need to create 3 new projects for the `app`, `api` and `web` apps. After selecting your repository, change the Root Directory option to the app of choice e.g. `apps/app`. This should automatically detect the Next.js setup and as such, the build command and output directory.

Then, add all your environment variables to the project.

Finally, just hit "Deploy" and Vercel will take care of the rest!

Want to see it in action? next-forge is featured on the [Vercel Marketplace](https://vercel.com/templates/Next.js/next-forge) - try deploying the `app`:

<VercelButton />

## Environment variables

If you're deploying on Vercel, we recommend making use of the Team Environment Variables feature. Variables used by libraries need to exist in all packages and duplicating them can be a headache. Learn more about how [environment variables](/docs/setup/env) work in next-forge.

## Integrations

We also recommend installing the [BetterStack](https://vercel.com/integrations/betterstack) and [Sentry](https://vercel.com/integrations/sentry) integrations. This will take care of the relevant [environment variables](/docs/setup/env).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Create an AI Chatbot
description: Let's use next-forge to create a simple AI chatbot.
type: guide
summary: Build a simple AI chatbot using next-forge and Vercel AI SDK.
prerequisites:
  - /docs/setup/quickstart
---

# Create an AI Chatbot





Today we're going to create an AI chatbot using next-forge and the built-in AI package, powered by [Vercel AI SDK](https://sdk.vercel.ai/).

<img alt="/images/ai-chatbot.png" src={__img0} placeholder="blur" />

## 1. Create a new project

```bash title="Terminal"
npx next-forge@latest init ai-chatbot
```

This will create a new project with the name `ai-chatbot` and install the necessary dependencies.

## 2. Configure your environment variables

Follow the guide on [Environment Variables](/docs/setup/env) to fill in your environment variables.

Specifically, make sure you set an `OPENAI_API_KEY` environment variable to your `apps/app/.env.local` file.

<Tip>
  Make sure you have some credits in your OpenAI account.
</Tip>

## 3. Create the chatbot UI

We're going to start by creating a simple chatbot UI with a text input and a button to send messages.

Create a new file called `Chatbot` in the `app/components` directory. We're going to use a few things here:

* `useChat` from our AI package to handle the chat logic.
* `Button` and `Input` components from our Design System to render the form.
* `Thread` and `Message` components from our AI package to render the chat history.
* `handleError` from our Design System to handle errors.
* `SendIcon` from `lucide-react` to create a send icon.

```tsx title="apps/app/app/(authenticated)/components/chatbot.tsx"
'use client';

import { Message } from '@repo/ai/components/message';
import { Thread } from '@repo/ai/components/thread';
import { useChat } from '@repo/ai/lib/react';
import { Button } from '@repo/design-system/components/ui/button';
import { Input } from '@repo/design-system/components/ui/input';
import { handleError } from '@repo/design-system/lib/utils';
import { SendIcon } from 'lucide-react';

export const Chatbot = () => {
  const { messages, input, handleInputChange, isLoading, handleSubmit } =
    useChat({
      onError: handleError,
      api: '/api/chat',
    });

  return (
    <div className="flex h-[calc(100vh-64px-16px)] flex-col divide-y overflow-hidden">
      <Thread>
        {messages.map((message) => (
          <Message key={message.id} data={message} />
        ))}
      </Thread>
      <form
        onSubmit={handleSubmit}
        className="flex shrink-0 items-center gap-2 px-8 py-4"
        aria-disabled={isLoading}
      >
        <Input
          placeholder="Ask a question!"
          value={input}
          onChange={handleInputChange}
        />
        <Button type="submit" size="icon" disabled={isLoading}>
          <SendIcon className="h-4 w-4" />
        </Button>
      </form>
    </div>
  );
};
```

## 4. Create the chatbot API route

Create a new file called `chat` in the `app/api` directory. This Next.js route handler will handle the chatbot's responses.

We're going to use the `streamText` function from our AI package to stream the chatbot's responses to the client. We'll also use the `provider` function from our AI package to get the OpenAI provider, and the `log` function from our Observability package to log the chatbot's responses.

```tsx title="apps/app/app/api/chat/route.ts"
import { streamText } from '@repo/ai';
import { log } from '@repo/observability/log';
import { models } from '@repo/ai/lib/models';

export const POST = async (req: Request) => {
  const body = await req.json();

  log.info('🤖 Chat request received.', { body });
  const { messages } = body;

  log.info('🤖 Generating response...');
  const result = streamText({
    model: models.chat,
    system: 'You are a helpful assistant.',
    messages,
  });

  log.info('🤖 Streaming response...');
  return result.toDataStreamResponse();
};
```

## 5. Update the app

Finally, we'll update the `app/page.tsx` file to be a simple entry point that renders the chatbot UI.

```tsx title="apps/app/app/(authenticated)/page.tsx"
import { auth } from '@repo/auth/server';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { Chatbot } from './components/chatbot';
import { Header } from './components/header';

const title = 'Acme Inc';
const description = 'My application.';

export const metadata: Metadata = {
  title,
  description,
};

const App = async () => {
  const { orgId } = await auth();

  if (!orgId) {
    notFound();
  }

  return (
    <>
      <Header pages={['Building Your Application']} page="AI Chatbot" />
      <Chatbot />
    </>
  );
};

export default App;
```

## 6. Run the app

Run the app development server and you should be able to see the chatbot UI at [http://localhost:3000](http://localhost:3000).

```sh title="Terminal"
bun dev --filter app
```

That's it! You've now created an AI chatbot using next-forge and the built-in AI package. If you have any questions, please reach out to me on [Twitter](https://x.com/haydenbleasel) or open an issue on [GitHub](https://github.com/vercel/next-forge).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Authentication
description: We use Clerk to handle authentication, user and organization management.
product: Authentication
type: reference
summary: How Clerk handles authentication and user management.
related:
  - /docs/packages/webhooks/inbound
---

# Authentication



next-forge manages authentication through the use of a `auth` package. By default, this package is a wrapper around [Clerk](https://clerk.com/) which provides a complete authentication and user management solution that integrates seamlessly with Next.js applications.

## In-App

The `@repo/auth` package exposes an `AuthProvider`, however you don't need to use this directly. The [`DesignSystemProvider`](/docs/packages/design-system/provider) includes all relevant providers and higher-order components.

From here, you can use all the pre-built components and hooks provided by Clerk. To demonstrate this, we've added the `<OrganizationSwitcher>` and `<UserButton>` components to the sidebar, as well as built out the Sign In and Sign Up pages.

## Webhooks

Clerk uses webhooks to handle authentication events and you can send these to your application. Read more about [inbound authentication webhooks](/docs/packages/webhooks/inbound#authentication-events).

## Email Templates

Clerk handles authentication and authorization emails automatically. You can configure the theming of Clerk-sent emails in their dashboard.

### Local Development

Currently there's no way to easily test Clerk webhooks locally, so you'll have to test them in a staging environment. This means deploying your app to a "production" state Vercel project with development environment variables e.g. `staging-api.example.com`. Then you can add this URL to your Clerk project's webhook settings.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Collaboration
description: next-forge is multiplayer out of the box.
product: Collaboration
type: reference
summary: How multiplayer collaboration works out of the box.
---

# Collaboration



next-forge maintains a `collaboration` package designed to provide real-time collaborative features to your apps. By default, we use [Liveblocks](https://liveblocks.io) as our collaboration engine. To showcase what you can do with this package, we've built a simple collaborative experience in the `app` application, featuring an avatar stack and live cursors.

<Tip>
  Collaboration is enabled by the existence of the 

  `LIVEBLOCKS_SECRET`

   environment variable.
</Tip>

## How it works

Liveblocks relies on the concept of rooms, digital spaces where people collaborate. To set this up, you need to [authenticate your users](https://liveblocks.io/docs/authentication/access-token/nextjs), and [add the correct providers](https://liveblocks.io/docs/get-started/nextjs), however next-forge has already integrated this, meaning you can start building your collaborative application immediately.

We've also wired up two key props for the Liveblocks provider, `resolveUsers` and `resolveMentionSuggestions`, which are used to resolve the users and mention suggestions respectively.

## Usage

Liveblocks provides a number of hooks making it easy to add real-time presence and document storage to your app. For example, [`useOthers`]() returns which users are currently connected, helpful for building avatars stacks and multiplayer cursors.

```tsx title="toolbar-avatars.tsx"
import { useOthers, useSelf } from "@liveblocks/react/suspense";

export function ToolbarAvatars() {
  const others = useOthers();
  const me = useSelf();

  return (
    <div>
      {/* Your avatar */}
      <Avatar src={me.info.avatar} name={me.info.name} />

      {/* Everyone else's avatars */}
      {others.map(({ connectionId, info }) => (
        <Avatar key={connectionId} src={info.avatar} name={info.name} />
      )}
    </div>
  );
}
```

### Multiplayer documents

You can take your collaborative app a step further, and set up multiplayer document state with [`useStorage`](https://liveblocks.io/docs/api-reference/liveblocks-react#useStorage) and [`useMutation`](https://liveblocks.io/docs/api-reference/liveblocks-react#useMutation). This is ideal for creating custom experiences, such as a a multiplayer drawing panel, spreadsheet, or just a simple shared input and that anyone can edit.

```tsx title="collaborative-input.tsx"
import { useStorage, useMutation } from "@liveblocks/react/suspense";

function CollaborativeInput() {
  // Get the input's value
  const inputValue = useStorage((root) => root.inputValue);
  
  // Set the input's value
  const setValue = useMutation(({ storage }, newValue) => {
    storage.set("inputValue", newValue);
  }, []);
  
  return <input value={inputValue} onChange={(e) => setValue(e.target.value)} />;
}
```

### Commenting

Liveblocks also provides ready-made customizable components for adding collaboration, such as [`Thread`](https://liveblocks.io/docs/api-reference/liveblocks-react-ui#Thread) and [`Composer`](https://liveblocks.io/docs/api-reference/liveblocks-react-ui#Composer).

```tsx title="comments.tsx"
import { useThreads } from "@liveblocks/react/suspense";
import { Thread, Composer } from "@liveblocks/react-ui";

function Comments() {
  // Get each thread of comments and render them
  const { threads } = useThreads();

  return (
    <div>
      {threads.map((thread) => (
        <Thread key={thread.id} thread={thread} />
      ))}
      <Composer />
    </div>
  );
}
```

### Collaborative editing

To add a rich-text editor with full collaborative editing and floating comments, you can set up a [Tiptap](https://liveblocks.io/docs/get-started/nextjs-tiptap) or [Lexical](https://liveblocks.io/docs/get-started/nextjs-lexical) editor in a few lines of code.

```tsx title="collaborative-editor.tsx"
import { useLiveblocksExtension, FloatingComposer, FloatingThreads } from "@liveblocks/react-tiptap";
import { useThreads, useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";

export function Editor() {
  const { threads } = useThreads();
  const liveblocks = useLiveblocksExtension();

  // Set up your multiplayer text editor
  const editor = useEditor({
    extensions: [
      liveblocks,
      StarterKit.configure({ history: false }),
    ],
    immediatelyRender: false,
  });

  return (
    <div>
      <EditorContent editor={editor} />
      <FloatingThreads
        editor={editor}
        threads={threads}
      />
      <FloatingComposer editor={editor} />
    </div>
  );
}
```

### Notifications

Liveblocks also provides [notification components](https://liveblocks.io/docs/api-reference/liveblocks-react-ui#InboxNotificationList), meaning you can send in-app notifications to users that are tagged in comments, are mentioned in the text editor, or for any custom purpose.

```tsx title="notifications.tsx"
import { useInboxNotifications } from "@liveblocks/react/suspense";
import {
  InboxNotification,
  InboxNotificationList,
} from "@liveblocks/react-ui";

export function CollaborativeApp() {
  // Get each notification for the current user
  const { inboxNotifications } = useInboxNotifications();

  return (
    <InboxNotificationList>
      {inboxNotifications.map((inboxNotification) => (
        <InboxNotification
          key={inboxNotification.id}
          inboxNotification={inboxNotification}
        />
      ))}
    </InboxNotificationList>
  );
}
```

### Infrastructure

Liveblocks not only provides these features, but it also has:

* [Browser DevTools](https://liveblocks.io/devtools) to help you build your app.
* [REST APIs](https://liveblocks.io/docs/api-reference/rest-api-endpoints) for sever-side changes.
* [Node.js SDK](https://liveblocks.io/docs/api-reference/liveblocks-node) for using the REST APIs with TypeScript.
* [Webhooks](https://liveblocks.io/docs/platform/webhooks) for triggering user-driven events.
* [Dashboard](https://liveblocks.io/docs/platform/projects) to help with bug spotting and analytics.

Learn more by checking out the Liveblocks [documentation](https://liveblocks.io/docs), [examples](https://liveblocks.io/examples), and [interactive tutorial](https://liveblocks.io/docs/tutorial/react/getting-started/welcome).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Cron Jobs
description: Create serverless functions that run on specific intervals.
product: Cron
type: reference
summary: How to create serverless cron jobs on specific intervals.
---

# Cron Jobs



By default, next-forge uses Vercel's cron jobs feature to trigger Next.js serverless functions on a schedule. However, you can replace this setup with the background job service of your choosing, such as trigger.dev or BetterStack.

## Writing functions

Next.js serverless functions act as the basis for scheduled jobs. You can write them in any runtime and trigger them using a simple HTTP call.

To demonstrate, we've created a example endpoint called `keep-alive` that creates, then subsequently deletes, a temporary `page` object in the database. This is a little workaround to prevent databases going to sleep after a period of inactivity. You should probably remove this if you don't need it.

Additionally, while you can add cron jobs to any app, we use the `api` app to keep our background jobs, webhooks and other such tasks isolated from our UI.

You can add new functions by creating the relevant `route.ts` files in the `apps/api/app/cron` folder. While you can put them anywhere, we use the aptly named `cron` folder to hold our endpoints designed to run as cron jobs. For example:

```ts title="apps/api/app/cron/[your-function]/route.ts"
import { database } from '@repo/database';

// Must use GET for Vercel cron jobs
export const GET = async () => {
  // Do stuff

  return new Response('OK', { status: 200 });
};
```

## Scheduling functions

After you write your serverless function, you can schedule it by amending the `vercel.json` file in the API app. To serve as an example, we've wired up the `keep-alive` job mentioned above to run every 10 minutes.

```json title="apps/api/vercel.json"
{
  "crons": [
    {
      "path": "/cron/keep-alive",
      "schedule": "0 1 * * *"
    }
  ]
}
```

## How Vercel triggers cron jobs

To trigger a cron job, Vercel makes an HTTP GET request to your project's production deployment URL, using the path provided in your project's vercel.json file. For example, Vercel might make a request to:

```
https://*.vercel.app/cron/keep-alive
```

## Triggering functions manually

Should you need to trigger a cron job manually, you can either do so in the Vercel dashboard or just hit the endpoint with a standard HTTP request. For example:

```sh title="Terminal"
curl -X GET http://localhost:3002/cron/keep-alive
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Database
description: How the database and ORM are configured in next-forge.
product: Database
type: reference
summary: How the database and ORM are configured.
related:
  - /docs/apps/studio
---

# Database



next-forge aims to provide a robust, type-safe database client that makes it easy to work with your database while maintaining strong typing throughout your application. We aim to support tooling that:

* Provide a declarative way to define your database schema
* Generate type-safe database client
* Handle database migrations
* Offer powerful query capabilities with full TypeScript support

## Default Configuration

By default, next-forge uses [Neon](https://neon.tech) as its database provider and [Prisma](https://prisma.io) as its ORM. However, you can easily switch to other providers if you'd prefer, for example:

* You can use other ORMs like [Drizzle](/docs/migrations/database/drizzle), Kysely or any other type-safe ORM.
* You can use other database providers like [PlanetScale](/docs/migrations/database/planetscale), [Prisma Postgres](/docs/migrations/database/prisma-postgres), [Supabase](/docs/migrations/database/supabase), [EdgeDB](/docs/migrations/database/edgedb), or any other PostgreSQL/MySQL provider.

## Usage

Database and ORM configuration is located in `@repo/database`. You can import this package into any server-side component, like so:

```tsx title="page.tsx {1,4}"
import { database } from '@repo/database';

const Page = async () => {
  const users = await database.user.findMany();

  // Do something with users!
}
```

## Schema Definition

The database schema is defined in `packages/database/prisma/schema.prisma`. This schema file uses Prisma's schema definition language to describe your database tables, relationships, and types.

### Adding a new model

Let's say we want to add a new model called `Post`, describing a blog post. The blog content will be in a JSON format, generated by a library like [Tiptap](https://tiptap.dev/). To do this, we would add the following to your schema:

```prisma title="packages/database/prisma/schema.prisma {1-7}"
model Post {
  id        String   @id @default(cuid())
  title     String
  content   Json
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
```

### Creating migrations

When you make changes to your schema during development, run:

```sh title="Terminal"
bun run migrate
```

This will format the schema, regenerate the Prisma client, and create a new migration file in `packages/database/prisma/migrations/`. You'll be prompted to name the migration.

### Deploying migrations

To apply pending migrations in CI or production (without creating new ones), run:

```sh title="Terminal"
bun run migrate:deploy
```

This runs `prisma migrate deploy`, which is safe for production environments — it only applies existing migration files and never modifies the schema.

### Prototyping

If you're rapidly iterating on your schema and don't need migration history yet, you can use:

```sh title="Terminal"
bun run db:push
```

This uses `prisma db push` to push schema changes directly to the database without creating migration files. This is useful for early prototyping but should not be used in production, as it can cause data loss and doesn't maintain a migration history.

## Visual database editor

next-forge includes a [visual database editor](/docs/apps/studio) that allows you to view and edit your database records.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Transactional Emails
product: Email
type: reference
summary: How transactional emails are configured and sent.
related:
  - /docs/apps/email
---

# Transactional Emails



We use [Resend](https://resend.com/) to send transactional emails. The templates, located in `@repo/email`, are powered by [React Email](https://react.email/) - a collection of high-quality, unstyled components for creating beautiful emails using React and TypeScript.

Resend is an optional integration. If `RESEND_TOKEN` is not set, the `resend` export will be `undefined`. Make sure to handle this when sending emails.

## Sending Emails

To send an email, you can use the `resend` object, which is imported from the `@repo/email` package:

```tsx title="apps/web/app/contact/actions/contact.tsx"
import { resend } from '@repo/email';

await resend.emails.send({
  from: 'sender@acme.com',
  to: 'recipient@acme.com',
  subject: 'The email subject',
  text: 'The email text',
});
```

## Email Templates

The `email` package is separated from the app folder for two reasons:

1. We can import the templates into the `email` app, allowing for previewing them in the UI; and
2. We can import both the templates and the SDK into our other apps and use them to send emails.

Resend and React Email play nicely together. For example, here's how you can send a transactional email using a React email template:

```tsx title="apps/web/app/contact/actions/contact.tsx"
import { resend } from '@repo/email';
import { ContactTemplate } from '@repo/email/templates/contact';

await resend.emails.send({
  from: 'sender@acme.com',
  to: 'recipient@acme.com',
  subject: 'The email subject',
  react: <ContactTemplate name={name} email={email} message={message} />,
});
```

## Previewing Emails

To preview the emails templates, simply run the [`email` app](/docs/apps/email).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Feature Flags
description: Control access to features in your application.
product: Flags
type: reference
summary: How feature flags control access to features.
related:
  - /docs/packages/toolbar
---

# Feature Flags





Feature flags (also known as feature toggles) allow you to control access to specific features in your application. next-forge provides a simple but powerful feature flag system that can be used to:

* Roll out features gradually to users
* A/B test new functionality
* Enable/disable features based on user segments
* Control access to beta or experimental features

## How it works

next-forge implements feature flags using Vercel's [Flags SDK](https://vercel.com/docs/workflow-collaboration/feature-flags/flags-pattern-nextjs), which is mostly an architectural pattern for working with feature flags. This setup exists in the `@repo/feature-flags` package, so you can import and use it in as many projects as you'd like.

We've created an intelligent function called `createFlag` that you can use to create new flags, complete with authentication, PostHog integration and [Vercel Toolbar](/docs/packages/toolbar) overrides without the hassle.

<Tip>
  For `FLAGS_SECRET`, you can run `node -e "console.log(crypto.randomBytes(32).toString('base64url'))"` or `openssl rand -base64 32` in your terminal to generate a random value.
</Tip>

## Adding a new flag

To add a new flag, simply create a feature flag in PostHog and then modify the feature flags index file. Let's add a new flag called `showAnalyticsFeature` that will be enabled for all users:

```ts title="packages/feature-flags/index.ts {4}"
import { createFlag } from './lib/create-flag';

export const showBetaFeature = createFlag('showBetaFeature');
export const showAnalyticsFeature = createFlag('showAnalyticsFeature');
```

Make sure the key you're using matches the key in PostHog exactly.

## Usage in your application

To use the flag in your application, simply import it from the `@repo/feature-flags` package and use it like a normal boolean value.

```tsx title="page.tsx"
import { showAnalyticsFeature } from '@repo/feature-flags';
import { notFound } from 'next/navigation';

export default function Page() {
  const isEnabled = showAnalyticsFeature();

  if (!isEnabled) {
    notFound();
  }

  return <div>Analytics feature is enabled</div>;
}
```

Because feature flags are tied to the user, they only work if the user is signed in. Otherwise, the flag will always return `false`.

## Overriding flags in development

You can override flags in development by using the Vercel Toolbar. Simply open the toolbar by clicking the "Flag" icon in the top right and then selecting the flag you'd like to override.

<img alt="/images/vercel-toolbar.png" src={__img0} placeholder="blur" />


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Formatting
description: Code formatting, linting and more.
product: Formatting
type: reference
summary: How code formatting and linting are configured.
---

# Formatting



next-forge uses [Ultracite](https://ultracite.dev) for code formatting and linting. Ultracite is a preconfigured setup of [Biome](https://biomejs.dev), a high-performance Rust-based toolchain which includes a formatter, linter, and more for JavaScript and TypeScript.

## Benefits

Ultracite provides several benefits:

* Zero configuration required - works out of the box
* Extremely fast performance
* Consistent code style across your project
* Built-in linting rules
* TypeScript support

## Usage

The formatter and linter are automatically configured in your project and will run on save. If you want to manually run them across your project, you can run the following commands:

* `bun run lint` - This will check all files across all apps and packages.
* `bun run format` - This will check and fix all files across all apps and packages.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Internationalization
description: How to add multiple languages to your application.
product: Internationalization
type: guide
summary: How to add multiple languages to your application.
---

# Internationalization



next-forge includes a powerful internationalization package that enables you to add multiple language support to your application with minimal configuration. The package handles language detection, routing, and content management through a dictionary-based approach.

<Tip>
  [Languine](https://dub.sh/0KOndf7) offers 500 translation keys for free. If you need more, you can upgrade to a paid plan with the discount code `nextforge` for 30% off!
</Tip>

## How it works

The internationalization system is built on [Next.js Internationalization](https://nextjs.org/docs/app/building-your-application/routing/internationalization)'s routing system and [Languine](https://dub.sh/0KOndf7)'s translation system.

It's configured by default for the `web` package to:

1. Detect the user's preferred language through browser headers
2. Route users to language-specific paths (e.g., `/en/about`, `/fr/about`)
3. Serve content from language-specific dictionaries

The package handles language detection and matching, ensuring users see content in their preferred language when available.

## Setup

To enable automatic translations, simply create a `.env` file in the `internationalization` package and set the `LANGUINE_PROJECT_ID` environment variable.

```txt title=".env"
LANGUINE_PROJECT_ID="your-project-id"
```

## Configuration

The internationalization package is configured through a `languine.json` file that defines:

* Source locale (e.g., `en`)
* Target locales (e.g., `["es", "de"]`)
* Dictionary file locations

## Dictionary Structure

Dictionaries are TypeScript files that export strongly-typed content for each supported language. The type system ensures consistency across translations:

```ts title="packages/internationalization/dictionaries/[locale].ts"
import type { Dictionary } from '@repo/internationalization';

const dictionary: Dictionary = {};

export default dictionary;
```

There is no need to create a dictionary for any non-source locale, as [Languine](https://dub.sh/0KOndf7) will automatically generate the translations for you.

## Usage

### Translating

To translate your application, you can run the following command:

```bash title="Terminal"
bun run translate
```

This will translate all of the content in your application and save the translations to the `dictionaries` folder.

### Frontend

To use the internationalization package, you need to:

1. Maintain a dictionary for your source locale.
2. Import the dictionary into your application.
3. Use the dictionary to render content.

You can find an example of a dictionary in the `web` package:

```tsx title="apps/web/app/page.tsx"
import { getDictionary } from '@repo/internationalization';

type HomeProps = {
  params: Promise<{
    locale: string;
  }>;
};

const Home = async ({ params }: HomeProps) => {
  const { locale } = await params;
  const dictionary = await getDictionary(locale);

  // ...
};
```

You can change the locale of the application by changing the `locale` parameter in the URL. For example, `https://example.com/en/about` will render the `about` page in English.

We've already configured the `web` package with a language switcher, so you don't need to worry about it.

### Middleware

The internationalization package also includes a middleware component that automatically detects the user's language and routes them to the appropriate language-specific page.

This has already been configured for the `web` package, so you don't need to worry about it.

```tsx title="apps/web/proxy.ts"
import { createNEMO } from '@rescale/nemo';
import { internationalizationMiddleware } from '@repo/internationalization';

// The i18n middleware is composed into the middleware chain
// alongside auth and security middleware using createNEMO:
const composed = createNEMO([...], {
  before: [internationalizationMiddleware, arcjetMiddleware],
});
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Notifications
description: In-app notifications for your users.
product: Notifications
type: reference
summary: How in-app notifications are configured for users.
---

# Notifications



next-forge offers a notifications package that allows you to send in-app notifications to your users. By default, it uses [Knock](https://knock.app/), a cross-channel notification platform that supports in-app, email, SMS, push, and chat notifications. Knock allows you to centralize your notification logic and templates in one place and [orchestrate complex workflows](https://docs.knock.app/designing-workflows/overview) with things like branching, batching, throttling, delays, and conditional sending.

## Setup

To use the notifications package, you need to add the required environment variables to your project, as specified in the `packages/notifications/keys.ts` file.

## In-app notifications feed

To render an in-app notifications feed, import the `NotificationsTrigger` component from the `@repo/notifications` package and use it in your app. We've already added this to the sidebar in the example app:

```tsx title="apps/app/app/(authenticated)/components/sidebar.tsx"
import { NotificationsTrigger } from '@repo/notifications/components/trigger';

<NotificationsTrigger>
  <Button variant="ghost" size="icon" className="shrink-0">
    <BellIcon size={16} className="text-muted-foreground" />
  </Button>
</NotificationsTrigger>
```

Pressing the button will open the in-app notifications feed, which displays all of the notifications for the current user.

## Send a notification

Knock sends notifications using workflows. To send an in-app notification, create a new [workflow](https://docs.knock.app/concepts/workflows) in the Knock dashboard that uses the [`in-app` channel provider](https://docs.knock.app/integrations/in-app/knock) and create a corresponding message template.

Then you can [trigger that workflow](https://docs.knock.app/send-notifications/triggering-workflows) for a particular user in your app, passing in the necessary data to populate the message template:

```tsx title="notify.ts"
import { notifications } from '@repo/notifications';

await notifications.workflows.trigger('workflow-key', {
  recipients: [{
    id: 'user-id',
    email: 'user-email',
  }],
  data: {
    message: 'Hello, world!',
  },
});
```

## Multi-channel notifications

Using Knock, you can add additional channel providers to your workflow to send notifications via email, SMS, push, or chat. To do this, create a new [channel provider](https://docs.knock.app/integrations) in the Knock dashboard, follow any configuration instructions for that provider, and add it to your workflow as a channel step.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Payments
description: How next-forge handles payments and billing.
product: Payments
type: reference
summary: How payments and billing are configured with Stripe.
related:
  - /docs/packages/webhooks/inbound
---

# Payments



next-forge uses [Stripe](https://stripe.com/) by default for payments and billing. Implementing Stripe in your project is straightforward.

Stripe is an optional integration. If `STRIPE_SECRET_KEY` is not set, the `stripe` export will be `undefined` and payment webhooks will be skipped.

## In-App Purchases

You can use Stripe anywhere in your app by importing the `stripe` object like so:

```ts title="page.tsx {1,5}"
import { stripe } from '@repo/payments';

// ...

await stripe?.prices.list();
```

## Webhooks

next-forge has a pre-built webhook handler for payment events. Read more about [inbound payment webhooks](/docs/packages/webhooks/inbound#payment-events).

## Anti-Fraud

As your app grows, you will inevitably encounter credit card fraud. [Stripe Radar](https://stripe.com/radar) is enabled by default if you integrate payments using their SDK as described above. This provides a set of tools to help you detect and prevent fraud.

Stripe Radar supports more [advanced anti-fraud features](https://docs.stripe.com/disputes/prevention/advanced-fraud-detection) if you embed the Stripe JS script in every page load. This is not enabled by default in next-forge, but you can add it as follows:

<Steps>
  <Step title="Edit the layout">
    Edit `apps/app/app/layout.tsx` and add `<Script src="https://js.stripe.com/v3/" />` after the opening `<html>` tag and before the opening `<body>` tag. You will also need to add `import Script from 'next/script'`

    ```tsx title="{5, 13}"
    import '@repo/design-system/styles/globals.css';
    import { DesignSystemProvider } from '@repo/design-system';
    import { fonts } from '@repo/design-system/lib/fonts';
    import type { ReactNode } from 'react';
    import Script from 'next/script';

    type RootLayoutProperties = {
      readonly children: ReactNode;
    };

    const RootLayout = ({ children }: RootLayoutProperties) => (
      <html lang="en" className={fonts} suppressHydrationWarning>
        <Script src="https://js.stripe.com/v3/" />
        <body>
          <DesignSystemProvider>{children}</DesignSystemProvider>
        </body>
      </html>
    );

    export default RootLayout;
    ```
  </Step>

  <Step title="Add script to the website">
    Add the same script to the website in `apps/web/app/layout.tsx`.
  </Step>

  <Step title="Prevent common fraud patterns with Arcjet">
    Prevent common fraud patterns by using [Arcjet](/docs/packages/security/application) [IP address analysis](https://docs.arcjet.com/reference/nextjs#ip-analysis) to [block requests from VPNs and proxies](https://docs.arcjet.com/blueprints/vpn-proxy-detection). These are commonly used by fraudsters to hide their location, but have legitimate uses as well so are not blocked by default. You could simply block these users, or you could adjust the checkout process to require approval before processing their payment.
  </Step>
</Steps>

For example, in `apps/app/app/(authenticated)/layout.tsx` you could add this after the call to `aj.protect()`:

```ts
import { redirect } from 'next/navigation';
// ...

if (
  decision.ip.isHosting() ||
  decision.ip.isVpn() ||
  decision.ip.isProxy() ||
  decision.ip.isRelay()
) {
  // The IP is from a hosting provider, VPN, or proxy. We can check the name
  // of the service and customize the response
  if (decision.ip.hasService()) {
    if (decision.ip.service !== 'Apple Private Relay') {
      // We trust Apple Private Relay because it requires an active iCloud
      // subscription, so deny all other VPNs
      redirect('/vpn-blocked');
    }
  } else {
    // The service name is not available, but we still think it's a VPN
    redirect('/vpn-blocked');
  }
}
```

In this case we are providing a friendly redirect to a page that explains why the user is being blocked (which you would need to create). You could also return a more generic error message. [See the Arcjet documentation](https://docs.arcjet.com/blueprints/payment-form#additional-checks) for more advanced examples.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Storage
description: How to store files in your application.
product: Storage
type: reference
summary: How file storage is configured with Vercel Blob.
---

# Storage



next-forge has a `storage` package that provides a simple wrapper around [Vercel Blob](https://vercel.com/docs/storage/vercel-blob) for file storage.

## Usage

### Server uploads

To use the `storage` package using Server Actions, follow the instructions on [Vercel's server action documentation](https://vercel.com/docs/storage/vercel-blob/server-upload#upload-a-file-using-server-actions).

```tsx title="page.tsx"
import { put } from '@repo/storage';
import { revalidatePath } from 'next/cache';
 
export async function Form() {
  async function uploadImage(formData: FormData) {
    'use server';
    const imageFile = formData.get('image') as File;
    const blob = await put(imageFile.name, imageFile, {
      access: 'public',
    });
    revalidatePath('/');
    return blob;
  }
 
  return (
    <form action={uploadImage}>
      <label htmlFor="image">Image</label>
      <input type="file" id="image" name="image" required />
      <button>Upload</button>
    </form>
  );
}
```

<Tip>
  If you need to upload files larger than 4.5 MB, consider using client uploads.
</Tip>

### Client uploads

For client side usage, check out the [Vercel's client upload documentation](https://vercel.com/docs/storage/vercel-blob/client-upload).

```tsx title="page.tsx"
'use client';
 
import { type PutBlobResult } from '@repo/storage';
import { upload } from '@repo/storage/client';
import { useState, useRef } from 'react';
 
export default function AvatarUploadPage() {
  const inputFileRef = useRef<HTMLInputElement>(null);
  const [blob, setBlob] = useState<PutBlobResult | null>(null);
  return (
    <>
      <h1>Upload Your Avatar</h1>
 
      <form
        onSubmit={async (event) => {
          event.preventDefault();
 
          if (!inputFileRef.current?.files) {
            throw new Error('No file selected');
          }
 
          const file = inputFileRef.current.files[0];
 
          const newBlob = await upload(file.name, file, {
            access: 'public',
            handleUploadUrl: '/api/avatar/upload',
          });
 
          setBlob(newBlob);
        }}
      >
        <input name="file" ref={inputFileRef} type="file" required />
        <button type="submit">Upload</button>
      </form>
      {blob && (
        <div>
          Blob url: <a href={blob.url}>{blob.url}</a>
        </div>
      )}
    </>
  );
}
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Toolbar
description: next-forge uses the Vercel Toolbar to allow you to override feature flags in development.
product: Toolbar
type: reference
summary: How the Vercel Toolbar overrides feature flags in development.
related:
  - /docs/packages/flags
---

# Toolbar



The [Vercel Toolbar](https://vercel.com/docs/workflow-collaboration/vercel-toolbar) is a tool that allows you to leave feedback, navigate through important dashboard pages, share deployments, use Draft Mode for previewing unpublished content, and Edit Mode for editing content in real-time. next-forge has the Vercel Toolbar enabled by default.

## Link your applications

Go into each application and run `vercel link`, like so:

```sh title="Terminal"
cd apps/app && vercel link && cd ../..
cd apps/web && vercel link && cd ../..
cd apps/api && vercel link && cd ../..
```

This will create a `.vercel/project.json` file in each application.

## Add the environment variable

Then, simply add a `FLAGS_SECRET` environment variable to each application's `.env.local` file, like so:

```js title=".env.local"
FLAGS_SECRET="test"
```

These two steps are optional, but they are recommended to ensure that the Vercel Toolbar is enabled in each application.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Environment Variables
description: How to configure environment variables in next-forge.
type: reference
summary: How environment variables are configured and used across the monorepo.
prerequisites:
  - /docs/setup/installation
---

# Environment Variables



next-forge uses environment variables for configuration. This guide will help you set up the required variables to get started quickly, and optionally configure additional features.

All integration environment variables are **optional** — next-forge will gracefully skip any integration that isn't configured. The only truly required variables are for core infrastructure (database and URLs).

## Quick Start (Minimum Setup)

To get next-forge running locally with basic functionality, you only need to configure these **required** variables:

### 1. Database (Required)

Add to `packages/database/.env`:

```bash
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
```

<Tip>
  For quick local development, we recommend [Neon](https://neon.tech) for a free PostgreSQL database. Sign up, create a project, and copy the connection string.
</Tip>

### 2. Local URLs (Pre-configured)

These are already set to sensible defaults for local development:

```bash
NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
NEXT_PUBLIC_API_URL="http://localhost:3002"
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000"
```

**That's it!** You can now run `npm run dev` and the app will start. Features like authentication, payments, analytics, email, and CMS will be disabled until you configure their environment variables.

## Optional Features

The following environment variables enable additional features. You can add them as needed — any unconfigured integration will be gracefully skipped at runtime.

### Authentication (Clerk)

Required for authentication, user, and organization management.

Add to `apps/app/.env.local` and `apps/web/.env.local`:

```bash
# Server
CLERK_SECRET_KEY="sk_test_..."

# Client
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in"
NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up"
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/"
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/"
```

<Steps>
  <Step>
    Sign up at [Clerk](https://clerk.com) and create an application
  </Step>

  <Step>
    Go to **API Keys** in your Clerk dashboard
  </Step>

  <Step>
    Copy the **Publishable key** (starts with `pk_`) and **Secret key** (starts with `sk_`)
  </Step>
</Steps>

### Content Management (BaseHub)

Required for the CMS functionality in `packages/cms`. If not configured, CMS queries will return empty results.

```bash
BASEHUB_TOKEN="bshb_..."
```

<Steps>
  <Step>
    Fork the [next-forge template](https://basehub.com/basehub/next-forge?fork=1) on BaseHub
  </Step>

  <Step>
    Navigate to **Settings → API Tokens**
  </Step>

  <Step>
    Copy your **Read Token** (starts with `bshb_pk_`)
  </Step>
</Steps>

### Email (Resend)

Required for sending transactional emails. If not configured, email features like the contact form will be disabled.

```bash
RESEND_TOKEN="re_..."
RESEND_FROM="noreply@yourdomain.com"
```

[Get your API key from Resend](https://resend.com/api-keys)

### Payments (Stripe)

Required for subscription and payment functionality. If not configured, the Stripe client and payment webhooks will be disabled.

```bash
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
```

<Steps>
  <Step>
    Get your keys from [Stripe Dashboard](https://dashboard.stripe.com/apikeys)
  </Step>

  <Step>
    For webhooks, install the [Stripe CLI](https://stripe.com/docs/stripe-cli) and run:

    ```bash
    stripe listen --forward-to localhost:3000/api/webhooks/stripe
    ```
  </Step>
</Steps>

### Analytics

#### Google Analytics

```bash
NEXT_PUBLIC_GA_MEASUREMENT_ID="G-..."
```

[Create a GA4 property](https://analytics.google.com/)

#### PostHog

```bash
NEXT_PUBLIC_POSTHOG_KEY="phc_..."
NEXT_PUBLIC_POSTHOG_HOST="https://app.posthog.com"
```

[Get your keys from PostHog](https://app.posthog.com/project/settings)

### Observability

#### Better Stack (Uptime monitoring)

```bash
BETTERSTACK_API_KEY="..."
BETTERSTACK_URL="..."
```

[Get your API key from Better Stack](https://betterstack.com/logs)

### Security

#### Arcjet (Rate limiting & security)

```bash
ARCJET_KEY="ajkey_..."
```

[Get your key from Arcjet](https://app.arcjet.com/)

### Real-time Features

#### Liveblocks (Collaboration)

```bash
LIVEBLOCKS_SECRET="sk_..."
```

[Get your secret from Liveblocks](https://liveblocks.io/dashboard)

### Notifications (Knock)

```bash
KNOCK_API_KEY="..."
KNOCK_SECRET_API_KEY="..."
KNOCK_FEED_CHANNEL_ID="..."
NEXT_PUBLIC_KNOCK_API_KEY="..."
NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID="..."
```

[Get your keys from Knock](https://dashboard.knock.app/)

### Feature Flags

```bash
FLAGS_SECRET="..."
```

Generate a random secret string for encrypting feature flag data.

### Webhooks (Svix)

```bash
SVIX_TOKEN="..."
```

[Get your token from Svix](https://dashboard.svix.com/)

### Clerk Webhooks

```bash
CLERK_WEBHOOK_SECRET="whsec_..."
```

<Steps>
  <Step>
    In your Clerk dashboard, go to **Webhooks**
  </Step>

  <Step>
    Add a new endpoint pointing to `https://your-domain.com/api/webhooks/clerk`
  </Step>

  <Step>
    Subscribe to the events you need (typically `user.created`, `user.updated`, etc.)
  </Step>

  <Step>
    Copy the **Signing Secret**
  </Step>
</Steps>

## Environment Variable Files

next-forge uses environment variables across multiple locations:

| File                                         | Purpose                     |
| -------------------------------------------- | --------------------------- |
| `apps/app/.env.local`                        | Main application variables  |
| `apps/web/.env.local`                        | Marketing website variables |
| `apps/api/.env.local`                        | API server variables        |
| `packages/database/.env`                     | Database connection string  |
| `packages/cms/.env.local`                    | CMS configuration           |
| `packages/internationalization/.env.example` | i18n configuration          |

<Info>
  The setup script automatically creates these files from `.env.example` templates. You only need to fill in the values.
</Info>

## Type Safety

Type safety is provided by [@t3-oss/env-nextjs](https://env.t3.gg/), which provides runtime validation and autocompletion for all environment variables. Each package defines its own environment variables in a `keys.ts` file with Zod validation schemas.

### Validation Rules

<Tip>
  Be as specific as possible with validation. For example, if a vendor secret starts with `sec_`, validate it as `z.string().min(1).startsWith('sec_')`. This makes your intent clearer and helps prevent errors at runtime.
</Tip>

## Adding a New Environment Variable

To add a new environment variable:

1. Add the variable to the relevant `.env.local` files
2. Add validation to the `server` or `client` object in the package's `keys.ts` file

Example in `packages/my-package/keys.ts`:

```ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const keys = createEnv({
  server: {
    MY_NEW_SECRET: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_MY_VALUE: z.string().optional(),
  },
  runtimeEnv: {
    MY_NEW_SECRET: process.env.MY_NEW_SECRET,
    NEXT_PUBLIC_MY_VALUE: process.env.NEXT_PUBLIC_MY_VALUE,
  },
});
```

## Deployment

When deploying to Vercel or other platforms:

1. Add all required environment variables to your deployment platform
2. Update URL variables (`NEXT_PUBLIC_APP_URL`, etc.) to production values
3. Some integrations (like Sentry) automatically inject their variables via marketplace integrations

<Info>
  Variables prefixed with `VERCEL_` are automatically available in Vercel deployments, such as `VERCEL_PROJECT_PRODUCTION_URL`.
</Info>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Installation
description: How to setup, install and run next-forge.
type: guide
summary: Step-by-step instructions for installing next-forge.
prerequisites:
  - /docs/setup/prerequisites
related:
  - /docs/setup/env
  - /docs/setup/quickstart
---

# Installation



## Initialization

Run the `next-forge` init command:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx next-forge@latest init
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx next-forge@latest init
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx next-forge@latest init
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x next-forge@latest init
    ```
  </CodeBlockTab>
</CodeBlockTabs>

You will be prompted for the project name and package manager.

```sh title="Terminal"
$ npx next-forge@latest init

┌  Let's start a next-forge project!
│
◇  What is your project named?
│  my-app
│
◇  Which package manager would you like to use?
│  bun
│
◇  Project initialized successfully!
│
└  Please make sure you install the Mintlify CLI and Stripe CLI before starting the project.
```

This will create a new directory with your project name and clone the repo into it. It will run a setup script to install dependencies and copy `.env` files. You can read more about environment variables [here](/docs/setup/env).

## Database

You will need to scaffold the database using the schema defined in `packages/database/prisma/schema.prisma`:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm run migrate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm run migrate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn migrate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun run migrate
    ```
  </CodeBlockTab>
</CodeBlockTabs>

For more details on the default Prisma configuration (using Neon), refer to the [Database Configuration Guide](https://www.next-forge.com/packages/database#default-configuration).

## CMS

You will need to setup the CMS. Follow the instructions [here](/docs/packages/cms/overview), but the summary is:

1. Fork the [`basehub/next-forge`](https://basehub.com/basehub/next-forge?fork=1) template
2. Get your Read Token from the "Connect to Your App" page
3. Add the `BASEHUB_TOKEN` to your [Environment Variables](/docs/setup/env)

## Development

Run the development server with:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm run dev
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm run dev
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dev
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun run dev
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Open the localhost URLs with the relevant ports listed above to see the app, e.g.

* [http://localhost:3000/](http://localhost:3000/) — The main app.
* [http://localhost:3001/](http://localhost:3001/) — The website.
* [http://localhost:3002/](http://localhost:3002/) — The API.
* [http://localhost:3003/](http://localhost:3003/) — Email preview server.
* [http://localhost:3004/](http://localhost:3004/) — The docs


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Prerequisites
description: How to configure your development environment for next-forge.
type: guide
summary: Tools and accounts required before setting up next-forge.
---

# Prerequisites



## Operating System

next-forge is designed to work on macOS, Linux and Windows. While next-forge itself is platform-agnostic, the tooling and dependencies we use have different requirements.

We've tested and confirmed that next-forge works on the following combinations of operating systems, Node.js versions and next-forge versions:

| Operating system                 | next-forge version | Node.js version | Notes                                                                            |
| -------------------------------- | ------------------ | --------------- | -------------------------------------------------------------------------------- |
| macOS Sequoia 15.0.1 (24A348)    | 5.3.2              | 20.12.2         |                                                                                  |
| Ubuntu 24.04 Arm64               | 5.3.2              | 20.18.0         |                                                                                  |
| Fedora, Release 41               | 5.3.2              | 22.11.0         |                                                                                  |
| Windows 11 Pro 24H2 (26100.2033) | 5.3.2              | 20.18.0         | Turborepo only supports specific architectures. `windows ia32` is not supported. |

We're aware of issues on [non-Ubuntu Linux distributions](https://github.com/vercel/next-forge/issues/238). While we don't officially support them, we'd love to know if you get it working!

## Package Manager

next-forge defaults to using [Bun](https://bun.sh) as a package manager, but you can use [npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/) or [pnpm](https://pnpm.io/) instead by passing a flag during the [installation](/docs/setup/installation) step.

## Stripe CLI

We use the [Stripe CLI](https://docs.stripe.com/stripe-cli) to forward [payments webhooks](/docs/packages/payments#webhooks) to your local machine.

Once installed, you can login to authenticate with your Stripe account.

```sh title="Terminal"
stripe login
```

## Mintlify CLI

We use the [Mintlify CLI](https://mintlify.com/docs/development) to preview the [docs](/docs/apps/docs) locally.

## Accounts

next-forge relies on various SaaS products. You will need to create accounts with the following services then set the API keys in your [environment variables](/docs/setup/env):

* [Arcjet](https://arcjet.com), for [application security](/docs/packages/security/application).
* [BetterStack](https://betterstack.com), for [logging](/docs/packages/observability/logging) and [uptime monitoring](/docs/packages/observability/uptime).
* [Clerk](https://clerk.com), for [authentication](/docs/packages/authentication).
* [Google Analytics](https://developers.google.com/analytics), for [web analytics](/docs/packages/analytics/web).
* [Posthog](https://posthog.com), for [product analytics](/docs/packages/analytics/product).
* [Resend](https://resend.com), for [transactional emails](/docs/packages/email).
* [Sentry](https://sentry.io), for [error tracking](/docs/packages/observability/error-capture).
* [Stripe](https://stripe.com), for [payments](/docs/packages/payments).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Quickstart
description: Get next-forge running locally in 5 minutes.
type: guide
summary: Get next-forge running locally in 5 minutes.
prerequisites:
  - /docs/setup/prerequisites
related:
  - /docs/setup/installation
  - /docs/setup/env
---

# Quickstart



This guide gets you from zero to a running dev server with the minimum required setup. Only two services are truly required to boot: **Clerk** (authentication) and a **PostgreSQL database**.

<Tip>
  Don't worry about other environment variables — features that need them (Stripe, analytics, email, etc.) will gracefully degrade or show warnings. You can add them later.
</Tip>

<Steps>
  <Step>
    ### Prerequisites

    You need [Node.js](https://nodejs.org) 18+ and a package manager. We recommend [Bun](https://bun.sh), but npm, yarn, and pnpm all work.

    ```sh title="Terminal"
    node -v # Should be v18 or higher
    ```
  </Step>

  <Step>
    ### Initialize the project

    <CodeBlockTabs defaultValue="npm">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npx next-forge@latest init
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm dlx next-forge@latest init
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn dlx next-forge@latest init
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun x next-forge@latest init
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    You'll be prompted for a project name and package manager. Once complete, `cd` into your new project directory.
  </Step>

  <Step>
    ### Set up Clerk

    1. Sign up at [clerk.com](https://clerk.com) and create a new application
    2. Go to **API Keys** in your Clerk dashboard
    3. Copy the **Publishable key** (`pk_test_...`) and **Secret key** (`sk_test_...`)

    Add them to both `apps/app/.env.local` and `apps/web/.env.local`:

    ```bash
    CLERK_SECRET_KEY="sk_test_..."
    NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
    NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in"
    NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up"
    NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/"
    NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/"
    ```

    <Info>
      The `NEXT_PUBLIC_CLERK_*_URL` values are already set in the `.env.example` files. You only need to add the two API keys.
    </Info>
  </Step>

  <Step>
    ### Set up the database

    Get a free PostgreSQL database from [Neon](https://neon.tech) (or use any Postgres instance), then add the connection string to `packages/database/.env`:

    ```bash
    DATABASE_URL="postgresql://user:password@host:5432/dbname"
    ```

    Then run the migration to scaffold your tables:

    <CodeBlockTabs defaultValue="npm">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm run migrate
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm run migrate
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn migrate
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun run migrate
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Step>

  <Step>
    ### Start the dev server

    <CodeBlockTabs defaultValue="npm">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm run dev
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm run dev
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn dev
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun run dev
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    Your apps are now running at:

    * [http://localhost:3000](http://localhost:3000) — Main app
    * [http://localhost:3001](http://localhost:3001) — Marketing website
    * [http://localhost:3002](http://localhost:3002) — API
    * [http://localhost:3003](http://localhost:3003) — Email preview
    * [http://localhost:3004](http://localhost:3004) — Docs
  </Step>
</Steps>

## Next steps

Now that you're up and running, you can:

* Review the full [environment variables](/docs/setup/env) page to enable optional features
* Set up [Stripe](/docs/packages/payments) for payments
* Set up [Resend](/docs/packages/email) for transactional emails
* Set up [BaseHub](/docs/packages/cms/overview) for CMS content
* Configure [analytics](/docs/packages/analytics/web), [error tracking](/docs/packages/observability/error-capture), and [logging](/docs/packages/observability/logging)
* Read the [deployment](/docs/deployment/vercel) guide when you're ready to go live


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Appwrite Auth
description: How to change the authentication provider to Appwrite Auth.
type: integration
summary: How to switch the authentication provider to Appwrite Auth.
prerequisites:
  - /docs/packages/authentication
related:
  - /docs/migrations/authentication/authjs
  - /docs/migrations/authentication/better-auth
  - /docs/migrations/authentication/supabase
  - /docs/migrations/database/appwrite
  - /docs/migrations/storage/appwrite
---

# Switch to Appwrite Auth



[Appwrite](https://appwrite.io) is an open-source backend-as-a-service platform that provides authentication, databases, storage, and more. Appwrite Auth supports email/password, OAuth, magic URL, phone, and anonymous authentication methods.

`next-forge` uses Clerk as the authentication provider. This guide will help you switch from Clerk to Appwrite Auth. Since Appwrite doesn't provide built-in organization management like Clerk, this guide includes a pattern to implement team/organization features using Appwrite's built-in Teams API.

## 1. Replace the `auth` package dependencies

Uninstall the existing Clerk dependencies from the `auth` package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>
</CodeBlockTabs>

...and install the Appwrite dependencies:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install appwrite node-appwrite --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add appwrite node-appwrite --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add appwrite node-appwrite --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add appwrite node-appwrite --filter @repo/auth
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Update environment variables

Add the following environment variables to your `.env.local` file in each Next.js application (`app`, `web`, and `api`). You can find these values in your Appwrite project's Settings page:

```bash title=".env.local"
NEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
NEXT_PUBLIC_APPWRITE_PROJECT_ID=your-project-id
APPWRITE_API_KEY=your-api-key
```

<Note>
  The endpoint and project ID are safe to use in client-side code. The API key should only be used server-side.
</Note>

## 3. Update the environment keys

Update the `keys.ts` file to validate the new Appwrite environment variables:

```ts title="packages/auth/keys.ts"
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const keys = () =>
  createEnv({
    server: {
      APPWRITE_API_KEY: z.string().min(1),
    },
    client: {
      NEXT_PUBLIC_APPWRITE_ENDPOINT: z.string().url(),
      NEXT_PUBLIC_APPWRITE_PROJECT_ID: z.string().min(1),
    },
    runtimeEnv: {
      APPWRITE_API_KEY: process.env.APPWRITE_API_KEY,
      NEXT_PUBLIC_APPWRITE_ENDPOINT:
        process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT,
      NEXT_PUBLIC_APPWRITE_PROJECT_ID:
        process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID,
    },
  });
```

## 4. Create the server client

Create a server-side Appwrite client using `node-appwrite` with cookie-based session management:

```ts title="packages/auth/server.ts"
import 'server-only';
import { Client, Account, Teams } from 'node-appwrite';
import { cookies } from 'next/headers';

const createAdminClient = () => {
  const client = new Client()
    .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
    .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!)
    .setKey(process.env.APPWRITE_API_KEY!);

  return {
    get account() {
      return new Account(client);
    },
    get teams() {
      return new Teams(client);
    },
  };
};

export const createSessionClient = async () => {
  const client = new Client()
    .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
    .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!);

  const cookieStore = await cookies();
  const session = cookieStore.get('appwrite-session');

  if (!session?.value) {
    throw new Error('No session');
  }

  client.setSession(session.value);

  return {
    get account() {
      return new Account(client);
    },
    get teams() {
      return new Teams(client);
    },
  };
};

export { createAdminClient };

export const currentUser = async () => {
  try {
    const { account } = await createSessionClient();
    return await account.get();
  } catch {
    return null;
  }
};

export const auth = async () => {
  const user = await currentUser();

  if (!user) {
    return { userId: null, orgId: null };
  }

  const orgId = user.prefs?.activeOrganizationId as string | null ?? null;

  return {
    userId: user.$id,
    orgId,
  };
};
```

## 5. Create the browser client

Create a client-side Appwrite client:

```ts title="packages/auth/client.ts"
'use client';

import { Client, Account, Teams } from 'appwrite';

const client = new Client()
  .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
  .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!);

export const account = new Account(client);
export const teams = new Teams(client);
export { client };
```

## 6. Update the middleware

Replace `proxy.ts` with `middleware.ts` to handle session validation:

```ts title="packages/auth/middleware.ts"
import 'server-only';
import { NextResponse, type NextRequest } from 'next/server';

export const authMiddleware = async (request: NextRequest) => {
  const session = request.cookies.get('appwrite-session');

  if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
    const url = request.nextUrl.clone();
    url.pathname = '/sign-in';
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
};
```

<Note>
  Delete the old `proxy.ts` file if it exists, as it was specific to Clerk's proxy functionality.
</Note>

## 7. Update the Provider file

Appwrite Auth doesn't require a Provider component, so replace it with a stub:

```tsx title="packages/auth/provider.tsx"
import type { ReactNode } from 'react';

type AuthProviderProps = {
  children: ReactNode;
};

export const AuthProvider = ({ children }: AuthProviderProps) => children;
```

## 8. Update the auth components

Update both the `sign-in.tsx` and `sign-up.tsx` components to use Appwrite Auth:

### Sign In

```tsx title="packages/auth/components/sign-in.tsx"
'use client';

import { account } from '../client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export const SignIn = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const router = useRouter();

  const handleSignIn = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      const session = await account.createEmailPasswordSession(email, password);

      // Set the session cookie
      document.cookie = `appwrite-session=${session.$id}; path=/; max-age=86400; secure; samesite=lax`;

      router.push('/dashboard');
      router.refresh();
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to sign in');
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSignIn}>
      {error && <div className="text-red-500">{error}</div>}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Signing in...' : 'Sign in'}
      </button>
    </form>
  );
};
```

### Sign Up

```tsx title="packages/auth/components/sign-up.tsx"
'use client';

import { account } from '../client';
import { ID } from 'appwrite';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export const SignUp = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [name, setName] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const router = useRouter();

  const handleSignUp = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      await account.create(ID.unique(), email, password, name);

      // Automatically sign in after sign up
      const session = await account.createEmailPasswordSession(email, password);
      document.cookie = `appwrite-session=${session.$id}; path=/; max-age=86400; secure; samesite=lax`;

      router.push('/dashboard');
      router.refresh();
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to sign up');
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSignUp}>
      {error && <div className="text-red-500">{error}</div>}
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
        required
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
        minLength={8}
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Signing up...' : 'Sign up'}
      </button>
    </form>
  );
};
```

## 9. Set up auth callback route for OAuth

Create a callback route to handle OAuth redirects:

```ts title="apps/app/app/api/auth/callback/route.ts"
import { createAdminClient } from '@repo/auth/server';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const userId = searchParams.get('userId');
  const secret = searchParams.get('secret');
  const next = searchParams.get('next') ?? '/dashboard';

  if (userId && secret) {
    const { account } = createAdminClient();
    const session = await account.createSession(userId, secret);

    const cookieStore = await cookies();
    cookieStore.set('appwrite-session', session.$id, {
      path: '/',
      httpOnly: true,
      sameSite: 'lax',
      secure: true,
      maxAge: 86400,
    });

    return NextResponse.redirect(`${origin}${next}`);
  }

  return NextResponse.redirect(`${origin}/sign-in`);
}
```

## 10. Implement organization management

Appwrite provides a built-in [Teams API](https://appwrite.io/docs/references/cloud/client-web/teams) for managing groups of users. Create helper functions to manage organizations:

```ts title="packages/auth/organizations.ts"
import 'server-only';
import { createSessionClient, createAdminClient } from './server';

export const createOrganization = async (name: string) => {
  const { teams, account } = await createSessionClient();

  const team = await teams.create('unique()', name);

  // Set as active organization
  await account.updatePrefs({
    activeOrganizationId: team.$id,
  });

  return team;
};

export const getOrganizations = async () => {
  const { teams } = await createSessionClient();
  const result = await teams.list();
  return result.teams;
};

export const switchOrganization = async (organizationId: string) => {
  const { account } = await createSessionClient();
  await account.updatePrefs({
    activeOrganizationId: organizationId,
  });
};

export const inviteToOrganization = async (
  organizationId: string,
  email: string,
  roles: string[] = ['member']
) => {
  const { teams } = await createSessionClient();
  await teams.createMembership(
    organizationId,
    roles,
    email
  );
};
```

## 11. Update your apps

Replace any remaining Clerk implementations in your apps with Appwrite equivalents:

### Server Components

```tsx
// Before (Clerk)
const { userId, orgId } = await auth();
const user = await currentUser();

// After (Appwrite)
import { auth, currentUser } from '@repo/auth/server';

const { userId, orgId } = await auth();
const user = await currentUser();
```

### Client Components

```tsx
// Before (Clerk)
import { useUser } from '@clerk/nextjs';
const { user } = useUser();

// After (Appwrite)
'use client';
import { account } from '@repo/auth/client';
import { useEffect, useState } from 'react';
import type { Models } from 'appwrite';

const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);

useEffect(() => {
  account.get().then(setUser).catch(() => setUser(null));
}, []);
```

### Sign Out

```tsx
// Before (Clerk)
import { SignOutButton } from '@clerk/nextjs';
<SignOutButton />

// After (Appwrite)
'use client';
import { account } from '@repo/auth/client';

const handleSignOut = async () => {
  await account.deleteSession('current');
  document.cookie = 'appwrite-session=; path=/; max-age=0';
  router.push('/');
  router.refresh();
};

<button onClick={handleSignOut}>Sign out</button>
```

## Additional features

### OAuth Authentication

To add OAuth providers, configure them in your Appwrite Console under Auth → Settings, then use:

```ts
import { account } from '@repo/auth/client';
import { OAuthProvider } from 'appwrite';

account.createOAuth2Session(
  OAuthProvider.Github, // or Google, Apple, etc.
  `${window.location.origin}/api/auth/callback`,
  `${window.location.origin}/sign-in`
);
```

### Magic URL Authentication

```ts
import { account } from '@repo/auth/client';
import { ID } from 'appwrite';

await account.createMagicURLToken(
  ID.unique(),
  email,
  `${window.location.origin}/api/auth/callback`
);
```

<Note>
  Appwrite uses a permissions system instead of Row Level Security. You can set document-level and collection-level permissions directly in the Appwrite Console or via the SDK. See the [Appwrite Permissions documentation](https://appwrite.io/docs/advanced/platform/permissions) for details.
</Note>

For more information, see the [Appwrite Auth documentation](https://appwrite.io/docs/products/auth).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Auth.js
description: How to change the authentication provider to Auth.js.
type: integration
summary: How to switch the authentication provider to Auth.js.
prerequisites:
  - /docs/packages/authentication
related:
  - /docs/migrations/authentication/better-auth
  - /docs/migrations/authentication/supabase
---

# Switch to Auth.js



<Warning>
  next-forge support for Auth.js is currently blocked by [this issue](https://github.com/nextauthjs/next-auth/issues/11076).
</Warning>

Here's how to switch from Clerk to [Auth.js](https://authjs.dev/).

## 1. Replace the dependencies

Uninstall the existing Clerk dependencies from the `auth` package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install the Auth.js dependencies.

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install next-auth@beta --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add next-auth@beta --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add next-auth@beta --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add next-auth@beta --filter @repo/auth
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Generate an Auth.js secret

Auth.js requires a random value secret, used by the library to encrypt tokens and email verification hashes. In each of the relevant app directories, generate a secret with the following command:

```sh title="Terminal"
cd apps/app && npx auth secret && cd -
cd apps/web && npx auth secret && cd -
cd apps/api && npx auth secret && cd -
```

This will automatically add an `AUTH_SECRET` environment variable to the `.env.local` file in each directory.

## 3. Replace the relevant files

Delete the existing `client.ts` and `server.ts` files in the `auth` package. Then, create the following file:

```tsx title="packages/auth/index.ts"
import NextAuth from "next-auth";
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [],
});
```

## 4. Update the middleware

Update the `middleware.ts` file in the `auth` package with the following content:

```tsx title="packages/auth/middleware.ts"
import 'server-only';

export { auth as authMiddleware } from './';
```

## 5. Update the auth components

Auth.js has no concept of "sign up", so we'll use the `signIn` function to sign up users. Update both the `sign-in.tsx` and `sign-up.tsx` components in the `auth` package with the same content:

### Sign In

```tsx title="packages/auth/components/sign-in.tsx"
import { signIn } from '../';

export const SignIn = () => (
  <form
    action={async () => {
      "use server";
      await signIn();
    }}
  >
    <button type="submit">Sign in</button>
  </form>
);
```

### Sign Up

```tsx title="packages/auth/components/sign-up.tsx"
import { signIn } from '../';

export const SignUp = () => (
  <form
    action={async () => {
      "use server";
      await signIn();
    }}
  >
    <button type="submit">Sign up</button>
  </form>
);
```

## 6. Update the Provider file

Auth.js has no concept of a Provider as a higher-order component, so you can either remove it entirely or just replace it with a stub, like so:

```tsx title="packages/auth/provider.tsx"
import type { ReactNode } from 'react';

type AuthProviderProps = {
  children: ReactNode;
};

export const AuthProvider = ({ children }: AuthProviderProps) => children;
```

## 7. Create an auth route handler

In your `app` application, create an auth route handler file with the following content:

```tsx title="apps/app/api/auth/[...nextauth]/route.ts"
import { handlers } from "@repo/auth"

export const { GET, POST } = handlers;
```

## 8. Update your apps

From here, you'll need to replace any remaining Clerk implementations in your apps with Auth.js references. This means swapping out references like:

```tsx title="page.tsx"
const { orgId } = await auth();
const { redirectToSignIn } = await auth();
const user = await currentUser();
```

Etcetera. Keep in mind that you'll need to build your own "organization" logic as Auth.js doesn't have a concept of organizations.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Better Auth
description: How to change the authentication provider to Better Auth.
type: integration
summary: How to switch the authentication provider to Better Auth.
prerequisites:
  - /docs/packages/authentication
related:
  - /docs/migrations/authentication/authjs
  - /docs/migrations/authentication/supabase
---

# Switch to Better Auth



Better Auth is a comprehensive, open-source authentication framework for TypeScript. It is designed to be framework agnostic, but integrates well with Next.js and provides a lot of features out of the box.

## 1. Swap out the `auth` package dependencies

Uninstall the existing Clerk dependencies from the `auth` package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>
</CodeBlockTabs>

...and install the Better Auth dependencies:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install better-auth next --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add better-auth next --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add better-auth next --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add better-auth next --filter @repo/auth
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Additionally, add `@repo/database` to the `auth` package dependencies.

## 2. Update your environment variables

Generate a secret with the following command to add it to the `.env.local` file in each Next.js application (`app`, `web` and `api`):

```sh title="Terminal"
npx @better-auth/cli@latest secret
```

This will add a `BETTER_AUTH_SECRET` environment variable to the `.env.local` file. You should also add the `BETTER_AUTH_URL` environment variable, pointing to your app's base URL:

```bash title=".env.local"
BETTER_AUTH_URL="http://localhost:3000"
```

## 3. Setup the server and client auth

Update the `auth` package files with the following code:

### Server

```ts title="packages/auth/server.ts"
import { betterAuth } from 'better-auth';
import { nextCookies } from "better-auth/next-js";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { database } from "@repo/database"

export const auth = betterAuth({
  database: prismaAdapter(database, {
    provider: 'postgresql',
  }),
  plugins: [
    nextCookies()
    // organization() // if you want to use organization plugin
  ],
  //...add more options here
});
```

### Client

```ts title="packages/auth/client.ts"
import { createAuthClient } from 'better-auth/react';

export const { signIn, signOut, signUp, useSession } = createAuthClient();
```

Read more in the Better Auth [installation guide](https://www.better-auth.com/docs/installation).

## 4. Update the auth components

Update both the `sign-in.tsx` and `sign-up.tsx` components in the `auth` package to use the `signIn` and `signUp` functions from the `client` file.

### Sign In

```tsx title="packages/auth/components/sign-in.tsx"
"use client";

import { signIn } from '../client';
import { useState } from 'react';

export const SignIn = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        await signIn.email({
          email,
          password,
        })
      }}
    >
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Sign in</button>
    </form>
  );
}
```

### Sign Up

```tsx title="packages/auth/components/sign-up.tsx"
"use client";

import { signUp } from '../client';
import { useState } from 'react';

export const SignUp = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [name, setName] = useState("");

  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        await signUp.email({
          email,
          password,
          name
        })
      }}
    >
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button type="submit">Sign up</button>
    </form>
  );
}
```

You can use different sign-in methods like social providers, phone, username etc. Read more about Better Auth [basic usage](https://better-auth.com/docs/basic-usage).

## 5. Generate Prisma Models

From the root folder, generate Prisma models for Better Auth by running the following command:

```sh title="Terminal"
npx @better-auth/cli@latest generate --output ./packages/database/prisma/schema.prisma --config ./packages/auth/server.ts
```

<Warning>
  You may have to comment out the `server-only` directive in `packages/database/index.ts` temporarily. Ensure you have environment variables set.
</Warning>

## 6. Update the Provider file

Better Auth has no concept of a Provider as a higher-order component, so you can either remove it entirely or just replace it with a stub, like so:

```tsx title="packages/auth/provider.tsx"
import type { ReactNode } from 'react';

type AuthProviderProps = {
  children: ReactNode;
};

export const AuthProvider = ({ children }: AuthProviderProps) => children;
```

## 7. Change Middleware

Change the middleware in the `auth` package to the following. The middleware checks for a session cookie and redirects unauthenticated users to the sign-in page. The optional `middlewareFn` parameter allows you to add custom logic before the authentication check:

```tsx title="packages/auth/middleware.ts"
import { getSessionCookie } from "better-auth/cookies";
import type { NextFetchEvent, NextRequest } from "next/server";
import { NextResponse } from "next/server";

export function authMiddleware(
  middlewareFn?: (
    _auth: { req: NextRequest; authorized: boolean },
    request: NextRequest,
    event: NextFetchEvent,
  ) => Promise<Response> | Response,
) {
  return async function middleware(request: NextRequest, event: NextFetchEvent) {
    const sessionCookie = getSessionCookie(request);
    const authorized = Boolean(sessionCookie);

    if (middlewareFn) {
      const response = await middlewareFn(
        { req: request, authorized },
        request,
        event
      );
      if (response && response.headers.get("Location")) {
        return response;
      }
    }

    if (!sessionCookie) {
      return NextResponse.redirect(new URL("/sign-in", request.url));
    }

    return NextResponse.next();
  };
}
```

## 8. Define and add Next.js Handlers to your app

> Unlike `Clerk`, you need to host auth handlers which will retrieve sessions, authenticate requests etc...

```tsx title="packages/auth/handlers.ts"
import 'server-only';
import { toNextJsHandler } from 'better-auth/next-js';

import { auth } from './server';

export const { POST, GET } = toNextJsHandler(auth);
```

```tsx title="apps/app/app/api/auth/[...all]/route.ts"
export { POST, GET } from '@repo/auth/handlers'
```

## 9. Update your apps

From here, you'll need to replace any remaining Clerk implementations in your apps with Better Auth.

Here is some inspiration:

```tsx
const user = await currentUser();
const { redirectToSignIn } = await auth();

// to

const session = await auth.api.getSession({
  headers: await headers(), // from next/headers
});
if (!session?.user) {
  return redirect('/sign-in'); // from next/navigation
}
// do something with session.user
```

```tsx
const { orgId } = await auth();

// to

const h = await headers(); // from next/headers
const session = await auth.api.getSession({
  headers: h,
});
const orgId = session?.session.activeOrganizationId;
const fullOrganization = await auth.api.getFullOrganization({
  headers: h,
  query: { organizationId: orgId },
});
```

```tsx title="webhooks/stripe/route.ts"
import { clerkClient } from '@repo/auth/server';

const clerk = await clerkClient();
const users = await clerk.users.getUserList();
const user = users.data.find(
  (user) => user.privateMetadata.stripeCustomerId === customerId
);

// to

import { database } from '@repo/database';

const user = await database.user.findFirst({
  where: {
    privateMetadata: {
      contains: { stripeCustomerId: customerId },
    },
  },
});
```

For using organization, check [organization plugin](https://better-auth.com/docs/plugins/organization) and more from the [Better Auth documentation](https://better-auth.com/docs).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Supabase Auth
description: How to change the authentication provider to Supabase Auth.
type: integration
summary: How to switch the authentication provider to Supabase Auth.
prerequisites:
  - /docs/packages/authentication
related:
  - /docs/migrations/authentication/authjs
  - /docs/migrations/authentication/better-auth
---

# Switch to Supabase Auth



[Supabase Auth](https://supabase.com/docs/guides/auth) is a comprehensive authentication system that integrates seamlessly with Supabase's Postgres database. It provides email/password, magic link, OAuth, and phone authentication, along with user management and session handling.

`next-forge` uses Clerk as the authentication provider. This guide will help you switch from Clerk to Supabase Auth. Since Supabase doesn't provide built-in organization management like Clerk, this guide includes a multi-tenancy schema pattern to implement team/organization features with role-based access control.

<Note>
  This guide assumes you've already [migrated to Supabase](/docs/migrations/database/supabase) for your database. If you haven't done so yet, complete that migration first.
</Note>

## 1. Replace the `auth` package dependencies

Uninstall the existing Clerk dependencies from the `auth` package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
    ```
  </CodeBlockTab>
</CodeBlockTabs>

...and install the Supabase Auth dependencies:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @supabase/supabase-js @supabase/ssr --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @supabase/supabase-js @supabase/ssr --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @supabase/supabase-js @supabase/ssr --filter @repo/auth
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @supabase/supabase-js @supabase/ssr --filter @repo/auth
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Additionally, add `@repo/database` to the `auth` package dependencies to enable organization/team management.

## 2. Update environment variables

Add the following environment variables to your `.env.local` file in each Next.js application (`app`, `web`, and `api`). You can find these values in your Supabase project's Settings → API page:

```bash title=".env.local"
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
```

<Note>
  The anon key is safe to use in client-side code as it respects your Row Level Security (RLS) policies. Supabase is transitioning `NEXT_PUBLIC_SUPABASE_ANON_KEY` to `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` — both work for now, but consider using the new name for new projects.
</Note>

## 3. Set up the database schema for organizations

Since Supabase doesn't provide built-in organization management, you'll need to create a multi-tenancy schema. Add the following to your Prisma schema:

```prisma title="packages/database/prisma/schema.prisma"
model Organization {
  id        String   @id @default(cuid())
  name      String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  members OrganizationMember[]
  @@map("organizations")
}

model OrganizationMember {
  id             String   @id @default(cuid())
  userId         String
  organizationId String
  role           String   @default("member") // owner, admin, member
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

  organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)

  @@unique([userId, organizationId])
  @@index([userId])
  @@index([organizationId])
  @@map("organization_members")
}
```

Then run the migration:

```sh title="Terminal"
bun run migrate
```

## 4. Create Supabase client utilities

Create utility functions to initialize Supabase clients for different contexts:

### Server Client

```ts title="packages/auth/server.ts"
import 'server-only';
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

export const createClient = async () => {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  );
};

// Helper function to get the current user
export const currentUser = async () => {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  return user;
};

// Helper function to get the current user's active organization
export const auth = async () => {
  const user = await currentUser();

  if (!user) {
    return { userId: null, orgId: null };
  }

  // Get active organization from user metadata
  const orgId = user.user_metadata?.activeOrganizationId as string | null;

  return {
    userId: user.id,
    orgId,
  };
};
```

### Client Component Client

```ts title="packages/auth/client.ts"
'use client';
import { createBrowserClient } from '@supabase/ssr';

export const createClient = () => {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
};
```

## 5. Update the middleware

Update the `middleware.ts` file to handle Supabase session refresh:

```ts title="packages/auth/middleware.ts"
import 'server-only';
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';

export const authMiddleware = async (request: NextRequest) => {
  let supabaseResponse = NextResponse.next({
    request,
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            request.cookies.set(name, value)
          );
          supabaseResponse = NextResponse.next({
            request,
          });
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          );
        },
      },
    }
  );

  // Refreshing the auth token
  const { data: { user } } = await supabase.auth.getUser();

  // Redirect to sign-in if accessing protected route without authentication
  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    const url = request.nextUrl.clone();
    url.pathname = '/sign-in';
    return NextResponse.redirect(url);
  }

  return supabaseResponse;
};
```

## 6. Update the auth components

Update both the `sign-in.tsx` and `sign-up.tsx` components to use Supabase Auth:

### Sign In

```tsx title="packages/auth/components/sign-in.tsx"
'use client';

import { createClient } from '../client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export const SignIn = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const router = useRouter();
  const supabase = createClient();

  const handleSignIn = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });

    if (error) {
      setError(error.message);
      setLoading(false);
    } else {
      router.push('/dashboard');
      router.refresh();
    }
  };

  return (
    <form onSubmit={handleSignIn}>
      {error && <div className="text-red-500">{error}</div>}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Signing in...' : 'Sign in'}
      </button>
    </form>
  );
};
```

### Sign Up

```tsx title="packages/auth/components/sign-up.tsx"
'use client';

import { createClient } from '../client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export const SignUp = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [name, setName] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const router = useRouter();
  const supabase = createClient();

  const handleSignUp = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    const { error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        data: {
          name,
        },
      },
    });

    if (error) {
      setError(error.message);
      setLoading(false);
    } else {
      router.push('/verify-email');
      router.refresh();
    }
  };

  return (
    <form onSubmit={handleSignUp}>
      {error && <div className="text-red-500">{error}</div>}
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
        required
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
        minLength={6}
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Signing up...' : 'Sign up'}
      </button>
    </form>
  );
};
```

## 7. Update the Provider file

Supabase Auth doesn't require a Provider component for basic functionality, so replace it with a stub:

```tsx title="packages/auth/provider.tsx"
import type { ReactNode } from 'react';

type AuthProviderProps = {
  children: ReactNode;
};

export const AuthProvider = ({ children }: AuthProviderProps) => children;
```

## 8. Implement organization management

Create helper functions to manage organizations in your application. Add these to a new file:

```ts title="packages/auth/organizations.ts"
import 'server-only';
import { database } from '@repo/database';
import { createClient } from './server';

export const createOrganization = async (name: string, userId: string) => {
  const organization = await database.organization.create({
    data: {
      name,
      members: {
        create: {
          userId,
          role: 'owner',
        },
      },
    },
  });

  // Set as active organization
  const supabase = await createClient();
  await supabase.auth.updateUser({
    data: { activeOrganizationId: organization.id },
  });

  return organization;
};

export const getOrganizations = async (userId: string) => {
  return await database.organization.findMany({
    where: {
      members: {
        some: {
          userId,
        },
      },
    },
    include: {
      members: true,
    },
  });
};

export const switchOrganization = async (organizationId: string) => {
  const supabase = await createClient();
  await supabase.auth.updateUser({
    data: { activeOrganizationId: organizationId },
  });
};

export const inviteToOrganization = async (
  organizationId: string,
  email: string,
  role: string = 'member'
) => {
  // Implement your invitation logic here
  // This could involve creating an invitation record and sending an email
};
```

## 9. Set up auth callback route

Create a callback route to handle authentication redirects:

```ts title="apps/app/app/api/auth/callback/route.ts"
import { createClient } from '@repo/auth/server';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get('code');
  const next = searchParams.get('next') ?? '/dashboard';

  if (code) {
    const supabase = await createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);

    if (!error) {
      return NextResponse.redirect(`${origin}${next}`);
    }
  }

  // Return the user to an error page with instructions
  return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}
```

## 10. Update your apps

Replace any remaining Clerk implementations in your apps with Supabase Auth equivalents:

### Server Components

```tsx
// Before (Clerk)
const { userId, orgId } = await auth();
const user = await currentUser();

// After (Supabase)
import { auth, currentUser } from '@repo/auth/server';

const { userId, orgId } = await auth();
const user = await currentUser();
```

### Client Components

```tsx
// Before (Clerk)
import { useUser } from '@clerk/nextjs';
const { user } = useUser();

// After (Supabase)
'use client';
import { createClient } from '@repo/auth/client';
import { useEffect, useState } from 'react';

const supabase = createClient();
const [user, setUser] = useState(null);

useEffect(() => {
  supabase.auth.getUser().then(({ data: { user } }) => setUser(user));

  const { data: { subscription } } = supabase.auth.onAuthStateChange(
    (_event, session) => setUser(session?.user ?? null)
  );

  return () => subscription.unsubscribe();
}, []);
```

### Sign Out

```tsx
// Before (Clerk)
import { SignOutButton } from '@clerk/nextjs';
<SignOutButton />

// After (Supabase)
'use client';
import { createClient } from '@repo/auth/client';

const handleSignOut = async () => {
  const supabase = createClient();
  await supabase.auth.signOut();
  router.push('/');
  router.refresh();
};

<button onClick={handleSignOut}>Sign out</button>
```

## Additional features

### Social Authentication

To add OAuth providers, configure them in your Supabase project settings, then use:

```ts
const { error } = await supabase.auth.signInWithOAuth({
  provider: 'github', // or 'google', 'apple', etc.
  options: {
    redirectTo: `${window.location.origin}/api/auth/callback`,
  },
});
```

### Magic Link Authentication

```ts
const { error } = await supabase.auth.signInWithOtp({
  email,
  options: {
    emailRedirectTo: `${window.location.origin}/api/auth/callback`,
  },
});
```

<Note>
  To secure your database tables with Row Level Security (RLS) policies, see the [Supabase database migration guide](/docs/migrations/database/supabase) for detailed setup instructions.
</Note>

For more information, see the [Supabase Auth documentation](https://supabase.com/docs/guides/auth).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Content Collections
description: How to switch to Content Collections.
type: integration
summary: How to switch to Content Collections for the CMS.
prerequisites:
  - /docs/packages/cms/overview
---

# Switch to Content Collections



It's possible to switch to [Content Collections](https://www.content-collections.dev/) to generate type-safe data collections from MDX files. This approach provides a structured way to manage blog posts while maintaining full type safety throughout your application.

## 1. Swap out the required dependencies

Remove the existing dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall basehub --filter @repo/cms
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove basehub --filter @repo/cms
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove basehub --filter @repo/cms
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove basehub --filter @repo/cms
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install the new dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @content-collections/mdx fumadocs-core --filter @repo/cms
    npm install -D @content-collections/cli @content-collections/core @content-collections/next --filter @repo/cms
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @content-collections/mdx fumadocs-core --filter @repo/cms
    pnpm add -D @content-collections/cli @content-collections/core @content-collections/next --filter @repo/cms
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @content-collections/mdx fumadocs-core --filter @repo/cms
    yarn add --dev @content-collections/cli @content-collections/core @content-collections/next --filter @repo/cms
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @content-collections/mdx fumadocs-core --filter @repo/cms
    bun add --dev @content-collections/cli @content-collections/core @content-collections/next --filter @repo/cms
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Update the `.gitignore` file

Add `.content-collections` to the root `.gitignore` file (in the root of your monorepo):

```txt title=".gitignore"
# content-collections
.content-collections
```

## 3. Modify the CMS package scripts

Now we need to modify the CMS package scripts to replace the `basehub` commands with `content-collections`.

```json title="packages/cms/package.json {3-5}"
{
  "scripts": {
    "dev": "content-collections build",
    "build": "content-collections build",
    "analyze": "content-collections build"
  },
}
```

<Note>
  We're using the Content Collections CLI directly to generate the collections prior to Next.js processes. The files are cached and not rebuilt in the Next.js build process. This is a workaround for [this issue](https://github.com/sdorra/content-collections/issues/214).
</Note>

## 4. Modify the relevant CMS package files

<Note>
  You may see TypeScript errors during this step. These will be resolved after you create your collections configuration and run the first build in step 6.
</Note>

### Next.js Config (CMS Package)

Update the CMS package's Next.js config to export the Content Collections wrapper:

```ts title="packages/cms/next-config.ts"
export { withContentCollections as withCMS } from '@content-collections/next';
```

This replaces the previous BaseHub configuration and maintains compatibility with your existing `next.config.ts` in the web app.

### Collections

```ts title="packages/cms/index.ts"
import { allPosts, allLegals } from 'content-collections';

export const blog = {
  postsQuery: null,
  latestPostQuery: null,
  postQuery: (slug: string) => null,
  getPosts: async () => allPosts,
  getLatestPost: async () =>
    allPosts.sort((a, b) => a.date.getTime() - b.date.getTime()).at(0),
  getPost: async (slug: string) =>
    allPosts.find(({ _meta }) => _meta.path === slug),
};

export const legal = {
  postsQuery: null,
  latestPostQuery: null,
  postQuery: (slug: string) => null,
  getPosts: async () => allLegals,
  getLatestPost: async () =>
    allLegals.sort((a, b) => a.date.getTime() - b.date.getTime()).at(0),
  getPost: async (slug: string) =>
    allLegals.find(({ _meta }) => _meta.path === slug),
};
```

### Components

```tsx title="packages/cms/components/body.tsx"
import { MDXContent } from '@content-collections/mdx/react';
import type { ComponentProps } from 'react';

type BodyProperties = Omit<ComponentProps<typeof MDXContent>, 'code'> & {
  content: ComponentProps<typeof MDXContent>['code'];
};

export const Body = ({ content, ...props }: BodyProperties) => (
  <MDXContent {...props} code={content} />
);
```

### TypeScript Config

Update your `tsconfig.json` in the `apps/web` directory to add the path mapping:

```json title="apps/web/tsconfig.json"
{
  "compilerOptions": {
    "paths": {
      "content-collections": ["./.content-collections/generated"]
    }
  }
}
```

<Note>
  Make sure to merge this with your existing `compilerOptions.paths` if you have any.
</Note>

### Toolbar

```tsx title="packages/cms/components/toolbar.tsx"
export const Toolbar = () => null;
```

### Table of Contents

```tsx title="packages/cms/components/toc.tsx"
import { getTableOfContents } from 'fumadocs-core/content/toc';

type TableOfContentsProperties = {
  data: string;
};

export const TableOfContents = async ({
  data,
}: TableOfContentsProperties) => {
  const toc = await getTableOfContents(data);

  return (
    <ul className="flex list-none flex-col gap-2 text-sm">
      {toc.map((item) => (
        <li
          key={item.url}
          style={{
            paddingLeft: `${item.depth - 2}rem`,
          }}
        >
          <a
            href={item.url}
            className="line-clamp-3 flex rounded-sm text-foreground text-sm underline decoration-foreground/0 transition-colors hover:decoration-foreground/50"
          >
            {item.title}
          </a>
        </li>
      ))}
    </ul>
  );
};
```

## 5. Update the `sitemap.ts` file

Update the `sitemap.ts` file to scan the `content` directory for MDX files:

```tsx title="apps/web/app/sitemap.ts"
// ...

const blogs = fs
  .readdirSync('content/blog', { withFileTypes: true })
  .filter((file) => !file.isDirectory())
  .filter((file) => !file.name.startsWith('_'))
  .filter((file) => !file.name.startsWith('('))
  .map((file) => file.name.replace('.mdx', ''));

const legals = fs
  .readdirSync('content/legal', { withFileTypes: true })
  .filter((file) => !file.isDirectory())
  .filter((file) => !file.name.startsWith('_'))
  .filter((file) => !file.name.startsWith('('))
  .map((file) => file.name.replace('.mdx', ''));

// ...
```

## 6. Create your collections

Create a new content collections configuration file in the `cms` package, then create a re-export file in the `web` app.

<Note>
  We're remapping the 

  `title`

   field to 

  `_title`

   and the 

  `_meta.path`

   field to 

  `_slug`

   to match the default next-forge CMS.
</Note>

### CMS Package

```ts title="packages/cms/collections.ts"
import { defineCollection, defineConfig } from '@content-collections/core';
import { compileMDX } from '@content-collections/mdx';

const posts = defineCollection({
  name: 'posts',
  directory: 'content/blog', // relative to apps/web
  include: '**/*.mdx',
  schema: (z) => ({
    title: z.string(),
    description: z.string(),
    date: z.string(),
    image: z.string(),
    authors: z.array(z.string()),
    tags: z.array(z.string()),
  }),
  transform: async ({ title, ...page }, context) => {
    const body = await context.cache(page.content, async () =>
      compileMDX(context, page)
    );

    return {
      ...page,
      _title: title,
      _slug: page._meta.path,
      body,
    };
  },
});

const legals = defineCollection({
  name: 'legals',
  directory: 'content/legal', // relative to apps/web
  include: '**/*.mdx',
  schema: (z) => ({
    title: z.string(),
    description: z.string(),
    date: z.string(),
  }),
  transform: async ({ title, ...page }, context) => {
    const body = await context.cache(page.content, async () =>
      compileMDX(context, page)
    );

    return {
      ...page,
      _title: title,
      _slug: page._meta.path,
      body,
    };
  },
});

export default defineConfig({
  collections: [posts, legals],
});
```

### Web App

Create a configuration file in the root of your `web` app:

```ts title="apps/web/content-collections.ts"
export { default } from '@repo/cms/collections';
```

<Note>
  After creating these files, you'll need to run `bun run build` in the `packages/cms` directory to generate the types. TypeScript errors about missing `content-collections` module will resolve after the first build.
</Note>

## 7. Create your content

Create the content directories if they don't exist:

* `apps/web/content/blog` for blog posts
* `apps/web/content/legal` for legal pages

To create a new blog post, add a new MDX file to the `apps/web/content/blog` directory. The file name will be used as the slug for the blog post and the frontmatter will be used to generate the blog post page. For example:

```mdx title="apps/web/content/blog/my-first-post.mdx"
---
title: 'My First Post'
description: 'This is my first blog post'
date: 2024-10-23
image: /blog/my-first-post.png
---
```

The same concept applies to the `legal` collection, which is used to generate the legal policy pages. Also, the `image` field is the path relative to the app's root `public` directory.

## 8. Remove the environment variables

Finally, remove all instances of `BASEHUB_TOKEN` from the `@repo/env` package.

## 9. Bonus features

### Fumadocs MDX Plugins

You can use the [Fumadocs](/docs/migrations/documentation/fumadocs) MDX plugins to enhance your MDX content.

```ts title="{1-6,8-13,20-23}"
import {
  type RehypeCodeOptions,
  rehypeCode,
  remarkGfm,
  remarkHeading,
} from 'fumadocs-core/mdx-plugins';

const rehypeCodeOptions: RehypeCodeOptions = {
  themes: {
    light: 'catppuccin-mocha',
    dark: 'catppuccin-mocha',
  },
};

const posts = defineCollection({
  // ...
  transform: async (page, context) => {
    // ...
    const body = await context.cache(page.content, async () =>
      compileMDX(context, page, {
        remarkPlugins: [remarkGfm, remarkHeading],
        rehypePlugins: [[rehypeCode, rehypeCodeOptions]],
      })
    );

    // ...
  },
});
```

### Reading Time

You can calculate reading time for your collection by adding a transform function.

```ts title="{1,10}"
import readingTime from 'reading-time';

const posts = defineCollection({
  // ...
  transform: async (page, context) => {
    // ...

    return {
      // ...
      readingTime: readingTime(page.content).text,
    };
  },
});
```

### Low-Quality Image Placeholder (LQIP)

You can generate a low-quality image placeholder for your collection by adding a transform function.

```ts title="{1,8-19,23,24}"
import { sqip } from 'sqip';

const posts = defineCollection({
  // ...
  transform: async (page, context) => {
    // ...

    const blur = await context.cache(page._meta.path, async () =>
      sqip({
        input: `./public/${page.image}`,
        plugins: [
          'sqip-plugin-primitive',
          'sqip-plugin-svgo',
          'sqip-plugin-data-uri',
        ],
      })
    );

    const result = Array.isArray(blur) ? blur[0] : blur;

    return {
      // ...
      image: page.image,
      imageBlur: result.metadata.dataURIBase64 as string,
    };
  },
});
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Appwrite Databases
description: How to change the database provider to Appwrite Databases.
type: integration
summary: How to switch the database provider to Appwrite Databases.
prerequisites:
  - /docs/packages/database
related:
  - /docs/migrations/database/supabase
  - /docs/migrations/authentication/appwrite
  - /docs/migrations/storage/appwrite
---

# Switch to Appwrite Databases



[Appwrite Databases](https://appwrite.io/docs/products/databases) is a document database service that's part of the Appwrite platform. It provides a flexible schema system with collections, documents, and built-in permissions.

`next-forge` uses Neon as the database provider with Prisma as the ORM. This guide will help you switch from Neon and Prisma to Appwrite Databases.

<Warning>
  Appwrite uses a document database model, not SQL. There is no Prisma ORM equivalent — you'll use Appwrite's SDK and Query builder instead. Your data model will need to be restructured around collections and documents rather than relational tables.
</Warning>

## 1. Replace the `database` package dependencies

Uninstall the existing Neon and Prisma dependencies from the `database` package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

...and install the Appwrite dependency:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install node-appwrite --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add node-appwrite --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add node-appwrite --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add node-appwrite --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Update environment variables

Add the following environment variables to your `.env.local` file. You can find these values in your Appwrite project's Settings page:

```bash title=".env.local"
NEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
NEXT_PUBLIC_APPWRITE_PROJECT_ID=your-project-id
APPWRITE_API_KEY=your-api-key
APPWRITE_DATABASE_ID=your-database-id
```

<Note>
  You'll need to create a database in the Appwrite Console first. The `APPWRITE_DATABASE_ID` is the ID of the database you create.
</Note>

## 3. Update the environment keys

Update the `keys.ts` file to validate the new environment variables:

```ts title="packages/database/keys.ts"
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const keys = () =>
  createEnv({
    server: {
      APPWRITE_API_KEY: z.string().min(1),
      APPWRITE_DATABASE_ID: z.string().min(1),
    },
    client: {
      NEXT_PUBLIC_APPWRITE_ENDPOINT: z.string().url(),
      NEXT_PUBLIC_APPWRITE_PROJECT_ID: z.string().min(1),
    },
    runtimeEnv: {
      APPWRITE_API_KEY: process.env.APPWRITE_API_KEY,
      APPWRITE_DATABASE_ID: process.env.APPWRITE_DATABASE_ID,
      NEXT_PUBLIC_APPWRITE_ENDPOINT:
        process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT,
      NEXT_PUBLIC_APPWRITE_PROJECT_ID:
        process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID,
    },
  });
```

## 4. Update the database package

Replace the contents of `index.ts` with a configured Appwrite Databases client:

```ts title="packages/database/index.ts"
import 'server-only';
import { Client, Databases, Query, ID, Permission, Role } from 'node-appwrite';

const client = new Client()
  .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
  .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!)
  .setKey(process.env.APPWRITE_API_KEY!);

export const database = new Databases(client);
export const databaseId = process.env.APPWRITE_DATABASE_ID!;

export { Query, ID, Permission, Role };
```

## 5. Remove Prisma files

Delete the Prisma-specific files that are no longer needed:

```sh title="Terminal"
rm -rf packages/database/prisma packages/database/prisma.config.ts
```

Also remove any Prisma-related scripts from your `package.json` files (e.g., `migrate`, `generate`, `studio`).

## 6. Understanding Appwrite's data model

Appwrite organizes data differently from SQL databases:

| SQL (Prisma) | Appwrite      |
| ------------ | ------------- |
| Database     | Database      |
| Table        | Collection    |
| Row          | Document      |
| Column       | Attribute     |
| Migration    | Console / SDK |

Collections are defined with typed attributes (string, integer, boolean, float, email, enum, URL, IP, datetime, relationship) and can be created via the Appwrite Console or programmatically with the SDK.

## 7. Create collections

You can create collections through the Appwrite Console, or programmatically using the server SDK. Here's an example of creating a `posts` collection:

```ts title="Example: Creating a collection"
import { database, databaseId, ID, Permission, Role } from '@repo/database';

await database.createCollection(
  databaseId,
  ID.unique(),
  'posts',
  [
    Permission.read(Role.any()),
    Permission.create(Role.users()),
    Permission.update(Role.users()),
    Permission.delete(Role.users()),
  ]
);
```

Then define its attributes:

```ts title="Example: Defining attributes"
const collectionId = 'your-collection-id';

await database.createStringAttribute(
  databaseId,
  collectionId,
  'title',
  255,
  true // required
);

await database.createStringAttribute(
  databaseId,
  collectionId,
  'content',
  10000,
  false // optional
);

await database.createStringAttribute(
  databaseId,
  collectionId,
  'authorId',
  255,
  true
);

await database.createDatetimeAttribute(
  databaseId,
  collectionId,
  'publishedAt',
  false
);
```

<Note>
  For most projects, it's easier to create collections and attributes through the Appwrite Console UI rather than programmatically.
</Note>

## 8. Perform CRUD operations

Here's how to perform basic CRUD operations with the Appwrite SDK:

### Create a document

```ts
import { database, databaseId, ID } from '@repo/database';

const post = await database.createDocument(
  databaseId,
  'posts', // collection ID
  ID.unique(),
  {
    title: 'Hello World',
    content: 'This is my first post.',
    authorId: 'user-123',
    publishedAt: new Date().toISOString(),
  }
);
```

### Read documents

```ts
import { database, databaseId, Query } from '@repo/database';

// Get a single document
const post = await database.getDocument(
  databaseId,
  'posts',
  'document-id'
);

// List documents with filters
const posts = await database.listDocuments(
  databaseId,
  'posts',
  [
    Query.equal('authorId', 'user-123'),
    Query.orderDesc('publishedAt'),
    Query.limit(10),
  ]
);
```

### Update a document

```ts
import { database, databaseId } from '@repo/database';

const updated = await database.updateDocument(
  databaseId,
  'posts',
  'document-id',
  {
    title: 'Updated Title',
  }
);
```

### Delete a document

```ts
import { database, databaseId } from '@repo/database';

await database.deleteDocument(
  databaseId,
  'posts',
  'document-id'
);
```

## 9. Permissions

Appwrite uses a document-level permissions system instead of SQL's Row Level Security. You can set permissions when creating or updating documents:

```ts
import { database, databaseId, ID, Permission, Role } from '@repo/database';

const post = await database.createDocument(
  databaseId,
  'posts',
  ID.unique(),
  {
    title: 'Private Post',
    content: 'Only I can see this.',
    authorId: 'user-123',
  },
  [
    Permission.read(Role.user('user-123')),
    Permission.update(Role.user('user-123')),
    Permission.delete(Role.user('user-123')),
  ]
);
```

Common permission patterns:

* `Role.any()` — Anyone (including guests)
* `Role.users()` — Any authenticated user
* `Role.user('userId')` — A specific user
* `Role.team('teamId')` — Members of a specific team
* `Role.team('teamId', 'admin')` — Team members with a specific role

## 10. Update your apps

Replace Prisma queries throughout your application with Appwrite SDK calls:

```tsx
// Before (Prisma)
import { database } from '@repo/database';
const posts = await database.post.findMany({
  where: { authorId: userId },
  orderBy: { createdAt: 'desc' },
});

// After (Appwrite)
import { database, databaseId, Query } from '@repo/database';
const { documents: posts } = await database.listDocuments(
  databaseId,
  'posts',
  [
    Query.equal('authorId', userId),
    Query.orderDesc('$createdAt'),
    Query.limit(25),
  ]
);
```

## Additional features

### Realtime subscriptions

Appwrite supports realtime subscriptions on the client side. You can listen for changes to documents:

```ts
import { client } from '@repo/auth/client';

const unsubscribe = client.subscribe(
  `databases.${databaseId}.collections.posts.documents`,
  (response) => {
    // Handle realtime event
    console.log(response);
  }
);
```

### Relationships

Appwrite supports relationships between collections. You can create one-to-one, one-to-many, and many-to-many relationships via the Console or SDK:

```ts
import { database, databaseId } from '@repo/database';

await database.createRelationshipAttribute(
  databaseId,
  'posts',
  'comments',
  'oneToMany',
  false,
  'postId',
  'comments'
);
```

### Indexes

For better query performance, create indexes on frequently queried attributes:

```ts
import { database, databaseId } from '@repo/database';

await database.createIndex(
  databaseId,
  'posts',
  'idx_authorId',
  'key',
  ['authorId']
);
```

For more information, see the [Appwrite Databases documentation](https://appwrite.io/docs/products/databases).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Convex
description: How to change the database provider to Convex.
type: integration
summary: How to switch the database provider to Convex.
prerequisites:
  - /docs/packages/database
---

# Switch to Convex



[Convex](https://convex.dev) is a reactive database platform with real-time sync, TypeScript-first backend functions, and fully managed infrastructure. Unlike traditional databases, Convex combines the database, server functions, and real-time subscriptions into a single platform.

`next-forge` uses Neon as the database provider with Prisma as the ORM. This guide will provide the steps you need to switch the database provider from Neon to Convex. Since Convex replaces both the database and ORM layers, this is a more significant change than switching between SQL databases.

Here's how to switch from Neon to [Convex](https://convex.dev) for your `next-forge` project.

## 1. Sign up to Convex

Create a free account at [convex.dev](https://convex.dev). You can manage your projects through the [Convex Dashboard](https://dashboard.convex.dev).

## 2. Replace the dependencies

Uninstall the existing dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install Convex:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install convex --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add convex --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add convex --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add convex --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 3. Initialize Convex

From the root of your project, run:

```sh title="Terminal"
npx convex dev
```

This will prompt you to log in, create a new project, and generate a `convex/` directory in your project root with the configuration files. It will also create a `.env.local` file with your `CONVEX_DEPLOYMENT` and `NEXT_PUBLIC_CONVEX_URL` variables.

## 4. Set up the Convex client provider

Create a client component to wrap your app with the Convex provider. Add this to your app:

```tsx title="packages/database/provider.tsx"
'use client';

import type { ReactNode } from 'react';
import { ConvexProvider, ConvexReactClient } from 'convex/react';
import { keys } from './keys';

const convex = new ConvexReactClient(keys().NEXT_PUBLIC_CONVEX_URL);

export const ConvexClientProvider = ({ children }: { children: ReactNode }) => (
  <ConvexProvider client={convex}>
    {children}
  </ConvexProvider>
);
```

Then wrap your app layout with the provider:

```tsx title="apps/app/app/layout.tsx"
import { ConvexClientProvider } from '@repo/database/provider';

// ...

const RootLayout = ({ children }: { children: ReactNode }) => (
  <html lang="en">
    <body>
      <ConvexClientProvider>
        {children}
      </ConvexClientProvider>
    </body>
  </html>
);

export default RootLayout;
```

## 5. Update the database package

Replace the contents of the database package's main export. Since Convex uses its own function system instead of a traditional client, the export changes significantly:

```ts title="packages/database/index.ts"
export { ConvexClientProvider } from './provider';
```

Delete the `prisma/` directory from `@repo/database`:

```sh title="Terminal"
rm -rf packages/database/prisma
```

Update `keys.ts` to use the Convex environment variable:

```ts title="packages/database/keys.ts"
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const keys = () =>
  createEnv({
    client: {
      NEXT_PUBLIC_CONVEX_URL: z.url(),
    },
    runtimeEnv: {
      NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
    },
  });
```

## 6. Define your schema

Create a schema file in the `convex/` directory. Here's an example equivalent to the default Prisma `Page` model:

```ts title="convex/schema.ts"
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';

export default defineSchema({
  pages: defineTable({
    title: v.string(),
    content: v.optional(v.string()),
  }),
});
```

Run `npx convex dev` to push your schema to Convex and generate types.

## 7. Write queries and mutations

Create server functions for your data access. Convex uses its own function system instead of raw SQL or an ORM:

```ts title="convex/pages.ts"
import { query, mutation } from './_generated/server';
import { v } from 'convex/values';

export const list = query({
  handler: async (ctx) => {
    return await ctx.db.query('pages').collect();
  },
});

export const create = mutation({
  args: {
    title: v.string(),
    content: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    return await ctx.db.insert('pages', args);
  },
});
```

## 8. Update your app code

Convex uses React hooks for data fetching with automatic real-time updates. Update your components to use `useQuery` and `useMutation`:

```tsx title="app/(authenticated)/components/pages-list.tsx"
'use client';

import { useQuery, useMutation } from 'convex/react';
import { api } from '@repo/convex/_generated/api';

export const PagesList = () => {
  const pages = useQuery(api.pages.list);
  const createPage = useMutation(api.pages.create);

  return (
    <div>
      <button onClick={() => createPage({ title: 'New Page' })}>
        Create Page
      </button>
      {pages?.map((page) => (
        <div key={page._id}>{page.title}</div>
      ))}
    </div>
  );
};
```

<Note>
  Convex queries are reactive by default — your UI will automatically update when the underlying data changes, without any additional configuration.
</Note>

For server-side data fetching (e.g. in Server Components), use the Convex HTTP client:

```tsx title="app/(authenticated)/page.tsx"
import { ConvexHttpClient } from 'convex/browser';
import { api } from '@repo/convex/_generated/api';

const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

const App = async () => {
  const pages = await convex.query(api.pages.list);

  return (
    <div>
      {pages.map((page) => (
        <div key={page._id}>{page.title}</div>
      ))}
    </div>
  );
};

export default App;
```

## 9. Replace Prisma Studio

Delete the now unused Prisma Studio app:

```sh title="Terminal"
rm -rf apps/studio
```

To manage your data, use the [Convex Dashboard](https://dashboard.convex.dev) which provides a data browser, function logs, and deployment management.

## 10. Deploy

When deploying your app, set the `NEXT_PUBLIC_CONVEX_URL` environment variable in your hosting provider (e.g. Vercel). You can find this URL in your [Convex Dashboard](https://dashboard.convex.dev) under your project's settings.

To deploy your Convex functions to production, run:

```sh title="Terminal"
npx convex deploy
```

This deploys your schema and server functions to your production Convex instance.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Drizzle
description: How to change the ORM to Drizzle.
type: integration
summary: How to switch the ORM to Drizzle.
prerequisites:
  - /docs/packages/database
related:
  - /docs/migrations/database/prisma-postgres
---

# Switch to Drizzle



Drizzle is a brilliant, type-safe ORM growing quickly in popularity. If you want to switch to Drizzle, you have two options:

1. Keep Prisma and add the Drizzle API to the Prisma client. Drizzle have a [great guide](https://orm.drizzle.team/docs/prisma) on how to do this.
2. Go all-in and switch to Drizzle.

Here, we'll assume you have a working Neon database and cover the second option.

<Callout type="warn">
  Before starting, make sure your Prisma schema has been pushed to your database (e.g. by running `npx prisma db push` in `packages/database`). The `drizzle-kit pull` command in Step 4 introspects the live database — if no tables exist yet, it will generate an empty schema file.
</Callout>

## 1. Swap out the required dependencies in `@repo/database`

Uninstall the existing dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @prisma/adapter-neon @prisma/client prisma --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @prisma/adapter-neon @prisma/client prisma --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @prisma/adapter-neon @prisma/client prisma --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @prisma/adapter-neon @prisma/client prisma --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

...and install the new ones:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install drizzle-orm @neondatabase/serverless --filter @repo/database
    npm install -D drizzle-kit --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add drizzle-orm @neondatabase/serverless --filter @repo/database
    pnpm add -D drizzle-kit --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add drizzle-orm @neondatabase/serverless --filter @repo/database
    yarn add --dev drizzle-kit --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add drizzle-orm @neondatabase/serverless --filter @repo/database
    bun add --dev drizzle-kit --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Update the database connection code

Delete everything in `@repo/database/index.ts` and replace it with the following:

```ts title="packages/database/index.ts"
import 'server-only';

import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import { env } from '@repo/env';

const client = neon(env.DATABASE_URL);

export const database = drizzle({ client });
```

## 3. Create a `drizzle.config.ts` file

Next we'll create a Drizzle configuration file, used by Drizzle Kit and contains all the information about your database connection, migration folder and schema files. Create a `drizzle.config.ts` file in the `packages/database` directory with the following contents:

```ts title="packages/database/drizzle.config.ts"
import { defineConfig } from 'drizzle-kit';
import { env } from '@repo/env';

export default defineConfig({
  schema: './schema.ts',
  out: './',
  dialect: 'postgresql',
  dbCredentials: {
    url: env.DATABASE_URL,
  },
});
```

## 4. Generate the schema file

Drizzle uses a schema file to define your database tables. Rather than create one from scratch, you can generate it from the existing database. In the `packages/database` folder, run the following command to generate the schema file:

```sh
npx drizzle-kit pull
```

This should pull the schema from the database, creating a `schema.ts` file containing the table definitions and some other files.

If the generated `schema.ts` is empty, your database likely has no tables yet. You can either push your Prisma schema first (`npx prisma db push`) and re-run the pull, or create the schema manually. For example, the default next-forge `Page` model translates to:

```ts title="packages/database/schema.ts"
import { pgTable, serial, text } from 'drizzle-orm/pg-core';

export const page = pgTable('Page', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
});
```

## 5. Update your queries

Now you can update your queries to use the Drizzle ORM.

For example, here's how we can update the `page` query in `app/(authenticated)/page.tsx`:

```ts title="apps/app/app/(authenticated)/page.tsx {2, 7}"
import { database } from '@repo/database';
import { page } from '@repo/database/schema';

// ...

const App = async () => {
  const pages = await database.select().from(page);

  // ...
};

export default App;
```

## 6. Remove Prisma Studio

You can also delete the now unused Prisma Studio app located at `apps/studio`:

```sh title="Terminal"
rm -fr apps/studio
```

## 7. Update the migration script in the root `package.json`

Change the migration script in the root `package.json` from Prisma to Drizzle. Update the `migrate` script to use Drizzle commands:

```json
"scripts": {
  "db:migrate": "cd packages/database && npx drizzle-kit migrate"
  "db:generate": "cd packages/database && npx drizzle-kit generate"
  "db:pull": "cd packages/database && npx drizzle-kit pull"
}
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to EdgeDB
description: How to change the database provider to EdgeDB.
type: integration
summary: How to switch the database provider to EdgeDB.
prerequisites:
  - /docs/packages/database
---

# Switch to EdgeDB



[EdgeDB](https://edgedb.com) is an open-source Postgres data layer designed to address major ergonomic SQL and relational schema modeling limitations while improving type safety and performance.

<Note>
  EdgeDB rebranded to "Gel" in February 2025. The `edgedb` npm packages and CLI commands still work via compatibility shims. See the [Gel announcement](https://www.geldata.com/blog/edgedb-is-now-gel-and-postgres-is-the-future) for details.
</Note>

`next-forge` uses Neon as the database provider with Prisma as the ORM as well as Clerk for authentication. This guide will provide the steps you need to switch the database provider from Neon to EdgeDB.

<Note>
  For authentication, another guide will be provided to switch to EdgeDB Auth with access policies, social
  auth providers, and more.
</Note>

Here's how to switch from Neon to [EdgeDB](https://edgedb.com) for your `next-forge` project.

## 1. Create a new EdgeDB database

Create an account at [EdgeDB Cloud](https://cloud.edgedb.com/). Once done, create a new instance (you can use EdgeDB's free tier). We'll later connect to it through the EdgeDB CLI.

## 2. Swap out the required dependencies in `@repo/database`

Uninstall the existing dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @prisma/adapter-neon @prisma/client prisma --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @prisma/adapter-neon @prisma/client prisma --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @prisma/adapter-neon @prisma/client prisma --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @prisma/adapter-neon @prisma/client prisma --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install the new dependencies:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install edgedb @edgedb/generate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add edgedb @edgedb/generate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add edgedb @edgedb/generate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add edgedb @edgedb/generate
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 3. Setup EdgeDB in `@repo/database` package

In the `@repo/database` directory, run:

```sh title="Terminal"
npx edgedb project init --server-instance <org_name>/<instance_name> --non-interactive
```

<Note>
  Replace `<org_name>` and `<instance_name>` with the EdgeDB's organization and instance you've previously created in the EdgeDB Cloud.
</Note>

The `init` command creates a new subdirectory called `dbschema`, which contains everything related to EdgeDB:

```sh
dbschema
├── default.esdl
└── migrations
```

This command also links your environment to the EdgeDB Cloud instance, allowing the EdgeDB client libraries to automatically connect to it without any additional configuration.

You can also delete `prisma/` directory from the `@repo/database`:

```sh title="Terminal"
rm -fr packages/database/prisma
```

## 4. Update the database connection code

Update the database connection code to use an EdgeDB client:

```ts title="packages/database/index.ts"
import 'server-only';

import { createClient } from "edgedb";

export const database = createClient();
```

## 5. Update the schema file and generate types

Now, you can modify the database schema:

```sql title="dbschema/default.esdl"
module default {
  type Page {
    email: str {
      constraint exclusive;
    }
    name: str
  }
}
```

And apply your changes by running:

```sh title="Terminal"
npx edgedb migration create
npx edgedb migration apply
```

Once complete, you can also generate a TypeScript query builder and types from your database schema:

```sh title="Terminal"
npx @edgedb/generate edgeql-js
npx @edgedb/generate interfaces
```

These commands introspect the schema of your database and generate code in the `dbschema` directory.

## 6. Update your queries

Now you can update your queries to use the EdgeDB client.

For example, here’s how we can update the `page` query in `app/(authenticated)/page.tsx`:

```tsx title="app/(authenticated)/page.tsx {2,7-8}"
import { database } from '@repo/database';
import edgeql from '@repo/database/dbschema/edgeql-js';

// ...

const App = async () => {
  const pagesQuery = edgeql.select(edgeql.Page, () => ({ ...edgeql.Page['*'] }));
  const pages = await pagesQuery.run(database);

  // ...
};

export default App;
```

## 7. Replace Prisma Studio with EdgeDB UI

You can also delete the now unused Prisma Studio app located at `apps/studio`:

```sh title="Terminal"
rm -fr apps/studio
```

To manage your database and browse your data, you can run:

```sh title="Terminal"
npx edgedb ui
```

## 8. Extract EdgeDB environment variables for deployment

When deploying your app, you need to provide the `EDGEDB_SECRET_KEY` and `EDGEDB_INSTANCE` environment variables in your app's cloud provider to connect to your EdgeDB Cloud instance.

You can generate a dedicated secret key for your instance with `npx edgedb cloud secretkey create` or via the web UI's "Secret Keys" pane in your instance dashboard.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to PlanetScale
description: How to change the database provider to PlanetScale.
type: integration
summary: How to switch the database provider to PlanetScale.
prerequisites:
  - /docs/packages/database
---

# Switch to PlanetScale



Here's how to switch from Neon to [PlanetScale](https://planetscale.com).

<Warning>
  PlanetScale removed their free/Hobby tier in March 2024. The minimum plan now starts at $39/month. Keep this in mind when evaluating PlanetScale for your project.
</Warning>

## 1. Create a new database on PlanetScale

Once you create a database on PlanetScale, you will get a connection string. It will look something like this:

```
mysql://<username>:<password>@<region>.aws.connect.psdb.cloud/<database>
```

Keep this connection string handy, you will need it in the next step.

## 2. Update your environment variables

Update your environment variables to use the new PlanetScale connection string:

```js title="apps/database/.env"
DATABASE_URL="mysql://<username>:<password>@<region>.aws.connect.psdb.cloud/<database>"
```

```js title="apps/app/.env.local"
DATABASE_URL="mysql://<username>:<password>@<region>.aws.connect.psdb.cloud/<database>"
```

Etcetera.

## 3. Swap out the required dependencies in `@repo/database`

Uninstall the existing dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

...and install the new ones:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @planetscale/database @prisma/adapter-planetscale --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @planetscale/database @prisma/adapter-planetscale --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @planetscale/database @prisma/adapter-planetscale --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @planetscale/database @prisma/adapter-planetscale --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 4. Update the database connection code

Update the database connection code to use the new PlanetScale adapter:

```ts title="packages/database/index.ts {3-4, 17-18}"
import 'server-only';

import { Client, connect } from '@planetscale/database';
import { PrismaPlanetScale } from '@prisma/adapter-planetscale';
import { PrismaClient } from '@prisma/client';
import { env } from '@repo/env';

declare global {
  var cachedPrisma: PrismaClient | undefined;
}

const client = connect({ url: env.DATABASE_URL });
const adapter = new PrismaPlanetScale(client);

export const database = new PrismaClient({ adapter });
```

## 5. Update your Prisma schema

Update your Prisma schema to use the new database provider:

```prisma title="packages/database/prisma/schema.prisma {10}"
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client"
  output   = "../generated"
}

datasource db {
  provider     = "mysql"
  relationMode = "prisma"
}

// This is a stub model.
// Delete it and add your own Prisma models.
model Page {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}
```

## 6. Add a `dev` script

Add a `dev` script to your `package.json`:

```json title="packages/database/package.json {3}"
{
  "scripts": {
    "dev": "pscale connect [database_name] [branch_name] --port 3309"
  }
}
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Prisma Postgres
description: How to change the database provider to Prisma Postgres.
type: integration
summary: How to switch the database provider to Prisma Postgres.
prerequisites:
  - /docs/packages/database
related:
  - /docs/migrations/database/drizzle
---

# Switch to Prisma Postgres



Here's how to switch from Neon to [Prisma Postgres](https://www.prisma.io/postgres) — a serverless database with zero cold starts and a generous free tier. You can learn more about its architecture that enables this [here](https://www.prisma.io/blog/announcing-prisma-postgres-early-access).

## 1. Create a new Prisma Postgres instance

Start by creating a new Prisma Postgres instance via the [Prisma Data Platform](https://console.prisma.io/) and get your connection string. It will look something like this:

```
prisma+postgres://accelerate.prisma-data.net/?api_key=ey....
```

## 2. Update your environment variables

Update your environment variables to use the new Prisma Postgres connection string:

```js title="apps/database/.env"
DATABASE_URL="prisma+postgres://accelerate.prisma-data.net/?api_key=ey...."
```

## 3. Swap out the required dependencies in `@repo/database`

Uninstall the existing dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @neondatabase/serverless @prisma/adapter-neon ws @types/ws
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install the new dependencies:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @prisma/extension-accelerate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @prisma/extension-accelerate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @prisma/extension-accelerate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @prisma/extension-accelerate
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 4. Update the database connection code

Update the database connection code to use the new Prisma Postgres adapter:

```ts title="packages/database/index.ts {4,7}"
import 'server-only';

import { env } from '@repo/env';
import { withAccelerate } from '@prisma/extension-accelerate';
import { PrismaClient } from '@prisma/client';

export const database = new PrismaClient().$extends(withAccelerate());
```

Your project is now configured to use your Prisma Postgres instance for migrations and queries.

## 5. Explore caching and real-time database events

Note that thanks to the first-class integration of other Prisma products, Prisma Postgres comes with additional features out-of-the-box that you may find useful:

* [Prisma Accelerate](https://www.prisma.io/accelerate): Enables connection pooling and global caching
* [Prisma Pulse](https://www.prisma.io/pulse): Enables real-time streaming of database events

### Caching

To cache a query with Prisma Client, you can add the [`swr`](https://www.prisma.io/docs/accelerate/caching#stale-while-revalidate-swr) and [`ttl`](https://www.prisma.io/docs/accelerate/caching#time-to-live-ttl) options to any given query, for example:

```ts title="page.tsx"
const pages = await prisma.page.findMany({
  cacheStrategy: {
    swr: 60, // 60 seconds
    ttl: 60, // 60 seconds
  },
});
```

Learn more in the [Accelerate documentation](https://www.prisma.io/docs/accelerate).

### Real-time database events

<Warning>
  Prisma Pulse (`@prisma/extension-pulse`) has been paused and may be deprecated. Check the [Prisma Pulse documentation](https://www.prisma.io/docs/pulse) for the latest status before proceeding.
</Warning>

To stream database change events from your database, you first need to install the Pulse extension:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @prisma/extension-pulse
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @prisma/extension-pulse
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @prisma/extension-pulse
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @prisma/extension-pulse
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Next, you need to add your Pulse API key as an environment variable:

```ts title="apps/database/.env"
PULSE_API_KEY="ey...."
```

<Info>
  You can find your Pulse API key in your Prisma Postgres connection string, it's the value of the `api_key` argument and starts with `ey...`. Alternatively, you can find the API key in your [Prisma Postgres Dashboard](https://console.prisma.io).
</Info>

Then, update the `env` package to include the new `PULSE_API_KEY` environment variable:

```ts title="packages/env/index.ts {3,11}"
export const server = {
  // ...
  PULSE_API_KEY: z.string().min(1).startsWith('ey'),
};

export const env = createEnv({
  client,
  server,
  runtimeEnv: {
    // ...
    PULSE_API_KEY: process.env.PULSE_API_KEY,
  },
});
```

Finally, update the database connection code to include the Pulse extension:

```ts title="packages/database/index.ts {2, 7-9}"
import 'server-only';
import { withPulse } from '@prisma/extension-pulse';
import { withAccelerate } from '@prisma/extension-accelerate';
import { PrismaClient } from '@prisma/client';
import { env } from '@repo/env';

export const database = new PrismaClient()
  .$extends(withAccelerate())
  .$extends(withPulse({ apiKey: env.PULSE_API_KEY })) ;
```

You can now stream any change events from your database using the following code:

```ts title="page.tsx"
const stream = await prisma.page.stream();

console.log(`Waiting for an event on the \`Page\` table ... `);

for await (const event of stream) {
  console.log('Received an event:', event);
}
```

Learn more in the [Pulse documentation](https://www.prisma.io/docs/pulse).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Supabase
description: How to change the database provider to Supabase.
type: integration
summary: How to switch the database provider to Supabase.
prerequisites:
  - /docs/packages/database
---

# Switch to Supabase



[Supabase](https://supabase.com) is an open source Firebase alternative providing a Postgres database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, and Storage.

`next-forge` uses Neon as the database provider with Prisma as the ORM as well as Clerk for authentication. This guide will provide the steps you need to switch the database provider from Neon to Supabase. This guide is based on a few existing resources, including [Supabase's guide](https://supabase.com/partners/integrations/prisma) and [Prisma's guide](https://www.prisma.io/docs/orm/overview/databases/supabase).

<Note>
  For authentication, see the [Supabase Auth migration guide](/docs/migrations/authentication/supabase) to switch from Clerk to Supabase Auth with organization management, user roles, and more.
</Note>

Here's how to switch from Neon to [Supabase](https://supabase.com) for your `next-forge` project.

## 1. Sign up to Supabase

Create a free account at [supabase.com](https://supabase.com). You can manage your projects through the Dashboard or use the [Supabase CLI](https://supabase.com/docs/guides/local-development).

*We'll be using both the Dashboard and CLI throughout this guide.*

## 2. Create a Project

Create a new project from the [Supabase Dashboard](https://supabase.com/dashboard). Give it a name and choose your preferred region. Once created, you'll get access to your project's connection details. Head to the **Settings** page, then click on **Database**.

We'll need to keep track of the following for the next step:

* The Database URL in `Transaction` mode, with the port ending in `6543`. We'll call this `DATABASE_URL`.
* The Database URL in `Session` mode, with the port ending in `5432`. We'll call this `DIRECT_URL`.

## 3. Update the environment variables

Update the `.env` file with the Supabase connection details. Make sure you add `?pgbouncer=true&connection_limit=1` to the end of the `DATABASE_URL` value.

```js title=".env"
DATABASE_URL="postgres://postgres:postgres@127.0.0.1:54322/postgres?pgbouncer=true&connection_limit=1"
DIRECT_URL="postgres://postgres:postgres@127.0.0.1:54322/postgres"
```

<Note>
  `pgbouncer=true` disables Prisma from generating prepared statements. This is required since our connection pooler does not support prepared statements in transaction mode yet. The `connection_limit=1` parameter is only required if you are using Prisma from a serverless environment.
</Note>

## 4. Replace the dependencies

Prisma doesn't have a Supabase adapter yet, so we just need to remove the Neon adapter and connect to Supabase directly.

First, remove the Neon dependencies from the project...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and add the Supabase dependencies:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install -D supabase --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add -D supabase --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add --dev supabase --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add --dev supabase --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 5. Update the database package

Update the `database` package. We'll remove the Neon extensions and connect to Supabase directly, which should automatically use the environment variables we set earlier.

```ts title="packages/database/index.ts"
import 'server-only';
import { PrismaClient } from '@prisma/client';

export const database = new PrismaClient();

export * from '@prisma/client';
```

## 6. Update the Prisma schema

Update the `prisma/schema.prisma` file so it contains the `DIRECT_URL`. This allows us to use the Prisma CLI to perform other actions on our database (e.g. migrations) by bypassing Supavisor.

```js title="prisma/schema.prisma {4}"
datasource db {
  provider     = "postgresql"
  url          = env("DATABASE_URL")
  directUrl    = env("DIRECT_URL")
}
```

<Note>
  You don't need `relationMode = "prisma"` here — Supabase is PostgreSQL and supports foreign keys natively. Only add it if you specifically need Prisma to emulate relations (e.g., for compatibility with databases that don't support foreign keys).
</Note>

Now you can run the migration from the root of your `next-forge` project:

```sh title="Terminal"
bun run migrate
```

## 7. Set up Row Level Security (RLS)

Row Level Security (RLS) is a powerful PostgreSQL feature that allows you to control access to database rows based on the authenticated user. This is essential for multi-tenant applications where users should only see their own data.

<Note>
  RLS policies use `auth.uid()` to get the authenticated user's ID from Supabase Auth. Make sure you've completed the [Supabase Auth migration](/docs/migrations/authentication/supabase) first.
</Note>

### Enable RLS on your tables

First, enable RLS on the tables you want to protect. You can do this via the Supabase dashboard or by running SQL migrations:

```sql title="Enable RLS"
-- Enable RLS on organizations table
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;

-- Enable RLS on organization_members table
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;

-- Enable RLS on any other tables that need protection
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
```

### Create RLS policies

Once RLS is enabled, create policies that define who can access what data:

#### Organization policies

```sql title="Organization RLS Policies"
-- Policy: Users can only view organizations they're members of
CREATE POLICY "Users can view their organizations"
ON organizations FOR SELECT
USING (
  id IN (
    SELECT organization_id
    FROM organization_members
    WHERE user_id = auth.uid()
  )
);

-- Policy: Only organization owners can update organizations
CREATE POLICY "Owners can update their organizations"
ON organizations FOR UPDATE
USING (
  id IN (
    SELECT organization_id
    FROM organization_members
    WHERE user_id = auth.uid() AND role = 'owner'
  )
);

-- Policy: Only organization owners can delete organizations
CREATE POLICY "Owners can delete their organizations"
ON organizations FOR DELETE
USING (
  id IN (
    SELECT organization_id
    FROM organization_members
    WHERE user_id = auth.uid() AND role = 'owner'
  )
);

-- Policy: Any authenticated user can create an organization
CREATE POLICY "Authenticated users can create organizations"
ON organizations FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
```

#### Organization member policies

```sql title="Organization Member RLS Policies"
-- Policy: Users can view members of their organizations
CREATE POLICY "Users can view organization members"
ON organization_members FOR SELECT
USING (
  organization_id IN (
    SELECT organization_id
    FROM organization_members
    WHERE user_id = auth.uid()
  )
);

-- Policy: Owners and admins can add members
CREATE POLICY "Owners and admins can add members"
ON organization_members FOR INSERT
WITH CHECK (
  organization_id IN (
    SELECT organization_id
    FROM organization_members
    WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
  )
);

-- Policy: Owners and admins can update member roles
CREATE POLICY "Owners and admins can update members"
ON organization_members FOR UPDATE
USING (
  organization_id IN (
    SELECT organization_id
    FROM organization_members
    WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
  )
);

-- Policy: Owners and admins can remove members
CREATE POLICY "Owners and admins can remove members"
ON organization_members FOR DELETE
USING (
  organization_id IN (
    SELECT organization_id
    FROM organization_members
    WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
  )
);
```

### Testing RLS policies

You can test your RLS policies using the Supabase SQL Editor with the following pattern:

```sql title="Test RLS as a specific user"
-- Set the user ID for testing
SELECT auth.uid(); -- This will be NULL initially

-- To test as a specific user, you would typically:
-- 1. Make requests through your application with that user's session
-- 2. Or use Supabase's testing tools in the dashboard

-- Check what organizations a user can see
SELECT * FROM organizations;

-- This query will automatically be filtered by your RLS policies
```

### Common RLS patterns

#### User-owned data

For data that belongs directly to a user (like user profiles or settings):

```sql
CREATE POLICY "Users can only access their own data"
ON user_data FOR ALL
USING (user_id = auth.uid());
```

#### Tenant isolation

For multi-tenant data where access is determined by an organization or tenant ID:

```sql
CREATE POLICY "Users can only access their tenant's data"
ON tenant_data FOR ALL
USING (
  tenant_id IN (
    SELECT tenant_id FROM user_tenants WHERE user_id = auth.uid()
  )
);
```

#### Public read, authenticated write

For data that everyone can read but only authenticated users can modify:

```sql
-- Read policy (no authentication required)
CREATE POLICY "Anyone can read"
ON public_data FOR SELECT
USING (true);

-- Write policy (authentication required)
CREATE POLICY "Authenticated users can write"
ON public_data FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
```

<Warning>
  When RLS is enabled on a table, all access is denied by default. You must create policies to allow access. Make sure to test thoroughly to avoid accidentally blocking legitimate access.
</Warning>

For more information, see the [Supabase Row Level Security guide](https://supabase.com/docs/guides/auth/row-level-security).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Turso
description: How to change the database provider to Turso.
type: integration
summary: How to switch the database provider to Turso.
prerequisites:
  - /docs/packages/database
---

# Switch to Turso



[Turso](https://turso.tech) is multi-tenant database platform built for all types of apps, including AI apps with on-device RAG, local-first vector search, offline writes, and privacy-focused data access with low latency.

Here's how to switch from Neon to [Turso](https://turso.tech) for your `next-forge` project.

## 1. Sign up to Turso

You can use the [Dashboard](https://app.turso.tech), or the [CLI](https://docs.turso.tech/cli) to manage your account, database, and auth tokens.

*We'll be using the CLI throughout this guide.*

## 2. Create a Database

Create a new database and give it a name using the Turso CLI:

```sh title="Terminal"
turso db create <database-name>
```

You can now fetch the URL to the database:

```sh title="Terminal"
turso db show <database-name> --url
```

It will look something like this:

```
libsql://<database-name>-<account-or-org-slug>.turso.io
```

## 3. Create a Database Auth Token

You will need to create an auth token to connect to your Turso database:

```sh title="Terminal"
turso db tokens create <database-name>
```

## 4. Update your environment variables

Update your environment variables to use the new Turso connection string:

```js title="apps/database/.env"
DATABASE_URL="libsql://<database-name>-<account-or-org-slug>.turso.io"
DATABASE_AUTH_TOKEN="..."
```

```js title="apps/app/.env.local"
DATABASE_URL="libsql://<database-name>-<account-or-org-slug>.turso.io"
DATABASE_AUTH_TOKEN="..."
```

Etcetera.

Now inside `packages/env/index.ts`, add `DATABASE_AUTH_TOKEN` to the `server` and `runtimeEnv` objects:

```ts title="{3,12}"
const server: Parameters<typeof createEnv>[0]["server"] = {
  // ...
  DATABASE_AUTH_TOKEN: z.string(),
  // ...
};

export const env = createEnv({
  client,
  server,
  runtimeEnv: {
    // ...
    DATABASE_AUTH_TOKEN: process.env.DATABASE_AUTH_TOKEN,
    // ...
  },
});
```

## 5. Install @libsql/client

The [`@libsql/client`](https://www.npmjs.com/@libsql/client) is used to connect to the hosted Turso database.

Uninstall the existing dependencies for Neon...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install the new dependencies for Turso & libSQL:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @libsql/client --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @libsql/client --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @libsql/client --filter @repo/database
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @libsql/client --filter @repo/database
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 6. Update the database connection code

Open `packages/database/index.ts` and make the following changes:

```ts title="packages/database/index.ts"
import "server-only";

import { createClient } from "@libsql/client";
import { env } from "@repo/env";

const libsql = createClient({
  url: env.DATABASE_URL,
  authToken: env.DATABASE_AUTH_TOKEN,
});

export const database = libsql;
```

## 7. Apply schema changes

Now connect to the Turso database using the CLI:

```sh title="Terminal"
turso db shell <database-name>
```

And apply the schema to the database:

```sql
CREATE TABLE pages (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  name TEXT
);
```

## 8. Update application code

Now wherever you would usually call Prisma, use the `libsql` client instead:

```ts title="packages/app/app/(authenticated)/page.tsx"
import { database } from "@repo/database";

type PageType = {
  id: number;
  email: string;
  name?: string;
};

// ...

const { rows } = await database.execute(`SELECT * FROM pages`);

const pages = rows as unknown as Array<PageType>;
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Fumadocs
description: How to change the documentation provider to Fumadocs.
type: integration
summary: How to switch the documentation provider to Fumadocs.
prerequisites:
  - /docs/apps/docs
---

# Switch to Fumadocs



[Fumadocs](https://fumadocs.dev) is a beautiful & powerful docs framework powered by Next.js, allowing advanced customisations.

## 1. Create a Fumadocs App

Fumadocs is similar to a set of libraries built on **Next.js App Router**, which works very differently from a hosted solution like Mintlify, or other frameworks/static site generator that takes complete control over your app.

To begin, you can use a command to initialize a Fumadocs app quicky:

```sh title="Terminal"
bunx create-fumadocs-app
```

Here we assume you have enabled Fumadocs MDX, Tailwind CSS, and without a default ESLint config.

### What is a Content Source?

The input/source of your content, it can be a CMS, or local data layers like **Content Collections** and **Fumadocs MDX** (the official content source).

Fumadocs is designed carefully to allow a custom content source, there's also examples for [Sanity](https://github.com/fuma-nama/fumadocs-sanity) if you are interested.

<Note>
  `lib/source.ts`

   is where you organize code for content sources.
</Note>

### Update your Tailwind CSS

Start the app with `bun dev`.

If some styles are missing, it could be due to your monorepo setup, you can change the `content` property in your Tailwind CSS config (`tailwind.config.mjs`) to ensure it works:

```js title="tailwind.config.mjs"
export default {
  content: [
    // from
    './node_modules/fumadocs-ui/dist/**/*.js',
    // to
    '../../node_modules/fumadocs-ui/dist/**/*.js',

    './components/**/*.{ts,tsx}',
    // ...
  ],
};
```

You can either keep the Tailwind config file isolated to the docs, or merge it with your existing config from the `tailwind-config` package.

## 2. Migrate MDX Files

Fumadocs, same as Mintlify, utilize MDX for content files. You can move the `.mdx` files from your Mintlify app to `content/docs` directory.

<Note>
  Fumadocs requires a 

  `title`

   frontmatter property.
</Note>

The MDX syntax of Fumadocs is almost identical to Mintlify, despite from having different components and usage for code blocks. Visit [Markdown](https://fumadocs.dev/docs/ui/markdown) for supported Markdown syntax.

### Code Block

Code block titles are formatted with `title="Title"`.

#### Before

````sh title="Mintlify"
```sh title="Name"
bun install
```
````

#### After

````sh title="Fumadocs"
```sh title="title="Name""
bun install
```
````

Code highlighting is done with an inline comment.

#### Before

````ts title="Mintlify"
```ts {1}
console.log('Highlighted');
```
````

#### After

````ts title="Fumadocs"
```ts
console.log('Highlighted'); // [!code highlight]
```
````

In Fumadocs, you can also highlight specific words.

```ts title="Fumadocs"
console.log('Highlighted'); // [!code word:Highlighted]
```

### Code Groups

For code groups, you can use the `Tabs` component:

#### Before

````tsx title="Mintlify"
<CodeGroup>

```ts title="Tab One"
console.log('Hello, world!');
```

```ts title="Tab Two"
console.log('Hello, world!');
```

</CodeGroup>
````

#### After

````tsx title="Fumadocs"
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
 
<Tabs items={["Tab 1", "Tab 2"]}>
 
```ts title="tab="Tab 1""
console.log('A');
```
 
```ts title="tab="Tab 2""
console.log('B');
```
 
</Tabs>
````

Fumadocs also has a built-in integration for TypeScript Twoslash, check it out in the [Setup Guide](https://fumadocs.dev/docs/ui/twoslash).

### Callout

Fumadocs uses a generic `Callout` component for callouts, as opposed to Mintlify's specific ones.

#### Before

```tsx title="Mintlify"
<Note>Hello World</Note>
<Warning>Hello World</Warning>
<Info>Hello World</Info>
<Tip>Hello World</Tip>
<Check>Hello World</Check>
```

#### After

```tsx title="Fumadocs"
<Callout title="Title" type="info">Hello World</Callout>
<Callout title="Title" type="warn">Hello World</Callout>
<Callout title="Title" type="error">Hello World</Callout>
```

### Adding Components

To use components without import, add them to your MDX component.

```tsx title="app/docs/[[...slug]]/page.tsx"
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
 
<MDX components={{ Tabs, Tab }} />;
```

## 3. Migrate `mint.json` File

Instead of a single file, you can configure Fumadocs using code.

### Sidebar Items

The sidebar items are generated from your file system, Fumadocs takes `meta.json` as the configurations of a folder.
You don't need to hardcode the sidebar config manually.

For example, to customise the order of pages in `content/docs/components` folder, you can create a `meta.json` folder in the directory:

```json title="meta.json"
{
  "title": "Components", // optional
  "pages": ["index", "apple"] // file names (without extension)
}
```

Fumadocs also support the rest operator (`...`) if you want to include the other pages.

```json title="meta.json"
{ 
  "title": "Components", // optional
  "pages": ["index", "apple", "..."] // file names (without extension)
}
```

Visit the [Pages Organization Guide](https://fumadocs.dev/docs/ui/page-conventions) for an overview of supported syntaxs.

### Appearance

The overall theme can be customised using CSS variables and/or presets.

#### CSS variables

In your global CSS file:

```css title="global.css"
:root {
  /* hsl values, like hsl(239 37% 50%) but without `hsl()` */
  --background: 239 37% 50%;

  /* Want a max width for docs layout? */
  --fd-layout-width: 1400px;
}

.dark {
  /* hsl values, like hsl(239 37% 50%) but without `hsl()` */
  --background: 239 37% 50%;
}
```

#### Tailwind Presets

In your Tailwind config, use the `preset` option.

```js title="tailwind.config.mjs"
import { createPreset } from 'fumadocs-ui/tailwind-plugin';
 
/** @type {import('tailwindcss').Config} */
export default {
  presets: [
    createPreset({
      preset: 'ocean',
    }),
  ],
};
```

See [all available presets](https://fumadocs.dev/docs/ui/theme#presets).

### Layout Styles

You can open `app/layout.config.tsx`, it contains the shared options for layouts.
Fumadocs offer a default **Docs Layout** for documentation pages, and **Home Layout** for other pages.

You can customise the layouts in `layout.tsx`.

### Search

`app/api/search/route.ts` contains the Route Handler for search, it is powered by [Orama](https://orama.com) by default.

### Navigation Links

Navigation links are passed to layouts, you can also customise them in your Layout config.

```tsx title="app/layout.config.tsx"
import { BookIcon } from 'lucide-react';
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
 
export const baseOptions: BaseLayoutProps = {
  links: [
    {
      icon: <BookIcon />,
      text: 'Blog',
      url: '/blog',
    },
  ],
};
```

See [all supported items](https://fumadocs.dev/docs/ui/blocks/links).

## Done

Now, you should be able to build and preview the docs.

Visit [Fumadocs](https://fumadocs.dev/docs/ui) for details and additional features.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Hypertune
description: How to change the feature flag provider to Hypertune.
type: integration
summary: How to switch the feature flag provider to Hypertune.
prerequisites:
  - /docs/packages/flags
---

# Switch to Hypertune



[Hypertune](https://www.hypertune.com/) is the most flexible platform for feature flags, A/B testing, analytics and app configuration. Built with full end-to-end type-safety, Git version control and local, synchronous, in-memory flag evaluation. Optimized for TypeScript, React and Next.js.

Here's how to switch your next-forge project to use Hypertune for feature flags!

## 1. Create a new Hypertune project

Go to Hypertune and create a new project using the [next-forge template](https://app.hypertune.com/?new_project=1\&new_project_template=next-forge). Then go to the Settings page of your project and copy the main token.

## 2. Update the environment variables

Update the environment variables across the project. For example:

```js title="apps/app/.env"
// Add this:
NEXT_PUBLIC_HYPERTUNE_TOKEN=""
```

Add a `.env` file to the `feature-flags` package with the following contents:

```js title="packages/feature-flags/.env"
NEXT_PUBLIC_HYPERTUNE_TOKEN=""
HYPERTUNE_FRAMEWORK=nextApp
HYPERTUNE_OUTPUT_DIRECTORY_PATH=generated
HYPERTUNE_PLATFORM=vercel
HYPERTUNE_GET_HYPERTUNE_IMPORT_PATH=../lib/getHypertune
```

## 3. Update the `keys.ts` file in the `feature-flags` package

Use the `NEXT_PUBLIC_HYPERTUNE_TOKEN` environment variable in the call to `createEnv`:

```ts title="packages/feature-flags/keys.ts {6-8,14}"
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const keys = () =>
  createEnv({
    client: {
      NEXT_PUBLIC_HYPERTUNE_TOKEN: z.string().min(1),
    },
    server: {
      FLAGS_SECRET: z.string().optional(),
    },
    runtimeEnv: {
      FLAGS_SECRET: process.env.FLAGS_SECRET,
      NEXT_PUBLIC_HYPERTUNE_TOKEN: process.env.NEXT_PUBLIC_HYPERTUNE_TOKEN,
    },
  });
```

## 4. Swap out the required dependencies

First, delete the `create-flag.ts` file.

Then, uninstall the existing dependencies from the `feature-flags` package:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @repo/analytics --filter @repo/feature-flags
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @repo/analytics --filter @repo/feature-flags
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @repo/analytics --filter @repo/feature-flags
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @repo/analytics --filter @repo/feature-flags
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Then, install the new dependencies:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install hypertune server-only --filter @repo/feature-flags
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add hypertune server-only --filter @repo/feature-flags
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add hypertune server-only --filter @repo/feature-flags
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add hypertune server-only --filter @repo/feature-flags
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 5. Set up Hypertune code generation

Add `analyze` and `build` scripts to the `package.json` file for the `feature-flags` package, which both execute the `hypertune` command:

```json title="packages/feature-flags/package.json"
{
  "scripts": {
    "analyze": "hypertune",
    "build": "hypertune"
  }
}
```

Then run code generation with the following command:

```sh title="Terminal"
bun run build --filter @repo/feature-flags 
```

This will generate the following files:

```txt
packages/feature-flags/generated/hypertune.ts
packages/feature-flags/generated/hypertune.react.tsx
packages/feature-flags/generated/hypertune.vercel.tsx
```

## 6. Set up Hypertune client instance

Add a `getHypertune.ts` file in the `feature-flags` package which defines a `getHypertune` function that returns an initialized instance of the Hypertune SDK on the server:

```ts title="packages/feature-flags/lib/getHypertune.ts"
import 'server-only';
import { auth } from '@repo/auth/server';
import { noStore } from 'next/cache';
import type { ReadonlyHeaders } from 'next/dist/server/web/spec-extension/adapters/headers';
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies';
import { createSource } from '../generated/hypertune';
import { getVercelOverride } from '../generated/hypertune.vercel';
import { keys } from '../keys';

const hypertuneSource = createSource({
  token: keys().NEXT_PUBLIC_HYPERTUNE_TOKEN,
});

export default async function getHypertune(params?: {
  headers?: ReadonlyHeaders;
  cookies?: ReadonlyRequestCookies;
}) {
  noStore();
  await hypertuneSource.initIfNeeded(); // Check for flag updates

  const { userId, orgId, sessionId } = await auth();

  // Respect flag overrides set by the Vercel Toolbar
  hypertuneSource.setOverride(await getVercelOverride());

  return hypertuneSource.root({
    args: {
      context: {
        environment: process.env.NODE_ENV,
        user: { id: userId ?? '', sessionId: sessionId ?? '' },
        org: { id: orgId ?? '' },
      },
    },
  });
}
```

## 7. Update `index.ts`

Hypertune automatically generates feature flag functions that use the `flags` package. To export them the same way as before, update the `index.ts` file to export everything from the `generated/hypertune.vercel.ts` file:

```ts title="packages/feature-flags/index.ts"
export * from "./generated/hypertune.vercel.tsx"
```

Hypertune adds a `Flag` suffix to all these generated feature flag functions, so you will need to update flag usages with this, e.g. `showBetaFeature` => `showBetaFeatureFlag`.

## 8. Add more feature flags

To add more feature flags, create them in the Hypertune UI and then re-run code generation. They will be automatically added to your generated files.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to ESLint
description: How to change the default linter to ESLint.
type: integration
summary: How to switch the linter to ESLint.
prerequisites:
  - /docs/packages/formatting
---

# Switch to ESLint



Here's how to switch from Biome to [ESLint](https://eslint.org). In this example, we'll also add the Next.js and React plugins, as well as the new ESLint Flat Config.

## 1. Swap out the required dependencies

First, uninstall the existing dependencies from the root `package.json` file...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @biomejs/biome ultracite
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @biomejs/biome ultracite
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @biomejs/biome ultracite
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @biomejs/biome ultracite
    ```
  </CodeBlockTab>
</CodeBlockTabs>

...and install the new ones:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install -D eslint @next/eslint-plugin-next eslint-plugin-react eslint-plugin-react-hooks typescript-eslint
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add -D eslint @next/eslint-plugin-next eslint-plugin-react eslint-plugin-react-hooks typescript-eslint
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add --dev eslint @next/eslint-plugin-next eslint-plugin-react eslint-plugin-react-hooks typescript-eslint
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add --dev eslint @next/eslint-plugin-next eslint-plugin-react eslint-plugin-react-hooks typescript-eslint
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Configure ESLint

Delete the existing `biome.json` file in the root of the project, and create a new `eslint.config.mjs` file:

```js title="eslint.config.mjs"
import react from 'eslint-plugin-react';
import next from '@next/eslint-plugin-next';
import hooks from 'eslint-plugin-react-hooks';
import ts from 'typescript-eslint'

export default [
  ...ts.configs.recommended,
  {
    ignores: ['**/.next'],
  },
  { 
    files: ['**/*.ts', '**/*.tsx'],
    plugins: {
      react: react,
      'react-hooks': hooks,
      '@next/next': next,
    },
    rules: {
      ...react.configs['jsx-runtime'].rules,
      ...hooks.configs.recommended.rules,
      ...next.configs.recommended.rules,
      ...next.configs['core-web-vitals'].rules,
      '@next/next/no-img-element': 'error',
    },
  },
]
```

## 3. Install the ESLint VSCode extension

<Tip>
  This is generally installed if you selected "JavaScript" as a language to support when you first set up Visual Studio Code.
</Tip>

Install the [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) VSCode extension to get linting and formatting support in your editor.

## 4. Update your `.vscode/settings.json` file

Add the following to your `.vscode/settings.json` file to match the following:

```json title=".vscode/settings.json"
{
  "editor.codeActionsOnSave": {
    "source.fixAll": true,
    "source.fixAll.eslint": true
  },
  "editor.defaultFormatter": "dbaeumer.vscode-eslint",
  "editor.formatOnPaste": true,
  "editor.formatOnSave": true,
  "emmet.showExpandedAbbreviation": "never",
  "prettier.enable": true,
  "tailwindCSS.experimental.configFile": "./packages/tailwind-config/config.ts",
  "typescript.tsdk": "node_modules/typescript/lib"
}
```

## 5. Re-enable the `lint` script

As Next.js uses ESLint for linting, we can re-enable the `lint` script in the root `package.json` files. In each of the Next.js apps, update the `package.json` file to include the following:

```json title="apps/app/package.json {3}"
{
  "scripts": {
    "lint": "bun --bun next lint"
  }
}
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Novu
description: How to change the notifications provider to Novu.
type: integration
summary: How to switch the notifications provider to Novu.
prerequisites:
  - /docs/packages/notifications
---

# Switch to Novu



[Novu](https://novu.co/) is an open-source notification infrastructure platform that supports in-app, email, SMS, push, and chat channels. It's self-hostable and provides a unified API for managing notifications across multiple channels. Here's how to switch the default notifications provider from Knock to Novu.

## 1. Swap out the required dependencies

First, uninstall the existing dependencies from the Notifications package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @knocklabs/node @knocklabs/react --filter @repo/notifications
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @knocklabs/node @knocklabs/react --filter @repo/notifications
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @knocklabs/node @knocklabs/react --filter @repo/notifications
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @knocklabs/node @knocklabs/react --filter @repo/notifications
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install the new dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @novu/api @novu/react --filter @repo/notifications
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @novu/api @novu/react --filter @repo/notifications
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @novu/api @novu/react --filter @repo/notifications
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @novu/api @novu/react --filter @repo/notifications
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Update the Notification keys

Update the required Notification keys in the `packages/notifications/keys.ts` file:

```ts title="packages/notifications/keys.ts"
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const keys = () =>
  createEnv({
    server: {
      NOVU_SECRET_KEY: z.string().optional(),
    },
    client: {
      NEXT_PUBLIC_NOVU_APP_ID: z.string().optional(),
    },
    runtimeEnv: {
      NOVU_SECRET_KEY: process.env.NOVU_SECRET_KEY,
      NEXT_PUBLIC_NOVU_APP_ID: process.env.NEXT_PUBLIC_NOVU_APP_ID,
    },
  });
```

## 3. Update the environment variables

Next, update the environment variables across the project, replacing the existing Knock keys with the new Novu keys:

```js title="apps/app/.env"
NOVU_SECRET_KEY=""
NEXT_PUBLIC_NOVU_APP_ID=""
```

## 4. Update the notifications client

Initialize the notifications client in the `packages/notifications/index.ts` file with the new API key:

```ts title="packages/notifications/index.ts"
import { Novu } from '@novu/api';
import { keys } from './keys';

const key = keys().NOVU_SECRET_KEY;

export const notifications = new Novu({ secretKey: key });
```

## 5. Update the notifications provider

Replace the Knock provider with Novu's `NovuProvider` in `packages/notifications/components/provider.tsx`:

```tsx title="packages/notifications/components/provider.tsx"
'use client';

import { NovuProvider } from '@novu/react';
import type { ReactNode } from 'react';
import { keys } from '../keys';

const novuAppId = keys().NEXT_PUBLIC_NOVU_APP_ID;

interface NotificationsProviderProps {
  children: ReactNode;
  theme: 'light' | 'dark';
  userId: string;
}

export const NotificationsProvider = ({
  children,
  theme,
  userId,
}: NotificationsProviderProps) => {
  if (!novuAppId) {
    return children;
  }

  return (
    <NovuProvider
      applicationIdentifier={novuAppId}
      subscriberId={userId}
      appearance={{ variables: { colorScheme: theme } }}
    >
      {children}
    </NovuProvider>
  );
};
```

## 6. Update the notifications trigger

Replace the Knock notification components with Novu's `Inbox` in `packages/notifications/components/trigger.tsx`:

```tsx title="packages/notifications/components/trigger.tsx"
'use client';

import { Inbox } from '@novu/react';
import { keys } from '../keys';

export const NotificationsTrigger = () => {
  if (!keys().NEXT_PUBLIC_NOVU_APP_ID) {
    return null;
  }

  return <Inbox />;
};
```

You can also remove the `packages/notifications/styles.css` file, as Novu's `Inbox` component handles its own styling.

## 7. Update the app-level notifications provider

Update the wrapper in `apps/app/app/(authenticated)/components/notifications-provider.tsx`. The existing wrapper should work as-is since the `NotificationsProvider` already accepts a `theme` prop. If the types differ, update accordingly:

```tsx title="apps/app/app/(authenticated)/components/notifications-provider.tsx"
'use client';

import { NotificationsProvider as RawNotificationsProvider } from '@repo/notifications/components/provider';
import { useTheme } from 'next-themes';
import type { ReactNode } from 'react';

interface NotificationsProviderProperties {
  children: ReactNode;
  userId: string;
}

export const NotificationsProvider = ({
  children,
  userId,
}: NotificationsProviderProperties) => {
  const { resolvedTheme } = useTheme();

  return (
    <RawNotificationsProvider
      theme={resolvedTheme as 'light' | 'dark'}
      userId={userId}
    >
      {children}
    </RawNotificationsProvider>
  );
};
```

## 8. Triggering notifications

To trigger a notification from the server, use the Novu API:

```ts
import { notifications } from '@repo/notifications';

await notifications.trigger({
  workflowId: 'your-workflow-id',
  to: {
    subscriberId: 'user-123',
  },
  payload: {
    message: 'Hello from Novu!',
  },
});
```

There's quite a lot you can do with Novu, so check out the following resources for more information:

* [Novu Documentation](https://docs.novu.co/)
* [Workflows](https://docs.novu.co/workflows/introduction)
* [Inbox Component](https://docs.novu.co/inbox/introduction)
* [Self-hosting](https://docs.novu.co/community/self-hosting-novu/introduction)


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Lemon Squeezy
description: How to change the default payment processor to Lemon Squeezy.
type: integration
summary: How to switch the payment processor to Lemon Squeezy.
prerequisites:
  - /docs/packages/payments
related:
  - /docs/migrations/payments/paddle
---

# Switch to Lemon Squeezy



[Lemon Squeezy](https://www.lemonsqueezy.com) is an all-in-one platform for running your SaaS business. It handles payments, subscriptions, global tax compliance, fraud prevention, multi-currency, and more. Here's how to switch the default payment processor from Stripe to Lemon Squeezy.

<Warning>
  Lemon Squeezy was acquired by Stripe in July 2024. New signups are being transitioned to Stripe Managed Payments. If you're starting a new project, consider using Stripe directly. Existing Lemon Squeezy integrations continue to work.
</Warning>

## 1. Swap out the required dependencies

First, uninstall the existing dependencies from the Payments package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall stripe --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove stripe --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove stripe --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove stripe --filter @repo/payments
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install the new dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @lemonsqueezy/lemonsqueezy.js --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @lemonsqueezy/lemonsqueezy.js --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @lemonsqueezy/lemonsqueezy.js --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @lemonsqueezy/lemonsqueezy.js --filter @repo/payments
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Update the environment variables

Next, update the environment variables across the project, for example:

```js title="apps/app/.env"
LEMON_SQUEEZY_API_KEY=""
```

Additionally, replace all instances of `STRIPE_SECRET_KEY` with `LEMON_SQUEEZY_API_KEY` in the `packages/env/index.ts` file.

<Note>
  The API key should be a server-side environment variable (without the `NEXT_PUBLIC_` prefix), as it should not be exposed to the client.
</Note>

## 3. Update the payments client

Initialize the payments client in the `packages/payments/index.ts` file with the new API key. Then, export the `lemonSqueezySetup` function from the file.

```ts title="packages/payments/index.ts"
import { env } from '@repo/env';
import { lemonSqueezySetup } from '@lemonsqueezy/lemonsqueezy.js';

lemonSqueezySetup({
  apiKey: env.LEMON_SQUEEZY_API_KEY,
  onError: (error) => console.error("Error!", error),
});

export * from '@lemonsqueezy/lemonsqueezy.js';
```

## 4. Update the payments webhook handler

Update the webhook handler for Lemon Squeezy:

```ts title="apps/api/app/webhooks/payments/route.ts"
import { NextResponse } from 'next/server';

export const POST = async (request: Request) => {
  return NextResponse.json({ message: 'Hello World' });
};
```

There's quite a lot you can do with Lemon Squeezy, so check out the following resources for more information:

* [Webhooks Overview](https://docs.lemonsqueezy.com/guides/developer-guide/webhooks)
* [Signing Requests](https://docs.lemonsqueezy.com/help/webhooks/signing-requests)

## 5. Use Lemon Squeezy in your app

Finally, use the new payments client in your app.

```tsx title="apps/app/app/(authenticated)/page.tsx"
import { getStore } from '@repo/payments';

const Page = async () => {
  const store = await getStore(123456);

  return (
    <pre>{JSON.stringify(store, null, 2)}</pre>
  );
};
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Paddle Billing
description: How to change the default payment processor to Paddle Billing.
type: integration
summary: How to switch the payment processor to Paddle Billing.
prerequisites:
  - /docs/packages/payments
related:
  - /docs/migrations/payments/lemon-squeezy
---

# Switch to Paddle Billing



[Paddle Billing](https://www.paddle.com/) is a merchant of record for selling digital products and subscriptions. It takes care of payments, global tax compliance, fraud prevention, localization, and subscriptions. Here's how to switch the default payment processor from Stripe to Paddle Billing.

<Note>
  This guide is for Paddle Billing, which is the latest version of Paddle. It doesn't include Paddle Classic.
</Note>

## 1. Swap out the required dependencies

First, uninstall the existing dependencies from the Payments package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall stripe --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove stripe --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove stripe --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove stripe --filter @repo/payments
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install the new dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @paddle/paddle-node-sdk --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @paddle/paddle-node-sdk --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @paddle/paddle-node-sdk --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @paddle/paddle-node-sdk --filter @repo/payments
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Update the Payment keys

Update the required Payment keys in the `packages/payments/keys.ts` file:

```ts title="packages/payments/keys.ts"
import { createEnv } from '@t3-oss/env-nextjs';
import { Environment } from '@paddle/paddle-node-sdk'
import { z } from 'zod';

export const keys = () =>
  createEnv({
    server: {
      PADDLE_SECRET_KEY: z.string().min(1),
      PADDLE_WEBHOOK_SECRET: z.string().optional(),
      PADDLE_ENV: z.enum([Environment.sandbox, Environment.production]).optional(),
    },
    client: {
      NEXT_PUBLIC_PADDLE_CLIENT_TOKEN: z
        .union([
          z.string().min(1).startsWith('live_'),
          z.string().min(1).startsWith('test_'),
        ]),
      NEXT_PUBLIC_PADDLE_ENV: z.enum([Environment.sandbox, Environment.production]).optional(),
    },
    runtimeEnv: {
      PADDLE_SECRET_KEY: process.env.PADDLE_SECRET_KEY,
      PADDLE_WEBHOOK_SECRET: process.env.PADDLE_WEBHOOK_SECRET,
      PADDLE_ENV: process.env.PADDLE_ENV,
      NEXT_PUBLIC_PADDLE_ENV: process.env.NEXT_PUBLIC_PADDLE_ENV,
      NEXT_PUBLIC_PADDLE_CLIENT_TOKEN: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
    },
  });

```

## 3. Update the environment variables

Next, update the environment variables across the project, replacing the existing Stripe keys with the new Paddle keys:

```js title="apps/app/.env"
# Server
PADDLE_SECRET_KEY=""
PADDLE_WEBHOOK_SECRET=""
PADDLE_ENV="sandbox"

# Client
NEXT_PUBLIC_PADDLE_ENV="sandbox"
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN="test_"
```

## 4. Update the payments client

Initialize the payments client in the `packages/payments/index.ts` file with the new API key.

```ts title="packages/payments/index.ts"
import 'server-only';
import { Paddle } from '@paddle/paddle-node-sdk';
import { keys } from './keys';

const { PADDLE_SECRET_KEY, PADDLE_ENV } = keys();

export const paddle = new Paddle(PADDLE_SECRET_KEY, {
  environment: PADDLE_ENV,
});

export * from '@paddle/paddle-node-sdk';
```

## 5. Update the payments webhook handler

Update the webhook handler for Paddle:

```ts title="apps/api/app/webhooks/payments/route.ts"
import { keys } from '@repo/payments/keys';
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import { paddle } from '@repo/payments';

export const POST = async (request: Request) => {
  try {
    const body = await request.text();
    const headerPayload = await headers();
    const signature = headerPayload.get('paddle-signature');

    if (!signature) {
      throw new Error('missing paddle-signature header');
    }

    const event = await paddle.webhooks.unmarshal(
      body,
      keys().PADDLE_WEBHOOK_SECRET,
      signature
    );

    switch (event.eventType) {}

    return NextResponse.json({ result: event, ok: true });
  } catch (error) {
    return NextResponse.json({ error: 'Webhook error' }, { status: 400 });
  }
};
```

There's quite a lot you can do with Paddle, so check out the following resources for more information:

* [Webhooks Overview](https://developer.paddle.com/webhooks/respond-to-webhooks)
* [Signature Verification](https://developer.paddle.com/webhooks/signature-verification)
* [Simulate Webhooks](https://developer.paddle.com/webhooks/test-webhooks)

## 6. Create a Checkout hook

Create a new file for `checkout` and install `paddle-js`:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @paddle/paddle-js --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @paddle/paddle-js --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @paddle/paddle-js --filter @repo/payments
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @paddle/paddle-js --filter @repo/payments
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Then, create a new hook to initialize Paddle in the `packages/payments/checkout.tsx` file:

```tsx title="packages/payments/checkout.tsx"
'use client';

import {
  type Environments,
  type Paddle,
  initializePaddle,
} from '@paddle/paddle-js';
import { useEffect, useState } from 'react';
import { keys } from './keys';

const { NEXT_PUBLIC_PADDLE_CLIENT_TOKEN, NEXT_PUBLIC_PADDLE_ENV } = keys();

export function usePaddle() {
  const [paddle, setPaddle] = useState<Paddle>();

  useEffect(() => {
    initializePaddle({
      environment: NEXT_PUBLIC_PADDLE_ENV,
      token: NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
      checkout: {
        settings: {
          variant: 'one-page',
        },
      },
    }).then((paddleInstance: Paddle | undefined) => {
      if (paddleInstance) {
        setPaddle(paddleInstance);
      }
    });
  }, []);

  return paddle;
}
```

## 7. Use the Checkout hook

Finally, open a checkout on your pricing page:

```tsx title="apps/web/app/pricing/page.tsx"
'use client';

import { usePaddle } from '@repo/payments/checkout';

const Pricing = () => {
  const paddle = usePaddle();

  function openCheckout(priceId: string) {
    paddle?.Checkout.open({
      items: [
        {
          priceId,
          quantity: 1,
        },
      ],
    });
  }

  return (
    <Button
      className="mt-8 gap-4"
      onClick={() => openCheckout('pri_01jkzb4x1hc91s8w38cr3m86yy')}
    >
      Subscribe now <MoveRight className="h-4 w-4" />
    </Button>
  );
};

export default Pricing;
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to Appwrite Storage
description: How to change the default storage provider to Appwrite Storage.
type: integration
summary: How to switch the storage provider to Appwrite Storage.
prerequisites:
  - /docs/packages/storage
related:
  - /docs/migrations/storage/upload-thing
  - /docs/migrations/authentication/appwrite
  - /docs/migrations/database/appwrite
---

# Switch to Appwrite Storage



[Appwrite Storage](https://appwrite.io/docs/products/storage) is a file storage service that's part of the Appwrite platform. It provides file uploads, downloads, previews, and image transformations with built-in permission controls.

`next-forge` uses Vercel Blob as the default storage provider. This guide will help you switch from Vercel Blob to Appwrite Storage.

## 1. Replace the `storage` package dependencies

Uninstall the existing Vercel Blob dependency from the storage package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @vercel/blob --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @vercel/blob --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @vercel/blob --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @vercel/blob --filter @repo/storage
    ```
  </CodeBlockTab>
</CodeBlockTabs>

...and install the Appwrite dependencies:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install node-appwrite appwrite --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add node-appwrite appwrite --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add node-appwrite appwrite --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add node-appwrite appwrite --filter @repo/storage
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Update environment variables

Add the following environment variables to your `.env.local` file:

```bash title=".env.local"
NEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
NEXT_PUBLIC_APPWRITE_PROJECT_ID=your-project-id
APPWRITE_API_KEY=your-api-key
APPWRITE_BUCKET_ID=your-bucket-id
```

<Note>
  You'll need to create a storage bucket in the Appwrite Console first. Navigate to Storage → Create Bucket and note the bucket ID.
</Note>

## 3. Update the environment keys

Update the `keys.ts` file to validate the new environment variables:

```ts title="packages/storage/keys.ts"
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const keys = () =>
  createEnv({
    server: {
      APPWRITE_API_KEY: z.string().min(1),
      APPWRITE_BUCKET_ID: z.string().min(1),
    },
    client: {
      NEXT_PUBLIC_APPWRITE_ENDPOINT: z.string().url(),
      NEXT_PUBLIC_APPWRITE_PROJECT_ID: z.string().min(1),
    },
    runtimeEnv: {
      APPWRITE_API_KEY: process.env.APPWRITE_API_KEY,
      APPWRITE_BUCKET_ID: process.env.APPWRITE_BUCKET_ID,
      NEXT_PUBLIC_APPWRITE_ENDPOINT:
        process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT,
      NEXT_PUBLIC_APPWRITE_PROJECT_ID:
        process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID,
    },
  });
```

## 4. Update the server storage file

Replace the contents of `index.ts` with a configured Appwrite Storage client:

```ts title="packages/storage/index.ts"
import 'server-only';
import { Client, Storage, ID, Permission, Role, InputFile } from 'node-appwrite';

const client = new Client()
  .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
  .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!)
  .setKey(process.env.APPWRITE_API_KEY!);

export const storage = new Storage(client);
export const bucketId = process.env.APPWRITE_BUCKET_ID!;

export { ID, Permission, Role, InputFile };
```

## 5. Update the client storage file

Update `client.ts` with client-side Appwrite Storage helpers:

```ts title="packages/storage/client.ts"
'use client';

import { Client, Storage, ID } from 'appwrite';

const client = new Client()
  .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
  .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!);

export const storage = new Storage(client);
export { ID };
```

## 6. Create a storage bucket

Create a storage bucket in the Appwrite Console:

1. Go to your Appwrite project → **Storage**
2. Click **Create Bucket**
3. Give it a name (e.g., `uploads`)
4. Configure allowed file extensions and maximum file size
5. Set the bucket permissions (e.g., allow authenticated users to create files)
6. Copy the bucket ID and set it as `APPWRITE_BUCKET_ID`

You can also create a bucket programmatically:

```ts title="Example: Creating a bucket"
import { storage, Permission, Role } from '@repo/storage';

await storage.createBucket(
  'uploads',
  'uploads',
  [
    Permission.read(Role.any()),
    Permission.create(Role.users()),
    Permission.update(Role.users()),
    Permission.delete(Role.users()),
  ],
  false, // fileSecurity
  true,  // enabled
  10 * 1024 * 1024, // maximumFileSize (10MB)
  ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf'] // allowedFileExtensions
);
```

## 7. File operations

### Upload a file (server-side)

```ts
import { storage, bucketId, ID, InputFile } from '@repo/storage';

const file = await storage.createFile(
  bucketId,
  ID.unique(),
  InputFile.fromBuffer(buffer, 'image.png')
);
```

### Upload a file (client-side)

```ts
import { storage, ID } from '@repo/storage/client';

const bucketId = process.env.NEXT_PUBLIC_APPWRITE_BUCKET_ID!;

const file = await storage.createFile(
  bucketId,
  ID.unique(),
  document.getElementById('file-input').files[0]
);
```

### Download a file

```ts
import { storage, bucketId } from '@repo/storage';

const fileData = await storage.getFileDownload(bucketId, 'file-id');
```

### Delete a file

```ts
import { storage, bucketId } from '@repo/storage';

await storage.deleteFile(bucketId, 'file-id');
```

### Get a file preview URL

Appwrite provides built-in image transformations through the preview endpoint:

```ts
import { storage, bucketId } from '@repo/storage';

// Get a preview with transformations
const preview = storage.getFilePreview(
  bucketId,
  'file-id',
  400,  // width
  300,  // height
  'center', // gravity
  90    // quality
);
```

### Get file metadata

```ts
import { storage, bucketId } from '@repo/storage';

const file = await storage.getFile(bucketId, 'file-id');

console.log(file.name);       // Original filename
console.log(file.sizeOriginal); // File size in bytes
console.log(file.mimeType);   // MIME type
```

## 8. Update your apps

Replace Vercel Blob usage throughout your application:

```tsx
// Before (Vercel Blob)
import { put, del } from '@repo/storage';
const blob = await put('image.png', file, { access: 'public' });
await del(blob.url);

// After (Appwrite)
import { storage, bucketId, ID, InputFile } from '@repo/storage';
const file = await storage.createFile(
  bucketId,
  ID.unique(),
  InputFile.fromBuffer(buffer, 'image.png')
);
await storage.deleteFile(bucketId, file.$id);
```

## 9. File permissions

Appwrite supports fine-grained file-level permissions. You can set permissions when creating files:

```ts
import { storage, bucketId, ID, Permission, Role, InputFile } from '@repo/storage';

const file = await storage.createFile(
  bucketId,
  ID.unique(),
  InputFile.fromBuffer(buffer, 'private-doc.pdf'),
  [
    Permission.read(Role.user('user-123')),
    Permission.update(Role.user('user-123')),
    Permission.delete(Role.user('user-123')),
  ]
);
```

## Additional features

### Image transformations

Appwrite Storage provides built-in image transformations without needing a separate image CDN:

* Resize (width, height)
* Crop with gravity (center, top-left, etc.)
* Quality adjustment
* Format conversion
* Border radius and background color
* Rotation and opacity

### Bucket configuration

Each bucket can be configured with:

* **Allowed file extensions** — Restrict which file types can be uploaded
* **Maximum file size** — Set upload size limits
* **Encryption** — Enable at-rest encryption
* **Antivirus** — Scan uploaded files for malware
* **Compression** — Automatic file compression (gzip, zstd)

For more information, see the [Appwrite Storage documentation](https://appwrite.io/docs/products/storage).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Switch to uploadthing
description: How to change the default storage provider to uploadthing.
type: integration
summary: How to switch the storage provider to uploadthing.
prerequisites:
  - /docs/packages/storage
---

# Switch to uploadthing



[uploadthing](https://uploadthing.com) is a platform for storing files in the cloud. It's a great alternative to AWS S3 and it's free for small projects. Here's how to switch the default storage provider to uploadthing.

## 1. Swap out the required dependencies

First, uninstall the existing dependencies from the Storage package...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm uninstall @vercel/blob --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm remove @vercel/blob --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn remove @vercel/blob --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun remove @vercel/blob --filter @repo/storage
    ```
  </CodeBlockTab>
</CodeBlockTabs>

... and install the new dependencies...

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install uploadthing @uploadthing/react --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add uploadthing @uploadthing/react --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add uploadthing @uploadthing/react --filter @repo/storage
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add uploadthing @uploadthing/react --filter @repo/storage
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## 2. Update the environment variables

Next, update the environment variables across the project, for example:

```js title="apps/app/.env"
// Remove this:
BLOB_READ_WRITE_TOKEN=""

// Add this:
UPLOADTHING_TOKEN=""
```

Additionally, replace all instances of `BLOB_READ_WRITE_TOKEN` with `UPLOADTHING_TOKEN` in the `packages/env/index.ts` file.

## 3. Update the existing storage files

Update the `index.ts` and `client.ts` to use the new `uploadthing` packages:

### Storage

```ts title="packages/storage/index.ts"
import { createUploadthing } from 'uploadthing/next';

export { type FileRouter, createRouteHandler } from 'uploadthing/next';
export { UploadThingError as UploadError, extractRouterConfig } from 'uploadthing/server';

export const storage = createUploadthing();
```

### Client

```ts title="packages/storage/client.ts"
export * from '@uploadthing/react';
```

## 4. Create new SSR file

We'll also need to create a new file for the storage package to handle the Tailwind CSS classes and SSR.

```ts title="packages/storage/ssr.ts"
export { NextSSRPlugin as StorageSSRPlugin } from '@uploadthing/react/next-ssr-plugin';
```

## 5. Create a file router in your app

Create a new file in your app's `lib` directory to define the file router. This file will be used to define the file routes for your app, using your [Auth](/docs/packages/authentication) package to get the current user.

```ts title="apps/app/app/lib/upload.ts"
import { currentUser } from '@repo/auth/server';
import { type FileRouter, UploadError, storage } from '@repo/storage';
  
export const router: FileRouter = {
  imageUploader: storage({
    image: {
      maxFileSize: '4MB',
      maxFileCount: 1,
    },
  })
    .middleware(async () => {
      const user = await currentUser();

      if (!user) {
        throw new UploadError('Unauthorized');
      }

      return { userId: user.id };
    })
    .onUploadComplete(({ metadata, file }) => ({ uploadedBy: metadata.userId }),
};
```

## 6. Create a route handler

Create a new route handler in your app's `api` directory to handle the file routes.

```ts title="apps/app/app/api/upload/route.ts"
import { router } from '@/app/lib/upload';
import { createRouteHandler } from '@repo/storage';

export const { GET, POST } = createRouteHandler({ router });
```

## 7. Update your root layout

Update your root layout to include the `StorageSSRPlugin`. This will add SSR hydration and avoid a loading state on your upload button.

```tsx title="apps/app/app/layout.tsx {4,5,7,16}"
import '@repo/design-system/styles/globals.css';
import { DesignSystemProvider } from '@repo/design-system';
import { fonts } from '@repo/design-system/lib/fonts';
import { extractRouterConfig } from '@repo/storage';
import { StorageSSRPlugin } from '@repo/storage/ssr';
import type { ReactNode } from 'react';
import { router } from './lib/upload';

type RootLayoutProperties = {
  readonly children: ReactNode;
};

const RootLayout = ({ children }: RootLayoutProperties) => (
  <html lang="en" className={fonts} suppressHydrationWarning>
    <body>
      <StorageSSRPlugin routerConfig={extractRouterConfig(router)} />
      <DesignSystemProvider>{children}</DesignSystemProvider>
    </body>
  </html>
);

export default RootLayout;
```

## 8. Update your Tailwind CSS

Update your design system's `globals.css` file to include the following:

```css title="packages/design-system/styles/globals.css"
@import "uploadthing/tw/v4";
@source "../node_modules/@uploadthing/react/dist";
```

## 9. Create your upload button

Create a new component for your upload button. This will use the `generateUploadButton` function to create a button that will upload files to the `imageUploader` endpoint.

```tsx title="apps/app/app/(authenticated)/components/upload-button.tsx"
'use client';

import type { router } from '@/app/lib/upload';
import { generateUploadButton } from '@repo/storage/client';
import { toast } from 'sonner';

const UploadButton = generateUploadButton<typeof router>();

export const UploadForm = () => (
  <UploadButton
    endpoint="imageUploader"
    onClientUploadComplete={(res) => {
      // Do something with the response
      console.log('Files: ', res);
      toast.success('Upload Completed');
    }}
    onUploadError={(error: Error) => {
      toast.error(`ERROR! ${error.message}`);
    }}
  />
);
```

Now you can import this component into your app and use it as a regular component.

## 10. Advanced configuration

uploadthing is a powerful platform that offers a lot of advanced configuration options. You can learn more about them in the [uploadthing documentation](https://docs.uploadthing.com/).

* [File Routes](https://docs.uploadthing.com/file-routes)
* [Security](https://docs.uploadthing.com/concepts/auth-security)


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Product Analytics
description: Captures product events and metrics.
product: Analytics
type: reference
summary: How product analytics captures events and metrics.
related:
  - /docs/packages/analytics/web
---

# Product Analytics



next-forge has support for product analytics via PostHog — a single platform to analyze, test, observe, and deploy new features.

PostHog is an optional integration. If `NEXT_PUBLIC_POSTHOG_KEY` and `NEXT_PUBLIC_POSTHOG_HOST` are not set, the `analytics` export will be `undefined` and client-side initialization will be skipped.

## Usage

To capture product events, you can use the `analytics` object exported from the `@repo/analytics` package. Since analytics is optional, use optional chaining when calling methods.

Start by importing the `analytics` object for the relevant environment:

```tsx
// For server-side code
import { analytics } from '@repo/analytics/server';

// For client-side code
import { analytics } from '@repo/analytics';
```

Then, you can use the `capture` method to send events:

```tsx
analytics?.capture({
  event: 'Product Purchased',
  distinctId: 'user_123',
});
```

## Webhooks

To automatically capture authentication and payment events, we've combined PostHog's Node.js server-side library with Clerk and Stripe webhooks to wire it up as follows:

<Mermaid
  chart={`
graph TD
A[User Action in App] -->|Triggers| B[Auth Webhook]
A -->|Triggers| E[Payments Webhook]
A -->|Client-Side Call| PostHog
B -->|Sends Data| C1[webhooks/auth]
E -->|Sends Data| C2[webhooks/payments]

subgraph API
  C1
  C2
end

subgraph PostHog
end

C1 -->|Auth Events| PostHog
C2 -->|Payments Events| PostHog
`}
/>

## Reverse Proxy

We've also setup Next.js rewrites to reverse proxy PostHog requests, meaning your client-side analytics events won't be blocked by ad blockers.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Web Analytics
description: Captures pageviews, pageleave and custom events.
product: Analytics
type: reference
summary: How web analytics captures pageviews and custom events.
related:
  - /docs/packages/analytics/product
---

# Web Analytics



next-forge comes with three web analytics libraries.

## Vercel Web Analytics

Vercel's built-in analytics tool offers detailed insights into your website's visitors with new metrics like top pages, top referrers, and demographics. All you have to do to enable it is visit the Analytics tab in your Vercel project and click Enable from the dialog.

Read more about it [here](https://vercel.com/docs/analytics/quickstart).

## Google Analytics

Google Analytics tracks user behavior, page views, session duration, and other engagement metrics to provide insights into user activity and marketing effectiveness. GA tracking code is injected using [@next/third-parties](https://nextjs.org/docs/app/building-your-application/optimizing/third-party-libraries#google-analytics) for performance reasons.

To enable it, simply add a `NEXT_PUBLIC_GA_MEASUREMENT_ID` environment variable to your project.

## PostHog

PostHog is a single platform to analyze, test, observe, and deploy new features. It comes with lots of products, including a web analytics tool, event analytics, feature flagging, and more.

PostHog's web analytics tool is enabled by default and captures pageviews, pageleave and custom events.

### Session Replay

PostHog's session replays let you see exactly what users do on your site. It records console logs and network errors, and captures performance data like resource timings and blocked requests. This is disabled by default, so make sure you enable it in your project settings.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Components
description: Components that come with the CMS package.
product: CMS
type: reference
summary: Components included with the CMS package.
related:
  - /docs/packages/cms/overview
  - /docs/packages/cms/metadata
---

# Components



The CMS package comes with a set of components that are designed to work with the CMS. At any point in time, you can extend these components to add your own custom functionality.

## The `Feed` component

The `Feed` component is a wrapper around BaseHub's `Pump` component — a React Server Component that gets generated with the basehub SDK. It leverages RSC, Server Actions, and the existing BaseHub client to subscribe to changes in real time with minimal development effort.

It's also setup by default to use Next.js [Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode), allowing you to preview draft content in your app.

## The `Body` component

The `Body` component is a wrapper around BaseHub's `RichText` component — BaseHub's rich text renderer which supports passing custom handlers for native html elements and BaseHub components.

## The `TableOfContents` component

The `TableOfContents` component leverages the `Body` component to render the table of contents for the current page.

## The `Image` component

The `Image` component is a wrapper around BaseHub's `BaseHubImage` component, which comes with built-in image resizing and optimization. BaseHub recommendeds using the `BaseHubImage` component instead of the standard Next.js `Image` component as it uses `Image` under the hood, but adds a custom loader to leverage BaseHub's image pipeline.

## The `Toolbar` component

The `Toolbar` component is a wrapper around BaseHub's `Toolbar` component, which helps manage draft mode and switch branches in your site previews. It's automatically mounted on CMS pages.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Metadata
description: How the title, description, and Open Graph images are configured in the CMS.
product: CMS
type: reference
summary: How title, description, and Open Graph images are configured.
related:
  - /docs/packages/cms/overview
  - /docs/packages/seo/metadata
---

# Metadata



To generate metadata for a particular page or collection item, we can use the BaseHub SDK to query the metadata, then use Next.js' `generateMetadata` function to generate the metadata.

For example, here's how we've wired up the metadata for the blog post page, using the `createMetadata` function from the [SEO](/docs/packages/seo/metadata) package:

```tsx apps/web/app/[locale]/blog/[slug]/page.tsx
import { blog } from '@repo/cms';

type BlogPostProperties = {
  readonly params: Promise<{
    slug: string;
  }>;
};

export const generateMetadata = async ({
  params,
}: BlogPostProperties): Promise<Metadata> => {
  const { slug } = await params;
  const post = await blog.getPost(slug);

  if (!post) {
    return {};
  }

  return createMetadata({
    title: post._title,
    description: post.description,
    image: post.image.url,
  });
};
```

`blog.getPost` is a function that abstracts the logic of fetching the blog post from the CMS. Under the hood, it uses the BaseHub SDK to fetch the blog post from the CMS:

```tsx packages/cms/index.ts
import { basehub, fragmentOn } from 'basehub';

const postFragment = fragmentOn('PostsItem', {
  _slug: true,
  _title: true,
  authors: {
    _title: true,
    avatar: imageFragment,
    xUrl: true,
  },
  body: {
    plainText: true,
    json: {
      content: true,
      toc: true,
    },
    readingTime: true,
  },
  categories: {
    _title: true,
  },
  date: true,
  description: true,
  image: imageFragment,
});

export const blog = {
  // ...

  postQuery: (slug: string) => ({
    blog: {
      posts: {
        __args: {
          filter: {
            _sys_slug: { eq: slug },
          },
        },
        item: postFragment,
      },
    },
  }),

  getPost: async (slug: string) => {
    const query = blog.postQuery(slug);
    const data = await basehub().query(query);

    return data.blog.posts.item;
  },
};
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Overview
description: How the CMS is configured in next-forge.
product: CMS
type: reference
summary: How the CMS is configured in next-forge.
related:
  - /docs/packages/cms/components
  - /docs/packages/cms/metadata
---

# Overview



next-forge has a dedicated CMS package that can be used to generate type-safe data collections from your content. This approach provides a structured way to manage your content while maintaining full type safety throughout your application. By default, next-forge uses [BaseHub](https://basehub.com) as the CMS.

BaseHub is an optional integration. If `BASEHUB_TOKEN` is not set, CMS queries will return empty arrays or `null` instead of throwing errors.

## Setup

Here's how to quickly get started with your new CMS.

### 1. Fork the [`basehub/next-forge`](https://basehub.com/basehub/next-forge?fork=1) template

You'll be forking a BaseHub repository which contains the next-forge compatible content schema.

Once you fork the repository, you'll need to get your Read Token from the "Connect to your App" page:

```
https://basehub.com/<team-slug>/<repo-slug>/dev/main/dev:connect
```

The token will look something like this:

```
bshb_pk_<password>
```

Keep this connection string handy, you will need it in the next step.

### 2. Update your environment variables

Update your [environment variables](/docs/setup/env) to use the new BaseHub token. For example:

```ts apps/web/.env
BASEHUB_TOKEN="<token>"
```

### 3. Start the dev server

When you run `bun dev`, the CMS package will generate the type-safe BaseHub SDK, and watch changes to your CMS's schema.

<Note>
  You might need to run 

  `Restart TS Server`

   in your IDE for TypeScript to pick up the new types.
</Note>

## Querying Basics

The structure of the CMS should look something like this:

```txt
- Blog
  - Posts
  - Authors
  - Categories
- Legal Pages
```

So in order to get all posts, you'd write a query like this:

```ts
{
  blog: {
    posts: {
      items: {
        _title: true,
        _slug: true,
        authors: { _title: true }, // references the authors collection
        // ...
      },
    },
  },
}
```

Starter queries are provided for you in the `cms` package, within the `blog` and `legal` objects. You can read more about the BaseHub SDK in [their docs](https://docs.basehub.com/nextjs-integration/).

## Revalidation

A key part of any good CMS integration is the ability to revalidate content when it changes. To do that, BaseHub comes with automatic [on-demand revalidation](https://docs.basehub.com/nextjs-integration/environments-and-caching#on-demand-revalidation-recommended).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Colors
description: CSS variables and how they work
product: Design System
type: reference
summary: How CSS variables and colors work in the design system.
related:
  - /docs/packages/design-system/dark-mode
  - /docs/packages/design-system/typography
---

# Colors



next-forge makes use of the CSS variables offered by [shadcn/ui](https://ui.shadcn.com/). They're a brilliant way of abstracting the scaling and maintenance difficulties associated with [Dark Mode](/docs/packages/design-system/dark-mode) and whitelabelling.

These colors have also been applied to other tools, such as the `AuthProvider`, to ensure that third-party components align with the application design as closely as possible.

## Usage

All default pages and components use these colors. You can also use them in your own components, like so:

```tsx title="component.tsx"
export const MyComponent = () => (
  <div className="bg-background text-foreground border rounded-4xl shadow">
    <p>I'm using CSS Variables!</p>
  </div>
);
```

You can also access colors in JavaScript through the `tailwind` utility exported from `@repo/tailwind-config`, like so:

```tsx title="component.tsx"
import { tailwind } from '@repo/tailwind-config';

export const MyComponent = () => (
  <div style={{
    background: tailwind.theme.colors.background,
    color: tailwind.theme.colors.muted.foreground,
  }}>
    <p>I'm using styles directly from the Tailwind config!</p>
  </div>
);
```

## Caveats

Currently, it's not possible to change the Clerk theme to match the exact theme of the app. This is because Clerk's Theme doesn't accept custom CSS variables. We'd like to be able to add the following in the future:

```jsx title="packages/design-system/providers/clerk.tsx {4-15}"
const variables: Theme['variables'] = {
  // ...

  colorBackground: 'hsl(var(--background))',
  colorPrimary: 'hsl(var(--primary))',
  colorDanger: 'hsl(var(--destructive))',
  colorInputBackground: 'hsl(var(--transparent))',
  colorInputText: 'hsl(var(--text-foreground))',
  colorNeutral: 'hsl(var(--neutral))',
  colorShimmer: 'hsl(var(--primary) / 10%)',
  colorSuccess: 'hsl(var(--success))',
  colorText: 'hsl(var(--text-foreground))',
  colorTextOnPrimaryBackground: 'hsl(var(--text-foreground))',
  colorTextSecondary: 'hsl(var(--text-muted-foreground))',
  colorWarning: 'hsl(var(--warning))',
};
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Components
description: next-forge offers a default component library by shadcn/ui
product: Design System
type: reference
summary: How the shadcn/ui component library is configured.
related:
  - /docs/packages/design-system/provider
  - /docs/apps/storybook
---

# Components



next-forge contains a design system out of the box powered by [shadcn/ui](https://ui.shadcn.com/).

## Default configuration

shadcn/ui has been configured by default to use the "New York" style, Tailwind's `neutral` color palette and CSS variables. You can customize the component configuration in `@repo/design-system`, specifically the `components.json` file. All components have been installed and are regularly updated.

## Installing components

To install a new component, use the `shadcn` CLI from the root:

```sh title="Terminal"
npx shadcn@latest add select -c packages/design-system
```

This will install the component into the Design System package.

## Updating components

To update shadcn/ui, you can run the following command from the root:

```sh title="Terminal"
npx shadcn@latest add --all --overwrite -c packages/design-system
```

We also have a dedicated command for this. Read more about [updates](/docs/updates).

## Changing libraries

If you prefer a different component library, you can replace it at any time with something similar, such as Tailwind's [Catalyst](https://catalyst.tailwindui.com/).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Dark Mode
description: How to use dark mode in the design system.
product: Design System
type: guide
summary: How to use dark mode in the design system.
related:
  - /docs/packages/design-system/colors
  - /docs/packages/design-system/provider
---

# Dark Mode



next-forge comes with built-in dark mode support through the combination of [Tailwind CSS](https://tailwindcss.com/docs/dark-mode) and [next-themes](https://github.com/pacocoursey/next-themes).

## Implementation

The dark mode implementation uses Tailwind's `darkMode: 'class'` strategy, which toggles dark mode by adding a `dark` class to the `html` element. This approach provides better control over dark mode and prevents flash of incorrect theme.

The `next-themes` provider is already configured in the application, handling theme persistence and system preference detection automatically. Third-party components like Clerk's Provider and Sonner have also been preconfigured to respect this setup.

## Usage

By default, each application theme will default to the user's operating system preference.

To allow the user to change theme manually, you can use the `ModeToggle` component which is located in the Design System package. We've already added it to the `app` sidebar and `web` navbar, but you can import it anywhere:

```tsx title="page.tsx"
import { ModeToggle } from '@repo/design-system/components/mode-toggle';

const MyPage = () => (
  <ModeToggle />
);
```

You can check the theme by using the `useTheme` hook directly from `next-themes`. For example:

```tsx title="page.tsx"
import { useTheme } from 'next-themes';

const MyPage = () => {
  const { resolvedTheme } = useTheme();

  return resolvedTheme === 'dark' ? 'Dark mode baby' : 'Light mode ftw';
}
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Provider
description: A single global provider to wrap your application
product: Design System
type: reference
summary: The global provider that wraps your application.
related:
  - /docs/packages/design-system/components
---

# Provider



The design system package also exports a `DesignSystemProvider` component which implements a number of contextual, functional and higher order components, including those for Tooltips, Toasts, Analytics, Dark Mode and more.

This provider is already added to the default apps. If you want to add a new app, make sure you add it to your root layout along with [fonts](/docs/packages/design-system/typography) and global CSS, like so:

```tsx title="layout.tsx"
import '@repo/design-system/styles/globals.css';
import { fonts } from '@repo/design-system/lib/fonts';
import { DesignSystemProvider } from '@repo/design-system';
import type { ReactNode } from 'react';

type RootLayoutProperties = {
  readonly children: ReactNode;
};

const RootLayout = ({ children }: RootLayoutProperties) => (
  <html lang="en" className={fonts} suppressHydrationWarning>
    <body>
      <DesignSystemProvider>{children}</DesignSystemProvider>
    </body>
  </html>
);

export default RootLayout;
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Typography
description: Custom fonts and how to use them
product: Design System
type: reference
summary: How custom fonts and typography are configured.
related:
  - /docs/packages/design-system/colors
---

# Typography



The design system package contains a pre-configured fonts file, which has been wired up to all the apps. This `fonts.ts` file imports the default font Geist from Google Fonts, configures the appropriate subset and CSS variable name, then exports a `className` you can use in your app. This CSS variable is then applied to the shared Tailwind configuration.

By default, `fonts.ts` exports a `sans` and `mono` font, but you can configure this to export as many as you need e.g. heading, body, secondary, etc. You can also replace fonts entirely simply by replacing the font name, like so:

```ts title="packages/design-system/lib/fonts.ts"
import { Acme } from 'next/font/google';

const sans = Acme({ subsets: ['latin'], variable: '--font-sans' });
```

You can also load fonts locally. Read more about this on the [Next.js docs](https://nextjs.org/docs/app/building-your-application/optimizing/fonts).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Bundle Analysis
description: How to analyze and optimize your app's bundle size
product: Next Config
type: guide
summary: How to analyze and optimize your app's bundle size.
related:
  - /docs/packages/next-config/overview
---

# Bundle Analysis



next-forge uses [@vercel/next-bundle-analyzer](https://github.com/vercel/next-bundle-analyzer) to analyze and optimize your app's bundle size. Each app has a `next.config.ts` file that is configured to use the analyzer when the `ANALYZE` environment variable is set to `true`.

## Usage

To run the analyzer, simply run the following command from the root of the project:

```sh title="Terminal"
bun run analyze
```

Turborepo will automatically run the analyzer for each app when the command is executed. Once the bundle analyzer finishes running for each app, it will open three HTML files in your default browser automatically: `client`, `nodejs` and `edge`. Each one shows a treemap, describing the size and impact of modules loaded on that particular environment.

You can then work on optimizing your app by removing or dynamically loading the heaviest modules.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Configuration
description: The next-config package, explained
product: Next Config
type: reference
summary: How the shared Next.js configuration package works.
related:
  - /docs/packages/next-config/bundle-analysis
---

# Configuration



The `next-config` package is a configuration package for Next.js. It is used to configure the Next.js app and is located in the `packages/next-config` directory.

## Images

The package configures Next.js image optimization to support AVIF and WebP formats. It also sets up remote patterns to allow loading images from Clerk securely (i.e. profile images).

## Prisma

For server-side builds, the package includes the Prisma plugin which helps handle Prisma in a Next.js monorepo setup correctly.

## Rewrites

The package configures URL rewrites to handle PostHog analytics integration:

* `/ingest/static/:path*` routes to PostHog's static assets
* `/ingest/:path*` routes to the main PostHog ingestion endpoint
* `/ingest/decide` routes to PostHog's feature flags endpoint

It also enables `skipTrailingSlashRedirect` to properly support PostHog API requests with trailing slashes.

## OpenTelemetry

The package includes a fix for OpenTelemetry instrumentation warnings by configuring webpack to ignore warnings from `@opentelemetry/instrumentation` packages.

The configuration can optionally be wrapped with `withAnalyzer()` to enable bundle analysis capabilities.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Debugging
description: How we've configured debugging in next-forge.
product: Observability
type: reference
summary: How debugging is configured in next-forge.
related:
  - /docs/packages/observability/logging
  - /docs/packages/observability/error-capture
---

# Debugging



next-forge has pre-configured [VSCode](https://code.visualstudio.com/) to work as a debugger. The `.vscode/launch.json` file contains the configuration for the debugger and is configured to work for all the apps in the monorepo.

To use the debugger, simply open the app in VSCode (or any VSCode-compatible editor) and go to the Debug panel (Ctrl+Shift+D on Windows/Linux, ⇧+⌘+D on macOS). Select a launch configuration, then press `F5` or select `Debug: Start Debugging` from the Command Palette to start your debugging session.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Error Capture
description: How we've configured error capture in next-forge.
product: Observability
type: reference
summary: How error capture is configured with Sentry.
related:
  - /docs/packages/observability/debugging
  - /docs/packages/observability/logging
---

# Error Capture



next-forge uses [Sentry](https://sentry.io/) for error tracking and performance monitoring. It helps identify, debug, and resolve issues by providing detailed error reports, stack traces, and performance metrics across both server and client environments. When an error occurs, Sentry captures the full context including the user's browser information, the sequence of events that led to the error, and relevant application state.

## Configuration

Sentry is configured and managed in three key places:

The `instrumentation.ts` file in your Next.js project initializes Sentry for both Node.js and Edge runtime environments, configuring the DSN (Data Source Name) that routes error data to your Sentry project.

The `sentry.client.config.ts` file configures Sentry for the client environment. This includes settings like the sample rate for errors and sessions, and the integrations to use.

The `next.config.ts` file imports shared Sentry-specific settings through `withSentryConfig`. This enables features like source map uploads for better stack traces and automatic instrumentation of Vercel Cron Monitors. The configuration also optimizes bundle size by tree-shaking logger statements and hiding source maps in production builds.

## Manual Usage

If you want to capture a specific exception rather than letting it bubble up to Sentry, you can use the `captureException` function:

```tsx title="page.tsx"
import { captureException } from '@sentry/nextjs';

captureException(new Error('My error message'));
```

## Tunneling

The `sentryConfig.tunnelRoute` option in `@repo/next-config`'s Sentry configuration routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. This can increase your server load as well as your hosting bill.

<Warning>
  Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-side errors will fail.
</Warning>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Logging
description: How we've configured logging in next-forge.
product: Observability
type: reference
summary: How logging is configured in next-forge.
related:
  - /docs/packages/observability/debugging
  - /docs/packages/observability/error-capture
---

# Logging



The logging functionality is abstracted through a simple wrapper that provides a consistent logging interface across environments.

## How it works

In development, logs are output to the console for easy debugging. In production, logs are sent to BetterStack Logs where they can be searched, filtered, and analyzed.

## Usage

To use this logging setup, simply import and use the `log` object. It shares the same interface as the `console` object, so you can replace `console` with `log` in your codebase.

```tsx title="page.tsx"
import { log } from '@repo/observability/log';

log.info('Hello, world!');
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Uptime Monitoring
description: How we've configured uptime monitoring in next-forge.
product: Observability
type: reference
summary: How uptime monitoring is configured with BetterStack.
related:
  - /docs/packages/observability/error-capture
---

# Uptime Monitoring



Uptime monitoring functionality is configured through BetterStack's dashboard.

## Setting up monitoring

When you create your project, I recommend adding some specific URLs to monitor. Assuming we're using `next-forge.com` and it's subdomains, here's what you should add:

1. `next-forge.com` - the `web` project, should be up if the index page returns a successful response.
2. `app.next-forge.com` - the `app` project, should be up if the index page returns a successful response.
3. `api.next-forge.com/health` - the `api` project, should be up if the `health` route returns a successful response. This is a stub endpoint that runs on Edge runtime so it's very quick.

## Usage in the UI

next-forge provides a `Status` component from `@repo/observability` that displays the current status of the application. You can see an example of this in the website footer.

The status component shows 3 potential states:

* `All systems normal` - 100% of the uptime monitors are reporting up
* `Partial outage` - at least one uptime monitor is reporting down
* `Degraded performance` - 0% of the uptime monitors are reporting up

This functionality relies on the `BETTERSTACK_API_KEY` and `BETTERSTACK_URL` environment variables.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Application Security
description: Security measures taken to protect your applications.
product: Security
type: reference
summary: Security measures that protect your applications.
related:
  - /docs/packages/security/headers
  - /docs/packages/security/rate-limiting
---

# Application Security



next-forge uses [Arcjet](https://arcjet.com/), a security as code product that includes several features that can be used individually or combined to provide defense in depth for your site. You can [sign up for a free account](https://arcjet.com/) and add the API key to the environment variables to use the features we have included.

<Note>
  Security is automatically enabled by the existence of the 

  `ARCJET_KEY`

   environment variable.
</Note>

## Philosophy

Proper security protections need the full context of the application, which is why security rules and protections should be located alongside the code they are protecting.

Arcjet security as code means you can version control your security rules, track changes through pull requests, and test them locally before deploying to production.

## Configuration

Arcjet is configured in next-forge with two main features: bot detection and the Arcjet Shield WAF:

* [Bot detection](https://docs.arcjet.com/bot-protection/concepts) is configured to allow search engines, preview link generators e.g. Slack and Twitter previews, and to allow common uptime monitoring services. All other bots, such as scrapers and AI crawlers, will be blocked. You can [configure additional bot types](https://docs.arcjet.com/bot-protection/identifying-bots) to allow or block.
* [Arcjet Shield WAF](https://docs.arcjet.com/shield/concepts) will detect and block common attacks such as SQL injection, cross-site scripting, and other [OWASP Top 10 vulnerabilities](https://owasp.org/www-project-top-ten/).

Both the `web` and `app` apps have Arcjet configured with a central client at `@repo/security`  that includes the Shield WAF rules. Each app then extends this client with additional rules:

### Web

For the `web` app, bot detection and the Arcjet Shield WAF are both configured in the Middleware to block scrapers and other bots, but still allow search engines, preview link generators, and monitoring services. This will run on every request by default, except for static assets.

### App

For `app`, the central client is extended in the authenticated route layout in `apps/app/app/(authenticated)/layout.tsx` with bot detection to block all bots except preview link generators. This will run just on authenticated routes. For additional protection you may want to configure Arcjet on the `apps/app/app/(unauthenticated)/layout.tsx` route as well, but Clerk includes bot detection and rate limiting in their login route handlers by default.

When a rule is triggered, the request will be blocked and an error returned. You can customize the error message in code, redirect to a different page, or handle the error in a different way as needed.

## Scaling up your security

next-forge includes a boilerplate setup for Arcjet that protects against common threats to SaaS applications, but since the rules are defined in code, you can easily adjust them dynamically at runtime.

For example, if you build out an API for your application you could use [Arcjet rate limiting](https://docs.arcjet.com/rate-limiting/quick-start) with different quotas depending on the pricing plan of the user.

Other features include [PII detection](https://docs.arcjet.com/sensitive-info/quick-start) and [email validation](https://docs.arcjet.com/email-validation/quick-start). They're not used in the boilerplate, but can be added as needed.

### What about DDoS attacks?

Network layer attacks are usually generic and high volume, making them best handled by your hosting platform. Most cloud providers offer network DDoS protection as a default feature.

Arcjet sits closer to your application so it can understand the context. This is important because some types of traffic may not look like a DDoS attack, but can still have the same effect e.g. making too many API requests. Arcjet works in all environments so there's no lock-in to platform-specific security.

Volumetric network attacks are best handled by your hosting provider. Application level attacks need to be handled by the application. That's where Arcjet helps.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Dependency Security
description: Security measures taken to keep your dependencies secure.
product: Security
type: reference
summary: How dependency security is managed.
related:
  - /docs/packages/security/application
---

# Dependency Security



next-forge has Dependabot configured in `.github/dependabot.yml` to check for updates every month. When there are package updates, a pull request will be opened.

You may want to consider a dependency analysis tool like Socket to check for issues with dependencies in pull requests. We also recommend enabling [GitHub Secret Scanning](https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning) or a tool [Gitleaks](https://github.com/gitleaks/gitleaks) or [Trufflehog](https://github.com/trufflesecurity/trufflehog) to check for secrets in your code.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Security Headers
description: Security headers used to protect your application.
product: Security
type: reference
summary: Security headers used to protect your application.
related:
  - /docs/packages/security/application
---

# Security Headers



next-forge uses [Nosecone](https://docs.arcjet.com/nosecone/quick-start) to set HTTP response headers related to security.

## Configuration

Here are the headers we have enabled:

* `Cross-Origin-Embedder-Policy` (COEP)
* `Cross-Origin-Opener-Policy`
* `Cross-Origin-Resource-Policy`
* `Origin-Agent-Cluster`
* `Referrer-Policy`
* `Strict-Transport-Security` (HSTS)
* `X-Content-Type-Options`
* `X-DNS-Prefetch-Control`
* `X-Download-Options`
* `X-Frame-Options`
* `X-Permitted-Cross-Domain-Policies`
* `X-XSS-Protection`

See the [Nosecone reference](https://docs.arcjet.com/nosecone/reference) for details on each header and configuration options.

## Usage

Recommended headers are set by default and configured in `@repo/security/middleware`. Changing the configuration here will affect all apps.

They are then attached to the response within the middleware in `apps/app/middleware` and `apps/web/middleware.ts`. Adjusting the configuration in these files will only affect the specific app.

## Content Security Policy (CSP)

The CSP header is not set by default because it requires specific configuration based on the next-forge features you have enabled.

In the meantime, you can set the CSP header using the Nosecone configuration. For example, the following CSP configuration will work with the default next-forge features:

```ts
import type { NoseconeOptions } from '@nosecone/next';
import { defaults as noseconeDefaults } from '@nosecone/next';

const noseconeOptions: NoseconeOptions = {
  ...noseconeDefaults,
  contentSecurityPolicy: {
    ...noseconeDefaults.contentSecurityPolicy,
    directives: {
      ...noseconeDefaults.contentSecurityPolicy.directives,
      scriptSrc: [
        // We have to use unsafe-inline because next-themes and Vercel Analytics
        // do not support nonce
        // https://github.com/pacocoursey/next-themes/issues/106
        // https://github.com/vercel/analytics/issues/122
        //...noseconeDefaults.contentSecurityPolicy.directives.scriptSrc,
        "'self'",
        "'unsafe-inline'",
        "https://www.googletagmanager.com",
        "https://*.clerk.accounts.dev",
        "https://va.vercel-scripts.com",
      ],
      connectSrc: [
        ...noseconeDefaults.contentSecurityPolicy.directives.connectSrc,
        "https://*.clerk.accounts.dev",
        "https://*.google-analytics.com",
        "https://clerk-telemetry.com",
      ],
      workerSrc: [
        ...noseconeDefaults.contentSecurityPolicy.directives.workerSrc,
        "blob:",
        "https://*.clerk.accounts.dev"
      ],
      imgSrc: [
        ...noseconeDefaults.contentSecurityPolicy.directives.imgSrc,
        "https://img.clerk.com"
      ],
      objectSrc: [
        ...noseconeDefaults.contentSecurityPolicy.directives.objectSrc,
      ],
      // We only set this in production because the server may be started
      // without HTTPS
      upgradeInsecureRequests: process.env.NODE_ENV === "production",
    },
  },
}
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: IP Geolocation
description: Accessing IP geolocation data in your application.
product: Security
type: reference
summary: How to access IP geolocation data in your application.
related:
  - /docs/packages/security/rate-limiting
---

# IP Geolocation



next-forge uses [Arcjet](https://arcjet.com) for [application security](/docs/packages/security/application) which includes [IP details and geolocation information](https://docs.arcjet.com/reference/nextjs#ip-analysis) you can use in your application.

In the `app` application, Arcjet is called in the `apps/app/app/(authenticated)/layout.tsx` file which runs on every authenticated route.

For the `web` application, Arcjet is called in the middleware, which runs on every request except for static assets.

In both cases you could apply app-/website-wide rules based on the IP details, such as showing different content based on the user's location.

## Accessing IP location data

The IP details are available in the Arcjet decision object, which is returned from the all to `aj.protect()`. The IP location fields may be `undefined`, so you can use various utility functions to retrieve the data.

```ts
// ...
const decision = await aj.protect(req);

if (decision.ip.hasCity() && decision.ip.city === "San Francisco") {
  // Create a custom response for San Francisco
}

if (decision.ip.hasRegion() && decision.ip.region === "California") {
  // Create a custom response for California
}

if (decision.ip.hasCountry() && decision.ip.country === "US") {
  // Create a custom response for the United States
}

if (decision.ip.hasContinent() && decision.ip.continent === "NA") {
  // Create a custom response for North America
}
```

See the Arcjet [IP analysis reference](https://docs.arcjet.com/reference/nextjs#ip-analysis) for more information on the fields available.

## Accessing IP details elsewhere

Next.js does not allow passing data from [the root layout](https://nextjs.org/docs/app/building-your-application/routing/layouts-and-templates) or middleware to other pages in your application. To access the IP details in other parts of your application, you need to remove the Arcjet call from the root layout (`app`) or middleware (`web`) and call it in the specific page where you need the IP details.

See the Arcjet documentation on how to call `aj.protect()` in [route handlers](https://docs.arcjet.com/reference/nextjs#protect), [pages / server components](https://docs.arcjet.com/reference/nextjs#pages--page-components), and [server actions](https://docs.arcjet.com/reference/nextjs#server-actions).

<Note>
  If you remove the Arcjet call from `apps/app/app/(authenticated)/layout.tsx` or `middleware.ts` it will no longer run on every request. You will need to call `aj.protect()` everywhere you wish to apply Arcjet rules, even if you don't need the IP details.
</Note>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Rate Limiting
description: Protecting your API routes from abuse.
product: Security
type: reference
summary: How rate limiting protects your API routes from abuse.
related:
  - /docs/packages/security/application
  - /docs/apps/api
---

# Rate Limiting







Modern applications need rate limiting to protect against abuse, manage resources efficiently, and enable tiered API access. Without rate limiting, your application is vulnerable to brute force attacks, scraping, and potential service disruptions from excessive usage.

next-forge has a `rate-limit` package powered by [`@upstash/ratelimit`](https://github.com/upstash/ratelimit-js), a connectionless (HTTP-based) rate limiting library designed specifically for serverless and edge environments.

## Setting up

Rate limiting is enabled for the `web` package contact form automatically by the existence of a `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` environment variable. If enabled, the contact form will limit the number of requests to 1 per day per IP address.

To get your environment variables, you can sign up at [Upstash Console](https://console.upstash.com) and create a Redis KV database. You can then find the REST URL and REST token in the database details page.

You can then paste these environment variables each of the [environment variables](/docs/setup/env) files.

## Adding rate limiting

You can add rate limiting to your API routes by using the `createRateLimiter` function. For example, to limit the number of AI requests to 10 per 10 seconds per IP address, you can do something like this:

```ts title="apps/app/api/chat/route.ts"
import { currentUser } from '@repo/auth/server';
import { createRateLimiter, slidingWindow } from '@repo/rate-limit';

export const GET = async (request: NextRequest) => {
  const user = await currentUser();

  const rateLimiter = createRateLimiter({
    limiter: slidingWindow(10, '10 s'),
  });

  const { success } = await rateLimiter.limit(`ai_${user?.id}`);

  if (!success) {
    return new Response(
      JSON.stringify({ error: "Too many requests" }), 
      { status: 429 }
    );
  }
};
```

## Configuration

The `rate-limit` package connects to an [Upstash Redis](https://upstash.com/docs/redis/overall/getstarted) database and automatically limits the number of requests to your API routes.

The default rate limiting configuration allows 10 requests per 10 seconds per identifier. `@upstash/ratelimit` also has other rate limiting algorithms such as:

* Fixed Window
* Sliding Window
* Token Bucket

You can learn more about the different rate limiting strategies other features in the [Upstash documentation](https://upstash.com/docs/redis/sdks/ratelimit-ts/algorithms).

```ts title="packages/rate-limit/index.ts"
export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, "10 s"),
  prefix: "next-forge",
})
```

## Usage

You can use rate limiting in any API Route by importing it from the `rate-limit` package. For example:

```typescript title="apps/api/app/ratelimit/upstash/route.ts {7}"
import { ratelimit } from "@repo/rate-limit";

export const GET = async (request: NextRequest) => {
  // Use any identifier like username, API key, or IP address
  const identifier = "your-identifier";
  
  const { success, limit, remaining } = await ratelimit.limit(identifier);
  
  if (!success) {
    return new Response(
      JSON.stringify({ error: "Too many requests" }), 
      { status: 429 }
    );
  }
  
  // Continue with your API logic
};
```

## Analytics

Upstash Ratelimit provides built-in analytics capabilities through the dashboard on [Upstash Console](https://console.upstash.com).  When enabled, Upstash collects information about:

* Hourly request patterns
* Identifier usage
* Success and failure rates

To enable analytics for your rate limiting, pass the `analytics` configuration to rate limit client:

```typescript title="packages/security/index.ts"
export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, "10 s"),
  prefix: "next-forge",
  analytics: true, // Enable Upstash analytics
})
```

### Dashboard

If the analytics is enabled, you can find information about how many requests were made with which identifiers and how many of the requests were blocked from the [Rate Limit dashboard in Upstash Console](https://console.upstash.com/ratelimit).

To find to the dashboard, simply click the three dots and choose the **Rate Limit Analytics** tab:

<img alt="/images/upstash-ratelimit-navbar.png" src={__img0} placeholder="blur" />

In the dashboard, you can find information on how many requests were accepted, how many were blocked and how many were received in total. Additionally, you can see requests over time; top allowed, rate limited and denied requests.

<img alt="/images/upstash-ratelimit-dashboard.png" src={__img1} placeholder="blur" />

## Best Practices

<Steps>
  <Step title="Choose Appropriate Identifiers">
    Use meaningful identifiers for rate limiting like:

    * User IDs for authenticated requests
    * API keys for external integrations
    * IP addresses for public endpoints
  </Step>

  <Step title="Configure Rate Limits">
    Consider your application's requirements and resources when setting limits. Start conservative and adjust based on usage patterns.
  </Step>

  <Step title="Implement Error Handling">
    Always return appropriate error responses when rate limits are exceeded. Include information about when the limit will reset if possible.
  </Step>

  <Step title="Monitor and Adjust">
    Use the analytics feature to monitor rate limit hits and adjust limits as needed based on actual usage patterns.
  </Step>
</Steps>

## Further Information

`@upstash/ratelimit` also provides several advanced features:

* **Caching**: Use in-memory caching to reduce Redis calls for blocked identifiers
* **Custom Timeouts**: Configure request timeout behavior
* **Multiple Limits**: Apply different rate limits based on user tiers
* **Custom Rates**: Adjust rate limiting based on batch sizes or request weight
* **Multi-Region Support**: Distribute rate limiting across multiple Redis instances for global applications

For detailed information about these features and their implementation, refer to the [Upstash Ratelimit documentation](https://upstash.com/docs/redis/sdks/ratelimit-ts/overview).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: JSON-LD
description: How we've implemented JSON-LD structured data.
product: SEO
type: reference
summary: How JSON-LD structured data is implemented.
related:
  - /docs/packages/seo/metadata
  - /docs/packages/seo/sitemap
---

# JSON-LD



## Default Configuration

next-forge has a dedicated JSON+LD helper designed to create fully validated Google structured data, making your content more likely to be featured in Google Search results.

By default, structured data is implemented on the following pages:

* `Blog` for the blog index
* `BlogPosting` for the blog post pages

## Usage

Our `@repo/seo` package provides a JSON+LD helper built on `schema-dts`, allowing for structured data generation in a type-safe way. You can declare your own JSON+LD implementations like so:

```tsx title="page.tsx"
import { JsonLd } from '@repo/seo/json-ld';
import type { WithContext, YourInterface } from '@repo/seo/json-ld';

const jsonLd: WithContext<YourInterface> = {
  // ...
};

return <JsonLd code={jsonLd} />;
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Metadata
description: How to customize the page metadata.
product: SEO
type: reference
summary: How to customize page metadata for SEO.
related:
  - /docs/packages/seo/json-ld
  - /docs/packages/seo/sitemap
---

# Metadata



next-forge relies on Next.js's built-in [Metadata](https://nextjs.org/docs/app/building-your-application/optimizing/metadata) API to handle most of our SEO needs. Specifically, the `@repo/seo` package provides a `createMetadata` function that you can use to generate metadata for your pages. For example:

```tsx title="page.tsx"
import { createMetadata } from '@repo/seo/metadata';

export const metadata = createMetadata({
  title: 'My Page',
  description: 'My page description',
});
```

While this looks similar to just exporting a `metadata` object from your page, the `createMetadata` function deep merges your metadata with our default metadata, allowing you to customize only the metadata that you need to. It's also much easier to specify a cover photo for the page, for example:

```tsx title="page.tsx {6}"
import { createMetadata } from '@repo/seo/metadata';

export const metadata = createMetadata({
  title: 'My Page',
  description: 'My page description',
  image: '/my-page-image.png',
});
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Sitemap
description: How we generate the sitemap for the website.
product: SEO
type: reference
summary: How the sitemap is generated for the website.
related:
  - /docs/packages/seo/metadata
  - /docs/packages/seo/json-ld
---

# Sitemap



next-forge automatically generates the sitemap for the website using Next.js's built-in sitemap generation functionality (`sitemap.ts`). The generation process scans different directories in the project and creates sitemap entries for various types of content:

## Page Collection

The system first scans the `app` directory to collect all page routes:

1. Reads all directories in the `app` folder
2. Filters out:
   * Directories starting with underscore (`_`) which are typically internal/private routes
   * Directories starting with parentheses (`(`)  which are usually Next.js route groups
3. Uses the remaining directory names as valid page routes

## Content Collection

The system fetches content from the CMS to include in the sitemap:

### Blog Posts

* Fetches all published blog posts via `blog.getPosts()` from `@repo/cms`
* Extracts the `_slug` from each post to generate URLs under `/blog/`

### Legal Pages

* Fetches all legal pages via `legal.getPosts()` from `@repo/cms`
* Extracts the `_slug` from each page to generate URLs under `/legal/`

## Sitemap Generation

The final sitemap is generated by combining all these routes:

1. Adds the base URL as the root entry
2. Adds all page routes prefixed with the base URL
3. Adds all blog posts under the `blog/` path
4. Adds all legal pages under the `legal/` path

Each sitemap entry includes:

* A full URL (combining the base URL with the route)
* A `lastModified` timestamp (set to the current date)

The sitemap is automatically regenerated during each build, ensuring it stays up to date with your content.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Inbound Webhooks
description: Receive inbound webhooks from other services.
product: Webhooks
type: reference
summary: How to receive inbound webhooks from other services.
related:
  - /docs/packages/webhooks/outbound
  - /docs/packages/authentication
---

# Inbound Webhooks



next-forge has pre-built webhook handlers for several key services.

## Payment Events

[Payment events](/docs/packages/payments) are handled in the `POST /webhooks/payments` route in the `api` app. This route constructs the event and then switches on the event type to determine how to process the event.

<Info>
  To test webhooks locally, we've configured the Stripe CLI to forward webhooks to your local server. This will start automatically when you run 

  `bun dev`

  .
</Info>

## Authentication Events

[Authentication events](/docs/packages/authentication) are handled in the `POST /webhooks/auth` route in the `api` app.

<Tip>
  Make sure you enable the webhook events you need in your Clerk project settings.
</Tip>

### Local Development

Currently there's no way to easily test Clerk webhooks locally, so you'll have to test them in a staging environment. This means deploying your app to a "production" state Vercel project with development environment variables e.g. `staging-api.example.com`. Then you can add this URL to your Clerk project's webhook settings.

## Database Events

One of the most common use cases for inbound webhooks is to notify your application when a database record is created, updated, or deleted. This allows you to react to changes asynchronously, rather than polling the database, cron jobs or other methods.

If you [migrate to Supabase](/docs/migrations/database/supabase), they have an incredibly powerful feature called [Database Webhooks](https://supabase.com/docs/guides/database/webhooks) that helps with this.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

---
title: Outbound Webhooks
description: Send webhooks to your users using Svix.
product: Webhooks
type: reference
summary: How to send outbound webhooks to your users with Svix.
related:
  - /docs/packages/webhooks/inbound
---

# Outbound Webhooks



next-forge supports sending webhooks to your users using [Svix](https://www.svix.com/). Svix is an enterprise-ready webhooks sending service.

<Note>
  Webhooks are automatically enabled by the existence of the `SVIX_TOKEN` environment variable.
</Note>

## How it works

next-forge uses the Svix API in a stateless manner. The organization ID from the authenticated user is used as the Svix application UID, which is created automatically when the first message is sent.

## Usage

### Send a webhook

To send a webhook, simply use the `send` function from the `@repo/webhooks` package:

```tsx
import { webhooks } from '@repo/webhooks';

await webhooks.send('invoice.created', {
  data: {
    id: 'inv_1234567890',
  },
});
```

### Add webhook endpoints

Svix provides a pre-built [consumer application portal](https://docs.svix.com/app-portal), where users add endpoints and manage everything related to their webhooks subscriptions. App portal access is based on short-lived sessions using special magic links, and can be [embed in an iframe in your dashboard](https://docs.svix.com/app-portal#embedding-in-a-react-application).

To get access to the application portal, use the `getAppPortal` function from `@repo/webhooks` and use the returned URL in an `iframe` on your dashboard.

```tsx
import { webhooks } from '@repo/webhooks';

const { url } = await webhooks.getAppPortal();

return (
  <iframe src={url} style="width: 100%; height: 100%; border: none;" allow="clipboard-write" loading="lazy" />
);
```

We have a prebuilt page at `/webhooks` that you can use as a starting point.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)