### 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:
Self-host Metabase Try Metabase 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 World Hello World Hello World Hello World Hello World ``` #### After ```tsx title="Fumadocs" Hello World Hello World Hello 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 = () => (
{ "use server"; await signIn(); }} >
); ``` ### Sign Up ```tsx title="packages/auth/components/sign-up.tsx" import { signIn } from '../'; export const SignUp = () => (
{ "use server"; await signIn(); }} >
); ``` ## 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 (
{ e.preventDefault(); await signIn.email({ email, password, }) }} > setEmail(e.target.value)} /> setPassword(e.target.value)} />
); } ``` ### 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 (
{ e.preventDefault(); await signUp.email({ email, password, name }) }} > setEmail(e.target.value)} /> setPassword(e.target.value)} /> setName(e.target.value)} />
); } ``` 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 (
{error &&
{error}
} setEmail(e.target.value)} placeholder="Email" required /> setPassword(e.target.value)} placeholder="Password" required />
); }; ``` ### 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; ``` ---