### README
# Geistdocs
A modern documentation template built with Next.js and [Fumadocs](https://fumadocs.dev). Designed for spinning up Vercel documentation sites quickly and consistently with built-in AI chat, GitHub discussions integration, and a beautiful UI.
## Features
- 📝 **MDX-powered documentation** - Write docs in MDX with full component support
- 🤖 **AI-powered chat** - Built-in AI assistant that understands your documentation
- 💬 **GitHub Discussions integration** - Allow users to provide feedback directly to GitHub
- 🎨 **Modern UI** - Beautiful, accessible components built with Radix UI
- 🔍 **Advanced search** - Fast, fuzzy search through all documentation
- 🌙 **Dark mode** - Built-in theme switching
- 📱 **Responsive** - Mobile-first design that works everywhere
- ⚡ **Fast** - Built on Next.js 16 with App Router for optimal performance
- 📰 **RSS** - Built-in RSS feed for your documentation
[Read the docs](https://preview.geistdocs.com/docs) to get started.
---
### Content/Docs/Examples/Ai Chatbot
---
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
---
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/).
## 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.
Make sure you have some credits in your OpenAI account.
## 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 (
{messages.map((message) => (
))}
);
};
```
## 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 (
<>
>
);
};
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).
---
### Content/Docs/Deployment/Docker
---
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
---
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"]
```
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`.
## 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.
---
### Content/Docs/Deployment/Netlify
---
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
---
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.
---
### Content/Docs/Deployment/Vercel
---
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
---
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`:
## 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).
---
### Content/Docs/Apps/Api
---
title: API
description: How the "API" application works in next-forge.
type: reference
product: API
summary: How the API application works in next-forge.
related:
- /docs/packages/security/rate-limiting
- /docs/packages/webhooks/inbound
---
The `api` application runs on port 3002. We recommend deploying it to `api.{yourdomain}.com`.
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.
---
### Content/Docs/Apps/App
---
title: App
description: How the main application works in next-forge.
type: reference
product: App
summary: How the main user-facing application works in next-forge.
related:
- /docs/packages/authentication
- /docs/packages/database
- /docs/packages/design-system/components
---
The `app` application runs on port 3000. We recommend deploying it to `app.{yourdomain}.com`.
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.
---
### Content/Docs/Apps/Docs
---
title: Documentation
description: How the documentation is configured in next-forge.
type: reference
product: Docs
summary: How the documentation application is configured.
related:
- /docs/packages/cms/overview
---
The `docs` application runs on port 3004. We recommend deploying it to `docs.{yourdomain}.com`.
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.
Customize your documentation using the mint.json file
Explore the variety of components available
---
### Content/Docs/Apps/Email
---
title: Email
description: How email templates work in next-forge
type: reference
product: Email
summary: How the email preview application works.
related:
- /docs/packages/email
---
The `email` application runs on port 3003.
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
```
---
### Content/Docs/Apps/Storybook
---
title: Storybook
description: Frontend workshop for the design system
type: reference
product: Storybook
summary: How the Storybook design system workshop works.
related:
- /docs/packages/design-system/components
---
The `storybook` application runs on port 6006.
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.
---
### Content/Docs/Apps/Studio
---
title: Studio
description: Visualize and edit your database in a UI.
type: reference
product: Studio
summary: How the database studio application works.
related:
- /docs/packages/database
---
The `studio` application runs on port 3005.
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
```
---
### Content/Docs/Apps/Web
---
title: Web
description: How the website application works in next-forge.
type: reference
product: Web
summary: How the marketing website application works.
related:
- /docs/packages/seo/metadata
- /docs/packages/cms/overview
---
The `web` application runs on port 3001. We recommend deploying it to `www.{yourdomain}.com`.
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.
---
### Content/Docs/Addons/C15t
---
title: c15t
description: How to add privacy consent management to your app with c15t.
type: integration
summary: How to add consent management with 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:
```package-install
npm install @c15t/nextjs
```
## 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 (
{children}
);
}
```
For local development or prototyping, you can use `mode: 'offline'` instead of `mode: 'c15t'` to store consent in cookies without a backend.
### 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 (
{children}
);
}
```
### 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"
```
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.
For more information and detailed documentation, visit the [c15t docs](https://c15t.com/docs/frameworks/next/quickstart).
---
### Content/Docs/Addons/Dub
---
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.
---
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).
## Creating a link
Once you've signed up, you can create a link by clicking the "Create Link" button in the top right corner.
## Adding link tracking to your app
From here, simply replace all `href` values with the Dub link!
```tsx
Example
Example
```
## Interfacing programmatically
Dub provides a simple SDK for creating links, managing customers, tracking leads and more. You can install it with:
```package-install
npm install dub
```
For more information on the SDK, you can refer to the [official documentation](https://dub.co/docs/api-reference/introduction).
---
### Content/Docs/Addons/Fuse
---
title: Fuse.js
description: A powerful, lightweight fuzzy-search library, with zero dependencies.
type: integration
summary: How to add fuzzy search with Fuse.js.
---
### Installation
To install `fuse.js`, simply run the following command:
```package-install
npm install fuse.js
```
### 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).
---
### Content/Docs/Addons/Joyful
---
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.
---
### Installation
To install `joyful`, simply run the following command:
```package-install
npm install joyful
```
### 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).
---
### Content/Docs/Addons/Metabase
---
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.
---
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:
## 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:
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).
---
### Content/Docs/Addons/Motion
---
title: Motion
description: A library for animating React components with ease.
type: integration
summary: How to add animations with the Motion library.
---
Motion was formerly known as Framer Motion.
### Installation
To install Motion, simply run the following command:
```package-install
npm install motion
```
### 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 (
This is a component that is animated.
);
}
```
### 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/).
---
### Content/Docs/Addons/Next Safe Action
---
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.
---
## Installation
To install Next Safe Action, simply run the following command:
```package-install
npm install next-safe-action zod --filter app
```
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 (
);
}
```
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).
---
### Content/Docs/Addons/Nuqs
---
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.
---
### Installation
To install NUQS, simply run the following command:
```package-install
npm install nuqs
```
### 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 (
setQuery(e.target.value)}
/>
Search Query: {query}
);
}
```
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/).
---
### Content/Docs/Addons/React Wrap Balancer
---
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.
---
### Installation
To install `react-wrap-balancer`, simply run the following command:
```package-install
npm install react-wrap-balancer
```
### 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 (
This is a title that is too long to fit in one line.
);
}
```
### 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/).
---
### Content/Docs/Addons/Trunk
---
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](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 }],
],
},
});
```
Disable automatic test retries in Vitest, as retries compromise flaky test detection accuracy.
### 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).
---
### Content/Docs/Addons/Zustand
---
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.
---
### Installation
To install Zustand, run the following command:
```package-install
npm install zustand
```
### 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 (
{count}
);
}
```
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).
---
### Content/Docs/Migrations/Storage/Appwrite
---
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
---
[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...
```package-install
npm uninstall @vercel/blob --filter @repo/storage
```
...and install the Appwrite dependencies:
```package-install
npm install node-appwrite appwrite --filter @repo/storage
```
## 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
```
You'll need to create a storage bucket in the Appwrite Console first. Navigate to Storage → Create Bucket and note the bucket ID.
## 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).
---
### Content/Docs/Migrations/Storage/Upload Thing
---
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
---
[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...
```package-install
npm uninstall @vercel/blob --filter @repo/storage
```
... and install the new dependencies...
```package-install
npm install uploadthing @uploadthing/react --filter @repo/storage
```
## 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) => (
{children}
);
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();
export const UploadForm = () => (
{
// 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)
---
### Content/Docs/Migrations/Payments/Lemon Squeezy
---
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
---
[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.
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.
## 1. Swap out the required dependencies
First, uninstall the existing dependencies from the Payments package...
```package-install
npm uninstall stripe --filter @repo/payments
```
... and install the new dependencies...
```package-install
npm install @lemonsqueezy/lemonsqueezy.js --filter @repo/payments
```
## 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.
The API key should be a server-side environment variable (without the `NEXT_PUBLIC_` prefix), as it should not be exposed to the client.
## 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 (
{JSON.stringify(store, null, 2)}
);
};
```
---
### Content/Docs/Migrations/Payments/Paddle
---
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
---
[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.
This guide is for Paddle Billing, which is the latest version of Paddle. It doesn't include Paddle Classic.
## 1. Swap out the required dependencies
First, uninstall the existing dependencies from the Payments package...
```package-install
npm uninstall stripe --filter @repo/payments
```
... and install the new dependencies...
```package-install
npm install @paddle/paddle-node-sdk --filter @repo/payments
```
## 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`:
```package-install
npm install @paddle/paddle-js --filter @repo/payments
```
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();
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 (
);
};
export default Pricing;
```
---
### Content/Docs/Migrations/Notifications/Novu
---
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
---
[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...
```package-install
npm uninstall @knocklabs/node @knocklabs/react --filter @repo/notifications
```
... and install the new dependencies...
```package-install
npm install @novu/api @novu/react --filter @repo/notifications
```
## 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 (
{children}
);
};
```
## 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 ;
};
```
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 (
{children}
);
};
```
## 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)
---
### Content/Docs/Migrations/Formatting/Eslint
---
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
---
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...
```package-install
npm uninstall @biomejs/biome ultracite
```
...and install the new ones:
```package-install
npm install -D eslint @next/eslint-plugin-next eslint-plugin-react eslint-plugin-react-hooks typescript-eslint
```
## 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
This is generally installed if you selected "JavaScript" as a language to support when you first set up Visual Studio Code.
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"
}
}
```
---
### Content/Docs/Migrations/Flags/Hypertune
---
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
---
[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:
```package-install
npm uninstall @repo/analytics --filter @repo/feature-flags
```
Then, install the new dependencies:
```package-install
npm install hypertune server-only --filter @repo/feature-flags
```
## 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.
---
### Content/Docs/Migrations/Documentation/Fumadocs
---
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
---
[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.
`lib/source.ts` is where you organize code for content sources.
### 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.
Fumadocs requires a `title` frontmatter property.
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"
```ts title="Tab One"
console.log('Hello, world!');
```
```ts title="Tab Two"
console.log('Hello, world!');
```
````
#### After
````tsx title="Fumadocs"
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
```ts title="tab="Tab 1""
console.log('A');
```
```ts title="tab="Tab 2""
console.log('B');
```
````
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"
Hello WorldHello WorldHello WorldHello WorldHello World
```
#### After
```tsx title="Fumadocs"
Hello WorldHello WorldHello World
```
### 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';
;
```
## 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: ,
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.
---
### Content/Docs/Migrations/Database/Appwrite
---
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
---
[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.
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.
## 1. Replace the `database` package dependencies
Uninstall the existing Neon and Prisma dependencies from the `database` package...
```package-install
npm uninstall @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
```
...and install the Appwrite dependency:
```package-install
npm install node-appwrite --filter @repo/database
```
## 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
```
You'll need to create a database in the Appwrite Console first. The `APPWRITE_DATABASE_ID` is the ID of the database you create.
## 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
);
```
For most projects, it's easier to create collections and attributes through the Appwrite Console UI rather than programmatically.
## 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).
---
### Content/Docs/Migrations/Database/Convex
---
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
---
[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...
```package-install
npm uninstall @neondatabase/serverless @prisma/adapter-neon @prisma/client prisma ws @types/ws --filter @repo/database
```
... and install Convex:
```package-install
npm install convex --filter @repo/database
```
## 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 }) => (
{children}
);
```
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 }) => (
{children}
);
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 (
{pages?.map((page) => (
{page.title}
))}
);
};
```
Convex queries are reactive by default — your UI will automatically update when the underlying data changes, without any additional configuration.
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 (
{pages.map((page) => (
{page.title}
))}
);
};
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.
---
### Content/Docs/Migrations/Database/Drizzle
---
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
---
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.
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.
## 1. Swap out the required dependencies in `@repo/database`
Uninstall the existing dependencies...
```package-install
npm uninstall @prisma/adapter-neon @prisma/client prisma --filter @repo/database
```
...and install the new ones:
```package-install
npm install drizzle-orm @neondatabase/serverless --filter @repo/database
npm install -D drizzle-kit --filter @repo/database
```
## 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"
}
```
---
### Content/Docs/Migrations/Database/Edgedb
---
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
---
[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.
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.
`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.
For authentication, another guide will be provided to switch to EdgeDB Auth with access policies, social
auth providers, and more.
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...
```package-install
npm uninstall @prisma/adapter-neon @prisma/client prisma --filter @repo/database
```
... and install the new dependencies:
```package-install
npm install edgedb @edgedb/generate
```
## 3. Setup EdgeDB in `@repo/database` package
In the `@repo/database` directory, run:
```sh title="Terminal"
npx edgedb project init --server-instance / --non-interactive
```
Replace `` and `` with the EdgeDB's organization and instance you've previously created in the EdgeDB Cloud.
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.
---
### Content/Docs/Migrations/Database/Planetscale
---
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
---
Here's how to switch from Neon to [PlanetScale](https://planetscale.com).
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.
## 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://:@.aws.connect.psdb.cloud/
```
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://:@.aws.connect.psdb.cloud/"
```
```js title="apps/app/.env.local"
DATABASE_URL="mysql://:@.aws.connect.psdb.cloud/"
```
Etcetera.
## 3. Swap out the required dependencies in `@repo/database`
Uninstall the existing dependencies...
```package-install
npm uninstall @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
```
...and install the new ones:
```package-install
npm install @planetscale/database @prisma/adapter-planetscale --filter @repo/database
```
## 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"
}
}
```
---
### Content/Docs/Migrations/Database/Prisma Postgres
---
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
---
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...
```package-install
npm uninstall @neondatabase/serverless @prisma/adapter-neon ws @types/ws
```
... and install the new dependencies:
```package-install
npm install @prisma/extension-accelerate
```
## 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
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.
To stream database change events from your database, you first need to install the Pulse extension:
```package-install
npm install @prisma/extension-pulse
```
Next, you need to add your Pulse API key as an environment variable:
```ts title="apps/database/.env"
PULSE_API_KEY="ey...."
```
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).
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).
---
### Content/Docs/Migrations/Database/Supabase
---
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
---
[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).
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.
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"
```
`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.
## 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...
```package-install
npm uninstall @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
```
... and add the Supabase dependencies:
```package-install
npm install -D supabase --filter @repo/database
```
## 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")
}
```
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).
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.
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.
### 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);
```
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.
For more information, see the [Supabase Row Level Security guide](https://supabase.com/docs/guides/auth/row-level-security).
---
### Content/Docs/Migrations/Database/Turso
---
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
---
[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
```
You can now fetch the URL to the database:
```sh title="Terminal"
turso db show --url
```
It will look something like this:
```
libsql://-.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
```
## 4. Update your environment variables
Update your environment variables to use the new Turso connection string:
```js title="apps/database/.env"
DATABASE_URL="libsql://-.turso.io"
DATABASE_AUTH_TOKEN="..."
```
```js title="apps/app/.env.local"
DATABASE_URL="libsql://-.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[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...
```package-install
npm uninstall @neondatabase/serverless @prisma/adapter-neon ws @types/ws --filter @repo/database
```
... and install the new dependencies for Turso & libSQL:
```package-install
npm install @libsql/client --filter @repo/database
```
## 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
```
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;
```
---
### Content/Docs/Migrations/Cms/Content Collections
---
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
---
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...
```package-install
npm uninstall basehub --filter @repo/cms
```
... and install the new dependencies...
```package-install
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
```
## 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"
},
}
```
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).
## 4. Modify the relevant CMS package files
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.
### 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, 'code'> & {
content: ComponentProps['code'];
};
export const Body = ({ content, ...props }: BodyProperties) => (
);
```
### 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"]
}
}
}
```
Make sure to merge this with your existing `compilerOptions.paths` if you have any.
### 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 (
);
};
```
## 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.
We're remapping the `title` field to `_title` and the `_meta.path` field to `_slug` to match the default next-forge CMS.
### 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';
```
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.
## 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,
};
},
});
```
---
### Content/Docs/Migrations/Authentication/Appwrite
---
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
---
[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...
```package-install
npm uninstall @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
```
...and install the Appwrite dependencies:
```package-install
npm install appwrite node-appwrite --filter @repo/auth
```
## 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
```
The endpoint and project ID are safe to use in client-side code. The API key should only be used server-side.
## 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:
```
/* Detailed source-code truncated for AI context efficiency. */
```
## 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();
};
```
Delete the old `proxy.ts` file if it exists, as it was specific to Clerk's proxy functionality.
## 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
```
/* Detailed source-code truncated for AI context efficiency. */
```
### Sign Up
```
/* Detailed source-code truncated for AI context efficiency. */
```
## 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 | null>(null);
useEffect(() => {
account.get().then(setUser).catch(() => setUser(null));
}, []);
```
### Sign Out
```tsx
// Before (Clerk)
import { SignOutButton } from '@clerk/nextjs';
// 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();
};
```
## 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`
);
```
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.
For more information, see the [Appwrite Auth documentation](https://appwrite.io/docs/products/auth).
---
### Content/Docs/Migrations/Authentication/Authjs
---
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
---
next-forge support for Auth.js is currently blocked by [this issue](https://github.com/nextauthjs/next-auth/issues/11076).
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...
```package-install
npm uninstall @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
```
... and install the Auth.js dependencies.
```package-install
npm install next-auth@beta --filter @repo/auth
```
## 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 = () => (
);
```
### Sign Up
```tsx title="packages/auth/components/sign-up.tsx"
import { signIn } from '../';
export const SignUp = () => (
);
```
## 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.
---
### Content/Docs/Migrations/Authentication/Better Auth
---
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
---
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...
```package-install
npm uninstall @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
```
...and install the Better Auth dependencies:
```package-install
npm install better-auth next --filter @repo/auth
```
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 (
);
}
```
### 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 (
);
}
```
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
```
You may have to comment out the `server-only` directive in `packages/database/index.ts` temporarily. Ensure you have environment variables set.
## 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,
) {
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).
---
### Content/Docs/Migrations/Authentication/Supabase
---
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
---
[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.
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.
## 1. Replace the `auth` package dependencies
Uninstall the existing Clerk dependencies from the `auth` package...
```package-install
npm uninstall @clerk/nextjs @clerk/themes @clerk/types --filter @repo/auth
```
...and install the Supabase Auth dependencies:
```package-install
npm install @supabase/supabase-js @supabase/ssr --filter @repo/auth
```
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
```
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.
## 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(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 (
);
};
```
### Sign Up
```
/* Detailed source-code truncated for AI context efficiency. */
```
## 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';
// After (Supabase)
'use client';
import { createClient } from '@repo/auth/client';
const handleSignOut = async () => {
const supabase = createClient();
await supabase.auth.signOut();
router.push('/');
router.refresh();
};
```
## 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`,
},
});
```
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.
For more information, see the [Supabase Auth documentation](https://supabase.com/docs/guides/auth).
---
### Content/Docs/Packages/Analytics/Product
---
title: Product Analytics
description: Captures product events and metrics.
type: reference
product: Analytics
summary: How product analytics captures events and metrics.
related:
- /docs/packages/analytics/web
---
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:
|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.
---
### Content/Docs/Packages/Analytics/Web
---
title: Web Analytics
description: Captures pageviews, pageleave and custom events.
type: reference
product: Analytics
summary: How web analytics captures pageviews and custom events.
related:
- /docs/packages/analytics/product
---
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.
---
### Content/Docs/Packages/Cms/Components
---
title: Components
description: Components that come with the CMS package.
type: reference
product: CMS
summary: Components included with the CMS package.
related:
- /docs/packages/cms/overview
- /docs/packages/cms/metadata
---
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.
---
### Content/Docs/Packages/Cms/Metadata
---
title: Metadata
description: How the title, description, and Open Graph images are configured in the CMS.
type: reference
product: CMS
summary: How title, description, and Open Graph images are configured.
related:
- /docs/packages/cms/overview
- /docs/packages/seo/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 => {
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;
},
};
```
---
### Content/Docs/Packages/Cms/Overview
---
title: Overview
description: How the CMS is configured in next-forge.
type: reference
product: CMS
summary: How the CMS is configured in next-forge.
related:
- /docs/packages/cms/components
- /docs/packages/cms/metadata
---
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///dev/main/dev:connect
```
The token will look something like this:
```
bshb_pk_
```
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=""
```
### 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.
You might need to run `Restart TS Server` in your IDE for TypeScript to pick up the new types.
## 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).
---
### Content/Docs/Packages/Design System/Colors
---
title: Colors
description: CSS variables and how they work
type: reference
product: Design System
summary: How CSS variables and colors work in the design system.
related:
- /docs/packages/design-system/dark-mode
- /docs/packages/design-system/typography
---
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 = () => (
I'm using CSS Variables!
);
```
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 = () => (
I'm using styles directly from the Tailwind config!
);
```
## 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))',
};
```
---
### Content/Docs/Packages/Design System/Components
---
title: Components
description: next-forge offers a default component library by shadcn/ui
type: reference
product: Design System
summary: How the shadcn/ui component library is configured.
related:
- /docs/packages/design-system/provider
- /docs/apps/storybook
---
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/).
---
### Content/Docs/Packages/Design System/Dark Mode
---
title: Dark Mode
description: How to use dark mode in the design system.
type: guide
product: Design System
summary: How to use dark mode in the design system.
related:
- /docs/packages/design-system/colors
- /docs/packages/design-system/provider
---
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 = () => (
);
```
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';
}
```
---
### Content/Docs/Packages/Design System/Provider
---
title: Provider
description: A single global provider to wrap your application
type: reference
product: Design System
summary: The global provider that wraps your application.
related:
- /docs/packages/design-system/components
---
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) => (
{children}
);
export default RootLayout;
```
---