[{"_id":"/docs/v1/authentication/advanced-methods","structured":{"contents":[{"heading":"possible-advanced-methods","content":"**Two-Factor Authentication (2FA)**: Enhance security by requiring a second form of verification."},{"heading":"possible-advanced-methods","content":"**Biometric Authentication**: Implement fingerprint or face recognition for mobile apps."},{"heading":"possible-advanced-methods","content":"**Single Sign-On (SSO)**: Allow users to access multiple applications with a single set of credentials."},{"heading":"possible-advanced-methods","content":"**JSON Web Tokens (JWT)**: Use JWTs for stateless authentication in your API."},{"heading":"possible-advanced-methods","content":"**OAuth 2.0**: Implement a full OAuth 2.0 flow for more complex authorization scenarios."},{"heading":"implementation-considerations","content":"Implementing advanced authentication methods often requires additional libraries, services, or custom server-side logic. Here are some general steps:"},{"heading":"implementation-considerations","content":"Choose the appropriate method based on your security requirements and user experience goals."},{"heading":"implementation-considerations","content":"Research and select any necessary third-party services or libraries."},{"heading":"implementation-considerations","content":"Implement the chosen method, ensuring it integrates well with Nextbase and Supabase."},{"heading":"implementation-considerations","content":"Thoroughly test the new authentication method, including edge cases and potential security vulnerabilities."},{"heading":"implementation-considerations","content":"Update your application's documentation and user guides to reflect the new authentication options."},{"heading":"implementation-considerations","content":"Remember, while advanced authentication methods can provide enhanced security, they may also introduce complexity to your application. Always balance security needs with user experience considerations."}],"headings":[{"id":"possible-advanced-methods","content":"Possible Advanced Methods"},{"id":"implementation-considerations","content":"Implementation Considerations"}]},"url":"/docs/v1/authentication/advanced-methods","title":"Advanced Authentication Methods","description":"While Nextbase provides built-in support for common authentication methods, you may need more advanced or specialized authentication options for certain use cases."},{"_id":"/docs/v1/authentication/login-with-api-key","structured":{"contents":[{"heading":"implementation","content":"API key authentication is typically implemented using a third-party service like Unkey. The exact implementation details may vary based on your specific requirements and the service you're using."},{"heading":"implementation","content":"Here's a general outline of how you might implement API key authentication:"},{"heading":"implementation","content":"Generate an API key for the user or service that needs access."},{"heading":"implementation","content":"Store the API key securely (never in plain text)."},{"heading":"implementation","content":"When a request comes in, validate the API key."},{"heading":"implementation","content":"If the key is valid, authenticate the request and proceed."},{"heading":"usage-example","content":"Here's a simplified example of how you might validate an API key in a Next.js API route:"},{"heading":"usage-example","content":"Remember to handle API keys securely and follow best practices for API authentication."}],"headings":[{"id":"implementation","content":"Implementation"},{"id":"usage-example","content":"Usage Example"}]},"url":"/docs/v1/authentication/login-with-api-key","title":"Login with API Key","description":"Nextbase Ultimate supports authentication using API keys, which is useful for integrating with external services or for machine-to-machine communication."},{"_id":"/docs/v1/authentication/login-with-magic-link","structured":{"contents":[{"heading":"server-action","content":"Here's the server action used to login with Magic Link:"},{"heading":"usage","content":"Here's an example of how to use the Magic Link authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to receive a login link via email, providing a passwordless authentication experience."}],"headings":[{"id":"server-action","content":"Server Action"},{"id":"usage","content":"Usage"}]},"url":"/docs/v1/authentication/login-with-magic-link","title":"Login with Magic Link","description":"Nextbase supports login with Magic Link, which is a secure way to authenticate users without requiring them to remember a password."},{"_id":"/docs/v1/authentication/login-with-password","structured":{"contents":[{"heading":"server-action","content":"Here's the server action used for password-based login:"},{"heading":"usage","content":"Here's an example of how to use password-based authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to log in using their email and password credentials."}],"headings":[{"id":"server-action","content":"Server Action"},{"id":"usage","content":"Usage"}]},"url":"/docs/v1/authentication/login-with-password","title":"Login with Password","description":"Nextbase supports traditional email and password authentication, providing a familiar login experience for users."},{"_id":"/docs/v1/authentication/login-with-social-providers","structured":{"contents":[{"content":"Here's the server action used for social provider login:"},{"heading":"usage","content":"Here's an example of how to use social provider authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to log in using their accounts from supported social providers."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v1/authentication/login-with-social-providers","title":"Login with Social Providers","description":"Nextbase supports authentication with various social providers, allowing users to log in using their existing accounts from platforms like Google, GitHub, and Twitter."},{"_id":"/docs/v1/authentication/overview","structured":{"contents":[{"content":"It only has small wrappers around the supabase javascript client."},{"heading":"supported-auth-methods","content":"Authentication Type"},{"heading":"supported-auth-methods","content":"Description"},{"heading":"supported-auth-methods","content":"Email"},{"heading":"supported-auth-methods","content":"Authentication using only the user's email."},{"heading":"supported-auth-methods","content":"Email + Password"},{"heading":"supported-auth-methods","content":"Authentication using the user's email and password."},{"heading":"supported-auth-methods","content":"Third Party Providers"},{"heading":"supported-auth-methods","content":"Authentication using third-party providers. Examples given are Google, GitHub, and Twitter.  Configuration is done via the Supabase dashboard."},{"heading":"supported-auth-methods","content":"Login with API Key"},{"heading":"supported-auth-methods","content":"Authentication using an API key using Unkey (Nextbase Ultimate only)."},{"heading":"providers","content":"Providers are configured using the RenderProviders component. It renders all the providers passed to it as props. For example, the login page component Login.tsx renders providers like so:"},{"heading":"providers","content":"You can configure whichever providers you like using the supabase dashboard. Head over to `https://app.supabase.com/project/{your-project}/auth/providers` and configure the providers you require."},{"heading":"providers","content":"They will automatically work in your frontend. No other configuration required."}],"headings":[{"id":"supported-auth-methods","content":"Supported auth methods"},{"id":"providers","content":"Providers"}]},"url":"/docs/v1/authentication/overview","title":"Overview","description":"NextBase comes with Email, Email + password and Third party providers authentication built-in."},{"_id":"/docs/v1/development-updates/code-and-migration-updates","structured":{"contents":[{"content":"We are constantly working on improving the codebase and migrating to new versions."},{"heading":"codebase","content":"All Nextbase repos, (nextbase essential, nextbase pro, nextbase ultimate) are fullstack nextjs applications.\nThey are all built with the same tech stack and have the same architecture."}],"headings":[{"id":"codebase","content":"Codebase"}]},"url":"/docs/v1/development-updates/code-and-migration-updates","title":"Code and migration updates","description":"Updates to the Nextbase codebase and migration updates."},{"_id":"/docs/v1/development-updates/version-control","structured":{"contents":[{"content":"We use git for version control.\nAll our repositories are hosted on github."}],"headings":[]},"url":"/docs/v1/development-updates/version-control","title":"Version Control","description":"How we version control our Nextbase repositories"},{"_id":"/docs/v1/getting-started","structured":{"contents":[{"heading":"getting-started-with-nextbase","content":"Welcome to NextBase! We're thrilled to have you on board. This guide will walk you through the initial steps to get started with your purchased NextBase repository."},{"heading":"accessing-your-purchase","content":"To access your purchase, navigate to your dashboard. Here, you will find all your purchased items. Look for the NextBase repository (Essential, Pro, or Ultimate) that you've purchased."},{"heading":"entering-your-github-username","content":"You will be prompted to enter your GitHub username. This step is crucial as it grants you access to the purchased repository. Please ensure you enter the correct username."},{"heading":"cloning-the-repository","content":"Once you've gained access, the next step is to clone the repository. Here's how:\nLet us say that you purchased the NextBase Pro repository. It is a private repository which is at https\\://github.com/imbhargav5/nextbase-pro.\nTo clone the repository, you will need to run the following command in your terminal:"},{"heading":"cloning-the-repository","content":"**Important Note:** We strongly discourage forking the repository. If you fork the repository, the code will be visible to others who have forked it. To maintain the privacy and integrity of your code, always opt for cloning.\nNext Steps"},{"heading":"cloning-the-repository","content":"After cloning the repository, you're all set to start exploring and working with NextBase. If you encounter any issues or have any questions, don't hesitate to reach out to our support team."}],"headings":[{"id":"getting-started-with-nextbase","content":"Getting Started with NextBase"},{"id":"accessing-your-purchase","content":"Accessing Your Purchase"},{"id":"entering-your-github-username","content":"Entering Your GitHub Username"},{"id":"cloning-the-repository","content":"Cloning the Repository"}]},"url":"/docs/v1/getting-started","title":"Getting Started with NextBase"},{"_id":"/docs/v1/guides/admin-panel","structured":{"contents":[{"heading":"user-impersonation-debugging","content":"As an application admin, you can debug bugs more quickly for any user by loggin in as them and debugging the issue from their account. It is available in the application admin users section."},{"heading":"user-details-table","content":"The user details table is a table that shows all the users in the application. It is available in the application admin users section."},{"heading":"workspaces-table","content":"The workspaces table is a table that shows all the workspaces in the application. It is available in the application admin workspaces section."},{"heading":"admin-controls-for-feedback-roadmap","content":"Admin users can manage feedback and roadmap items. They can update feedback status, type, priority etc."}],"headings":[{"id":"user-impersonation-debugging","content":"User impersonation debugging"},{"id":"user-details-table","content":"User details table"},{"id":"workspaces-table","content":"Workspaces table"},{"id":"admin-controls-for-feedback-roadmap","content":"Admin controls for feedback, roadmap"}]},"url":"/docs/v1/guides/admin-panel","title":"Admin Panel Guide","description":"The Admin panel has features user impersonation, fix bugs more quickly, and more."},{"_id":"/docs/v1/guides/asynchronous-data-fetching","structured":{"contents":[{"content":"In Nextbase starter kit, we to rely on server components as much as possible to fetch data. The advantages are"},{"content":"smaller bundle sizes: because we don't have to ship the data fetching logic to the client"},{"content":"faster page loads: because we rely on the cpu power of the server instead of the client to fetch data. Server CPUs are usually faster than client CPUs."},{"heading":"data-fetching-in-server-components","content":"Since server components can by async, we can just await on the data which we require for that component. Next.js layouts and pages are server components\nby default unless you explicitly specify them as client components."},{"heading":"data-fetching-in-server-components","content":"For instance, take a look at our the page rendering our `/blog/[slug]` route:"},{"heading":"data-fetching-in-server-components","content":"Notice how we are awaiting on the `anonGetPublishedBlogPostBySlug` function to fetch the blog post data. This function is a server action which is defined in `src/data/anon/internalBlog.ts`. And since the `BlogPostPage` is a server component, we can await on the data fetching function."},{"heading":"streaming-rendering","content":"Server components have the added benefit of streaming rendering. Consider the page responsible for rendering the blog list page at `/blog`. This page has to fetch two sets of data: the list of blog posts and the list of tags. We can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components: `Tags` and `BlogList`."},{"heading":"streaming-rendering","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"streaming-rendering","content":"So the user experience for this page looks like this:"},{"heading":"streaming-rendering","content":"the page starts rendering"},{"heading":"streaming-rendering","content":"\"All blog posts\" title is rendered"},{"heading":"streaming-rendering","content":"\"Loading tags...\" is rendered and when the tags are fetched, the tags are rendered"},{"heading":"streaming-rendering","content":"\"Loading posts...\" is rendered and when the posts are fetched, the posts are rendered"},{"heading":"streaming-rendering","content":"This is a much better user experience than having to wait for the page to render until all the data is fetched."},{"heading":"data-fetching-in-client-components","content":"We don't fetching data much in client components unless in very rare cases. To fetch data in client components simply rely on `useQuery` from\nreact-query. For eg: the login button in the navigation menu is a client component and it fetches the user data using `useQuery`:"}],"headings":[{"id":"data-fetching-in-server-components","content":"Data Fetching in Server components"},{"id":"streaming-rendering","content":"Streaming rendering"},{"id":"data-fetching-in-client-components","content":"Data fetching in client components"}]},"url":"/docs/v1/guides/asynchronous-data-fetching","title":"Asynchronous Data Fetching Guide","description":"Learn how to fetch data asynchronously in Next.js using server actions and react query"},{"_id":"/docs/v1/guides/authentication","structured":{"contents":[{"heading":"authentication","content":"See Authentication Overview here."},{"heading":"authentication","content":"More coming soon..."}],"headings":[{"id":"authentication","content":"Authentication"}]},"url":"/docs/v1/guides/authentication","title":"Authentication","description":"Authenticating users in Nextbase Starter kit"},{"_id":"/docs/v1/guides/creating-application-admin-page","structured":{"contents":[{"content":"Admin Panel pages are internal web app pages that are only accessible to application administrators. These pages usually require the user to be authenticated and have admin privileges."},{"content":"All admin panel pages should be children of the layout located at `src/app/(dynamic-pages)/(authenticated-pages)/app_admin/layout.tsx`. This layout handles checking if the user is authenticated and if they have admin privileges. If the user is not authenticated or does not have admin privileges, they will be redirected to the login page."},{"content":"Here is an example of how to create an admin panel page:"}],"headings":[{"id":"admin-panel-layout","content":"Admin panel layout"}]},"url":"/docs/v1/guides/creating-application-admin-page","title":"Creating Admin Panel Page","description":"Guide on how to create an admin panel page in  Nextbase."},{"_id":"/docs/v1/guides/creating-docs-page","structured":{"contents":[{"content":"Docs pages are generated at build time. To create a docs page, create a markdown file in the `src/(static-pages)/(developer-resources)/docs` folder.\nThe pages should be written in mdx. Nextbase is configured to use mdx within `next.config.mjs` and custom components are configured using the\n`src/mdx-components.tsx` file. MDX is also configured to use `rehype` and `remark` plugins to allow for syntax highlighting and other features."},{"content":"Here is the relevant `next.config.mjs` configuration."},{"heading":"seo","content":"You can add SEO to your mdx page in the same way you would add to a typescript page.tsx component, using metadata."},{"heading":"seo","content":"Here is an example:"}],"headings":[{"id":"seo","content":"SEO"}]},"url":"/docs/v1/guides/creating-docs-page","title":"Creating Docs Page","description":"Guide on how to create a docs page in a Next.js application such as in Nextbase."},{"_id":"/docs/v1/guides/creating-in-app-notifications","structured":{"contents":[{"content":"In-app notifications are a great way to communicate with your users. They can be used to inform users about new features, promotions, or any other important information.\nNextbase Ultimate comes with a built-in in-app notification system that allows you to create and send notifications to your users."},{"heading":"notifications-table","content":"User notifications are managed in the `user_notifications` table which is created automatically by Nextbase Ultimate supabase migrations during installation. Here is the relevant part of the table schema:"},{"heading":"notifications-table","content":"As you can see, the table has a foreign key to the `user_profiles` table, which means that each notification is associated with a user. This is important because it allows you to send notifications to specific users."},{"heading":"notifications-table","content":"Also, the table has a `payload` column which is a JSONB column that can be used to store any data you want to send to the user."},{"heading":"notifications-table","content":"Additionally, the table has a `is_read` and `is_seen` columns that can be used to track whether the user has read or seen the notification."},{"heading":"notifications-table","content":"Finally the RLS policies ensure that users can only read, update, and delete their own notifications."},{"heading":"creating-a-notification","content":"To create a notification, you can use the `createNotification` function which is available in the `src/data/user/notifications` module:"},{"heading":"creating-a-notification","content":"This function allows adding a notification of any payload to a user."},{"heading":"typesafety","content":"It is important to note that the `payload` column is of type `JSONB` which means that you can store any data you want in it. However, this also means that you will not get any type safety when accessing the data.\nFor that purpose, we use zod schemas to define the payload type. It is present in `src/utils/zod-schemas/notifications.ts`"},{"heading":"typesafety","content":"As you can see, we define a `UserNotification` type which is a union of all the possible notification types. This allows us to get type safety when accessing the notification payload.\nWhen you want to create a new notification type you can simply add it to the union type and define the zod schema for it.\nFor eg. if you want to create a notification type for when a user's profile was seen by another user, you can do the following:"},{"heading":"parsing-notification-payload","content":"We utilize a parse notification function in `src/utils/parseNotification.ts`, where we parse and normalize the notification payload. This is useful when we want to display the notification in the UI."},{"heading":"parsing-notification-payload","content":"So to add our new notification type, we can simply add a new case to the switch statement:"},{"heading":"viewing-notifications","content":"UI to mark a notification as seen and read is already built into Nextbase Ultimate."}],"headings":[{"id":"notifications-table","content":"Notifications Table"},{"id":"creating-a-notification","content":"Creating a notification"},{"id":"typesafety","content":"Typesafety"},{"id":"parsing-notification-payload","content":"Parsing notification payload"},{"id":"viewing-notifications","content":"Viewing notifications"}]},"url":"/docs/v1/guides/creating-in-app-notifications","title":"Creating in-app notifications","description":"Guide on how to create in-app notifications in Nextbase."},{"_id":"/docs/v1/guides/creating-new-supabase-table","structured":{"contents":[{"content":"When you are creating a new feature in your app, you might need to create a new table in your Supabase database.\nIn this guide, we'll learn how to create a new table in Supabase and use the new table in your Next.js app."},{"heading":"creating-a-table","content":"Let us say, you want to create a table to store a list of todos. You can create a table in Supabase using the SQL editor."},{"heading":"creating-a-table","content":"Go to the SQL editor in your Supabase project."},{"heading":"creating-a-table","content":"Run the following SQL query to create a table named `todos`:"},{"heading":"creating-a-table","content":"You can also create a table using the Supabase UI. Go to the **Tables** tab in your Supabase project and click on the **New table** button and create the columns in the GUI editor."},{"heading":"generate-types-for-the-table","content":"Nextbase includes a CLI command to generate TypeScript types for your Supabase tables. You can run the following command to generate types for the\nnewly modified database."},{"heading":"generate-types-for-the-table","content":"This will regenerate the types for your local database. And the types are located at `@/src/lib/database.types.ts`"},{"heading":"why-do-we-need-to-generate-types","content":"Types are useful for type checking and auto-completion in your IDE. You can use the generated types to type check your database queries and also use them to auto-complete the column names in your queries.\nFor example, if you want to query the `todos` table, you can use the generated types to type check the query and also auto-complete the column names."},{"heading":"why-do-we-need-to-generate-types","content":"The above code returns `data` which will be an array of `Todo` objects. And the types will be generated within the `@/src/lib/database.types.ts` file."}],"headings":[{"id":"creating-a-table","content":"Creating a table"},{"id":"generate-types-for-the-table","content":"Generate types for the table"},{"id":"why-do-we-need-to-generate-types","content":"Why do we need to generate types?"}]},"url":"/docs/v1/guides/creating-new-supabase-table","title":"Creating a new Supabase table","description":"Learn how to create a new table in Supabase and use the new table in your Next.js app."},{"_id":"/docs/v1/guides/creating-protected-page","structured":{"contents":[{"content":"Protected pages are internal web app pages such as dashboard, user settings etc. These pages usually\nrequire a condition to be met, and the common case is that the user must be authenticated."},{"content":"All authenticated user pages should be children of this layout - `src/app/(dynamic-pages)/(authenticated-pages)/layout.tsx`.\nBecause this layout handles checking if the user is authenticated or not. If the user is not authenticated, the user will be redirected to the login page."},{"heading":"middleware-protection","content":"In addition to the layout-based protection, Nextbase uses middleware to enhance security and performance. The middleware runs before server components are rendered, allowing for early authorization checks."},{"heading":"middleware-protection","content":"This middleware:"},{"heading":"middleware-protection","content":"Defines unprotected pages that don't require authentication"},{"heading":"middleware-protection","content":"Checks if the user is authenticated for protected routes"},{"heading":"middleware-protection","content":"Handles user onboarding and admin access"},{"heading":"middleware-protection","content":"Redirects unauthenticated users to the login page"},{"heading":"middleware-protection","content":"By implementing both layout-based and middleware-based protection, you ensure robust security for your protected pages while maintaining good performance."}],"headings":[{"id":"middleware-protection","content":"Middleware Protection"}]},"url":"/docs/v1/guides/creating-protected-page","title":"Creating Protected Page","description":"Guide on how to create a protected page in a Next.js application such as in Nextbase."},{"_id":"/docs/v1/guides/creating-public-dynamic-page","structured":{"contents":[{"content":"A public dynamic page in a SaaS site can be something like a product page, a user profile page, etc. These pages are dynamic and are part of the application. They are not just static pages that are part of the marketing site.\nIn Nextbase, we have a `(dynamic-pages)` route segment (folder) within our `src/app` folder which contains all the dynamic pages."},{"content":"So if you want to create a new dynamic page, you can just create a new folder within the `(dynamic-pages)` folder and create a new page within that folder. You can nest\nyour dynamic pages as deep as you want to get the route you are looking for."},{"content":"The key difference here is that this page is not cached and is always served fresh."},{"content":"The layout file for the dynamic pages is at `src/app/(dynamic-pages)/layout.tsx`."}],"headings":[]},"url":"/docs/v1/guides/creating-public-dynamic-page","title":"Creating Public Dynamic Page","description":"Guide on how to create a public dynamic page in a Next.js application such as in Nextbase."},{"_id":"/docs/v1/guides/creating-public-static-page","structured":{"contents":[{"content":"A public static page in a SaaS site can be something like a pricing page, a contact page, a about page, etc. These pages are not dynamic and are not part of the application. They are just static pages that are part of the marketing site.\nIn Nextbase, we have a `(static-pages)` route segment (folder) within our `src/app` folder which contains all the static pages."},{"content":"So if you want to create a new static page, you can just create a new folder within the `(static-pages)` folder and create a new page within that folder. You can nest\nyour static pages as deep as you want to get the route you are looking for."},{"content":"The layout file for the static pages is at `src/app/(static-pages)/layout.tsx`."},{"content":"As you can see the layout has no additional configuration such as `export const dynamic` etc. Which means that this layout defauls to the next.js defaults and is static."}],"headings":[]},"url":"/docs/v1/guides/creating-public-static-page","title":"Creating Public Static Page","description":"Guide on how to create a public static page in a Next.js application such as in Nextbase."},{"_id":"/docs/v1/guides/handling-realtime-data-with-supabase","structured":{"contents":[{"content":"In this guide, we'll learn how to handle realtime data in Supabase using server actions and react query.\nWe implemented notifications as a realtime feature in Nextbase Ultimate and we can use that as a reference."},{"heading":"supabase-realtime","content":"Supabase provides a realtime service which can be used to listen to changes in your database.\nWhen used with a React client component useEffect it looks something like this:"},{"heading":"supabase-realtime","content":"In the above code, we are listening to changes in the `user_notifications` table and refetching the data when a change occurs.\nDepending on the scenario, you can also just use the payload from the realtime data to `setState` or update a `react query` cache directly."}],"headings":[{"id":"supabase-realtime","content":"Supabase Realtime"}]},"url":"/docs/v1/guides/handling-realtime-data-with-supabase","title":"Handling Realtime Data With Supabase","description":"Learn how to handle realtime data in Supabase using server actions and react query"},{"_id":"/docs/v1/guides/managing-users","structured":{"contents":[{"content":"Users are the core of your application. Supabase Auth provides a complete user management system, including sign up, sign in, email verification, password recovery, and more."},{"heading":"how-does-supabase-store-user-data","content":"Supabase stores user data in a table called `auth.users`. `auth` is an internal schema that is created when you initialize Supabase."},{"heading":"how-does-supabase-store-user-data","content":"When new users sign up, they are added to this table."},{"heading":"how-does-supabase-store-user-data","content":"This table and tables related in the auth schema are automatically updated when users update their login information"},{"heading":"storing-more-information-about-users","content":"However this table doesn't contain any more data other than email and identifying information. Since we want to store more information about our users, we use a table called `user_profiles`."},{"heading":"storing-more-information-about-users","content":"`user_profiles` is a table which allows users to update their public information such as name and avatar. (avatar image is stored in supabase storage and the url is stored here)."},{"heading":"storing-more-information-about-users","content":"Users can also store their private preferences in the `user_private_info` table. This table is only accessible by the user whose id matches the \\`user\\_id\\` column. Both the tables mentioned above have auth.users.id as a foreign key."},{"heading":"storing-more-information-about-users","content":"Hence if you want to add more public information about your users, you can add a new column to \\`user\\_profiles\\` and if you want to add more private information, you can add a new column to \\`user\\_private\\_info\\`."}],"headings":[{"id":"how-does-supabase-store-user-data","content":"How does Supabase store user data?"},{"id":"storing-more-information-about-users","content":"Storing more information about users"}]},"url":"/docs/v1/guides/managing-users","title":"Managing Users","description":"Learn how to manage users with Supabase"},{"_id":"/docs/v1/guides/mutating-data-with-supabase","structured":{"contents":[{"content":"In this guide, we'll learn how to mutate data asynchronously in Supabase using server actions and react query.\nUsually, the scenario looks like this."},{"content":"The server and client components for the route are loaded."},{"content":"User performs an action that triggers a mutation, eg. clicking a button, filling out a form, etc."},{"content":"The client sends a request to the server to perform the mutation."},{"content":"In previous versions of Next.js, we would have to write API routes to handle the request.\nBut as of Next.js 13, and now in 14 and beyond, we have a new way to handle this. These are called route handlers and server actions."},{"heading":"what-are-route-handlers","content":"In Next.js 14, Route handlers have replaced API routes.\nThese are simple `route.ts` files that can be placed in the `app` folder at any level."},{"heading":"what-are-route-handlers","content":"Here is an example of a route handler that handles a `POST` request to `/api/hello`."},{"heading":"what-are-route-handlers","content":"As of Next.js 14, Route handlers have replaced API routes. These are simple route.ts files that can be placed in the app folder at any level. They can handle multiple HTTP methods (GET, POST, PUT, DELETE, etc.) in a single file."},{"heading":"what-are-route-handlers","content":"Here's an example of a simple POST request using Route handlers:"},{"heading":"what-are-route-handlers","content":"Now both the above examples are functionally equivalent. But the latter is much more concise and easier to read."},{"heading":"what-are-server-actions","content":"Server actions are convenient wrappers around route handlers that allow you to perform mutations on your data within\nyour client / server components. Instead of writing a route handler, you can simply write a server action and call it from your client component."},{"heading":"what-are-server-actions","content":"Here's an example of a server action that handles a `POST` request similar to the example above."},{"heading":"what-are-server-actions","content":"And here's how you would call it from your client component."},{"heading":"what-are-server-actions","content":"In the above example, we are using the `useMutation` hook from react-query to call our server action."},{"heading":"what-are-server-actions","content":"Note how we are directly invoking the `hello` server action from our client component."},{"heading":"what-are-server-actions","content":"This will internally be converted to a `POST` request to a route handler automatically created by Next.js and mapped to a route."},{"heading":"usetoastmutation-hook","content":"Nextbase starter kit provides a convenience hook called `useToastMutation` that can be used to display a toast notification throughout the lifecycle of a mutation.\nIt is a thin wrapper around `useMutation` from react-query and uses `sonner` to display toast notifications."}],"headings":[{"id":"what-are-route-handlers","content":"What are route handlers?"},{"id":"what-are-server-actions","content":"What are server actions?"},{"id":"usetoastmutation-hook","content":"useToastMutation hook"}]},"url":"/docs/v1/guides/mutating-data-with-supabase","title":"Mutating Data With Supabase","description":"Learn how to mutate data asynchronously in Supabase using server actions and react query"},{"_id":"/docs/v1/guides/optimizing-data-operations-nextjs-supabase","structured":{"contents":[{"content":"In this guide, we'll explore best practices for fetching and mutating data in Nextbase applications, focusing on performance and efficiency."},{"heading":"server-side-data-fetching","content":"Prefer fetching data for components on the server using server components. This approach utilizes the higher CPU power of servers and minimizes bundle sizes for the data fetching operation."},{"heading":"mutations-with-usetoastmutation","content":"For mutations, use the `useToastMutation` hook. This allows for direct server-side mutations while providing user feedback through toast notifications."},{"heading":"realtime-queries-with-supabase","content":"For realtime queries, such as notifications, use Supabase's realtime APIs directly on the client side."},{"heading":"avoiding-usestate-for-data-fetching","content":"Avoid using `useState` for most data fetching operations. Instead, either use server components and fetch data within them, or use `useQuery` if you really need to, but you generally shouldn't need to use `useState`."},{"heading":"avoiding-usestate-for-data-fetching","content":"By following these guidelines, you can optimize data operations in your Nextbase applications for better performance and user experience."}],"headings":[{"id":"server-side-data-fetching","content":"Server-Side Data Fetching"},{"id":"mutations-with-usetoastmutation","content":"Mutations with useToastMutation"},{"id":"realtime-queries-with-supabase","content":"Realtime Queries with Supabase"},{"id":"avoiding-usestate-for-data-fetching","content":"Avoiding useState for Data Fetching"}]},"url":"/docs/v1/guides/optimizing-data-operations-nextjs-supabase","title":"Optimizing Data Operations in Next.js 14 and Supabase","description":"Learn how to efficiently fetch and mutate data in Next.js 14 and Supabase using server components, useToastMutation, and Supabase real-time APIs."},{"_id":"/docs/v1/guides/row-level-security","structured":{"contents":[{"content":"This article in the supabase website is a great start to understand how RLS works.\nhttps\\://supabase.com/docs/v2/guides/auth/row-level-security"},{"heading":"rls-configured-within-nextbase","content":"NextBase ships with RLS policies for tables such as organizations, organization\\_team\\_members, subscriptions, customers and more."},{"heading":"rls-configured-within-nextbase","content":"It also comes with RLS for storage buckets."},{"heading":"setup-rls-for-your-new-tables","content":"TODO"}],"headings":[{"id":"rls-configured-within-nextbase","content":"RLS configured within NextBase"},{"id":"setup-rls-for-your-new-tables","content":"Setup RLS for your new tables"}]},"url":"/docs/v1/guides/row-level-security","title":"Row Level Security","description":"Row Level Security in NextBase is a powerful feature that allows you to control access to rows in your database based on the user who is querying the data."},{"_id":"/docs/v1/guides/stripe-subscriptions","structured":{"contents":[{"content":"This guide will walk you through how Stripe has been set up in different files in Nextbase Starter Kit.."},{"heading":"stripe-configuration","content":"Stripe configuration is stored in the `.env` file. You need to provide your Stripe API keys here."},{"heading":"stripe-client","content":"A Stripe client is created in `src/utils/stripe.ts`. This client is used to interact with the Stripe API."},{"heading":"stripe-webhook","content":"A webhook endpoint is set up in `src/pages/api/stripe/webhook.ts` to handle events from Stripe."},{"heading":"stripe-checkout-and-customer-portal","content":"A checkout session is created in `src/data/user/organizations.ts`."},{"heading":"stripe-subscriptions","content":"Subscriptions are managed in `src/utils/supabase-admin.ts`. Subscriptions are at the organization level.\nWhen a subscription is created, the `manageSubscriptionStatusChange` function is called. This function creates a new subscription record in the database."},{"heading":"organization-admins","content":"Organization admins can manage subscriptions in `/organization/[organizationId]/settings/billing` route.\nAll members can see the current subscription status in `/organization/[organizationId]/settings/billing` route. However,\nonly organization admins can manage subscriptions."}],"headings":[{"id":"stripe-configuration","content":"Stripe Configuration"},{"id":"stripe-client","content":"Stripe Client"},{"id":"stripe-webhook","content":"Stripe Webhook"},{"id":"stripe-checkout-and-customer-portal","content":"Stripe Checkout and Customer Portal"},{"id":"stripe-subscriptions","content":"Stripe Subscriptions"},{"id":"organization-admins","content":"Organization Admins"}]},"url":"/docs/v1/guides/stripe-subscriptions","title":"Stripe Setup in Nextbase Ultimate","description":"This guide walks you through how Stripe has been set up in different files in Nextbase Ultimate."},{"_id":"/docs/v1/guides/writing-integration-tests","structured":{"contents":[{"content":"All versions of Nextbase Starter Kit come with Playwright installed. Playwright is a powerful end-to-end testing framework that allows you to write integration tests for your application."},{"heading":"configuration","content":"Playwright is configured in the `playwright.config.js` file."},{"heading":"configuration","content":"As you can see, the configuration file is quite simple. The playwright projects are configured in this manner:"},{"heading":"configuration","content":"Projects which setup authentication are configured in the `with-auth` project."},{"heading":"configuration","content":"Projects which test logged in users are configured in the `Logged In Users (Desktop Chrome)` project."},{"heading":"configuration","content":"Projects which test logged out users are configured in the `Logged Out Users (Desktop Chrome)` project."},{"heading":"configuration","content":"The `Logged In Users (Desktop Chrome)` project is configured to use the `with-auth` project as a dependency. This means that the `with-auth` project will run first, and then the `Logged In Users (Desktop Chrome)` project will run.\nHowever, the tests themselves run in parallel in each project."},{"heading":"running-tests","content":"Playwright runs in `NODE_ENV=test` mode. It also relies on `.env.test` for environment variables.\nNow, .env.test is configured to use **local supabase** which is setup by running `pnpm supabase start` command.\nThe `pnpm supabase start` command sets up a local supabase instance using docker."},{"heading":"running-tests","content":"The supabase api url is available at &#x2A;*http\\://localhost:54321/** (This is the most important url)"},{"heading":"running-tests","content":"The supabase db url is at &#x2A;*http\\://localhost:54322/**"},{"heading":"running-tests","content":"The supabase studio is available at &#x2A;*http\\://localhost:54323/** (You can use this to use the UI interface )"},{"heading":"running-tests","content":"The supabase inbucket instance is available at &#x2A;*http\\://localhost:54324/** (We use this for testing emails)"},{"heading":"running-tests","content":"Now that supabase is running locally. All we need to do is to run playwright."},{"heading":"running-playwright-in-headless-mode","content":"**Since playwright has been configured to start the local dev server, you don't need to run `pnpm dev` separately.**"},{"heading":"writing-tests","content":"All the test cases are written in the `e2e` folder. The `e2e` folder is divided into two folders:"},{"heading":"writing-tests","content":"Tests which require a user logged in need to be named `test-case.user.spec.ts`"},{"heading":"writing-tests","content":"Tests which don't require a user logged in need to be named `test-case.spec.ts`"},{"heading":"create-organization-test-case-example","content":"This is an example of a test case which requires a user to be logged in."}],"headings":[{"id":"configuration","content":"Configuration"},{"id":"running-tests","content":"Running tests"},{"id":"running-playwright-with-ui","content":"Running playwright with UI"},{"id":"running-playwright-in-headless-mode","content":"Running playwright in headless mode"},{"id":"writing-tests","content":"Writing tests"},{"id":"create-organization-test-case-example","content":"Create Organization Test case example"}]},"url":"/docs/v1/guides/writing-integration-tests","title":"Writing Integration Tests with Playwright","description":"Learn how to write integration tests using Playwright, a powerful end-to-end testing framework"},{"_id":"/docs/v1/guides/writing-unit-tests","structured":{"contents":[{"content":"All versions of Nextbase Starter Kit come with Vitest installed. Vitest is a lightweight and fast testing framework that allows you to write unit tests for your application."},{"heading":"configuration","content":"Vitest is configured in the `vitest.config.ts` file."},{"heading":"configuration","content":"As you can see, the configuration file is quite simple. You can customize it according to your needs."},{"heading":"running-tests","content":"Vitest runs in `NODE_ENV=test` mode. It also relies on `.env.test` for environment variables."},{"heading":"writing-tests","content":"All the test cases are written in the `tests` folder."},{"heading":"helper-function-test-case-example","content":"This is an example of a test case for a helper function."},{"heading":"using-vitest-with-github-actions","content":"You can easily integrate Vitest with GitHub Actions to run your tests automatically on every push or pull request. Here is a basic example of a GitHub Actions workflow:"},{"heading":"using-vitest-with-github-actions","content":"Most SaaS solo developers or small teams write integration tests more, hence we only added Vitest setup and a dummy test case, but you can update the test suite as needed."}],"headings":[{"id":"configuration","content":"Configuration"},{"id":"running-tests","content":"Running tests"},{"id":"running-vitest","content":"Running Vitest"},{"id":"writing-tests","content":"Writing tests"},{"id":"helper-function-test-case-example","content":"Helper Function Test case example"},{"id":"using-vitest-with-github-actions","content":"Using Vitest with GitHub Actions"}]},"url":"/docs/v1/guides/writing-unit-tests","title":"Writing Unit Tests with Vitest","description":"Learn how to write unit tests using Vitest, a lightweight and fast testing framework"},{"_id":"/docs/v1","structured":{"contents":[{"heading":"welcome-to-nextbase","content":"Thanks for purchasing NextBase. NextBase is the fastest way to start your SaaS project. It comes with incredible set of features pre-built for you. It is built on top of Supabase and NextJS. It is a fullstack SaaS starter kit. Here is the complete stack:"},{"heading":"welcome-to-nextbase","content":"NextJS 13 with App Directory"},{"heading":"welcome-to-nextbase","content":"Supabase"},{"heading":"welcome-to-nextbase","content":"React Server Components"},{"heading":"welcome-to-nextbase","content":"Tailwind CSS"},{"heading":"welcome-to-nextbase","content":"Typescript (with strict mode)"},{"heading":"welcome-to-nextbase","content":"Zod (for validation)"},{"heading":"welcome-to-nextbase","content":"React Query"},{"heading":"welcome-to-nextbase","content":"React Hook Form"},{"heading":"welcome-to-nextbase","content":"React Hot Toast"},{"heading":"welcome-to-nextbase","content":"Lucide Icons"},{"heading":"welcome-to-nextbase","content":"Resend and React email (for emails)"},{"heading":"welcome-to-nextbase","content":"Sentry"},{"heading":"welcome-to-nextbase","content":"Posthog (for analytics)"},{"heading":"welcome-to-nextbase","content":"Google Analytics (for analytics)"},{"heading":"feature-list","content":"NextBase is a full-stack SaaS starter kit built on top of Supabase and NextJS. It comes with an incredible set of pre-built features that can help you kickstart your SaaS project. The features are categorized into different tiers - Essential, Pro, Ultimate, and Custom. Here's a detailed breakdown of the features available in each tier:"},{"heading":"all-variants","content":"For a detailed breakdown of all the variants, please check out our Pricing Page."}],"headings":[{"id":"welcome-to-nextbase","content":"Welcome to NextBase"},{"id":"feature-list","content":"Feature list"},{"id":"all-variants","content":"All Variants"}]},"url":"/docs/v1","title":"Welcome to Nextbase v3!"},{"_id":"/docs/v1/installation/create-stripe-products","structured":{"contents":[{"content":"With the dev server running, go to the stripe developers -> webhooks section. The steps are the same as in the previous section. However instead of listening to the events locally and triggering fake events, we will create the products directly in Stripe."},{"content":"Run a service like ngrok to expose your localhost to the internet. Grab the URL. (e.g. `https://12345678.ngrok.io`)"},{"content":"Create a webhook endpoint and set the url to your ngrok url. (e.g. `https://12345678.ngrok.io/api/stripe/webhooks`). Select all events."},{"content":"Make sure that \\` All events \\` are forwared to our webhook listener. (Select the checkbox suggesting to select all events)."},{"heading":"start-creating-products","content":"Now create a couple of `Products` in Stripe, also make sure to create `prices` for each of the products."},{"heading":"start-creating-products","content":"Our database will automatically stay in sync with Stripe and create the necessary rows in the database."}],"headings":[{"id":"start-creating-products","content":"Start creating products"}]},"url":"/docs/v1/installation/create-stripe-products","title":"Create stripe products and Ngrok","description":"Create stripe products in the database. Our database will automatically stay in sync with Stripe and create the necessary rows in the database."},{"_id":"/docs/v1/installation/dev-environment","structured":{"contents":[{"heading":"setup-env-variables","content":"In your repository directory, you should have a .env.local.example file which looks like this. Create a `.env.local` file right e it but instead of dummy values like `YOUR_SUPABASE_PROJECT_REF`, fill the actual details."},{"heading":"run-your-dev-server","content":"We are ready to start our dev server. Run"},{"heading":"run-your-dev-server","content":"`pnpm dev`"},{"heading":"run-your-dev-server","content":"Our local development server is now running at `localhost:3000`."}],"headings":[{"id":"link-your-repository-to-supabase","content":"Link your repository to supabase"},{"id":"setup-env-variables","content":"Setup env variables"},{"id":"run-your-dev-server","content":"Run your dev server"}]},"url":"/docs/v1/installation/dev-environment","title":"Dev environment","description":"Setup your dev environment for the Supabase Stripe Starter Kit. Link your repository to Supabase, setup env variables, and run your dev server."},{"_id":"/docs/v1/installation/first-steps","structured":{"contents":[{"content":"Welcome to the Nextbase repository on GitHub!"},{"heading":"prerequisites","content":"Before you get started, make sure you have the following:"},{"heading":"prerequisites","content":"Git installed on your system. If you don't already have it, you can download it from the official Git website (https\\://git-scm.com/)."},{"heading":"prerequisites","content":"A terminal or command prompt. This will be used to run commands and navigate directories."},{"heading":"prerequisites","content":"Node.js installed on your system. If you don't already have it, you can download it from the official Node.js website (https\\://nodejs.org/)."},{"heading":"cloning-the-repository","content":"To get started with the Nextbase project, you will need to clone the repository onto your local machine. Here's how to do it:"},{"heading":"cloning-the-repository","content":"Open a terminal or command prompt and navigate to the directory where you want to store the Nextbase repository."},{"heading":"cloning-the-repository","content":"Run the following command to clone the repository:"},{"heading":"cloning-the-repository","content":"This will create a new directory called \"nextbase\" in your current location, and clone the contents of the repository into it."},{"heading":"cloning-the-repository","content":"Navigate into the repository by running the following command:"},{"heading":"cloning-the-repository","content":"You are now ready to start working with the Nextbase project! To update the repository with the latest changes, run the following command:"},{"heading":"cloning-the-repository","content":"That's it! You should now have a local copy of the Nextbase repository on your machine."}],"headings":[{"id":"prerequisites","content":"Prerequisites"},{"id":"cloning-the-repository","content":"Cloning the Repository"}]},"url":"/docs/v1/installation/first-steps","title":"First Steps","description":"Welcome to the Nextbase repository on GitHub!"},{"_id":"/docs/v1/installation/login","structured":{"contents":[{"heading":"sign-up-using-email","content":"Go to `http://localhost:3000/login` . If you are logging in for the first time, you can sign up and setup your email login credentials."},{"heading":"sign-up-using-email","content":"Once your account is created in supabase local studio, you will receive an email in inbucket, which you can access at http\\://localhost:54324/monitor\\`. Click on the link and login."},{"heading":"sign-up-using-email","content":"Confirm your email and you will be taken to the dashboard! 🎉"},{"heading":"sign-up-using-email-and-password","content":"Go to `http://localhost:3000/login` . For the first time, you can sign up and setup your email and password login credentials."},{"heading":"sign-up-using-email-and-password","content":"Supabase auto-confirms the credentials and does not send any confirmation email."},{"heading":"sign-up-using-email-and-password","content":"You can login now using the same credentials and you will be taken to the dashboard! 🎉"}],"headings":[{"id":"sign-up-using-email","content":"Sign up using email"},{"id":"sign-up-using-email-and-password","content":"Sign up using email and password"}]},"url":"/docs/v1/installation/login","title":"Login to app in development","description":"Login to app in development using email and password."},{"_id":"/docs/v1/installation/setting-up-stripe","structured":{"contents":[{"heading":"create-a-stripe-account","content":"Go to the Stripe website (https\\://stripe.com/) and click on the \"Sign Up\" button in the top right corner.\nFollow the prompts to create a new account. You will need to provide your email address and choose a password.\nOnce you have created your account, log in and complete the onboarding process. You don't need to setup everything while you are setting up."},{"heading":"enter-test-mode","content":"By default your account will be in `Test Mode`. This means you can test your integration without processing real payments. You can switch to `Live Mode` when you are ready to start processing real payments.\nIf you are not in `Test Mode`, you can switch to it by clicking on the `Test Mode` toggle in the top right part of your dashboard."},{"heading":"get-your-api-keys","content":"Once you have setup stripe, head to the Developers section and in the Keys section, grab the Secret key and Publishable key from there. Save them to a secure location. You will need them."},{"heading":"get-your-api-keys","content":"So as of now you have two keys."},{"heading":"setup-webhooks-for-local-testing","content":"Webhooks are a way for you to receive real-time updates from Stripe. You can use webhooks to receive notifications when a payment is made, when a customer is created, and more. This is important for your Supabase project to know when a payment is made so that you can update your database accordingly.\nYou can use webhooks to also track `product` and `price` updates in your app. It has already been configured in Nextbase.\nIn Nextbase, we have configured a webhook endpoint at `/api/stripe/webhooks`. This is the endpoint that Stripe will send the events to. But for Stripe to send events to your local machine, you need to expose your local server to the internet. You can use a tool like ngrok to do this, which can be more flexible, but you can also use Stripe CLI to trigger fixture events and test your webhook locally."},{"heading":"setting-up-using-stripe-cli","content":"Install the Stripe CLI from here."},{"heading":"setting-up-using-stripe-cli","content":"Login to your Stripe account using the CLI."},{"heading":"setting-up-using-stripe-cli","content":"This will ask you to open a link in your browser. Once you have logged in, you can close the browser and return to the terminal."},{"heading":"setting-up-using-stripe-cli","content":"Forward the webhook events to your local server."},{"heading":"setting-up-using-stripe-cli","content":"This will give you a webhook signing secret. Add this to your `.env.local` file."},{"heading":"setting-up-using-stripe-cli","content":"Trigger the following events to create your product and price."},{"heading":"setting-up-using-stripe-cli","content":"Confirm that you see the events in your terminal and the products and prices are created in supabase. Here is how they might look like in your supabase studio. You can visit `http://localhost:54323` and look at your table editor to confirm."},{"heading":"setting-up-using-stripe-cli","content":"Now as your dev server is already running. You can go your app and head over to the organization billing page and you can see the created product with its price there."},{"heading":"setting-up-using-stripe-cli","content":"Test out the `Free Trial` option and the `Choose` option to see if the payment is successful. Use test payment methods and test card numbers from here."}],"headings":[{"id":"create-a-stripe-account","content":"Create a Stripe account"},{"id":"enter-test-mode","content":"Enter Test Mode"},{"id":"get-your-api-keys","content":"Get your API keys"},{"id":"setup-webhooks-for-local-testing","content":"Setup Webhooks for local testing"},{"id":"setting-up-using-stripe-cli","content":"Setting up using Stripe CLI"}]},"url":"/docs/v1/installation/setting-up-stripe","title":"Setting up Stripe","description":"Setting up Stripe for your Supabase project. Learn how to create products, webhooks, and more."},{"_id":"/docs/v1/installation/setting-up-supabase-locally","structured":{"contents":[{"content":"To get started with Supabase locally, you will just need to pull the docker image. Here's how to do it:"},{"heading":"setting-up-docker-for-supabase","content":"First, you will need to install Docker on your machine. You can download the installer from the Docker website since it is the recommended way to install Docker on Windows and macOS. If you are using Linux, you can follow the instructions for your specific distribution on the Docker documentation."},{"heading":"setting-up-docker-for-supabase","content":"Once you have Docker installed and running in your machine, you can now pull the Supabase Docker image by running `pnpm supabase start` in your terminal."},{"heading":"setting-up-docker-for-supabase","content":"> Please note that for the first time you run the `pnpm supabase start` command, it will take some time to pull the Docker image. Subsequent runs will be faster since the images will be cached and volumes will be persisted."},{"heading":"setting-up-docker-for-supabase","content":"After running the command, you should see the output simmilar to the following:"},{"heading":"setting-up-docker-for-supabase","content":"You can now access the Supabase Studio by visiting the URL `http://localhost:54323` in your browser."},{"heading":"setting-up-docker-for-supabase","content":"You can also access the API and GraphQL URLs to interact with your local Supabase instance."},{"heading":"setting-up-docker-for-supabase","content":"To connect database clients to your local Supabase instance, you can use the DB URL `postgresql://postgres:postgres@localhost:54322/postgres`"},{"heading":"setting-up-docker-for-supabase","content":"To monitor your inboxes, you can use the Inbucket URL `http://localhost:54324`"},{"heading":"setting-up-docker-for-supabase","content":"You can also use the JWT secret, anon key, and service role key for local development."},{"heading":"setting-up-docker-for-supabase","content":"You can stop the Supabase server by running the `pnpm supabase stop` command, to save on system resources."},{"heading":"setting-up-envlocal-for-supabase","content":"To connect your Next.js application to your local Supabase instance, you will need to create a `.env.local` file in the root of your project and add the following environment variables:"}],"headings":[{"id":"setting-up-docker-for-supabase","content":"Setting up Docker for Supabase"},{"id":"setting-up-envlocal-for-supabase","content":"Setting up .env.local for Supabase"}]},"url":"/docs/v1/installation/setting-up-supabase-locally","title":"Setting up Supabase Locally","description":"Learn how to set up Supabase locally for development. This guide will show you how to pull the Docker image and set up the environment variables for your Next.js application."},{"_id":"/docs/v1/installation/supabase-for-production","structured":{"contents":[{"content":"To get started with Supabase, you will need to set up an account and create a project. Here's how to do it:"},{"heading":"creating-an-account","content":"Go to the Supabase website (https\\://supabase.com/) and click on the \"Sign Up\" button in the top right corner."},{"heading":"creating-an-account","content":"Follow the prompts to create a new account. You will need to provide your email address and choose a password."},{"heading":"creating-a-project","content":"Once you have created your account, log in and click on the \"Create Project\" button in the dashboard."},{"heading":"creating-a-project","content":"Give your project a name and choose a region for your data to be stored in. You will also need to create a database password. Make sure to save this password in a secure location, as you will need it later to access your database. Then click on the \"Create Project\" button to finish creating your project."},{"heading":"accessing-your-project","content":"You should now see your new project listed in the dashboard. Click on the project name to open it and start working with Supabase."},{"heading":"auth-configuration","content":"Go to the \"Authentication\" section of your project usually at `https://app.supabase.io/project/<project-id>/auth`."},{"heading":"auth-configuration","content":"Go to the URL Configuration tab."},{"heading":"auth-configuration","content":"Set the Site URL to `http://localhost:3000` or it can also be your production domain e.g. `https://myapp.com`. (This is not as important as the Redirect URL configuration)"},{"heading":"auth-configuration","content":"Set Redirect URLs to\na. `http://localhost:3000/auth/callback` (for local development) or `http://localhost:3000/**` (this means all routes under `http://localhost:3000` will be accepted as a valid redirect URL)\nb. `https://myapp.com/auth/callback` (for production) or `https://myapp.com/**` (this means all routes under `https://myapp.com` will be accepted as a valid redirect URL)"},{"heading":"install-dependencies-and-link-your-project","content":"run `pnpm install`"},{"heading":"install-dependencies-and-link-your-project","content":"Supabase login within your terminal"},{"heading":"install-dependencies-and-link-your-project","content":"Note: If you haven't used supabase before in the terminal, you will need to login with supabase in the terminal."},{"heading":"install-dependencies-and-link-your-project","content":"Create an access token for supabase here https\\://app.supabase.com/account/tokens"},{"heading":"install-dependencies-and-link-your-project","content":"Copy the access token and run `pnpm supabase login`"},{"heading":"install-dependencies-and-link-your-project","content":"It will prompt for an access token. Paste the token there. It should log you in."},{"heading":"install-dependencies-and-link-your-project","content":"Run `pnpm supabase link --project-ref {projectRef}` . Your project ref is available in the project settings section within the supabase dashboard."},{"heading":"install-dependencies-and-link-your-project","content":"Once you run the command, you will prompted for the database password. Enter the password, you saved earlier."},{"heading":"install-dependencies-and-link-your-project","content":"When it completes, your project will be linked to supabase."},{"heading":"install-dependencies-and-link-your-project","content":"Run `pnpm supabase db push` . This will push all the nextbase magic into your own supabase project. Teams, admin panel, projects etc will now be ready for use."},{"heading":"setup-env-variables-for-production","content":"In your repository directory, you should have a .env.local.example file which looks like this. Create a `.env.local` file right beside it but instead of dummy values like `YOUR_SUPABASE_PROJECT_REF`, fill the actual details."},{"heading":"setup-env-variables-for-production","content":"**NEXT\\_PUBLIC\\_SUPABASE\\_URL** is available in the project api settings section in supabase. It's called Project URL over there."},{"heading":"setup-env-variables-for-production","content":"**NEXT\\_PUBLIC\\_SUPABASE\\_PUBLISHABLE\\_KEY** and **SUPABASE\\_SECRET\\_KEY** are also available in the Project API settings section in supabase."},{"heading":"setup-env-variables-for-production","content":"Setup a Sendgrid / Post mark account and once you do, grab your authorised email sender address and add it to **ADMIN\\_EMAIL**. Same goes for **SENDGRID\\_API\\_KEY**. (If you want to use a different email provider, create a different environment variable here and set the relevant value)."},{"heading":"setup-env-variables-for-production","content":"Ensure that **NEXT\\_PUBLIC\\_SITE\\_URL** points to your domain."},{"heading":"setup-env-variables-for-production","content":"The other env variables should be relatively easy to setup."}],"headings":[{"id":"creating-an-account","content":"Creating an Account"},{"id":"creating-a-project","content":"Creating a Project"},{"id":"accessing-your-project","content":"Accessing Your Project"},{"id":"auth-configuration","content":"Auth configuration"},{"id":"install-dependencies-and-link-your-project","content":"Install dependencies and link your project"},{"id":"setup-env-variables-for-production","content":"Setup env variables for production"}]},"url":"/docs/v1/installation/supabase-for-production","title":"Setting up Supabase for Production","description":"Learn how to set up Supabase for production. This guide will walk you through creating an account, creating a project, and setting up environment variables for production."},{"_id":"/docs/v1/integrations/api-key-authentication-with-unkey","structured":{"contents":[{"content":"Nextbase Ultimate comes with unkey configured."},{"content":"When using the latest Supabase with asymmetric keys, the JWT signing approach has changed. New\ndocs are on the way, but please view this GitHub\ncomment in the meantime."},{"heading":"why-use-unkey","content":"Unkey is a service that allows you to create API keys for your users."},{"heading":"why-use-unkey","content":"It allows you to create API keys for your users without having to store them in your database."},{"heading":"why-use-unkey","content":"It allows you to revoke API keys for your users without having to update your database."},{"heading":"why-use-unkey","content":"Keys can be created for limited time periods too."},{"heading":"why-use-unkey","content":"The keys can then be used to authenticate your users to your API endpoints."},{"heading":"why-use-unkey","content":"Usage with Zapier, Make and Integromat are also now easy."},{"heading":"configuring-unkey","content":"Necessary env variables that you need for Unkey after creating an account there"},{"heading":"configuring-unkey","content":"1 UNKEY\\_ROOT\\_KEY:"},{"heading":"configuring-unkey","content":"This variable likely represents the root key associated with Unkey.\nIt may be used for specific administrative or configuration purposes within Unkey."},{"heading":"configuring-unkey","content":"2 UNKEY\\_API\\_ID:"},{"heading":"configuring-unkey","content":"This variable seems to be associated with an API ID in the context of Unkey.\nIt may serve as an identifier for your Unkey API integration."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Import Unkey Middleware\nImport the `withUnkeyAuth` middleware from the Unkey package at the beginning of your route handler file."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Implement the Route Handler\nCreate your route handler logic, ensuring that it includes the withUnkeyAuth middleware. This middleware handles the API key authentication process."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Now, this route handler is protected, and only requests with valid API keys will be allowed to access it."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"By following these steps, you can configure Unkey and create route handlers with API key authentication in your Nextbase project, ensuring secure and authenticated access to your protected routes."},{"heading":"disabling-unkey","content":"To disable Unkey, you can either remove its middleware from your route handlers or adjust your configuration to bypass Unkey authentication."},{"heading":"removing-unkey-middleware","content":"In your route handler files, look for the import statement that includes Unkey middleware and remove or comment out the line."},{"heading":"exposing-routes-which-can-be-used-with-api-key-auth","content":"To expose routes with API key authentication, follow these steps:"},{"heading":"1-import-unkey-middleware","content":"In your route handler files, import the Unkey middleware (`withUnkeyAuth`) at the beginning of the file. This middleware will handle the API key authentication process."},{"heading":"2-use-unkey-middleware-in-route-handlers","content":"Apply the Unkey middleware to the routes you want to expose with API key authentication. Wrap your route handler logic with withUnkeyAuth:"},{"heading":"2-use-unkey-middleware-in-route-handlers","content":"By applying withUnkeyAuth to a route handler, you ensure that the route is protected and requires a valid API key for access."},{"heading":"bonus-usage-with-zapier","content":"Since API key auth is now built in to Nextbase, configuring Zapier, Make or Integromat is now easy."},{"heading":"bonus-usage-with-zapier","content":"Zapier guide coming soon."},{"heading":"bonus-usage-with-zapier","content":"TODO"}],"headings":[{"id":"why-use-unkey","content":"Why use Unkey?"},{"id":"configuring-unkey","content":"Configuring unkey"},{"id":"steps-for-creating-unkey-root-key","content":"Steps for creating Unkey Root key"},{"id":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Creating an route handler which can be used with api key auth"},{"id":"disabling-unkey","content":"Disabling unkey"},{"id":"removing-unkey-middleware","content":"Removing Unkey Middleware"},{"id":"exposing-routes-which-can-be-used-with-api-key-auth","content":"Exposing routes which can be used with API key auth"},{"id":"1-import-unkey-middleware","content":"1\\. Import Unkey Middleware"},{"id":"2-use-unkey-middleware-in-route-handlers","content":"2\\. Use Unkey Middleware in Route Handlers"},{"id":"bonus-usage-with-zapier","content":"(Bonus): Usage with Zapier"}]},"url":"/docs/v1/integrations/api-key-authentication-with-unkey","title":"API Key Authentication with Unkey","description":"Learn how to use Unkey for API key authentication in Nextbase Ultimate and how to expose routes with API key auth."},{"_id":"/docs/v1/integrations/resend-and-react-email-integration","structured":{"contents":[{"heading":"resend-and-react-email-integration","content":"Nextbase comes with Resend and React Email configured. This makes creating/modifying email templates and sending them to your customers very easy."},{"heading":"resendcom","content":"Resend serves as the backbone for handling email delivery in Nextbase.\nIt streamlines the process of sending emails, managing templates, and handling various aspects of email communication.\nWith Resend, you can focus on creating compelling content, knowing that the delivery mechanism is robust and reliable."},{"heading":"resendcom","content":"**You can use a different email service provider if you want to.**"},{"heading":"how-to-use-resend-in-nextjs","content":"Resend is usually configured like this in Next.js."},{"heading":"how-to-use-resend-in-nextjs","content":"Install resend as a dependency and then import it into your application."},{"heading":"how-to-use-resend-in-nextjs","content":"Create an account in Resend and get your API key."},{"heading":"how-to-use-resend-in-nextjs","content":"Add your API key to your environment variables."},{"heading":"how-to-use-resend-in-nextjs","content":"Then, you can use it like this."},{"heading":"react-email-declarative-email-templating","content":"`react-email` is a React library that allows you to compose email templates using familiar React components. Instead of dealing with complex HTML templates, you can leverage the power of React to create modular and dynamic email content. This integration aligns with the React philosophy, making it intuitive for developers already familiar with React development.\nNextbase provides a seamless integration of Resend and `react-email` so that you can harness the benefits of both tools effortlessly."},{"heading":"emails-folder","content":"Nextbase comes with the `emails` folder configured as the repository for your React Email components. You can write your emails here and also preview them with the email development server by running `pnpm email-dev`."},{"heading":"react-email-development-server","content":"In Nextbase, a React Email development server is set up to streamline the process of writing and previewing emails. This development server allows developers to interactively design and visualize emails in a browser, facilitating a more efficient email template creation workflow."},{"heading":"react-email-development-server","content":"**Package.json Script**"},{"heading":"react-email-development-server","content":"The React Email development server is typically configured as a script in the package.json file. The script is named \"email:dev\":"},{"heading":"react-email-development-server","content":"To start the React Email development server, developers can run the following command in the terminal:"},{"heading":"react-email-development-server","content":"This starts the React Email development server and provides a convenient way to preview email templates."},{"heading":"react-email-development-server","content":"**/src/emails:**"},{"heading":"react-email-development-server","content":"Each file in the `emails` folder corresponds to a specific type of email, and it contains a React component responsible for rendering the content of that email.\nWe can then send the emails using `Resend`. Here are some emails we already have in the `emails` folder:"},{"heading":"welcomeemailtsx","content":"In this example, the WelcomeEmail component represents the content of a welcome email. This declarative approach simplifies the process of creating and maintaining various email templates in your application."},{"heading":"invitationtojointsx","content":"This is the organization invitation email template. When a user is invited to join an organization, this email is sent to them."},{"heading":"invitationtojointsx","content":"The email is then sent like this."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"It is recommended to use a third-party SMTP service for production environments in Supabase."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"Reliability: Supabase built-in SMTP server is severely rate limited. It is only suitable for development and testing purposes. Supabase recommends using a third-party SMTP service for production environments. Resend provides a robust and reliable email delivery mechanism."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"Observability: Resend provides a dashboard to monitor email delivery and other metrics."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"In a production environment, it's crucial to ensure reliable email delivery. Ensure that Resend or another third-party SMTP service is configured in your Supabase project."},{"heading":"react-email-and-resend-integration-in-nextbase","content":"See `src/utils/api-routes/utils.ts` for an example of how to we use Resend in Nextbase.\nHere the `sendEmail` function is used to send emails using Resend. It receives the email options as an argument and sends the email using Resend."},{"heading":"configuration","content":"You only require the API key to configure Resend. You can find the API key in the Resend dashboard."}],"headings":[{"id":"resend-and-react-email-integration","content":"Resend and React Email Integration"},{"id":"resendcom","content":"Resend.com"},{"id":"how-to-use-resend-in-nextjs","content":"How to use Resend in Next.js?"},{"id":"react-email-declarative-email-templating","content":"React-email: Declarative Email Templating"},{"id":"emails-folder","content":"`emails` folder"},{"id":"react-email-development-server","content":"React Email Development Server"},{"id":"welcomeemailtsx","content":"WelcomeEmail.tsx:"},{"id":"invitationtojointsx","content":"InvitationToJoin.tsx"},{"id":"optional-configure-resend-with-supabase-smtp-server","content":"(Optional) Configure Resend with Supabase SMTP Server"},{"id":"react-email-and-resend-integration-in-nextbase","content":"React Email and Resend Integration in Nextbase"},{"id":"configuration","content":"Configuration"}]},"url":"/docs/v1/integrations/resend-and-react-email-integration","title":"Resend and React Email Integration","description":"Learn how to use Resend and React Email in Nextbase."},{"_id":"/docs/v1/payments/stripe-configuration","structured":{"contents":[{"content":"Stripe is implemented using an Abstract payment provider. It has the following shape."}],"headings":[]},"url":"/docs/v1/payments/stripe-configuration","title":"Stripe Configuration"},{"_id":"/docs/v1/payments/stripe-webhooks","structured":{"contents":[{"content":"Stripe webhooks are crucial for keeping your application in sync with events that happen in Stripe, such as successful payments, subscription updates, or failed charges. In NextBase, we've set up a webhook endpoint to handle these events. Let's break down how it's configured:"},{"heading":"webhook-endpoint","content":"The webhook endpoint is set up in `src/app/api/stripe/webhooks/route.ts`. This file contains a POST route handler that processes incoming webhook events from Stripe."},{"heading":"webhook-endpoint","content":"The webhook syncs products, prices, and subscriptions with our database."}],"headings":[{"id":"webhook-endpoint","content":"Webhook Endpoint"}]},"url":"/docs/v1/payments/stripe-webhooks","title":"Stripe Webhooks"},{"_id":"/docs/v1/performance/strategies-for-pages","structured":{"contents":[{"heading":"strategies-for-pages","content":"In this Next.js 14 codebase, we employ several strategies to enhance page performance using async/await and Suspense. These techniques are crucial for optimizing data fetching and rendering, ensuring a smooth user experience."},{"heading":"strategies-for-pages","content":"Asynchronous Data Fetching in Server Components"},{"heading":"strategies-for-pages","content":"Server components in Next.js can execute asynchronously, allowing us to fetch data on the server before rendering the component. This approach reduces bundle sizes and leverages server CPU power for faster data fetching."},{"heading":"strategies-for-pages","content":"For example, in the roadmap page, we asynchronously fetch data for different roadmap sections like planned, in progress, and completed tasks using async functions before rendering the components."},{"heading":"strategies-for-pages","content":"Parallel Data Fetching with Suspense"},{"heading":"strategies-for-pages","content":"We use React's Suspense component to fetch multiple data sources in parallel and render child components as soon as the data is available. This technique allows the page to start rendering without waiting for all data to be fetched, improving perceived performance."},{"heading":"strategies-for-pages","content":"For instance, the blog list page fetches blog posts and tags in parallel, wrapped in Suspense, enhancing the user experience by streaming rendering."},{"heading":"strategies-for-pages","content":"Server components have the added benefit of streaming rendering. Consider the page responsible for rendering the blog list page at `/blog`. This page has to fetch two sets of data: the list of blog posts and the list of tags. We can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components: `Tags` and `BlogList`."},{"heading":"strategies-for-pages","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"strategies-for-pages","content":"Suspense for Incremental Rendering"},{"heading":"strategies-for-pages","content":"We also use Suspense to incrementally render parts of the page as data becomes available. This approach is evident in the changelog page, where the changelog list is rendered asynchronously and wrapped in Suspense to display a loading state until the data is fetched."}],"headings":[{"id":"strategies-for-pages","content":"Strategies for pages"}]},"url":"/docs/v1/performance/strategies-for-pages","title":"Performance Strategies"},{"_id":"/docs/v1/performance/streaming-suspense-rendering","structured":{"contents":[{"heading":"streaming-server-rendering","content":"In our Next.js 14 pages, we leverage streaming server rendering primarily through the use of server components and the Suspense component for data fetching and rendering optimization. This approach allows us to start rendering the page even before all the data is fetched, improving the user experience by displaying content as soon as it's available."},{"heading":"example-of-streaming-server-rendering","content":"In the blog list page, we demonstrate streaming server rendering by fetching and rendering two sets of data in parallel: the list of blog posts and the list of tags. This is facilitated by moving the data fetching into two separate components, Tags and BlogList, which are server components. By wrapping these components in Suspense, we can render them in parallel and stream the rendering of the page as soon as the data is available, without waiting for all data to be fetched."},{"heading":"example-of-streaming-server-rendering","content":"Server components have the added benefit of streaming rendering.\nConsider the page responsible for rendering the blog list page at **/blog** route.\nThis page has to fetch two sets of data: the list of blog posts and the list of tags.\nWe can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components:"},{"heading":"example-of-streaming-server-rendering","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"benefits","content":"Improved User Experience: The page starts rendering immediately, displaying the \"All blog posts\" title and placeholders for tags and posts until the actual data is fetched. This approach significantly enhances the perceived performance of the application."},{"heading":"benefits","content":"Efficient Data Fetching: By fetching data in server components, we leverage the server's CPU power, leading to faster data retrieval and smaller bundle sizes since we don't have to ship the data fetching logic to the client.\nImplementation"},{"heading":"benefits","content":"The implementation of streaming server rendering in our Next.js 14 pages is a testament to the framework's capabilities in optimizing performance and enhancing the user experience. By judiciously using server components and Suspense, we ensure that our pages are not only fast but also provide immediate feedback to users, which is crucial for modern web applications."}],"headings":[{"id":"streaming-server-rendering","content":"Streaming server rendering"},{"id":"example-of-streaming-server-rendering","content":"Example of Streaming Server Rendering"},{"id":"benefits","content":"Benefits"}]},"url":"/docs/v1/performance/streaming-suspense-rendering","title":"Streaming Suspense Server Rendering"},{"_id":"/docs/v1/quick-setup","structured":{"contents":[{"heading":"super-quick-dev-setup","content":"This guide covers the basic setup for Supabase and Next.js."},{"heading":"super-quick-dev-setup","content":"For integrating Stripe, refer to the comprehensive setup guide."},{"heading":"1-clone-the-repository-and-install-dependencies","content":"This command installs all necessary dependencies."},{"heading":"2-copy-envlocalexample","content":"Duplicate the `.env.local.example` file at the project root as `.env.local`."},{"heading":"3-launch-supabase-locally","content":"**Note:** Ensure Docker is installed on your system. Visit Docker's documentation for installation details."},{"heading":"4-start-the-app-by-running-the-following-command","content":"You're all set! The app should now be accessible at http\\://localhost:3000.\nStripe and pricing aren't configured yet, but you can start building your app with Supabase and Next.js."},{"heading":"optional-setting-up-stripe-locally","content":"To set up Stripe locally, refer to the comprehensive setup guide."}],"headings":[{"id":"super-quick-dev-setup","content":"Super quick dev setup"},{"id":"1-clone-the-repository-and-install-dependencies","content":"1\\. Clone the repository and install dependencies:"},{"id":"2-copy-envlocalexample","content":"2\\. Copy .env.local.example"},{"id":"3-launch-supabase-locally","content":"3\\. Launch Supabase locally:"},{"id":"4-start-the-app-by-running-the-following-command","content":"4\\. Start the App by running the following command:"},{"id":"optional-setting-up-stripe-locally","content":"(Optional) Setting up Stripe locally"}]},"url":"/docs/v1/quick-setup","title":"Quick setup","description":"Setting up nextbase is easy. Follow these steps to get started."},{"_id":"/docs/v1/security/access-control-on-supabase-functions","structured":{"contents":[{"content":"Supabase functions can be accessed by anyone, but we can control access to them using Supabase RBAC."}],"headings":[]},"url":"/docs/v1/security/access-control-on-supabase-functions","title":"Access control on Supabase functions","description":"How to control access to your Supabase functions."},{"_id":"/docs/v1/security/protected-nextjs-areas","structured":{"contents":[{"heading":"protected-nextjs-pages-and-api-routes","content":"The `app_admin` folder is only accessible to application admins. Hence all pages which start with the route `app_admin` are only accessible to app admins."},{"heading":"protected-nextjs-pages-and-api-routes","content":"The `dashboard` route , `setting` routes, `organization` routes are all only accessible to logged in users."},{"heading":"protected-nextjs-pages-and-api-routes","content":"Other pages are public."},{"heading":"creating-new-pags","content":"To create pages only accessible to logged in users, create them within the `(authenticated-pages)/(user-pages)` folders. This will make them available to all pages including app\\_admins."},{"heading":"creating-new-pags","content":"To create pages only accessible to app\\_admin users, create them within the `(authenticated-users)/(app_admin)` folder."},{"heading":"creating-new-pags","content":"`(external-pages)` are available to logged out users as well."},{"heading":"creating-new-pags","content":"`(login-pages)` should contain all authentication related pages such as login, signup etc."}],"headings":[{"id":"protected-nextjs-pages-and-api-routes","content":"Protected Next.js pages and api routes"},{"id":"creating-new-pags","content":"Creating new pags"}]},"url":"/docs/v1/security/protected-nextjs-areas","title":"Protected Next.js areas","description":"How to create new pages and routes that are protected."},{"_id":"/docs/v1/security/row-level-security","structured":{"contents":[{"content":"Row Level Security (RLS) is a feature in PostgreSQL that provides the ability to control which users can `SELECT`, `INSERT`, `UPDATE`, or `DELETE` which rows in a table. It adds an additional layer of security in multi-user database environments and is crucial for protecting sensitive data.\nUnderstanding Row Level Security"},{"content":"RLS works by enabling you to define policies that restrict, on a per-user basis, whether certain rows can be returned by a `SELECT` query or modified by an `INSERT`, `UPDATE`, or `DELETE` query. This is particularly useful in multi-tenant environments where you want to ensure that users can only access their own data."},{"content":"In PostgreSQL, RLS is disabled by default for all tables. To enable RLS, you use the `ALTER TABLE` command with the ENABLE ROW LEVEL SECURITY clause. Once RLS is enabled, all users (except the table owner and superusers) are denied access to the table—unless a policy is explicitly added to grant them access."},{"heading":"importance-of-row-level-security","content":"RLS is important for a variety of reasons:"},{"heading":"importance-of-row-level-security","content":"Data Protection: RLS helps to protect sensitive data by ensuring that users can only access data that they are authorized to see. This is particularly important in industries such as healthcare or finance where regulations require strict controls over data access."},{"heading":"importance-of-row-level-security","content":"Multi-tenancy: In multi-tenant environments, RLS can be used to ensure that each tenant can only access their own data. This is much more efficient and secure than having separate tables or databases for each tenant."},{"heading":"importance-of-row-level-security","content":"Flexibility: RLS policies can be defined using any boolean SQL expression, which provides a great deal of flexibility. For example, you could define a policy that only allows users to access data that was created within the last 30 days."}],"headings":[{"id":"importance-of-row-level-security","content":"Importance of Row Level Security"}]},"url":"/docs/v1/security/row-level-security","title":"Row Level Security","description":"Row Level Security (RLS) is a feature in PostgreSQL that provides the ability to control which users can SELECT, INSERT, UPDATE, or DELETE which rows in a table. It adds an additional layer of security in multi-user database environments and is crucial for protecting sensitive data."},{"_id":"/docs/v1/tech-stack/why-nextjs","structured":{"contents":[{"content":"There are several reasons why we chose Next.js over other options when building a server-rendered React application:"},{"content":"Ease of use: Next.js is designed to be easy to learn and use, with a simple and intuitive API and a wide range of built-in features. This can make it a good choice for developers who are new to server-rendered React applications or who want to get up and running quickly."},{"content":"Performance: Next.js is optimized for performance, with features such as automatic code splitting and server rendering that can help to improve the load time and overall performance of our application."},{"content":"Scalability: Next.js is designed to be scalable, with support for static exports and the ability to run on a serverless architecture. This can make it a good choice for applications that are expected to handle a large number of users or a high volume of traffic."},{"content":"Community and ecosystem: Next.js has a large and active community of developers, with a wide range of resources and tools available to support building applications with the framework. This can make it easier to find help and support when working with Next.js."},{"content":"Next 13 is massive leap in terms of performance and developer experience and we experienced it while building apps in our other projects and we think no one should miss out on using Next.js 14+."},{"content":"It really is taking the world by storm.\nhttps\\://mobilecoderz.com/blog/next-js-framework-the-next-big-thing-of-2022/"}],"headings":[]},"url":"/docs/v1/tech-stack/why-nextjs","title":"Why Next.js?","description":"Why we chose Next.js for server-rendered React applications."},{"_id":"/docs/v1/tech-stack/why-shadcn-ui","structured":{"contents":[{"content":"Shadcn UI is a component library that is built on top of Radix UI. It is a set of reusable UI components that are styled with Tailwind CSS. It is designed to be used with Next.js and React."},{"content":"We chose Shadcn UI because it is a modern and easy to use component library that is built on top of Radix UI. It is designed to be used with Next.js and React."},{"content":"It is built with Tailwind CSS and TypeScript. It is designed to be used with Next.js and React."},{"content":"It is designed to be used with Next.js and React."}],"headings":[]},"url":"/docs/v1/tech-stack/why-shadcn-ui","title":"Why Shadcn UI?","description":"Why we chose Shadcn UI for our design system."},{"_id":"/docs/v1/tech-stack/why-stripe","structured":{"contents":[{"content":"While there are many payments providers today, we went with Stripe which is the leading payments provider of choice for most SaaS companies today."},{"content":"Wide range of payment options: Stripe supports a wide range of payment methods, including credit cards, debit cards, and online payment services such as Apple Pay and Google Pay. This can make it easy for businesses to accept payments from a variety of sources."},{"content":"Customizable payment forms: Stripe allows businesses to customize the look and feel of their payment forms to match their brand. This can help to create a seamless and professional payment experience for customers."},{"content":"Advanced fraud protection: Stripe provides advanced fraud protection to help businesses detect and prevent fraudulent activity. This can help to reduce the risk of chargebacks and other fraudulent activity."},{"content":"Easy integration: Stripe offers a wide range of tools and libraries to make it easy to integrate the platform into a business's existing systems and processes. This can help to streamline the payment process and reduce the time and effort required to manage payments."}],"headings":[]},"url":"/docs/v1/tech-stack/why-stripe","title":"Why Stripe?","description":"Why Stripe is the leading payments provider of choice for most SaaS companies today. Wide range of payment options, customizable payment forms, advanced fraud protection, and easy integration."},{"_id":"/docs/v1/tech-stack/why-supabase","structured":{"contents":[{"content":"Supabase has had a breakthrough last couple of years in SaaS products usage. It is the new Firebase but better."},{"content":"We went with Supabase because it makes backend and database development a breeze."},{"content":"It is realtime first, open source and built on top of Postgres. It provides a powerful dashboard, with many many features. Some include"},{"content":"Easy to use Javascript and Node.js clients. They also have Flutter, Swift and Dart clients to easily built mobile apps later."},{"content":"Realtime with just a checkbox. All your database tables can be made realtime with just a click of a button."},{"content":"Database hooks - Allows you to integrate with Zapier, Make etc and connect your app to other apps"},{"content":"Edge functions"},{"content":"Database triggers and functions"},{"content":"Admin Panel"},{"content":"Custom reports and Graphs"},{"content":"Errors dashboard"},{"content":"Foreign Table Wrappers (In development) and so many more."},{"content":"If you want to build robust webapps quickly, Supabase is the way to go in 2023."}],"headings":[]},"url":"/docs/v1/tech-stack/why-supabase","title":"Why Supabase?","description":"Why we chose Supabase for our backend and database needs"},{"_id":"/docs/v1/troubleshooting/login-callback-wrong","structured":{"contents":[{"content":"This is generally because the Supabase Authentication / URL configuration is not setup correctly. Ensure the following:"},{"content":"The Site URL is set to your production domain eg: `https://usenextbase.com`"},{"content":"The Redirect urls include both your production and development hosts"},{"content":"eg : `https://usenextbase/com/**` and `http://localhost:3000/**`"}],"headings":[]},"url":"/docs/v1/troubleshooting/login-callback-wrong","title":"I am unable to login","description":"This is generally because the Supabase Authentication / URL configuration is not setup correctly."},{"_id":"/docs/v1/ui/dark-mode","structured":{"contents":[{"heading":"dark-mode","content":"NextBase supports dark mode for your application."}],"headings":[{"id":"dark-mode","content":"Dark Mode"}]},"url":"/docs/v1/ui/dark-mode","title":"Dark Mode","description":"NextBase supports dark mode for your application."},{"_id":"/docs/v1/ui/nprogress","structured":{"contents":[{"heading":"nprogress","content":"NProgress is a library that provides a progress bar for your application."}],"headings":[{"id":"nprogress","content":"NProgress"}]},"url":"/docs/v1/ui/nprogress","title":"NProgress","description":"NProgress is a library that provides a progress bar for your application."},{"_id":"/docs/v1/ui/shadcn-ui","structured":{"contents":[{"heading":"shadcn-ui","content":"Shadcn UI is a very popular library for building UI components.\nIt is built on top of Radix UI and Tailwind CSS."},{"heading":"shadcn-ui","content":"Checkout their website for more information."}],"headings":[{"id":"shadcn-ui","content":"Shadcn UI"}]},"url":"/docs/v1/ui/shadcn-ui","title":"Shadcn UI","description":"Shadcn UI is a very popular library for building UI components."},{"_id":"/docs/v1/ui/sonner","structured":{"contents":[{"content":"Sonner is quite a simple library to use. We chose sonner because it's styling doesn't get in the way and it's easy to use."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v1/ui/sonner","title":"Sonner","description":"Sonner is a library that provides a toast notification for your application."},{"_id":"/docs/v1/ui/tailwindcss","structured":{"contents":[{"content":"Tailwind has been configured with next.js."},{"content":"We have the following plugins configured:"},{"content":"`tailwindcss-animate`: This plugin adds a set of helpful animation utilities to Tailwind CSS."},{"content":"`@tailwindcss/typography`: This plugin provides a set of `prose` classes for beautiful typographic defaults."},{"content":"Additionally, we've extended the default Tailwind configuration with:"},{"content":"Custom color palette including primary, secondary, destructive, muted, accent, popover, and card colors."},{"content":"Custom border radius values."},{"content":"Custom font families:"},{"content":"`sans`: Uses the Inter font"},{"content":"`display`: Uses the Lexend font"},{"content":"`sans2`: Uses the Manrope font"},{"content":"Custom keyframes and animations:"},{"content":"`accordion-down` and `accordion-up`"},{"content":"`scale-up` and `scale-down`"},{"content":"Extended max-width with an `8xl` size."},{"content":"The configuration also includes a dark mode setup using the `class` strategy, allowing for easy implementation of a dark theme in your application."}],"headings":[]},"url":"/docs/v1/ui/tailwindcss","title":"Tailwind CSS","description":"Tailwind CSS is a utility-first CSS framework."},{"_id":"/docs/v2/admin-panel/blog","structured":{"contents":[{"content":"The blog feature in the admin panel allows you to manage your blog posts efficiently. Here's an overview of its functionality:"},{"content":"Blog posts can be written in Markdown format."},{"content":"You can add images to your blog posts."},{"content":"You can add tags to your blog posts."},{"content":"You can add categories to your blog posts."},{"content":"You can add a summary to your blog posts."},{"content":"You can add a thumbnail to your blog posts."},{"content":"You can add a published date to your blog posts."},{"content":"You can add an author to your blog posts."}],"headings":[]},"url":"/docs/v2/admin-panel/blog","title":"Blog","description":"Blog feature in the admin panel"},{"_id":"/docs/v2/authentication/advanced-methods","structured":{"contents":[{"heading":"possible-advanced-methods","content":"**Two-Factor Authentication (2FA)**: Enhance security by requiring a second form of verification."},{"heading":"possible-advanced-methods","content":"**Biometric Authentication**: Implement fingerprint or face recognition for mobile apps."},{"heading":"possible-advanced-methods","content":"**Single Sign-On (SSO)**: Allow users to access multiple applications with a single set of credentials."},{"heading":"possible-advanced-methods","content":"**JSON Web Tokens (JWT)**: Use JWTs for stateless authentication in your API."},{"heading":"possible-advanced-methods","content":"**OAuth 2.0**: Implement a full OAuth 2.0 flow for more complex authorization scenarios."},{"heading":"implementation-considerations","content":"Implementing advanced authentication methods often requires additional libraries, services, or custom server-side logic. Here are some general steps:"},{"heading":"implementation-considerations","content":"Choose the appropriate method based on your security requirements and user experience goals."},{"heading":"implementation-considerations","content":"Research and select any necessary third-party services or libraries."},{"heading":"implementation-considerations","content":"Implement the chosen method, ensuring it integrates well with Nextbase and Supabase."},{"heading":"implementation-considerations","content":"Thoroughly test the new authentication method, including edge cases and potential security vulnerabilities."},{"heading":"implementation-considerations","content":"Update your application's documentation and user guides to reflect the new authentication options."},{"heading":"implementation-considerations","content":"Remember, while advanced authentication methods can provide enhanced security, they may also introduce complexity to your application. Always balance security needs with user experience considerations."}],"headings":[{"id":"possible-advanced-methods","content":"Possible Advanced Methods"},{"id":"implementation-considerations","content":"Implementation Considerations"}]},"url":"/docs/v2/authentication/advanced-methods","title":"Advanced Authentication Methods","description":"While Nextbase provides built-in support for common authentication methods, you may need more advanced or specialized authentication options for certain use cases."},{"_id":"/docs/v2/authentication/login-with-api-key","structured":{"contents":[{"heading":"implementation","content":"API key authentication is typically implemented using a third-party service like Unkey. The exact implementation details may vary based on your specific requirements and the service you're using."},{"heading":"implementation","content":"Here's a general outline of how you might implement API key authentication:"},{"heading":"implementation","content":"Generate an API key for the user or service that needs access."},{"heading":"implementation","content":"Store the API key securely (never in plain text)."},{"heading":"implementation","content":"When a request comes in, validate the API key."},{"heading":"implementation","content":"If the key is valid, authenticate the request and proceed."},{"heading":"usage-example","content":"Here's a simplified example of how you might validate an API key in a Next.js API route:"},{"heading":"usage-example","content":"Remember to handle API keys securely and follow best practices for API authentication."}],"headings":[{"id":"implementation","content":"Implementation"},{"id":"usage-example","content":"Usage Example"}]},"url":"/docs/v2/authentication/login-with-api-key","title":"Login with API Key","description":"Nextbase Ultimate supports authentication using API keys, which is useful for integrating with external services or for machine-to-machine communication."},{"_id":"/docs/v2/authentication/login-with-magic-link","structured":{"contents":[{"heading":"server-action","content":"Here's the server action used to login with Magic Link:"},{"heading":"usage","content":"Here's an example of how to use the Magic Link authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to receive a login link via email, providing a passwordless authentication experience."}],"headings":[{"id":"server-action","content":"Server Action"},{"id":"usage","content":"Usage"}]},"url":"/docs/v2/authentication/login-with-magic-link","title":"Login with Magic Link","description":"Nextbase supports login with Magic Link, which is a secure way to authenticate users without requiring them to remember a password."},{"_id":"/docs/v2/authentication/login-with-password","structured":{"contents":[{"heading":"server-action","content":"Here's the server action used for password-based login:"},{"heading":"usage","content":"Here's an example of how to use password-based authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to log in using their email and password credentials."}],"headings":[{"id":"server-action","content":"Server Action"},{"id":"usage","content":"Usage"}]},"url":"/docs/v2/authentication/login-with-password","title":"Login with Password","description":"Nextbase supports traditional email and password authentication, providing a familiar login experience for users."},{"_id":"/docs/v2/authentication/login-with-social-providers","structured":{"contents":[{"content":"Here's the server action used for social provider login:"},{"heading":"usage","content":"Here's an example of how to use social provider authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to log in using their accounts from supported social providers."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v2/authentication/login-with-social-providers","title":"Login with Social Providers","description":"Nextbase supports authentication with various social providers, allowing users to log in using their existing accounts from platforms like Google, GitHub, and Twitter."},{"_id":"/docs/v2/authentication/overview","structured":{"contents":[{"content":"It only has small wrappers around the supabase javascript client."},{"heading":"supported-auth-methods","content":"Authentication Type"},{"heading":"supported-auth-methods","content":"Description"},{"heading":"supported-auth-methods","content":"Email"},{"heading":"supported-auth-methods","content":"Authentication using only the user's email."},{"heading":"supported-auth-methods","content":"Email + Password"},{"heading":"supported-auth-methods","content":"Authentication using the user's email and password."},{"heading":"supported-auth-methods","content":"Third Party Providers"},{"heading":"supported-auth-methods","content":"Authentication using third-party providers. Examples given are Google, GitHub, and Twitter.  Configuration is done via the Supabase dashboard."},{"heading":"supported-auth-methods","content":"Login with API Key"},{"heading":"supported-auth-methods","content":"Authentication using an API key using Unkey (Nextbase Ultimate only)."},{"heading":"providers","content":"Providers are configured using the RenderProviders component. It renders all the providers passed to it as props. For example, the login page component Login.tsx renders providers like so:"},{"heading":"providers","content":"You can configure whichever providers you like using the supabase dashboard. Head over to `https://app.supabase.com/project/{your-project}/auth/providers` and configure the providers you require."},{"heading":"providers","content":"They will automatically work in your frontend. No other configuration required."}],"headings":[{"id":"supported-auth-methods","content":"Supported auth methods"},{"id":"providers","content":"Providers"}]},"url":"/docs/v2/authentication/overview","title":"Overview","description":"NextBase comes with Email, Email + password and Third party providers authentication built-in."},{"_id":"/docs/v2/development-updates/code-and-migration-updates","structured":{"contents":[{"content":"We are constantly working on improving the codebase and migrating to new versions."},{"heading":"codebase","content":"All Nextbase repos, (nextbase essential, nextbase pro, nextbase ultimate) are fullstack nextjs applications.\nThey are all built with the same tech stack and have the same architecture."}],"headings":[{"id":"codebase","content":"Codebase"}]},"url":"/docs/v2/development-updates/code-and-migration-updates","title":"Code and migration updates","description":"Updates to the Nextbase codebase and migration updates."},{"_id":"/docs/v2/development-updates/version-control","structured":{"contents":[{"content":"We use git for version control.\nAll our repositories are hosted on github."}],"headings":[]},"url":"/docs/v2/development-updates/version-control","title":"Version Control","description":"How we version control our Nextbase repositories"},{"_id":"/docs/v2/getting-started","structured":{"contents":[{"heading":"getting-started-with-nextbase","content":"Welcome to NextBase! We're thrilled to have you on board. This guide will walk you through the initial steps to get started with your purchased NextBase repository."},{"heading":"accessing-your-purchase","content":"To access your purchase, navigate to your dashboard. Here, you will find all your purchased items. Look for the NextBase repository (Essential, Pro, or Ultimate) that you've purchased."},{"heading":"entering-your-github-username","content":"You will be prompted to enter your GitHub username. This step is crucial as it grants you access to the purchased repository. Please ensure you enter the correct username."},{"heading":"cloning-the-repository","content":"Once you've gained access, the next step is to clone the repository. Here's how:\nLet us say that you purchased the NextBase Pro repository. It is a private repository which is at https\\://github.com/imbhargav5/nextbase-pro.\nTo clone the repository, you will need to run the following command in your terminal:"},{"heading":"cloning-the-repository","content":"**Important Note:** We strongly discourage forking the repository. If you fork the repository, the code will be visible to others who have forked it. To maintain the privacy and integrity of your code, always opt for cloning.\nNext Steps"},{"heading":"cloning-the-repository","content":"After cloning the repository, you're all set to start exploring and working with NextBase. If you encounter any issues or have any questions, don't hesitate to reach out to our support team."}],"headings":[{"id":"getting-started-with-nextbase","content":"Getting Started with NextBase"},{"id":"accessing-your-purchase","content":"Accessing Your Purchase"},{"id":"entering-your-github-username","content":"Entering Your GitHub Username"},{"id":"cloning-the-repository","content":"Cloning the Repository"}]},"url":"/docs/v2/getting-started","title":"Getting Started with NextBase"},{"_id":"/docs/v2/guides/admin-panel","structured":{"contents":[{"heading":"user-impersonation-debugging","content":"As an application admin, you can debug bugs more quickly for any user by loggin in as them and debugging the issue from their account. It is available in the application admin users section."},{"heading":"user-details-table","content":"The user details table is a table that shows all the users in the application. It is available in the application admin users section."},{"heading":"workspaces-table","content":"The workspaces table is a table that shows all the workspaces in the application. It is available in the application admin workspaces section."},{"heading":"admin-controls-for-feedback-roadmap","content":"Admin users can manage feedback and roadmap items. They can update feedback status, type, priority etc."}],"headings":[{"id":"user-impersonation-debugging","content":"User impersonation debugging"},{"id":"user-details-table","content":"User details table"},{"id":"workspaces-table","content":"Workspaces table"},{"id":"admin-controls-for-feedback-roadmap","content":"Admin controls for feedback, roadmap"}]},"url":"/docs/v2/guides/admin-panel","title":"Admin Panel Guide","description":"The Admin panel has features user impersonation, fix bugs more quickly, and more."},{"_id":"/docs/v2/guides/asynchronous-data-fetching","structured":{"contents":[{"content":"In Nextbase starter kit, we to rely on server components as much as possible to fetch data. The advantages are"},{"content":"smaller bundle sizes: because we don't have to ship the data fetching logic to the client"},{"content":"faster page loads: because we rely on the cpu power of the server instead of the client to fetch data. Server CPUs are usually faster than client CPUs."},{"heading":"data-fetching-in-server-components","content":"Since server components can by async, we can just await on the data which we require for that component. Next.js layouts and pages are server components\nby default unless you explicitly specify them as client components."},{"heading":"data-fetching-in-server-components","content":"For instance, take a look at our the page rendering our `/blog/[slug]` route:"},{"heading":"data-fetching-in-server-components","content":"Notice how we are awaiting on the `anonGetPublishedBlogPostBySlug` function to fetch the blog post data. This function is a server action which is defined in `src/data/anon/internalBlog.ts`. And since the `BlogPostPage` is a server component, we can await on the data fetching function."},{"heading":"streaming-rendering","content":"Server components have the added benefit of streaming rendering. Consider the page responsible for rendering the blog list page at `/blog`. This page has to fetch two sets of data: the list of blog posts and the list of tags. We can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components: `Tags` and `BlogList`."},{"heading":"streaming-rendering","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"streaming-rendering","content":"So the user experience for this page looks like this:"},{"heading":"streaming-rendering","content":"the page starts rendering"},{"heading":"streaming-rendering","content":"\"All blog posts\" title is rendered"},{"heading":"streaming-rendering","content":"\"Loading tags...\" is rendered and when the tags are fetched, the tags are rendered"},{"heading":"streaming-rendering","content":"\"Loading posts...\" is rendered and when the posts are fetched, the posts are rendered"},{"heading":"streaming-rendering","content":"This is a much better user experience than having to wait for the page to render until all the data is fetched."},{"heading":"data-fetching-in-client-components","content":"We don't fetching data much in client components unless in very rare cases. To fetch data in client components simply rely on `useQuery` from\nreact-query. For eg: the login button in the navigation menu is a client component and it fetches the user data using `useQuery`:"}],"headings":[{"id":"data-fetching-in-server-components","content":"Data Fetching in Server components"},{"id":"streaming-rendering","content":"Streaming rendering"},{"id":"data-fetching-in-client-components","content":"Data fetching in client components"}]},"url":"/docs/v2/guides/asynchronous-data-fetching","title":"Asynchronous Data Fetching Guide","description":"Learn how to fetch data asynchronously in Next.js using server actions and react query"},{"_id":"/docs/v2/guides/authentication","structured":{"contents":[{"heading":"authentication","content":"See Authentication Overview here."},{"heading":"authentication","content":"More coming soon..."}],"headings":[{"id":"authentication","content":"Authentication"}]},"url":"/docs/v2/guides/authentication","title":"Authentication","description":"Authenticating users in Nextbase Starter kit"},{"_id":"/docs/v2/guides/creating-application-admin-page","structured":{"contents":[{"content":"Admin Panel pages are internal web app pages that are only accessible to application administrators. These pages usually require the user to be authenticated and have admin privileges."},{"content":"All admin panel pages should be children of the layout located at `src/app/(dynamic-pages)/(authenticated-pages)/app_admin/layout.tsx`. This layout handles checking if the user is authenticated and if they have admin privileges. If the user is not authenticated or does not have admin privileges, they will be redirected to the login page."},{"content":"Here is an example of how to create an admin panel page:"}],"headings":[{"id":"admin-panel-layout","content":"Admin panel layout"}]},"url":"/docs/v2/guides/creating-application-admin-page","title":"Creating Admin Panel Page","description":"Guide on how to create an admin panel page in  Nextbase."},{"_id":"/docs/v2/guides/creating-docs-page","structured":{"contents":[{"content":"Docs pages are generated at build time. To create a docs page, create a markdown file in the `src/(static-pages)/(developer-resources)/docs` folder.\nThe pages should be written in mdx. Nextbase is configured to use mdx within `next.config.mjs` and custom components are configured using the\n`src/mdx-components.tsx` file. MDX is also configured to use `rehype` and `remark` plugins to allow for syntax highlighting and other features."},{"content":"Here is the relevant `next.config.mjs` configuration."},{"heading":"seo","content":"You can add SEO to your mdx page in the same way you would add to a typescript page.tsx component, using metadata."},{"heading":"seo","content":"Here is an example:"}],"headings":[{"id":"seo","content":"SEO"}]},"url":"/docs/v2/guides/creating-docs-page","title":"Creating Docs Page","description":"Guide on how to create a docs page in a Next.js application such as in Nextbase."},{"_id":"/docs/v2/guides/creating-in-app-notifications","structured":{"contents":[{"content":"In-app notifications are a great way to communicate with your users. They can be used to inform users about new features, promotions, or any other important information.\nNextbase Ultimate comes with a built-in in-app notification system that allows you to create and send notifications to your users."},{"heading":"notifications-table","content":"User notifications are managed in the `user_notifications` table which is created automatically by Nextbase Ultimate supabase migrations during installation. Here is the relevant part of the table schema:"},{"heading":"notifications-table","content":"As you can see, the table has a foreign key to the `user_profiles` table, which means that each notification is associated with a user. This is important because it allows you to send notifications to specific users."},{"heading":"notifications-table","content":"Also, the table has a `payload` column which is a JSONB column that can be used to store any data you want to send to the user."},{"heading":"notifications-table","content":"Additionally, the table has a `is_read` and `is_seen` columns that can be used to track whether the user has read or seen the notification."},{"heading":"notifications-table","content":"Finally the RLS policies ensure that users can only read, update, and delete their own notifications."},{"heading":"creating-a-notification","content":"To create a notification, you can use the `createNotification` function which is available in the `src/data/user/notifications` module:"},{"heading":"creating-a-notification","content":"This function allows adding a notification of any payload to a user."},{"heading":"typesafety","content":"It is important to note that the `payload` column is of type `JSONB` which means that you can store any data you want in it. However, this also means that you will not get any type safety when accessing the data.\nFor that purpose, we use zod schemas to define the payload type. It is present in `src/utils/zod-schemas/notifications.ts`"},{"heading":"typesafety","content":"As you can see, we define a `UserNotification` type which is a union of all the possible notification types. This allows us to get type safety when accessing the notification payload.\nWhen you want to create a new notification type you can simply add it to the union type and define the zod schema for it.\nFor eg. if you want to create a notification type for when a user's profile was seen by another user, you can do the following:"},{"heading":"parsing-notification-payload","content":"We utilize a parse notification function in `src/utils/parseNotification.ts`, where we parse and normalize the notification payload. This is useful when we want to display the notification in the UI."},{"heading":"parsing-notification-payload","content":"So to add our new notification type, we can simply add a new case to the switch statement:"},{"heading":"viewing-notifications","content":"UI to mark a notification as seen and read is already built into Nextbase Ultimate."}],"headings":[{"id":"notifications-table","content":"Notifications Table"},{"id":"creating-a-notification","content":"Creating a notification"},{"id":"typesafety","content":"Typesafety"},{"id":"parsing-notification-payload","content":"Parsing notification payload"},{"id":"viewing-notifications","content":"Viewing notifications"}]},"url":"/docs/v2/guides/creating-in-app-notifications","title":"Creating in-app notifications","description":"Guide on how to create in-app notifications in Nextbase."},{"_id":"/docs/v2/guides/creating-new-supabase-table","structured":{"contents":[{"content":"When you are creating a new feature in your app, you might need to create a new table in your Supabase database.\nIn this guide, we'll learn how to create a new table in Supabase and use the new table in your Next.js app."},{"heading":"creating-a-table","content":"Let us say, you want to create a table to store a list of todos. You can create a table in Supabase using the SQL editor."},{"heading":"creating-a-table","content":"Go to the SQL editor in your Supabase project."},{"heading":"creating-a-table","content":"Run the following SQL query to create a table named `todos`:"},{"heading":"creating-a-table","content":"You can also create a table using the Supabase UI. Go to the **Tables** tab in your Supabase project and click on the **New table** button and create the columns in the GUI editor."},{"heading":"generate-types-for-the-table","content":"Nextbase includes a CLI command to generate TypeScript types for your Supabase tables. You can run the following command to generate types for the\nnewly modified database."},{"heading":"generate-types-for-the-table","content":"This will regenerate the types for your local database. And the types are located at `@/src/lib/database.types.ts`"},{"heading":"why-do-we-need-to-generate-types","content":"Types are useful for type checking and auto-completion in your IDE. You can use the generated types to type check your database queries and also use them to auto-complete the column names in your queries.\nFor example, if you want to query the `todos` table, you can use the generated types to type check the query and also auto-complete the column names."},{"heading":"why-do-we-need-to-generate-types","content":"The above code returns `data` which will be an array of `Todo` objects. And the types will be generated within the `@/src/lib/database.types.ts` file."}],"headings":[{"id":"creating-a-table","content":"Creating a table"},{"id":"generate-types-for-the-table","content":"Generate types for the table"},{"id":"why-do-we-need-to-generate-types","content":"Why do we need to generate types?"}]},"url":"/docs/v2/guides/creating-new-supabase-table","title":"Creating a new Supabase table","description":"Learn how to create a new table in Supabase and use the new table in your Next.js app."},{"_id":"/docs/v2/guides/creating-protected-page","structured":{"contents":[{"content":"Protected pages are internal web app pages such as dashboard, user settings etc. These pages usually\nrequire a condition to be met, and the common case is that the user must be authenticated."},{"content":"All authenticated user pages should be children of this layout - `src/app/(dynamic-pages)/(authenticated-pages)/layout.tsx`.\nBecause this layout handles checking if the user is authenticated or not. If the user is not authenticated, the user will be redirected to the login page."},{"heading":"middleware-protection","content":"In addition to the layout-based protection, Nextbase uses middleware to enhance security and performance. The middleware runs before server components are rendered, allowing for early authorization checks."},{"heading":"middleware-protection","content":"This middleware:"},{"heading":"middleware-protection","content":"Defines unprotected pages that don't require authentication"},{"heading":"middleware-protection","content":"Checks if the user is authenticated for protected routes"},{"heading":"middleware-protection","content":"Handles user onboarding and admin access"},{"heading":"middleware-protection","content":"Redirects unauthenticated users to the login page"},{"heading":"middleware-protection","content":"By implementing both layout-based and middleware-based protection, you ensure robust security for your protected pages while maintaining good performance."}],"headings":[{"id":"middleware-protection","content":"Middleware Protection"}]},"url":"/docs/v2/guides/creating-protected-page","title":"Creating Protected Page","description":"Guide on how to create a protected page in a Next.js application such as in Nextbase."},{"_id":"/docs/v2/guides/creating-public-dynamic-page","structured":{"contents":[{"content":"A public dynamic page in a SaaS site can be something like a product page, a user profile page, etc. These pages are dynamic and are part of the application. They are not just static pages that are part of the marketing site.\nIn Nextbase, we have a `(dynamic-pages)` route segment (folder) within our `src/app` folder which contains all the dynamic pages."},{"content":"So if you want to create a new dynamic page, you can just create a new folder within the `(dynamic-pages)` folder and create a new page within that folder. You can nest\nyour dynamic pages as deep as you want to get the route you are looking for."},{"content":"The key difference here is that this page is not cached and is always served fresh."},{"content":"The layout file for the dynamic pages is at `src/app/(dynamic-pages)/layout.tsx`."}],"headings":[]},"url":"/docs/v2/guides/creating-public-dynamic-page","title":"Creating Public Dynamic Page","description":"Guide on how to create a public dynamic page in a Next.js application such as in Nextbase."},{"_id":"/docs/v2/guides/creating-public-static-page","structured":{"contents":[{"content":"A public static page in a SaaS site can be something like a pricing page, a contact page, a about page, etc. These pages are not dynamic and are not part of the application. They are just static pages that are part of the marketing site.\nIn Nextbase, we have a `(static-pages)` route segment (folder) within our `src/app` folder which contains all the static pages."},{"content":"So if you want to create a new static page, you can just create a new folder within the `(static-pages)` folder and create a new page within that folder. You can nest\nyour static pages as deep as you want to get the route you are looking for."},{"content":"The layout file for the static pages is at `src/app/(static-pages)/layout.tsx`."},{"content":"As you can see the layout has no additional configuration such as `export const dynamic` etc. Which means that this layout defauls to the next.js defaults and is static."}],"headings":[]},"url":"/docs/v2/guides/creating-public-static-page","title":"Creating Public Static Page","description":"Guide on how to create a public static page in a Next.js application such as in Nextbase."},{"_id":"/docs/v2/guides/handling-realtime-data-with-supabase","structured":{"contents":[{"content":"In this guide, we'll learn how to handle realtime data in Supabase using server actions and react query.\nWe implemented notifications as a realtime feature in Nextbase Ultimate and we can use that as a reference."},{"heading":"supabase-realtime","content":"Supabase provides a realtime service which can be used to listen to changes in your database.\nWhen used with a React client component useEffect it looks something like this:"},{"heading":"supabase-realtime","content":"In the above code, we are listening to changes in the `user_notifications` table and refetching the data when a change occurs.\nDepending on the scenario, you can also just use the payload from the realtime data to `setState` or update a `react query` cache directly."}],"headings":[{"id":"supabase-realtime","content":"Supabase Realtime"}]},"url":"/docs/v2/guides/handling-realtime-data-with-supabase","title":"Handling Realtime Data With Supabase","description":"Learn how to handle realtime data in Supabase using server actions and react query"},{"_id":"/docs/v2/guides/managing-users","structured":{"contents":[{"content":"Users are the core of your application. Supabase Auth provides a complete user management system, including sign up, sign in, email verification, password recovery, and more."},{"heading":"how-does-supabase-store-user-data","content":"Supabase stores user data in a table called `auth.users`. `auth` is an internal schema that is created when you initialize Supabase."},{"heading":"how-does-supabase-store-user-data","content":"When new users sign up, they are added to this table."},{"heading":"how-does-supabase-store-user-data","content":"This table and tables related in the auth schema are automatically updated when users update their login information"},{"heading":"storing-more-information-about-users","content":"However this table doesn't contain any more data other than email and identifying information. Since we want to store more information about our users, we use a table called `user_profiles`."},{"heading":"storing-more-information-about-users","content":"`user_profiles` is a table which allows users to update their public information such as name and avatar. (avatar image is stored in supabase storage and the url is stored here)."},{"heading":"storing-more-information-about-users","content":"Users can also store their private preferences in the `user_private_info` table. This table is only accessible by the user whose id matches the \\`user\\_id\\` column. Both the tables mentioned above have auth.users.id as a foreign key."},{"heading":"storing-more-information-about-users","content":"Hence if you want to add more public information about your users, you can add a new column to \\`user\\_profiles\\` and if you want to add more private information, you can add a new column to \\`user\\_private\\_info\\`."}],"headings":[{"id":"how-does-supabase-store-user-data","content":"How does Supabase store user data?"},{"id":"storing-more-information-about-users","content":"Storing more information about users"}]},"url":"/docs/v2/guides/managing-users","title":"Managing Users","description":"Learn how to manage users with Supabase"},{"_id":"/docs/v2/guides/mutating-data-with-supabase","structured":{"contents":[{"content":"In this guide, we'll learn how to mutate data asynchronously in Supabase using server actions and react query.\nUsually, the scenario looks like this."},{"content":"The server and client components for the route are loaded."},{"content":"User performs an action that triggers a mutation, eg. clicking a button, filling out a form, etc."},{"content":"The client sends a request to the server to perform the mutation."},{"content":"In previous versions of Next.js, we would have to write API routes to handle the request.\nBut as of Next.js 13, and now in 14 and beyond, we have a new way to handle this. These are called route handlers and server actions."},{"heading":"what-are-route-handlers","content":"In Next.js 14, Route handlers have replaced API routes.\nThese are simple `route.ts` files that can be placed in the `app` folder at any level."},{"heading":"what-are-route-handlers","content":"Here is an example of a route handler that handles a `POST` request to `/api/hello`."},{"heading":"what-are-route-handlers","content":"As of Next.js 14, Route handlers have replaced API routes. These are simple route.ts files that can be placed in the app folder at any level. They can handle multiple HTTP methods (GET, POST, PUT, DELETE, etc.) in a single file."},{"heading":"what-are-route-handlers","content":"Here's an example of a simple POST request using Route handlers:"},{"heading":"what-are-route-handlers","content":"Now both the above examples are functionally equivalent. But the latter is much more concise and easier to read."},{"heading":"what-are-server-actions","content":"Server actions are convenient wrappers around route handlers that allow you to perform mutations on your data within\nyour client / server components. Instead of writing a route handler, you can simply write a server action and call it from your client component."},{"heading":"what-are-server-actions","content":"Here's an example of a server action that handles a `POST` request similar to the example above."},{"heading":"what-are-server-actions","content":"And here's how you would call it from your client component."},{"heading":"what-are-server-actions","content":"In the above example, we are using the `useMutation` hook from react-query to call our server action."},{"heading":"what-are-server-actions","content":"Note how we are directly invoking the `hello` server action from our client component."},{"heading":"what-are-server-actions","content":"This will internally be converted to a `POST` request to a route handler automatically created by Next.js and mapped to a route."},{"heading":"usetoastmutation-hook","content":"Nextbase starter kit provides a convenience hook called `useToastMutation` that can be used to display a toast notification throughout the lifecycle of a mutation.\nIt is a thin wrapper around `useMutation` from react-query and uses `sonner` to display toast notifications."}],"headings":[{"id":"what-are-route-handlers","content":"What are route handlers?"},{"id":"what-are-server-actions","content":"What are server actions?"},{"id":"usetoastmutation-hook","content":"useToastMutation hook"}]},"url":"/docs/v2/guides/mutating-data-with-supabase","title":"Mutating Data With Supabase","description":"Learn how to mutate data asynchronously in Supabase using server actions and react query"},{"_id":"/docs/v2/guides/optimizing-data-operations-nextjs-supabase","structured":{"contents":[{"content":"In this guide, we'll explore best practices for fetching and mutating data in Nextbase applications, focusing on performance and efficiency."},{"heading":"server-side-data-fetching","content":"Prefer fetching data for components on the server using server components. This approach utilizes the higher CPU power of servers and minimizes bundle sizes for the data fetching operation."},{"heading":"mutations-with-usetoastmutation","content":"For mutations, use the `useToastMutation` hook. This allows for direct server-side mutations while providing user feedback through toast notifications."},{"heading":"realtime-queries-with-supabase","content":"For realtime queries, such as notifications, use Supabase's realtime APIs directly on the client side."},{"heading":"avoiding-usestate-for-data-fetching","content":"Avoid using `useState` for most data fetching operations. Instead, either use server components and fetch data within them, or use `useQuery` if you really need to, but you generally shouldn't need to use `useState`."},{"heading":"avoiding-usestate-for-data-fetching","content":"By following these guidelines, you can optimize data operations in your Nextbase applications for better performance and user experience."}],"headings":[{"id":"server-side-data-fetching","content":"Server-Side Data Fetching"},{"id":"mutations-with-usetoastmutation","content":"Mutations with useToastMutation"},{"id":"realtime-queries-with-supabase","content":"Realtime Queries with Supabase"},{"id":"avoiding-usestate-for-data-fetching","content":"Avoiding useState for Data Fetching"}]},"url":"/docs/v2/guides/optimizing-data-operations-nextjs-supabase","title":"Optimizing Data Operations in Next.js 14 and Supabase","description":"Learn how to efficiently fetch and mutate data in Next.js 14 and Supabase using server components, useToastMutation, and Supabase real-time APIs."},{"_id":"/docs/v2/guides/row-level-security","structured":{"contents":[{"content":"This article in the supabase website is a great start to understand how RLS works.\nhttps\\://supabase.com/docs/v2/guides/auth/row-level-security"},{"heading":"rls-configured-within-nextbase","content":"NextBase ships with RLS policies for tables such as organizations, organization\\_team\\_members, subscriptions, customers and more."},{"heading":"rls-configured-within-nextbase","content":"It also comes with RLS for storage buckets."},{"heading":"setup-rls-for-your-new-tables","content":"TODO"}],"headings":[{"id":"rls-configured-within-nextbase","content":"RLS configured within NextBase"},{"id":"setup-rls-for-your-new-tables","content":"Setup RLS for your new tables"}]},"url":"/docs/v2/guides/row-level-security","title":"Row Level Security","description":"Row Level Security in NextBase is a powerful feature that allows you to control access to rows in your database based on the user who is querying the data."},{"_id":"/docs/v2/guides/stripe-subscriptions","structured":{"contents":[{"content":"This guide will walk you through how Stripe has been set up in different files in Nextbase Starter Kit.."},{"heading":"stripe-configuration","content":"Stripe configuration is stored in the `.env` file. You need to provide your Stripe API keys here."},{"heading":"stripe-client","content":"A Stripe client is created in `src/utils/stripe.ts`. This client is used to interact with the Stripe API."},{"heading":"stripe-webhook","content":"A webhook endpoint is set up in `src/pages/api/stripe/webhook.ts` to handle events from Stripe."},{"heading":"stripe-checkout-and-customer-portal","content":"A checkout session is created in `src/data/user/organizations.ts`."},{"heading":"stripe-subscriptions","content":"Subscriptions are managed in `src/utils/supabase-admin.ts`. Subscriptions are at the organization level.\nWhen a subscription is created, the `manageSubscriptionStatusChange` function is called. This function creates a new subscription record in the database."},{"heading":"organization-admins","content":"Organization admins can manage subscriptions in `/organization/[organizationId]/settings/billing` route.\nAll members can see the current subscription status in `/organization/[organizationId]/settings/billing` route. However,\nonly organization admins can manage subscriptions."}],"headings":[{"id":"stripe-configuration","content":"Stripe Configuration"},{"id":"stripe-client","content":"Stripe Client"},{"id":"stripe-webhook","content":"Stripe Webhook"},{"id":"stripe-checkout-and-customer-portal","content":"Stripe Checkout and Customer Portal"},{"id":"stripe-subscriptions","content":"Stripe Subscriptions"},{"id":"organization-admins","content":"Organization Admins"}]},"url":"/docs/v2/guides/stripe-subscriptions","title":"Stripe Setup in Nextbase Ultimate","description":"This guide walks you through how Stripe has been set up in different files in Nextbase Ultimate."},{"_id":"/docs/v2/guides/writing-integration-tests","structured":{"contents":[{"content":"All versions of Nextbase Starter Kit come with Playwright installed. Playwright is a powerful end-to-end testing framework that allows you to write integration tests for your application."},{"heading":"configuration","content":"Playwright is configured in the `playwright.config.js` file."},{"heading":"configuration","content":"As you can see, the configuration file is quite simple. The playwright projects are configured in this manner:"},{"heading":"configuration","content":"Projects which setup authentication are configured in the `with-auth` project."},{"heading":"configuration","content":"Projects which test logged in users are configured in the `Logged In Users (Desktop Chrome)` project."},{"heading":"configuration","content":"Projects which test logged out users are configured in the `Logged Out Users (Desktop Chrome)` project."},{"heading":"configuration","content":"The `Logged In Users (Desktop Chrome)` project is configured to use the `with-auth` project as a dependency. This means that the `with-auth` project will run first, and then the `Logged In Users (Desktop Chrome)` project will run.\nHowever, the tests themselves run in parallel in each project."},{"heading":"running-tests","content":"Playwright runs in `NODE_ENV=test` mode. It also relies on `.env.test` for environment variables.\nNow, .env.test is configured to use **local supabase** which is setup by running `pnpm supabase start` command.\nThe `pnpm supabase start` command sets up a local supabase instance using docker."},{"heading":"running-tests","content":"The supabase api url is available at &#x2A;*http\\://localhost:54321/** (This is the most important url)"},{"heading":"running-tests","content":"The supabase db url is at &#x2A;*http\\://localhost:54322/**"},{"heading":"running-tests","content":"The supabase studio is available at &#x2A;*http\\://localhost:54323/** (You can use this to use the UI interface )"},{"heading":"running-tests","content":"The supabase inbucket instance is available at &#x2A;*http\\://localhost:54324/** (We use this for testing emails)"},{"heading":"running-tests","content":"Now that supabase is running locally. All we need to do is to run playwright."},{"heading":"running-playwright-in-headless-mode","content":"**Since playwright has been configured to start the local dev server, you don't need to run `pnpm dev` separately.**"},{"heading":"writing-tests","content":"All the test cases are written in the `e2e` folder. The `e2e` folder is divided into two folders:"},{"heading":"writing-tests","content":"Tests which require a user logged in need to be named `test-case.user.spec.ts`"},{"heading":"writing-tests","content":"Tests which don't require a user logged in need to be named `test-case.spec.ts`"},{"heading":"create-organization-test-case-example","content":"This is an example of a test case which requires a user to be logged in."}],"headings":[{"id":"configuration","content":"Configuration"},{"id":"running-tests","content":"Running tests"},{"id":"running-playwright-with-ui","content":"Running playwright with UI"},{"id":"running-playwright-in-headless-mode","content":"Running playwright in headless mode"},{"id":"writing-tests","content":"Writing tests"},{"id":"create-organization-test-case-example","content":"Create Organization Test case example"}]},"url":"/docs/v2/guides/writing-integration-tests","title":"Writing Integration Tests with Playwright","description":"Learn how to write integration tests using Playwright, a powerful end-to-end testing framework"},{"_id":"/docs/v2/guides/writing-unit-tests","structured":{"contents":[{"content":"All versions of Nextbase Starter Kit come with Vitest installed. Vitest is a lightweight and fast testing framework that allows you to write unit tests for your application."},{"heading":"configuration","content":"Vitest is configured in the `vitest.config.ts` file."},{"heading":"configuration","content":"As you can see, the configuration file is quite simple. You can customize it according to your needs."},{"heading":"running-tests","content":"Vitest runs in `NODE_ENV=test` mode. It also relies on `.env.test` for environment variables."},{"heading":"writing-tests","content":"All the test cases are written in the `tests` folder."},{"heading":"helper-function-test-case-example","content":"This is an example of a test case for a helper function."},{"heading":"using-vitest-with-github-actions","content":"You can easily integrate Vitest with GitHub Actions to run your tests automatically on every push or pull request. Here is a basic example of a GitHub Actions workflow:"},{"heading":"using-vitest-with-github-actions","content":"Most SaaS solo developers or small teams write integration tests more, hence we only added Vitest setup and a dummy test case, but you can update the test suite as needed."}],"headings":[{"id":"configuration","content":"Configuration"},{"id":"running-tests","content":"Running tests"},{"id":"running-vitest","content":"Running Vitest"},{"id":"writing-tests","content":"Writing tests"},{"id":"helper-function-test-case-example","content":"Helper Function Test case example"},{"id":"using-vitest-with-github-actions","content":"Using Vitest with GitHub Actions"}]},"url":"/docs/v2/guides/writing-unit-tests","title":"Writing Unit Tests with Vitest","description":"Learn how to write unit tests using Vitest, a lightweight and fast testing framework"},{"_id":"/docs/v2","structured":{"contents":[{"heading":"welcome-to-nextbase","content":"Thanks for purchasing NextBase. NextBase is the fastest way to start your SaaS project. It comes with incredible set of features pre-built for you. It is built on top of Supabase and NextJS. It is a fullstack SaaS starter kit. Here is the complete stack:"},{"heading":"welcome-to-nextbase","content":"NextJS 13 with App Directory"},{"heading":"welcome-to-nextbase","content":"Supabase"},{"heading":"welcome-to-nextbase","content":"React Server Components"},{"heading":"welcome-to-nextbase","content":"Tailwind CSS"},{"heading":"welcome-to-nextbase","content":"Typescript (with strict mode)"},{"heading":"welcome-to-nextbase","content":"Zod (for validation)"},{"heading":"welcome-to-nextbase","content":"React Query"},{"heading":"welcome-to-nextbase","content":"React Hook Form"},{"heading":"welcome-to-nextbase","content":"React Hot Toast"},{"heading":"welcome-to-nextbase","content":"Lucide Icons"},{"heading":"welcome-to-nextbase","content":"Resend and React email (for emails)"},{"heading":"welcome-to-nextbase","content":"Sentry"},{"heading":"welcome-to-nextbase","content":"Posthog (for analytics)"},{"heading":"welcome-to-nextbase","content":"Google Analytics (for analytics)"},{"heading":"feature-list","content":"NextBase is a full-stack SaaS starter kit built on top of Supabase and NextJS. It comes with an incredible set of pre-built features that can help you kickstart your SaaS project. The features are categorized into different tiers - Essential, Pro, Ultimate, and Custom. Here's a detailed breakdown of the features available in each tier:"},{"heading":"all-variants","content":"For a detailed breakdown of all the variants, please check out our Pricing Page."}],"headings":[{"id":"welcome-to-nextbase","content":"Welcome to NextBase"},{"id":"feature-list","content":"Feature list"},{"id":"all-variants","content":"All Variants"}]},"url":"/docs/v2","title":"Welcome to Nextbase v3!"},{"_id":"/docs/v2/installation/create-stripe-products","structured":{"contents":[{"content":"With the dev server running, go to the stripe developers -> webhooks section. The steps are the same as in the previous section. However instead of listening to the events locally and triggering fake events, we will create the products directly in Stripe."},{"content":"Run a service like ngrok to expose your localhost to the internet. Grab the URL. (e.g. `https://12345678.ngrok.io`)"},{"content":"Create a webhook endpoint and set the url to your ngrok url. (e.g. `https://12345678.ngrok.io/api/stripe/webhooks`). Select all events."},{"content":"Make sure that \\` All events \\` are forwared to our webhook listener. (Select the checkbox suggesting to select all events)."},{"heading":"start-creating-products","content":"Now create a couple of `Products` in Stripe, also make sure to create `prices` for each of the products."},{"heading":"start-creating-products","content":"Our database will automatically stay in sync with Stripe and create the necessary rows in the database."}],"headings":[{"id":"start-creating-products","content":"Start creating products"}]},"url":"/docs/v2/installation/create-stripe-products","title":"Create stripe products and Ngrok","description":"Create stripe products in the database. Our database will automatically stay in sync with Stripe and create the necessary rows in the database."},{"_id":"/docs/v2/installation/dev-environment","structured":{"contents":[{"heading":"setup-env-variables","content":"In your repository directory, you should have a .env.local.example file which looks like this. Create a `.env.local` file right e it but instead of dummy values like `YOUR_SUPABASE_PROJECT_REF`, fill the actual details."},{"heading":"run-your-dev-server","content":"We are ready to start our dev server. Run"},{"heading":"run-your-dev-server","content":"`pnpm dev`"},{"heading":"run-your-dev-server","content":"Our local development server is now running at `localhost:3000`."}],"headings":[{"id":"link-your-repository-to-supabase","content":"Link your repository to supabase"},{"id":"setup-env-variables","content":"Setup env variables"},{"id":"run-your-dev-server","content":"Run your dev server"}]},"url":"/docs/v2/installation/dev-environment","title":"Dev environment","description":"Setup your dev environment for the Supabase Stripe Starter Kit. Link your repository to Supabase, setup env variables, and run your dev server."},{"_id":"/docs/v2/installation/first-steps","structured":{"contents":[{"content":"Welcome to the Nextbase repository on GitHub!"},{"heading":"prerequisites","content":"Before you get started, make sure you have the following:"},{"heading":"prerequisites","content":"Git installed on your system. If you don't already have it, you can download it from the official Git website (https\\://git-scm.com/)."},{"heading":"prerequisites","content":"A terminal or command prompt. This will be used to run commands and navigate directories."},{"heading":"prerequisites","content":"Node.js installed on your system. If you don't already have it, you can download it from the official Node.js website (https\\://nodejs.org/)."},{"heading":"cloning-the-repository","content":"To get started with the Nextbase project, you will need to clone the repository onto your local machine. Here's how to do it:"},{"heading":"cloning-the-repository","content":"Open a terminal or command prompt and navigate to the directory where you want to store the Nextbase repository."},{"heading":"cloning-the-repository","content":"Run the following command to clone the repository:"},{"heading":"cloning-the-repository","content":"This will create a new directory called \"nextbase\" in your current location, and clone the contents of the repository into it."},{"heading":"cloning-the-repository","content":"Navigate into the repository by running the following command:"},{"heading":"cloning-the-repository","content":"You are now ready to start working with the Nextbase project! To update the repository with the latest changes, run the following command:"},{"heading":"cloning-the-repository","content":"That's it! You should now have a local copy of the Nextbase repository on your machine."}],"headings":[{"id":"prerequisites","content":"Prerequisites"},{"id":"cloning-the-repository","content":"Cloning the Repository"}]},"url":"/docs/v2/installation/first-steps","title":"First Steps","description":"Welcome to the Nextbase repository on GitHub!"},{"_id":"/docs/v2/installation/login","structured":{"contents":[{"heading":"sign-up-using-email","content":"Go to `http://localhost:3000/login` . If you are logging in for the first time, you can sign up and setup your email login credentials."},{"heading":"sign-up-using-email","content":"Once your account is created in supabase local studio, you will receive an email in inbucket, which you can access at http\\://localhost:54324/monitor\\`. Click on the link and login."},{"heading":"sign-up-using-email","content":"Confirm your email and you will be taken to the dashboard! 🎉"},{"heading":"sign-up-using-email-and-password","content":"Go to `http://localhost:3000/login` . For the first time, you can sign up and setup your email and password login credentials."},{"heading":"sign-up-using-email-and-password","content":"Supabase auto-confirms the credentials and does not send any confirmation email."},{"heading":"sign-up-using-email-and-password","content":"You can login now using the same credentials and you will be taken to the dashboard! 🎉"}],"headings":[{"id":"sign-up-using-email","content":"Sign up using email"},{"id":"sign-up-using-email-and-password","content":"Sign up using email and password"}]},"url":"/docs/v2/installation/login","title":"Login to app in development","description":"Login to app in development using email and password."},{"_id":"/docs/v2/installation/setting-up-stripe","structured":{"contents":[{"heading":"create-a-stripe-account","content":"Go to the Stripe website (https\\://stripe.com/) and click on the \"Sign Up\" button in the top right corner.\nFollow the prompts to create a new account. You will need to provide your email address and choose a password.\nOnce you have created your account, log in and complete the onboarding process. You don't need to setup everything while you are setting up."},{"heading":"enter-test-mode","content":"By default your account will be in `Test Mode`. This means you can test your integration without processing real payments. You can switch to `Live Mode` when you are ready to start processing real payments.\nIf you are not in `Test Mode`, you can switch to it by clicking on the `Test Mode` toggle in the top right part of your dashboard."},{"heading":"get-your-api-keys","content":"Once you have setup stripe, head to the Developers section and in the Keys section, grab the Secret key and Publishable key from there. Save them to a secure location. You will need them."},{"heading":"get-your-api-keys","content":"So as of now you have two keys."},{"heading":"setup-webhooks-for-local-testing","content":"Webhooks are a way for you to receive real-time updates from Stripe. You can use webhooks to receive notifications when a payment is made, when a customer is created, and more. This is important for your Supabase project to know when a payment is made so that you can update your database accordingly.\nYou can use webhooks to also track `product` and `price` updates in your app. It has already been configured in Nextbase.\nIn Nextbase, we have configured a webhook endpoint at `/api/stripe/webhooks`. This is the endpoint that Stripe will send the events to. But for Stripe to send events to your local machine, you need to expose your local server to the internet. You can use a tool like ngrok to do this, which can be more flexible, but you can also use Stripe CLI to trigger fixture events and test your webhook locally."},{"heading":"setting-up-using-stripe-cli","content":"Install the Stripe CLI from here."},{"heading":"setting-up-using-stripe-cli","content":"Login to your Stripe account using the CLI."},{"heading":"setting-up-using-stripe-cli","content":"This will ask you to open a link in your browser. Once you have logged in, you can close the browser and return to the terminal."},{"heading":"setting-up-using-stripe-cli","content":"Forward the webhook events to your local server."},{"heading":"setting-up-using-stripe-cli","content":"This will give you a webhook signing secret. Add this to your `.env.local` file."},{"heading":"setting-up-using-stripe-cli","content":"Trigger the following events to create your product and price."},{"heading":"setting-up-using-stripe-cli","content":"Confirm that you see the events in your terminal and the products and prices are created in supabase. Here is how they might look like in your supabase studio. You can visit `http://localhost:54323` and look at your table editor to confirm."},{"heading":"setting-up-using-stripe-cli","content":"Now as your dev server is already running. You can go your app and head over to the organization billing page and you can see the created product with its price there."},{"heading":"setting-up-using-stripe-cli","content":"Test out the `Free Trial` option and the `Choose` option to see if the payment is successful. Use test payment methods and test card numbers from here."}],"headings":[{"id":"create-a-stripe-account","content":"Create a Stripe account"},{"id":"enter-test-mode","content":"Enter Test Mode"},{"id":"get-your-api-keys","content":"Get your API keys"},{"id":"setup-webhooks-for-local-testing","content":"Setup Webhooks for local testing"},{"id":"setting-up-using-stripe-cli","content":"Setting up using Stripe CLI"}]},"url":"/docs/v2/installation/setting-up-stripe","title":"Setting up Stripe","description":"Setting up Stripe for your Supabase project. Learn how to create products, webhooks, and more."},{"_id":"/docs/v2/installation/setting-up-supabase-locally","structured":{"contents":[{"content":"To get started with Supabase locally, you will just need to pull the docker image. Here's how to do it:"},{"heading":"setting-up-docker-for-supabase","content":"First, you will need to install Docker on your machine. You can download the installer from the Docker website since it is the recommended way to install Docker on Windows and macOS. If you are using Linux, you can follow the instructions for your specific distribution on the Docker documentation."},{"heading":"setting-up-docker-for-supabase","content":"Once you have Docker installed and running in your machine, you can now pull the Supabase Docker image by running `pnpm supabase start` in your terminal."},{"heading":"setting-up-docker-for-supabase","content":"> Please note that for the first time you run the `pnpm supabase start` command, it will take some time to pull the Docker image. Subsequent runs will be faster since the images will be cached and volumes will be persisted."},{"heading":"setting-up-docker-for-supabase","content":"After running the command, you should see the output simmilar to the following:"},{"heading":"setting-up-docker-for-supabase","content":"You can now access the Supabase Studio by visiting the URL `http://localhost:54323` in your browser."},{"heading":"setting-up-docker-for-supabase","content":"You can also access the API and GraphQL URLs to interact with your local Supabase instance."},{"heading":"setting-up-docker-for-supabase","content":"To connect database clients to your local Supabase instance, you can use the DB URL `postgresql://postgres:postgres@localhost:54322/postgres`"},{"heading":"setting-up-docker-for-supabase","content":"To monitor your inboxes, you can use the Inbucket URL `http://localhost:54324`"},{"heading":"setting-up-docker-for-supabase","content":"You can also use the JWT secret, anon key, and service role key for local development."},{"heading":"setting-up-docker-for-supabase","content":"You can stop the Supabase server by running the `pnpm supabase stop` command, to save on system resources."},{"heading":"setting-up-envlocal-for-supabase","content":"To connect your Next.js application to your local Supabase instance, you will need to create a `.env.local` file in the root of your project and add the following environment variables:"}],"headings":[{"id":"setting-up-docker-for-supabase","content":"Setting up Docker for Supabase"},{"id":"setting-up-envlocal-for-supabase","content":"Setting up .env.local for Supabase"}]},"url":"/docs/v2/installation/setting-up-supabase-locally","title":"Setting up Supabase Locally","description":"Learn how to set up Supabase locally for development. This guide will show you how to pull the Docker image and set up the environment variables for your Next.js application."},{"_id":"/docs/v2/installation/supabase-for-production","structured":{"contents":[{"content":"To get started with Supabase, you will need to set up an account and create a project. Here's how to do it:"},{"heading":"creating-an-account","content":"Go to the Supabase website (https\\://supabase.com/) and click on the \"Sign Up\" button in the top right corner."},{"heading":"creating-an-account","content":"Follow the prompts to create a new account. You will need to provide your email address and choose a password."},{"heading":"creating-a-project","content":"Once you have created your account, log in and click on the \"Create Project\" button in the dashboard."},{"heading":"creating-a-project","content":"Give your project a name and choose a region for your data to be stored in. You will also need to create a database password. Make sure to save this password in a secure location, as you will need it later to access your database. Then click on the \"Create Project\" button to finish creating your project."},{"heading":"accessing-your-project","content":"You should now see your new project listed in the dashboard. Click on the project name to open it and start working with Supabase."},{"heading":"auth-configuration","content":"Go to the \"Authentication\" section of your project usually at `https://app.supabase.io/project/<project-id>/auth`."},{"heading":"auth-configuration","content":"Go to the URL Configuration tab."},{"heading":"auth-configuration","content":"Set the Site URL to `http://localhost:3000` or it can also be your production domain e.g. `https://myapp.com`. (This is not as important as the Redirect URL configuration)"},{"heading":"auth-configuration","content":"Set Redirect URLs to\na. `http://localhost:3000/auth/callback` (for local development) or `http://localhost:3000/**` (this means all routes under `http://localhost:3000` will be accepted as a valid redirect URL)\nb. `https://myapp.com/auth/callback` (for production) or `https://myapp.com/**` (this means all routes under `https://myapp.com` will be accepted as a valid redirect URL)"},{"heading":"install-dependencies-and-link-your-project","content":"run `pnpm install`"},{"heading":"install-dependencies-and-link-your-project","content":"Supabase login within your terminal"},{"heading":"install-dependencies-and-link-your-project","content":"Note: If you haven't used supabase before in the terminal, you will need to login with supabase in the terminal."},{"heading":"install-dependencies-and-link-your-project","content":"Create an access token for supabase here https\\://app.supabase.com/account/tokens"},{"heading":"install-dependencies-and-link-your-project","content":"Copy the access token and run `pnpm supabase login`"},{"heading":"install-dependencies-and-link-your-project","content":"It will prompt for an access token. Paste the token there. It should log you in."},{"heading":"install-dependencies-and-link-your-project","content":"Run `pnpm supabase link --project-ref {projectRef}` . Your project ref is available in the project settings section within the supabase dashboard."},{"heading":"install-dependencies-and-link-your-project","content":"Once you run the command, you will prompted for the database password. Enter the password, you saved earlier."},{"heading":"install-dependencies-and-link-your-project","content":"When it completes, your project will be linked to supabase."},{"heading":"install-dependencies-and-link-your-project","content":"Run `pnpm supabase db push` . This will push all the nextbase magic into your own supabase project. Teams, admin panel, projects etc will now be ready for use."},{"heading":"setup-env-variables-for-production","content":"In your repository directory, you should have a .env.local.example file which looks like this. Create a `.env.local` file right beside it but instead of dummy values like `YOUR_SUPABASE_PROJECT_REF`, fill the actual details."},{"heading":"setup-env-variables-for-production","content":"**NEXT\\_PUBLIC\\_SUPABASE\\_URL** is available in the project api settings section in supabase. It's called Project URL over there."},{"heading":"setup-env-variables-for-production","content":"**NEXT\\_PUBLIC\\_SUPABASE\\_PUBLISHABLE\\_KEY** and **SUPABASE\\_SECRET\\_KEY** are also available in the Project API settings section in supabase."},{"heading":"setup-env-variables-for-production","content":"Setup a Sendgrid / Post mark account and once you do, grab your authorised email sender address and add it to **ADMIN\\_EMAIL**. Same goes for **SENDGRID\\_API\\_KEY**. (If you want to use a different email provider, create a different environment variable here and set the relevant value)."},{"heading":"setup-env-variables-for-production","content":"Ensure that **NEXT\\_PUBLIC\\_SITE\\_URL** points to your domain."},{"heading":"setup-env-variables-for-production","content":"The other env variables should be relatively easy to setup."}],"headings":[{"id":"creating-an-account","content":"Creating an Account"},{"id":"creating-a-project","content":"Creating a Project"},{"id":"accessing-your-project","content":"Accessing Your Project"},{"id":"auth-configuration","content":"Auth configuration"},{"id":"install-dependencies-and-link-your-project","content":"Install dependencies and link your project"},{"id":"setup-env-variables-for-production","content":"Setup env variables for production"}]},"url":"/docs/v2/installation/supabase-for-production","title":"Setting up Supabase for Production","description":"Learn how to set up Supabase for production. This guide will walk you through creating an account, creating a project, and setting up environment variables for production."},{"_id":"/docs/v2/integrations/api-key-authentication-with-unkey","structured":{"contents":[{"content":"Nextbase Ultimate comes with unkey configured."},{"content":"When using the latest Supabase with asymmetric keys, the JWT signing approach has changed. New\ndocs are on the way, but please view this GitHub\ncomment in the meantime."},{"heading":"why-use-unkey","content":"Unkey is a service that allows you to create API keys for your users."},{"heading":"why-use-unkey","content":"It allows you to create API keys for your users without having to store them in your database."},{"heading":"why-use-unkey","content":"It allows you to revoke API keys for your users without having to update your database."},{"heading":"why-use-unkey","content":"Keys can be created for limited time periods too."},{"heading":"why-use-unkey","content":"The keys can then be used to authenticate your users to your API endpoints."},{"heading":"why-use-unkey","content":"Usage with Zapier, Make and Integromat are also now easy."},{"heading":"configuring-unkey","content":"Necessary env variables that you need for Unkey after creating an account there"},{"heading":"configuring-unkey","content":"1 UNKEY\\_ROOT\\_KEY:"},{"heading":"configuring-unkey","content":"This variable likely represents the root key associated with Unkey.\nIt may be used for specific administrative or configuration purposes within Unkey."},{"heading":"configuring-unkey","content":"2 UNKEY\\_API\\_ID:"},{"heading":"configuring-unkey","content":"This variable seems to be associated with an API ID in the context of Unkey.\nIt may serve as an identifier for your Unkey API integration."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Import Unkey Middleware\nImport the `withUnkeyAuth` middleware from the Unkey package at the beginning of your route handler file."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Implement the Route Handler\nCreate your route handler logic, ensuring that it includes the withUnkeyAuth middleware. This middleware handles the API key authentication process."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Now, this route handler is protected, and only requests with valid API keys will be allowed to access it."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"By following these steps, you can configure Unkey and create route handlers with API key authentication in your Nextbase project, ensuring secure and authenticated access to your protected routes."},{"heading":"disabling-unkey","content":"To disable Unkey, you can either remove its middleware from your route handlers or adjust your configuration to bypass Unkey authentication."},{"heading":"removing-unkey-middleware","content":"In your route handler files, look for the import statement that includes Unkey middleware and remove or comment out the line."},{"heading":"exposing-routes-which-can-be-used-with-api-key-auth","content":"To expose routes with API key authentication, follow these steps:"},{"heading":"1-import-unkey-middleware","content":"In your route handler files, import the Unkey middleware (`withUnkeyAuth`) at the beginning of the file. This middleware will handle the API key authentication process."},{"heading":"2-use-unkey-middleware-in-route-handlers","content":"Apply the Unkey middleware to the routes you want to expose with API key authentication. Wrap your route handler logic with withUnkeyAuth:"},{"heading":"2-use-unkey-middleware-in-route-handlers","content":"By applying withUnkeyAuth to a route handler, you ensure that the route is protected and requires a valid API key for access."},{"heading":"bonus-usage-with-zapier","content":"Since API key auth is now built in to Nextbase, configuring Zapier, Make or Integromat is now easy."},{"heading":"bonus-usage-with-zapier","content":"Zapier guide coming soon."},{"heading":"bonus-usage-with-zapier","content":"TODO"}],"headings":[{"id":"why-use-unkey","content":"Why use Unkey?"},{"id":"configuring-unkey","content":"Configuring unkey"},{"id":"steps-for-creating-unkey-root-key","content":"Steps for creating Unkey Root key"},{"id":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Creating an route handler which can be used with api key auth"},{"id":"disabling-unkey","content":"Disabling unkey"},{"id":"removing-unkey-middleware","content":"Removing Unkey Middleware"},{"id":"exposing-routes-which-can-be-used-with-api-key-auth","content":"Exposing routes which can be used with API key auth"},{"id":"1-import-unkey-middleware","content":"1\\. Import Unkey Middleware"},{"id":"2-use-unkey-middleware-in-route-handlers","content":"2\\. Use Unkey Middleware in Route Handlers"},{"id":"bonus-usage-with-zapier","content":"(Bonus): Usage with Zapier"}]},"url":"/docs/v2/integrations/api-key-authentication-with-unkey","title":"API Key Authentication with Unkey","description":"Learn how to use Unkey for API key authentication in Nextbase Ultimate and how to expose routes with API key auth."},{"_id":"/docs/v2/integrations/resend-and-react-email-integration","structured":{"contents":[{"heading":"resend-and-react-email-integration","content":"Nextbase comes with Resend and React Email configured. This makes creating/modifying email templates and sending them to your customers very easy."},{"heading":"resendcom","content":"Resend serves as the backbone for handling email delivery in Nextbase.\nIt streamlines the process of sending emails, managing templates, and handling various aspects of email communication.\nWith Resend, you can focus on creating compelling content, knowing that the delivery mechanism is robust and reliable."},{"heading":"resendcom","content":"**You can use a different email service provider if you want to.**"},{"heading":"how-to-use-resend-in-nextjs","content":"Resend is usually configured like this in Next.js."},{"heading":"how-to-use-resend-in-nextjs","content":"Install resend as a dependency and then import it into your application."},{"heading":"how-to-use-resend-in-nextjs","content":"Create an account in Resend and get your API key."},{"heading":"how-to-use-resend-in-nextjs","content":"Add your API key to your environment variables."},{"heading":"how-to-use-resend-in-nextjs","content":"Then, you can use it like this."},{"heading":"react-email-declarative-email-templating","content":"`react-email` is a React library that allows you to compose email templates using familiar React components. Instead of dealing with complex HTML templates, you can leverage the power of React to create modular and dynamic email content. This integration aligns with the React philosophy, making it intuitive for developers already familiar with React development.\nNextbase provides a seamless integration of Resend and `react-email` so that you can harness the benefits of both tools effortlessly."},{"heading":"emails-folder","content":"Nextbase comes with the `emails` folder configured as the repository for your React Email components. You can write your emails here and also preview them with the email development server by running `pnpm email-dev`."},{"heading":"react-email-development-server","content":"In Nextbase, a React Email development server is set up to streamline the process of writing and previewing emails. This development server allows developers to interactively design and visualize emails in a browser, facilitating a more efficient email template creation workflow."},{"heading":"react-email-development-server","content":"**Package.json Script**"},{"heading":"react-email-development-server","content":"The React Email development server is typically configured as a script in the package.json file. The script is named \"email:dev\":"},{"heading":"react-email-development-server","content":"To start the React Email development server, developers can run the following command in the terminal:"},{"heading":"react-email-development-server","content":"This starts the React Email development server and provides a convenient way to preview email templates."},{"heading":"react-email-development-server","content":"**/src/emails:**"},{"heading":"react-email-development-server","content":"Each file in the `emails` folder corresponds to a specific type of email, and it contains a React component responsible for rendering the content of that email.\nWe can then send the emails using `Resend`. Here are some emails we already have in the `emails` folder:"},{"heading":"welcomeemailtsx","content":"In this example, the WelcomeEmail component represents the content of a welcome email. This declarative approach simplifies the process of creating and maintaining various email templates in your application."},{"heading":"invitationtojointsx","content":"This is the organization invitation email template. When a user is invited to join an organization, this email is sent to them."},{"heading":"invitationtojointsx","content":"The email is then sent like this."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"It is recommended to use a third-party SMTP service for production environments in Supabase."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"Reliability: Supabase built-in SMTP server is severely rate limited. It is only suitable for development and testing purposes. Supabase recommends using a third-party SMTP service for production environments. Resend provides a robust and reliable email delivery mechanism."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"Observability: Resend provides a dashboard to monitor email delivery and other metrics."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"In a production environment, it's crucial to ensure reliable email delivery. Ensure that Resend or another third-party SMTP service is configured in your Supabase project."},{"heading":"react-email-and-resend-integration-in-nextbase","content":"See `src/utils/api-routes/utils.ts` for an example of how to we use Resend in Nextbase.\nHere the `sendEmail` function is used to send emails using Resend. It receives the email options as an argument and sends the email using Resend."},{"heading":"configuration","content":"You only require the API key to configure Resend. You can find the API key in the Resend dashboard."}],"headings":[{"id":"resend-and-react-email-integration","content":"Resend and React Email Integration"},{"id":"resendcom","content":"Resend.com"},{"id":"how-to-use-resend-in-nextjs","content":"How to use Resend in Next.js?"},{"id":"react-email-declarative-email-templating","content":"React-email: Declarative Email Templating"},{"id":"emails-folder","content":"`emails` folder"},{"id":"react-email-development-server","content":"React Email Development Server"},{"id":"welcomeemailtsx","content":"WelcomeEmail.tsx:"},{"id":"invitationtojointsx","content":"InvitationToJoin.tsx"},{"id":"optional-configure-resend-with-supabase-smtp-server","content":"(Optional) Configure Resend with Supabase SMTP Server"},{"id":"react-email-and-resend-integration-in-nextbase","content":"React Email and Resend Integration in Nextbase"},{"id":"configuration","content":"Configuration"}]},"url":"/docs/v2/integrations/resend-and-react-email-integration","title":"Resend and React Email Integration","description":"Learn how to use Resend and React Email in Nextbase."},{"_id":"/docs/v2/payments/stripe-configuration","structured":{"contents":[{"content":"Stripe is implemented using an Abstract payment provider. It has the following shape."}],"headings":[]},"url":"/docs/v2/payments/stripe-configuration","title":"Stripe Configuration"},{"_id":"/docs/v2/payments/stripe-webhooks","structured":{"contents":[{"content":"Stripe webhooks are crucial for keeping your application in sync with events that happen in Stripe, such as successful payments, subscription updates, or failed charges. In NextBase, we've set up a webhook endpoint to handle these events. Let's break down how it's configured:"},{"heading":"webhook-endpoint","content":"The webhook endpoint is set up in `src/app/api/stripe/webhooks/route.ts`. This file contains a POST route handler that processes incoming webhook events from Stripe."},{"heading":"webhook-endpoint","content":"The webhook syncs products, prices, and subscriptions with our database."}],"headings":[{"id":"webhook-endpoint","content":"Webhook Endpoint"}]},"url":"/docs/v2/payments/stripe-webhooks","title":"Stripe Webhooks"},{"_id":"/docs/v2/performance/strategies-for-pages","structured":{"contents":[{"heading":"strategies-for-pages","content":"In this Next.js 14 codebase, we employ several strategies to enhance page performance using async/await and Suspense. These techniques are crucial for optimizing data fetching and rendering, ensuring a smooth user experience."},{"heading":"strategies-for-pages","content":"Asynchronous Data Fetching in Server Components"},{"heading":"strategies-for-pages","content":"Server components in Next.js can execute asynchronously, allowing us to fetch data on the server before rendering the component. This approach reduces bundle sizes and leverages server CPU power for faster data fetching."},{"heading":"strategies-for-pages","content":"For example, in the roadmap page, we asynchronously fetch data for different roadmap sections like planned, in progress, and completed tasks using async functions before rendering the components."},{"heading":"strategies-for-pages","content":"Parallel Data Fetching with Suspense"},{"heading":"strategies-for-pages","content":"We use React's Suspense component to fetch multiple data sources in parallel and render child components as soon as the data is available. This technique allows the page to start rendering without waiting for all data to be fetched, improving perceived performance."},{"heading":"strategies-for-pages","content":"For instance, the blog list page fetches blog posts and tags in parallel, wrapped in Suspense, enhancing the user experience by streaming rendering."},{"heading":"strategies-for-pages","content":"Server components have the added benefit of streaming rendering. Consider the page responsible for rendering the blog list page at `/blog`. This page has to fetch two sets of data: the list of blog posts and the list of tags. We can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components: `Tags` and `BlogList`."},{"heading":"strategies-for-pages","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"strategies-for-pages","content":"Suspense for Incremental Rendering"},{"heading":"strategies-for-pages","content":"We also use Suspense to incrementally render parts of the page as data becomes available. This approach is evident in the changelog page, where the changelog list is rendered asynchronously and wrapped in Suspense to display a loading state until the data is fetched."}],"headings":[{"id":"strategies-for-pages","content":"Strategies for pages"}]},"url":"/docs/v2/performance/strategies-for-pages","title":"Performance Strategies"},{"_id":"/docs/v2/performance/streaming-suspense-rendering","structured":{"contents":[{"heading":"streaming-server-rendering","content":"In our Next.js 14 pages, we leverage streaming server rendering primarily through the use of server components and the Suspense component for data fetching and rendering optimization. This approach allows us to start rendering the page even before all the data is fetched, improving the user experience by displaying content as soon as it's available."},{"heading":"example-of-streaming-server-rendering","content":"In the blog list page, we demonstrate streaming server rendering by fetching and rendering two sets of data in parallel: the list of blog posts and the list of tags. This is facilitated by moving the data fetching into two separate components, Tags and BlogList, which are server components. By wrapping these components in Suspense, we can render them in parallel and stream the rendering of the page as soon as the data is available, without waiting for all data to be fetched."},{"heading":"example-of-streaming-server-rendering","content":"Server components have the added benefit of streaming rendering.\nConsider the page responsible for rendering the blog list page at **/blog** route.\nThis page has to fetch two sets of data: the list of blog posts and the list of tags.\nWe can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components:"},{"heading":"example-of-streaming-server-rendering","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"benefits","content":"Improved User Experience: The page starts rendering immediately, displaying the \"All blog posts\" title and placeholders for tags and posts until the actual data is fetched. This approach significantly enhances the perceived performance of the application."},{"heading":"benefits","content":"Efficient Data Fetching: By fetching data in server components, we leverage the server's CPU power, leading to faster data retrieval and smaller bundle sizes since we don't have to ship the data fetching logic to the client.\nImplementation"},{"heading":"benefits","content":"The implementation of streaming server rendering in our Next.js 14 pages is a testament to the framework's capabilities in optimizing performance and enhancing the user experience. By judiciously using server components and Suspense, we ensure that our pages are not only fast but also provide immediate feedback to users, which is crucial for modern web applications."}],"headings":[{"id":"streaming-server-rendering","content":"Streaming server rendering"},{"id":"example-of-streaming-server-rendering","content":"Example of Streaming Server Rendering"},{"id":"benefits","content":"Benefits"}]},"url":"/docs/v2/performance/streaming-suspense-rendering","title":"Streaming Suspense Server Rendering"},{"_id":"/docs/v2/quick-setup","structured":{"contents":[{"heading":"super-quick-dev-setup","content":"This guide covers the basic setup for Supabase and Next.js."},{"heading":"super-quick-dev-setup","content":"For integrating Stripe, refer to the comprehensive setup guide."},{"heading":"1-clone-the-repository-and-install-dependencies","content":"This command installs all necessary dependencies."},{"heading":"2-copy-envlocalexample","content":"Duplicate the `.env.local.example` file at the project root as `.env.local`."},{"heading":"3-launch-supabase-locally","content":"**Note:** Ensure Docker is installed on your system. Visit Docker's documentation for installation details."},{"heading":"4-start-the-app-by-running-the-following-command","content":"You're all set! The app should now be accessible at http\\://localhost:3000.\nStripe and pricing aren't configured yet, but you can start building your app with Supabase and Next.js."},{"heading":"optional-setting-up-stripe-locally","content":"To set up Stripe locally, refer to the comprehensive setup guide."}],"headings":[{"id":"super-quick-dev-setup","content":"Super quick dev setup"},{"id":"1-clone-the-repository-and-install-dependencies","content":"1\\. Clone the repository and install dependencies:"},{"id":"2-copy-envlocalexample","content":"2\\. Copy .env.local.example"},{"id":"3-launch-supabase-locally","content":"3\\. Launch Supabase locally:"},{"id":"4-start-the-app-by-running-the-following-command","content":"4\\. Start the App by running the following command:"},{"id":"optional-setting-up-stripe-locally","content":"(Optional) Setting up Stripe locally"}]},"url":"/docs/v2/quick-setup","title":"Quick setup","description":"Setting up nextbase is easy. Follow these steps to get started."},{"_id":"/docs/v2/security/access-control-on-supabase-functions","structured":{"contents":[{"content":"Supabase functions can be accessed by anyone, but we can control access to them using Supabase RBAC."}],"headings":[]},"url":"/docs/v2/security/access-control-on-supabase-functions","title":"Access control on Supabase functions","description":"How to control access to your Supabase functions."},{"_id":"/docs/v2/security/protected-nextjs-areas","structured":{"contents":[{"heading":"protected-nextjs-pages-and-api-routes","content":"The `app_admin` folder is only accessible to application admins. Hence all pages which start with the route `app_admin` are only accessible to app admins."},{"heading":"protected-nextjs-pages-and-api-routes","content":"The `dashboard` route , `setting` routes, `organization` routes are all only accessible to logged in users."},{"heading":"protected-nextjs-pages-and-api-routes","content":"Other pages are public."},{"heading":"creating-new-pags","content":"To create pages only accessible to logged in users, create them within the `(authenticated-pages)/(user-pages)` folders. This will make them available to all pages including app\\_admins."},{"heading":"creating-new-pags","content":"To create pages only accessible to app\\_admin users, create them within the `(authenticated-users)/(app_admin)` folder."},{"heading":"creating-new-pags","content":"`(external-pages)` are available to logged out users as well."},{"heading":"creating-new-pags","content":"`(login-pages)` should contain all authentication related pages such as login, signup etc."}],"headings":[{"id":"protected-nextjs-pages-and-api-routes","content":"Protected Next.js pages and api routes"},{"id":"creating-new-pags","content":"Creating new pags"}]},"url":"/docs/v2/security/protected-nextjs-areas","title":"Protected Next.js areas","description":"How to create new pages and routes that are protected."},{"_id":"/docs/v2/security/row-level-security","structured":{"contents":[{"content":"Row Level Security (RLS) is a feature in PostgreSQL that provides the ability to control which users can `SELECT`, `INSERT`, `UPDATE`, or `DELETE` which rows in a table. It adds an additional layer of security in multi-user database environments and is crucial for protecting sensitive data.\nUnderstanding Row Level Security"},{"content":"RLS works by enabling you to define policies that restrict, on a per-user basis, whether certain rows can be returned by a `SELECT` query or modified by an `INSERT`, `UPDATE`, or `DELETE` query. This is particularly useful in multi-tenant environments where you want to ensure that users can only access their own data."},{"content":"In PostgreSQL, RLS is disabled by default for all tables. To enable RLS, you use the `ALTER TABLE` command with the ENABLE ROW LEVEL SECURITY clause. Once RLS is enabled, all users (except the table owner and superusers) are denied access to the table—unless a policy is explicitly added to grant them access."},{"heading":"importance-of-row-level-security","content":"RLS is important for a variety of reasons:"},{"heading":"importance-of-row-level-security","content":"Data Protection: RLS helps to protect sensitive data by ensuring that users can only access data that they are authorized to see. This is particularly important in industries such as healthcare or finance where regulations require strict controls over data access."},{"heading":"importance-of-row-level-security","content":"Multi-tenancy: In multi-tenant environments, RLS can be used to ensure that each tenant can only access their own data. This is much more efficient and secure than having separate tables or databases for each tenant."},{"heading":"importance-of-row-level-security","content":"Flexibility: RLS policies can be defined using any boolean SQL expression, which provides a great deal of flexibility. For example, you could define a policy that only allows users to access data that was created within the last 30 days."}],"headings":[{"id":"importance-of-row-level-security","content":"Importance of Row Level Security"}]},"url":"/docs/v2/security/row-level-security","title":"Row Level Security","description":"Row Level Security (RLS) is a feature in PostgreSQL that provides the ability to control which users can SELECT, INSERT, UPDATE, or DELETE which rows in a table. It adds an additional layer of security in multi-user database environments and is crucial for protecting sensitive data."},{"_id":"/docs/v2/tech-stack/why-nextjs","structured":{"contents":[{"content":"There are several reasons why we chose Next.js over other options when building a server-rendered React application:"},{"content":"Ease of use: Next.js is designed to be easy to learn and use, with a simple and intuitive API and a wide range of built-in features. This can make it a good choice for developers who are new to server-rendered React applications or who want to get up and running quickly."},{"content":"Performance: Next.js is optimized for performance, with features such as automatic code splitting and server rendering that can help to improve the load time and overall performance of our application."},{"content":"Scalability: Next.js is designed to be scalable, with support for static exports and the ability to run on a serverless architecture. This can make it a good choice for applications that are expected to handle a large number of users or a high volume of traffic."},{"content":"Community and ecosystem: Next.js has a large and active community of developers, with a wide range of resources and tools available to support building applications with the framework. This can make it easier to find help and support when working with Next.js."},{"content":"Next 13 is massive leap in terms of performance and developer experience and we experienced it while building apps in our other projects and we think no one should miss out on using Next.js 14+."},{"content":"It really is taking the world by storm.\nhttps\\://mobilecoderz.com/blog/next-js-framework-the-next-big-thing-of-2022/"}],"headings":[]},"url":"/docs/v2/tech-stack/why-nextjs","title":"Why Next.js?","description":"Why we chose Next.js for server-rendered React applications."},{"_id":"/docs/v2/tech-stack/why-shadcn-ui","structured":{"contents":[{"content":"Shadcn UI is a component library that is built on top of Radix UI. It is a set of reusable UI components that are styled with Tailwind CSS. It is designed to be used with Next.js and React."},{"content":"We chose Shadcn UI because it is a modern and easy to use component library that is built on top of Radix UI. It is designed to be used with Next.js and React."},{"content":"It is built with Tailwind CSS and TypeScript. It is designed to be used with Next.js and React."},{"content":"It is designed to be used with Next.js and React."}],"headings":[]},"url":"/docs/v2/tech-stack/why-shadcn-ui","title":"Why Shadcn UI?","description":"Why we chose Shadcn UI for our design system."},{"_id":"/docs/v2/tech-stack/why-stripe","structured":{"contents":[{"content":"While there are many payments providers today, we went with Stripe which is the leading payments provider of choice for most SaaS companies today."},{"content":"Wide range of payment options: Stripe supports a wide range of payment methods, including credit cards, debit cards, and online payment services such as Apple Pay and Google Pay. This can make it easy for businesses to accept payments from a variety of sources."},{"content":"Customizable payment forms: Stripe allows businesses to customize the look and feel of their payment forms to match their brand. This can help to create a seamless and professional payment experience for customers."},{"content":"Advanced fraud protection: Stripe provides advanced fraud protection to help businesses detect and prevent fraudulent activity. This can help to reduce the risk of chargebacks and other fraudulent activity."},{"content":"Easy integration: Stripe offers a wide range of tools and libraries to make it easy to integrate the platform into a business's existing systems and processes. This can help to streamline the payment process and reduce the time and effort required to manage payments."}],"headings":[]},"url":"/docs/v2/tech-stack/why-stripe","title":"Why Stripe?","description":"Why Stripe is the leading payments provider of choice for most SaaS companies today. Wide range of payment options, customizable payment forms, advanced fraud protection, and easy integration."},{"_id":"/docs/v2/tech-stack/why-supabase","structured":{"contents":[{"content":"Supabase has had a breakthrough last couple of years in SaaS products usage. It is the new Firebase but better."},{"content":"We went with Supabase because it makes backend and database development a breeze."},{"content":"It is realtime first, open source and built on top of Postgres. It provides a powerful dashboard, with many many features. Some include"},{"content":"Easy to use Javascript and Node.js clients. They also have Flutter, Swift and Dart clients to easily built mobile apps later."},{"content":"Realtime with just a checkbox. All your database tables can be made realtime with just a click of a button."},{"content":"Database hooks - Allows you to integrate with Zapier, Make etc and connect your app to other apps"},{"content":"Edge functions"},{"content":"Database triggers and functions"},{"content":"Admin Panel"},{"content":"Custom reports and Graphs"},{"content":"Errors dashboard"},{"content":"Foreign Table Wrappers (In development) and so many more."},{"content":"If you want to build robust webapps quickly, Supabase is the way to go in 2023."}],"headings":[]},"url":"/docs/v2/tech-stack/why-supabase","title":"Why Supabase?","description":"Why we chose Supabase for our backend and database needs"},{"_id":"/docs/v2/troubleshooting/login-callback-wrong","structured":{"contents":[{"content":"This is generally because the Supabase Authentication / URL configuration is not setup correctly. Ensure the following:"},{"content":"The Site URL is set to your production domain eg: `https://usenextbase.com`"},{"content":"The Redirect urls include both your production and development hosts"},{"content":"eg : `https://usenextbase/com/**` and `http://localhost:3000/**`"}],"headings":[]},"url":"/docs/v2/troubleshooting/login-callback-wrong","title":"I am unable to login","description":"This is generally because the Supabase Authentication / URL configuration is not setup correctly."},{"_id":"/docs/v2/ui/dark-mode","structured":{"contents":[{"heading":"dark-mode","content":"NextBase supports dark mode for your application."}],"headings":[{"id":"dark-mode","content":"Dark Mode"}]},"url":"/docs/v2/ui/dark-mode","title":"Dark Mode","description":"NextBase supports dark mode for your application."},{"_id":"/docs/v2/ui/nprogress","structured":{"contents":[{"heading":"nprogress","content":"NProgress is a library that provides a progress bar for your application."}],"headings":[{"id":"nprogress","content":"NProgress"}]},"url":"/docs/v2/ui/nprogress","title":"NProgress","description":"NProgress is a library that provides a progress bar for your application."},{"_id":"/docs/v2/ui/shadcn-ui","structured":{"contents":[{"heading":"shadcn-ui","content":"Shadcn UI is a very popular library for building UI components.\nIt is built on top of Radix UI and Tailwind CSS."},{"heading":"shadcn-ui","content":"Checkout their website for more information."}],"headings":[{"id":"shadcn-ui","content":"Shadcn UI"}]},"url":"/docs/v2/ui/shadcn-ui","title":"Shadcn UI","description":"Shadcn UI is a very popular library for building UI components."},{"_id":"/docs/v2/ui/sonner","structured":{"contents":[{"content":"Sonner is quite a simple library to use. We chose sonner because it's styling doesn't get in the way and it's easy to use."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v2/ui/sonner","title":"Sonner","description":"Sonner is a library that provides a toast notification for your application."},{"_id":"/docs/v2/ui/tailwindcss","structured":{"contents":[{"content":"Tailwind has been configured with next.js."},{"content":"We have the following plugins configured:"},{"content":"`tailwindcss-animate`: This plugin adds a set of helpful animation utilities to Tailwind CSS."},{"content":"`@tailwindcss/typography`: This plugin provides a set of `prose` classes for beautiful typographic defaults."},{"content":"Additionally, we've extended the default Tailwind configuration with:"},{"content":"Custom color palette including primary, secondary, destructive, muted, accent, popover, and card colors."},{"content":"Custom border radius values."},{"content":"Custom font families:"},{"content":"`sans`: Uses the Inter font"},{"content":"`display`: Uses the Lexend font"},{"content":"`sans2`: Uses the Manrope font"},{"content":"Custom keyframes and animations:"},{"content":"`accordion-down` and `accordion-up`"},{"content":"`scale-up` and `scale-down`"},{"content":"Extended max-width with an `8xl` size."},{"content":"The configuration also includes a dark mode setup using the `class` strategy, allowing for easy implementation of a dark theme in your application."}],"headings":[]},"url":"/docs/v2/ui/tailwindcss","title":"Tailwind CSS","description":"Tailwind CSS is a utility-first CSS framework."},{"_id":"/docs/v3/admin-panel/blog","structured":{"contents":[{"content":"The blog feature in the admin panel allows you to manage your blog posts efficiently. Here's an overview of its functionality:"},{"content":"Blog posts can be written in Markdown format."},{"content":"You can add images to your blog posts."},{"content":"You can add tags to your blog posts."},{"content":"You can add categories to your blog posts."},{"content":"You can add a summary to your blog posts."},{"content":"You can add a thumbnail to your blog posts."},{"content":"You can add a published date to your blog posts."},{"content":"You can add an author to your blog posts."}],"headings":[]},"url":"/docs/v3/admin-panel/blog","title":"Blog","description":"Blog feature in the admin panel"},{"_id":"/docs/v3/admin-panel/changelog","structured":{"contents":[{"content":"TODO"}],"headings":[]},"url":"/docs/v3/admin-panel/changelog","title":"Changelog","description":"Changelog feature in the admin panel"},{"_id":"/docs/v3/admin-panel/feedback","structured":{"contents":[{"content":"TODO"}],"headings":[]},"url":"/docs/v3/admin-panel/feedback","title":"Feedback","description":"Feedback management in the admin panel"},{"_id":"/docs/v3/admin-panel/make-user-app-admin","structured":{"contents":[{"content":"The Admin Panel is only available in Pro and Ultimate starter kits. Nextbase Pro has features such\nas user management, billing and workspace management. Ultimate starter kit has all the features of\nthe Admin Panel which are in Pro plus Changelog Manager, Feedback Manager, Blog manager etc."},{"content":"Admin panel allows a SAAS owner to manage data of their application at a high level.\nTo make a user an application admin, go to the supabase dashboard."},{"heading":"making-a-user-application-admin","content":"Make sure the user has created an account in your application. Once they have it, the user id of this user can be found in Supabase in the `auth.users` or in the `public.user_profiles` table."},{"heading":"re-login","content":"Note: The user will have to logout and login again for the changes to take effect."},{"heading":"re-login","content":"This is necessary because a new session with elevated privileges is created for\nthe user."},{"heading":"admin-panel","content":"Once the user is an application admin, they should be able to see the Admin Panel sub nav in the sidebar. It looks like this:"},{"heading":"admin-panel","content":"Workspace admins can manage data of their workspace. However, they are still regular users of\nthe application. Application admins are superadmins who can manage data of the entire\napplication."},{"heading":"admin-panel","content":"No. Application admins are assigned only via the Supabase dashboard, which is only accessible to\nthe owner of the application. It is not within the application at all. As long as the Supabase\ndatabase is secure, a user won't be able to make themselves application admins just by tampering\nwith the application code."}],"headings":[{"id":"making-a-user-application-admin","content":"Making a User Application Admin"},{"id":"re-login","content":"Re-Login"},{"id":"admin-panel","content":"Admin Panel"}]},"url":"/docs/v3/admin-panel/make-user-app-admin","title":"Making a User Application Admin","description":"Learn how to grant application admin privileges to users in your Nextbase application"},{"_id":"/docs/v3/admin-panel/roadmap","structured":{"contents":[{"content":"TODO"}],"headings":[]},"url":"/docs/v3/admin-panel/roadmap","title":"Roadmap","description":"Product roadmap management in the admin panel"},{"_id":"/docs/v3/admin-panel/setting-up-payments","structured":{"contents":[{"heading":"setting-up-stripe","content":"First, navigate to the Stripe Developer Dashboard. This is where you'll find your API keys and set up webhooks."},{"heading":"create-products-and-prices","content":"If you don't have any products and prices created in Stripe, you can create them in the Stripe dashboard."},{"heading":"get-environment-variables","content":"Find your `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY` in the developers section and paste them in the `.env.local` file.\nDon't worry about the `STRIPE_WEBHOOK_SECRET` for now. We will setup webhooks later."},{"heading":"sync-stripe-products-and-prices-with-application","content":"Sync payments with your database from the \\`\\`Payments section in the admin panel:"},{"heading":"sync-stripe-products-and-prices-with-application","content":"Stripe products and pricesare now visible in the admin panel:"},{"heading":"ready-to-collect-payments","content":"You are now ready to collect payments. Now when your user logs into the application and see the workspace billing page, they will see the products and prices:"},{"heading":"ready-to-collect-payments","content":"Products and prices are neatly organized into subscriptions and one-time payments. You can change the logic for this in the code."},{"heading":"configuring-webhooks","content":"Set up your webhook endpoint in the Stripe dashboard:"},{"heading":"configuring-webhooks","content":"Select the events you want to listen for:"},{"heading":"configuring-webhooks","content":"Add your endpoint URL:"},{"heading":"workspace-subscriptions-flow","content":"Webhooks need to be set up in Stripe before you can see the subscription purchase flow. That is\nbecause the subscription details are synced from Stripe to the application via webhooks. If\nwebhooks are not setup, then even after billing is done, the subscription details will not be\nsynced to the application."},{"heading":"workspace-subscriptions-flow","content":"Here's how the subscription purchase process looks:"},{"heading":"workspace-subscriptions-flow","content":"And the subscription card after purchase:"},{"heading":"workspace-subscriptions-flow","content":"Users can view invoices:"},{"heading":"workspace-subscriptions-flow","content":"And make one-time payments:"},{"heading":"admin-panel---monitoring-and-metrics","content":"Keep track of your payment metrics in the admin panel:"},{"heading":"admin-panel---monitoring-and-metrics","content":"You can view customers, subscriptions and invoices details from the admin panel."},{"heading":"admin-panel---monitoring-and-metrics","content":"\\[WIP] Once set up in Stripe, your products will be visible in the admin panel:"}],"headings":[{"id":"setting-up-stripe","content":"Setting up Stripe"},{"id":"create-products-and-prices","content":"Create products and prices"},{"id":"get-environment-variables","content":"Get environment variables"},{"id":"sync-stripe-products-and-prices-with-application","content":"Sync Stripe Products and prices with Application"},{"id":"ready-to-collect-payments","content":"Ready to collect payments"},{"id":"configuring-webhooks","content":"Configuring Webhooks"},{"id":"workspace-subscriptions-flow","content":"Workspace subscriptions flow"},{"id":"admin-panel---monitoring-and-metrics","content":"Admin panel - Monitoring and Metrics"}]},"url":"/docs/v3/admin-panel/setting-up-payments","title":"Setting up Payments","description":"Setting up Stripe payments using the admin panel"},{"_id":"/docs/v3/authentication/advanced-methods","structured":{"contents":[{"heading":"possible-advanced-methods","content":"**Two-Factor Authentication (2FA)**: Enhance security by requiring a second form of verification."},{"heading":"possible-advanced-methods","content":"**Biometric Authentication**: Implement fingerprint or face recognition for mobile apps."},{"heading":"possible-advanced-methods","content":"**Single Sign-On (SSO)**: Allow users to access multiple applications with a single set of credentials."},{"heading":"possible-advanced-methods","content":"**JSON Web Tokens (JWT)**: Use JWTs for stateless authentication in your API."},{"heading":"possible-advanced-methods","content":"**OAuth 2.0**: Implement a full OAuth 2.0 flow for more complex authorization scenarios."},{"heading":"implementation-considerations","content":"Implementing advanced authentication methods often requires additional libraries, services, or custom server-side logic. Here are some general steps:"},{"heading":"implementation-considerations","content":"Choose the appropriate method based on your security requirements and user experience goals."},{"heading":"implementation-considerations","content":"Research and select any necessary third-party services or libraries."},{"heading":"implementation-considerations","content":"Implement the chosen method, ensuring it integrates well with Nextbase and Supabase."},{"heading":"implementation-considerations","content":"Thoroughly test the new authentication method, including edge cases and potential security vulnerabilities."},{"heading":"implementation-considerations","content":"Update your application's documentation and user guides to reflect the new authentication options."},{"heading":"implementation-considerations","content":"Remember, while advanced authentication methods can provide enhanced security, they may also introduce complexity to your application. Always balance security needs with user experience considerations."}],"headings":[{"id":"possible-advanced-methods","content":"Possible Advanced Methods"},{"id":"implementation-considerations","content":"Implementation Considerations"}]},"url":"/docs/v3/authentication/advanced-methods","title":"Advanced Authentication Methods","description":"While Nextbase provides built-in support for common authentication methods, you may need more advanced or specialized authentication options for certain use cases."},{"_id":"/docs/v3/authentication/login-with-api-key","structured":{"contents":[{"heading":"implementation","content":"API key authentication is typically implemented using a third-party service like Unkey. The exact implementation details may vary based on your specific requirements and the service you're using."},{"heading":"implementation","content":"Here's a general outline of how you might implement API key authentication:"},{"heading":"implementation","content":"Generate an API key for the user or service that needs access."},{"heading":"implementation","content":"Store the API key securely (never in plain text)."},{"heading":"implementation","content":"When a request comes in, validate the API key."},{"heading":"implementation","content":"If the key is valid, authenticate the request and proceed."},{"heading":"usage-example","content":"Here's a simplified example of how you might validate an API key in a Next.js API route:"},{"heading":"usage-example","content":"Remember to handle API keys securely and follow best practices for API authentication."}],"headings":[{"id":"implementation","content":"Implementation"},{"id":"usage-example","content":"Usage Example"}]},"url":"/docs/v3/authentication/login-with-api-key","title":"Login with API Key","description":"Nextbase Ultimate supports authentication using API keys, which is useful for integrating with external services or for machine-to-machine communication."},{"_id":"/docs/v3/authentication/login-with-magic-link","structured":{"contents":[{"heading":"server-action","content":"Here's the server action used to login with Magic Link:"},{"heading":"usage","content":"Here's an example of how to use the Magic Link authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to receive a login link via email, providing a passwordless authentication experience."}],"headings":[{"id":"server-action","content":"Server Action"},{"id":"usage","content":"Usage"}]},"url":"/docs/v3/authentication/login-with-magic-link","title":"Login with Magic Link","description":"Nextbase supports login with Magic Link, which is a secure way to authenticate users without requiring them to remember a password."},{"_id":"/docs/v3/authentication/login-with-password","structured":{"contents":[{"content":"Here's the server action used for password-based signup. Login is similar, but uses a different schema and action."},{"content":"We use next-safe-action to ensure type safety and error handling for our server actions.\nNext safe action errors are also passed along to react hook form with field level errors to correctly display errors in the UI."},{"heading":"usage","content":"Here's an example of how to use password-based authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to log in using their email and password credentials."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v3/authentication/login-with-password","title":"Login with Password","description":"Nextbase supports traditional email and password authentication, providing a familiar login experience for users."},{"_id":"/docs/v3/authentication/login-with-social-providers","structured":{"contents":[{"content":"Here's the server action used for social provider login:"},{"heading":"usage","content":"Here's an example of how to use social provider authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to log in using their accounts from supported social providers."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v3/authentication/login-with-social-providers","title":"Login with Social Providers","description":"Nextbase supports authentication with various social providers, allowing users to log in using their existing accounts from platforms like Google, GitHub, and Twitter."},{"_id":"/docs/v3/authentication/overview","structured":{"contents":[{"content":"It only has small wrappers around the supabase javascript client."},{"heading":"supported-auth-methods","content":"Authentication Type"},{"heading":"supported-auth-methods","content":"Description"},{"heading":"supported-auth-methods","content":"Email"},{"heading":"supported-auth-methods","content":"Authentication using only the user's email."},{"heading":"supported-auth-methods","content":"Email + Password"},{"heading":"supported-auth-methods","content":"Authentication using the user's email and password."},{"heading":"supported-auth-methods","content":"Third Party Providers"},{"heading":"supported-auth-methods","content":"Authentication using third-party providers. Examples given are Google, GitHub, and Twitter.  Configuration is done via the Supabase dashboard."},{"heading":"supported-auth-methods","content":"Login with API Key"},{"heading":"supported-auth-methods","content":"Authentication using an API key using Unkey (Nextbase Ultimate only)."},{"heading":"providers","content":"Providers are configured using the RenderProviders component. It renders all the providers passed to it as props. For example, the login page component Login.tsx renders providers like so:"},{"heading":"providers","content":"You can configure whichever providers you like using the supabase dashboard. Head over to `https://app.supabase.com/project/{your-project}/auth/providers` and configure the providers you require."},{"heading":"providers","content":"They will automatically work in your frontend. No other configuration required."}],"headings":[{"id":"supported-auth-methods","content":"Supported auth methods"},{"id":"providers","content":"Providers"}]},"url":"/docs/v3/authentication/overview","title":"Overview","description":"NextBase comes with Email, Email + password and Third party providers authentication built-in."},{"_id":"/docs/v3/development-updates/code-and-migration-updates","structured":{"contents":[{"content":"We are constantly working on improving the codebase and migrating to new versions."},{"heading":"codebase","content":"All Nextbase repos, (nextbase essential, nextbase pro, nextbase ultimate) are fullstack nextjs applications.\nThey are all built with the same tech stack and have the same architecture."}],"headings":[{"id":"codebase","content":"Codebase"}]},"url":"/docs/v3/development-updates/code-and-migration-updates","title":"Code and migration updates","description":"Updates to the Nextbase codebase and migration updates."},{"_id":"/docs/v3/development-updates/version-control","structured":{"contents":[{"content":"We use git for version control.\nAll our repositories are hosted on github."}],"headings":[]},"url":"/docs/v3/development-updates/version-control","title":"Version Control","description":"How we version control our Nextbase repositories"},{"_id":"/docs/v3/getting-started","structured":{"contents":[{"heading":"getting-started-with-nextbase","content":"Welcome to NextBase! We're thrilled to have you on board. This guide will walk you through the initial steps to get started with your purchased NextBase repository."},{"heading":"accessing-your-purchase","content":"To access your purchase, navigate to your dashboard. Here, you will find all your purchased items. Look for the NextBase repository (Essential, Pro, or Ultimate) that you've purchased."},{"heading":"entering-your-github-username","content":"You will be prompted to enter your GitHub username. This step is crucial as it grants you access to the purchased repository. Please ensure you enter the correct username."},{"heading":"cloning-the-repository","content":"Once you've gained access, the next step is to clone the repository. Here's how:\nLet us say that you purchased the NextBase Pro repository. It is a private repository which is at https\\://github.com/imbhargav5/nextbase-pro.\nTo clone the repository, you will need to run the following command in your terminal:"},{"heading":"cloning-the-repository","content":"**Important Note:** We strongly discourage forking the repository. If you fork the repository, the code will be visible to others who have forked it. To maintain the privacy and integrity of your code, always opt for cloning.\nNext Steps"},{"heading":"cloning-the-repository","content":"After cloning the repository, you're all set to start exploring and working with NextBase. If you encounter any issues or have any questions, don't hesitate to reach out to our support team."}],"headings":[{"id":"getting-started-with-nextbase","content":"Getting Started with NextBase"},{"id":"accessing-your-purchase","content":"Accessing Your Purchase"},{"id":"entering-your-github-username","content":"Entering Your GitHub Username"},{"id":"cloning-the-repository","content":"Cloning the Repository"}]},"url":"/docs/v3/getting-started","title":"Getting Started with NextBase"},{"_id":"/docs/v3/guides/admin-panel","structured":{"contents":[{"heading":"user-impersonation-debugging","content":"As an application admin, you can debug bugs more quickly for any user by loggin in as them and debugging the issue from their account. It is available in the application admin users section."},{"heading":"user-details-table","content":"The user details table is a table that shows all the users in the application. It is available in the application admin users section."},{"heading":"workspaces-table","content":"The workspaces table is a table that shows all the workspaces in the application. It is available in the application admin workspaces section."},{"heading":"admin-controls-for-feedback-roadmap","content":"Admin users can manage feedback and roadmap items. They can update feedback status, type, priority etc."}],"headings":[{"id":"user-impersonation-debugging","content":"User impersonation debugging"},{"id":"user-details-table","content":"User details table"},{"id":"workspaces-table","content":"Workspaces table"},{"id":"admin-controls-for-feedback-roadmap","content":"Admin controls for feedback, roadmap"}]},"url":"/docs/v3/guides/admin-panel","title":"Admin Panel Guide","description":"The Admin panel has features user impersonation, fix bugs more quickly, and more."},{"_id":"/docs/v3/guides/asynchronous-data-fetching","structured":{"contents":[{"content":"In Nextbase starter kit, we to rely on server components as much as possible to fetch data. The advantages are"},{"content":"smaller bundle sizes: because we don't have to ship the data fetching logic to the client"},{"content":"faster page loads: because we rely on the cpu power of the server instead of the client to fetch data. Server CPUs are usually faster than client CPUs."},{"heading":"data-fetching-in-server-components","content":"Since server components can by async, we can just await on the data which we require for that component. Next.js layouts and pages are server components\nby default unless you explicitly specify them as client components."},{"heading":"data-fetching-in-server-components","content":"For instance, take a look at our the page rendering our `/blog/[slug]` route:"},{"heading":"data-fetching-in-server-components","content":"Notice how we are awaiting on the `anonGetPublishedBlogPostBySlug` function to fetch the blog post data. This function is a server action which is defined in `src/data/anon/internalBlog.ts`. And since the `BlogPostPage` is a server component, we can await on the data fetching function."},{"heading":"streaming-rendering","content":"Server components have the added benefit of streaming rendering. Consider the page responsible for rendering the blog list page at `/blog`. This page has to fetch two sets of data: the list of blog posts and the list of tags. We can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components: `Tags` and `BlogList`."},{"heading":"streaming-rendering","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"streaming-rendering","content":"So the user experience for this page looks like this:"},{"heading":"streaming-rendering","content":"the page starts rendering"},{"heading":"streaming-rendering","content":"\"All blog posts\" title is rendered"},{"heading":"streaming-rendering","content":"\"Loading tags...\" is rendered and when the tags are fetched, the tags are rendered"},{"heading":"streaming-rendering","content":"\"Loading posts...\" is rendered and when the posts are fetched, the posts are rendered"},{"heading":"streaming-rendering","content":"This is a much better user experience than having to wait for the page to render until all the data is fetched."},{"heading":"data-fetching-in-client-components","content":"We don't fetching data much in client components unless in very rare cases. To fetch data in client components simply rely on `useQuery` from\nreact-query. For eg: the login button in the navigation menu is a client component and it fetches the user data using `useQuery`:"}],"headings":[{"id":"data-fetching-in-server-components","content":"Data Fetching in Server components"},{"id":"streaming-rendering","content":"Streaming rendering"},{"id":"data-fetching-in-client-components","content":"Data fetching in client components"}]},"url":"/docs/v3/guides/asynchronous-data-fetching","title":"Asynchronous Data Fetching Guide","description":"Learn how to fetch data asynchronously in Next.js using server actions and react query"},{"_id":"/docs/v3/guides/authentication","structured":{"contents":[{"heading":"authentication","content":"See Authentication Overview here."},{"heading":"authentication","content":"More coming soon..."}],"headings":[{"id":"authentication","content":"Authentication"}]},"url":"/docs/v3/guides/authentication","title":"Authentication","description":"Authenticating users in Nextbase Starter kit"},{"_id":"/docs/v3/guides/creating-application-admin-page","structured":{"contents":[{"content":"Admin Panel pages are internal web app pages that are only accessible to application administrators. These pages usually require the user to be authenticated and have admin privileges."},{"content":"All admin panel pages should be children of the layout located at `src/app/(dynamic-pages)/(authenticated-pages)/app_admin/layout.tsx`. This layout handles checking if the user is authenticated and if they have admin privileges. If the user is not authenticated or does not have admin privileges, they will be redirected to the login page."},{"content":"Here is an example of how to create an admin panel page:"}],"headings":[{"id":"admin-panel-layout","content":"Admin panel layout"}]},"url":"/docs/v3/guides/creating-application-admin-page","title":"Creating Admin Panel Page","description":"Guide on how to create an admin panel page in  Nextbase."},{"_id":"/docs/v3/guides/creating-docs-page","structured":{"contents":[{"content":"Docs are written using fumadocs."},{"content":"Index page for docs is in a different layout mainly because fumadocs doesn't support index layouts. It only has a docs page which renders a single mdx page at a time."},{"content":"All the actual docs are generated from content-collections. All the docs are written as mdx files in `src/content/docs`."},{"content":"They are then converted into json files by content-collections."},{"content":"Fumadocs and content collections work together to render the docs page."},{"content":"<File name=\"page.tsx\" />"},{"content":"<File name=\"layout.tsx\" />"},{"content":"<File name=\"page.tsx\" />"},{"content":"<File name=\"layout.tsx\" />"},{"content":"<File name=\"page.tsx\" />"},{"content":"<File name=\"global.css\" />"},{"content":"<File name=\"button.tsx\" />"},{"content":"<File name=\"tabs.tsx\" />"},{"content":"<File name=\"dialog.tsx\" />"},{"content":"<File name=\"getting-started.mdx\" />"},{"content":"<File name=\"package.json\" />"}],"headings":[]},"url":"/docs/v3/guides/creating-docs-page","title":"Creating Docs Page","description":"Guide on how to create a docs page in Nextbase."},{"_id":"/docs/v3/guides/creating-in-app-notifications","structured":{"contents":[{"content":"In-app notifications are a great way to communicate with your users. They can be used to inform users about new features, promotions, or any other important information.\nNextbase Ultimate comes with a built-in in-app notification system that allows you to create and send notifications to your users."},{"heading":"notifications-table","content":"User notifications are managed in the `user_notifications` table which is created automatically by Nextbase Ultimate supabase migrations during installation. Here is the relevant part of the table schema:"},{"heading":"notifications-table","content":"As you can see, the table has a foreign key to the `user_profiles` table, which means that each notification is associated with a user. This is important because it allows you to send notifications to specific users."},{"heading":"notifications-table","content":"Also, the table has a `payload` column which is a JSONB column that can be used to store any data you want to send to the user."},{"heading":"notifications-table","content":"Additionally, the table has a `is_read` and `is_seen` columns that can be used to track whether the user has read or seen the notification."},{"heading":"notifications-table","content":"Finally the RLS policies ensure that users can only read, update, and delete their own notifications."},{"heading":"creating-a-notification","content":"To create a notification, you can use the `createNotification` function which is available in the `src/data/user/notifications` module:"},{"heading":"creating-a-notification","content":"This function allows adding a notification of any payload to a user."},{"heading":"typesafety","content":"It is important to note that the `payload` column is of type `JSONB` which means that you can store any data you want in it. However, this also means that you will not get any type safety when accessing the data.\nFor that purpose, we use zod schemas to define the payload type. It is present in `src/utils/zod-schemas/notifications.ts`"},{"heading":"typesafety","content":"As you can see, we define a `UserNotification` type which is a union of all the possible notification types. This allows us to get type safety when accessing the notification payload.\nWhen you want to create a new notification type you can simply add it to the union type and define the zod schema for it.\nFor eg. if you want to create a notification type for when a user's profile was seen by another user, you can do the following:"},{"heading":"parsing-notification-payload","content":"We utilize a parse notification function in `src/utils/parseNotification.ts`, where we parse and normalize the notification payload. This is useful when we want to display the notification in the UI."},{"heading":"parsing-notification-payload","content":"So to add our new notification type, we can simply add a new case to the switch statement:"},{"heading":"viewing-notifications","content":"UI to mark a notification as seen and read is already built into Nextbase Ultimate."}],"headings":[{"id":"notifications-table","content":"Notifications Table"},{"id":"creating-a-notification","content":"Creating a notification"},{"id":"typesafety","content":"Typesafety"},{"id":"parsing-notification-payload","content":"Parsing notification payload"},{"id":"viewing-notifications","content":"Viewing notifications"}]},"url":"/docs/v3/guides/creating-in-app-notifications","title":"Creating in-app notifications","description":"Guide on how to create in-app notifications in Nextbase."},{"_id":"/docs/v3/guides/creating-new-supabase-table","structured":{"contents":[{"content":"When creating a new feature in your app, you might need to create a new table in your Supabase database. In this guide, we'll learn how to create a new table in Supabase, manage migrations, and use the new table in your Next.js app."},{"heading":"1-postgres-queries","content":"Let's say you want to create a table to store a list of todos. Here's the PostgreSQL query to create a `todos` table:"},{"heading":"2-approaches-to-creating-tables","content":"There are two main approaches to creating tables in Supabase:"},{"heading":"2-approaches-to-creating-tables","content":"**Direct SQL Execution**: Run the SQL query directly in the Supabase SQL editor (localhost:54323 for local development or in your production Supabase database)."},{"heading":"2-approaches-to-creating-tables","content":"**Using Migrations**: Create and manage database changes through migrations, which is the recommended approach for better tracking and version control."},{"heading":"2-approaches-to-creating-tables","content":"While the first approach is quick, it's not ideal for maintaining and tracking changes over time. Let's focus on the migration approach."},{"heading":"3-managing-migrations","content":"Nextbase provides commands to create and manage migrations:"},{"heading":"creating-a-new-migration","content":"To create a new migration:"},{"heading":"creating-a-new-migration","content":"This command will create a new timestamped SQL file in the `supabase/migrations` directory. Open this file and add your SQL query:"},{"heading":"applying-migrations-locally","content":"To apply new migrations to your local database without losing data:"},{"heading":"resetting-local-database","content":"If you need to reset your local database and apply all migrations from scratch:"},{"heading":"pushing-migrations-to-production","content":"To push your migrations to the production Supabase database:"},{"heading":"pushing-migrations-to-production","content":"This command will apply any new migrations to your production database."},{"heading":"4-generating-types-locally","content":"After creating or modifying tables, you should regenerate TypeScript types for your Supabase tables. Run the following command:"},{"heading":"4-generating-types-locally","content":"This command regenerates the types for your local database. The generated types are located in `apps/database/lib/database.types.ts` and exported from the `database` workspace package as `database/types`."},{"heading":"5-using-generated-types","content":"Types are crucial for type checking and auto-completion in your IDE. You can use the generated types to type-check your database queries and auto-complete column names."},{"heading":"5-using-generated-types","content":"Example usage:"},{"heading":"5-using-generated-types","content":"In this example, `data` will be an array of `Todo` objects, with types generated from your database schema."},{"heading":"conclusion","content":"By following this workflow, you can create new tables, manage database changes through migrations, and ensure type safety in your Nextbase project. This approach provides better version control, easier collaboration, and a more maintainable codebase."}],"headings":[{"id":"1-postgres-queries","content":"1\\. Postgres Queries"},{"id":"2-approaches-to-creating-tables","content":"2\\. Approaches to Creating Tables"},{"id":"3-managing-migrations","content":"3\\. Managing Migrations"},{"id":"creating-a-new-migration","content":"Creating a New Migration"},{"id":"applying-migrations-locally","content":"Applying Migrations Locally"},{"id":"resetting-local-database","content":"Resetting Local Database"},{"id":"pushing-migrations-to-production","content":"Pushing Migrations to Production"},{"id":"4-generating-types-locally","content":"4\\. Generating Types Locally"},{"id":"5-using-generated-types","content":"5\\. Using Generated Types"},{"id":"conclusion","content":"Conclusion"}]},"url":"/docs/v3/guides/creating-new-supabase-table","title":"Creating a new Supabase table","description":"Learn how to create a new table in Supabase, manage migrations, and use the new table in your Next.js app."},{"_id":"/docs/v3/guides/creating-protected-page","structured":{"contents":[{"content":"Nextbase implements a two-tier protection system for routes that require authentication:"},{"content":"**Middleware Protection**: Fast, preliminary check"},{"content":"**Layout-based Protection**: Thorough, database-validated check"},{"heading":"middleware-protection","content":"The middleware provides a quick, preliminary check based on the presence of an authentication cookie. This happens before the page rendering begins, offering a performance-optimized way to redirect unauthenticated users."},{"heading":"middleware-protection","content":"This middleware quickly checks for the presence of a session, providing a fast redirect to the login page if no session is found. This enhances website performance by avoiding unnecessary page renders for unauthenticated users."},{"heading":"layout-based-protection","content":"The layout protection provides a more thorough check, validating the session against the database to ensure it's still valid and not stale."},{"heading":"layout-based-protection","content":"This layout-based check provides a more comprehensive validation, ensuring that even if a user has a cookie, it corresponds to a valid, active session in the database. This prevents access with stale or invalid sessions."},{"heading":"example-creating-a-new-protected-route-my-protected-page","content":"Let's walk through the correct process of creating a new protected route \"/my-protected-page\" in Nextbase, leveraging the two-tier protection system."},{"heading":"step-1-create-the-page-file","content":"Create a new file at the following path:"},{"heading":"step-1-create-the-page-file","content":"Add your page content to this file:"},{"heading":"step-2-update-middleware-configuration","content":"You need to manually add the new route to the `protectedPaths` array in the middleware configuration. Open the `middleware.ts` file and update it as follows:"},{"heading":"step-2-update-middleware-configuration","content":"By manually adding the new route to the `protectedPaths` array, you ensure that it's covered by the middleware protection, providing the fast, preliminary check."},{"heading":"step-3-layout-based-protection-automatic","content":"Your new page is automatically protected by the authenticated layout, as it's placed under the `(authenticated-pages)` directory:"},{"heading":"step-3-layout-based-protection-automatic","content":"This layout performs the thorough, database-validated check for all pages within this directory, ensuring that even if a user passes the middleware check, their session is still valid and active."},{"heading":"step-4-add-navigation-optional","content":"If you want to add navigation to your new protected page, update your navigation component:"},{"heading":"testing-your-new-protected-route","content":"To test your new protected route and verify the two-tier protection:"},{"heading":"testing-your-new-protected-route","content":"Start your Nextbase application"},{"heading":"testing-your-new-protected-route","content":"Try accessing \"/my-protected-page\" without logging in"},{"heading":"testing-your-new-protected-route","content":"You should be quickly redirected to the login page by the middleware"},{"heading":"testing-your-new-protected-route","content":"Log in with a valid user account"},{"heading":"testing-your-new-protected-route","content":"Access \"/my-protected-page\""},{"heading":"testing-your-new-protected-route","content":"The middleware will allow access (fast check)"},{"heading":"testing-your-new-protected-route","content":"The layout protection will perform a thorough check"},{"heading":"testing-your-new-protected-route","content":"You should see your protected page content"},{"heading":"testing-your-new-protected-route","content":"Invalidate your session (e.g., by logging out in another tab)"},{"heading":"testing-your-new-protected-route","content":"Try accessing \"/my-protected-page\" again"},{"heading":"testing-your-new-protected-route","content":"The middleware might initially allow access (if the cookie is still present)"},{"heading":"testing-your-new-protected-route","content":"The layout protection should detect the invalid session and redirect to login"},{"heading":"testing-your-new-protected-route","content":"By following these steps and understanding the two-tier protection system, you've successfully created a new protected route in your Nextbase application. This route is secured by both fast middleware checks and thorough layout-based validation, ensuring optimal performance and robust security for your application."}],"headings":[{"id":"middleware-protection","content":"Middleware Protection"},{"id":"layout-based-protection","content":"Layout-based Protection"},{"id":"example-creating-a-new-protected-route-my-protected-page","content":"Example: Creating a New Protected Route \"/my-protected-page\""},{"id":"step-1-create-the-page-file","content":"Step 1: Create the Page File"},{"id":"step-2-update-middleware-configuration","content":"Step 2: Update Middleware Configuration"},{"id":"step-3-layout-based-protection-automatic","content":"Step 3: Layout-based Protection (Automatic)"},{"id":"step-4-add-navigation-optional","content":"Step 4: Add Navigation (Optional)"},{"id":"testing-your-new-protected-route","content":"Testing Your New Protected Route"}]},"url":"/docs/v3/guides/creating-protected-page","title":"Creating a Protected Page","description":"A complete guide on creating and managing protected routes in Nextbase using a two-tier protection system, including correct middleware configuration and an example scenario."},{"_id":"/docs/v3/guides/creating-public-dynamic-page","structured":{"contents":[{"content":"A public dynamic page in a SaaS site can be something like a product page, a user profile page, etc. These pages are dynamic and are part of the application. They are not just static pages that are part of the marketing site.\nIn Nextbase, we have a `(dynamic-pages)` route segment (folder) within our `src/app` folder which contains all the dynamic pages."},{"content":"So if you want to create a new dynamic page, you can just create a new folder within the `(dynamic-pages)` folder and create a new page within that folder. You can nest\nyour dynamic pages as deep as you want to get the route you are looking for."},{"content":"The key difference here is that this page is not cached and is always served fresh."},{"content":"The layout file for the dynamic pages is at `src/app/(dynamic-pages)/layout.tsx`."}],"headings":[]},"url":"/docs/v3/guides/creating-public-dynamic-page","title":"Creating Public Dynamic Page","description":"Guide on how to create a public dynamic page in a Next.js application such as in Nextbase."},{"_id":"/docs/v3/guides/creating-public-static-page","structured":{"contents":[{"content":"A public static page in a SaaS site can be something like a pricing page, a contact page, a about page, etc. These pages are not dynamic and are not part of the application. They are just static pages that are part of the marketing site.\nIn Nextbase, we have a `(static-pages)` route segment (folder) within our `src/app` folder which contains all the static pages."},{"content":"So if you want to create a new static page, you can just create a new folder within the `(static-pages)` folder and create a new page within that folder. You can nest\nyour static pages as deep as you want to get the route you are looking for."},{"content":"The layout file for the static pages is at `src/app/(static-pages)/layout.tsx`."},{"content":"As you can see the layout has no additional configuration such as `export const dynamic` etc. Which means that this layout defauls to the next.js defaults and is static."}],"headings":[]},"url":"/docs/v3/guides/creating-public-static-page","title":"Creating Public Static Page","description":"Guide on how to create a public static page in a Next.js application such as in Nextbase."},{"_id":"/docs/v3/guides/handling-realtime-data-with-supabase","structured":{"contents":[{"content":"In this guide, we'll learn how to handle realtime data in Supabase using server actions and react query.\nWe implemented notifications as a realtime feature in Nextbase Ultimate and we can use that as a reference."},{"heading":"supabase-realtime","content":"Supabase provides a realtime service which can be used to listen to changes in your database.\nWhen used with a React client component useEffect it looks something like this:"},{"heading":"supabase-realtime","content":"In the above code, we are listening to changes in the `user_notifications` table and refetching the data when a change occurs.\nDepending on the scenario, you can also just use the payload from the realtime data to `setState` or update a `react query` cache directly."}],"headings":[{"id":"supabase-realtime","content":"Supabase Realtime"}]},"url":"/docs/v3/guides/handling-realtime-data-with-supabase","title":"Handling Realtime Data With Supabase","description":"Learn how to handle realtime data in Supabase using server actions and react query"},{"_id":"/docs/v3/guides/managing-users","structured":{"contents":[{"content":"Users are the core of your application. Supabase Auth provides a complete user management system, including sign up, sign in, email verification, password recovery, and more."},{"heading":"how-does-supabase-store-user-data","content":"Supabase stores user data in a table called `auth.users`. `auth` is an internal schema that is created when you initialize Supabase."},{"heading":"how-does-supabase-store-user-data","content":"When new users sign up, they are added to this table."},{"heading":"how-does-supabase-store-user-data","content":"This table and tables related in the auth schema are automatically updated when users update their login information"},{"heading":"storing-more-information-about-users","content":"However this table doesn't contain any more data other than email and identifying information. Since we want to store more information about our users, we use a table called `user_profiles`."},{"heading":"storing-more-information-about-users","content":"`user_profiles` is a table which allows users to update their public information such as name and avatar. (avatar image is stored in supabase storage and the url is stored here)."},{"heading":"storing-more-information-about-users","content":"Users can also store their private preferences in the `user_private_info` table. This table is only accessible by the user whose id matches the \\`user\\_id\\` column. Both the tables mentioned above have auth.users.id as a foreign key."},{"heading":"storing-more-information-about-users","content":"Hence if you want to add more public information about your users, you can add a new column to \\`user\\_profiles\\` and if you want to add more private information, you can add a new column to \\`user\\_private\\_info\\`."}],"headings":[{"id":"how-does-supabase-store-user-data","content":"How does Supabase store user data?"},{"id":"storing-more-information-about-users","content":"Storing more information about users"}]},"url":"/docs/v3/guides/managing-users","title":"Managing Users","description":"Learn how to manage users with Supabase"},{"_id":"/docs/v3/guides/mutating-data-with-supabase","structured":{"contents":[{"content":"In this guide, we'll learn how to mutate data asynchronously in Supabase using server actions and react query.\nUsually, the scenario looks like this."},{"content":"The server and client components for the route are loaded."},{"content":"User performs an action that triggers a mutation, eg. clicking a button, filling out a form, etc."},{"content":"The client sends a request to the server to perform the mutation."},{"content":"In previous versions of Next.js, we would have to write API routes to handle the request.\nBut as of Next.js 13, and now in 14 and beyond, we have a new way to handle this. These are called route handlers and server actions."},{"heading":"what-are-route-handlers","content":"In Next.js 14, Route handlers have replaced API routes.\nThese are simple `route.ts` files that can be placed in the `app` folder at any level."},{"heading":"what-are-route-handlers","content":"Here is an example of a route handler that handles a `POST` request to `/api/hello`."},{"heading":"what-are-route-handlers","content":"As of Next.js 14, Route handlers have replaced API routes. These are simple route.ts files that can be placed in the app folder at any level. They can handle multiple HTTP methods (GET, POST, PUT, DELETE, etc.) in a single file."},{"heading":"what-are-route-handlers","content":"Here's an example of a simple POST request using Route handlers:"},{"heading":"what-are-route-handlers","content":"Now both the above examples are functionally equivalent. But the latter is much more concise and easier to read."},{"heading":"what-are-server-actions","content":"Server actions are convenient wrappers around route handlers that allow you to perform mutations on your data within\nyour client / server components. Instead of writing a route handler, you can simply write a server action and call it from your client component."},{"heading":"what-are-server-actions","content":"Here's an example of a server action that handles a `POST` request similar to the example above."},{"heading":"what-are-server-actions","content":"And here's how you would call it from your client component."},{"heading":"what-are-server-actions","content":"In the above example, we are using the `useMutation` hook from react-query to call our server action."},{"heading":"what-are-server-actions","content":"Note how we are directly invoking the `hello` server action from our client component."},{"heading":"what-are-server-actions","content":"This will internally be converted to a `POST` request to a route handler automatically created by Next.js and mapped to a route."},{"heading":"usetoastmutation-hook","content":"Nextbase starter kit provides a convenience hook called `useToastMutation` that can be used to display a toast notification throughout the lifecycle of a mutation.\nIt is a thin wrapper around `useMutation` from react-query and uses `sonner` to display toast notifications."}],"headings":[{"id":"what-are-route-handlers","content":"What are route handlers?"},{"id":"what-are-server-actions","content":"What are server actions?"},{"id":"usetoastmutation-hook","content":"useToastMutation hook"}]},"url":"/docs/v3/guides/mutating-data-with-supabase","title":"Mutating Data With Supabase","description":"Learn how to mutate data asynchronously in Supabase using server actions and react query"},{"_id":"/docs/v3/guides/optimizing-data-operations-nextjs-supabase","structured":{"contents":[{"content":"In this guide, we'll explore best practices for fetching and mutating data in Nextbase applications, focusing on performance and efficiency."},{"heading":"server-side-data-fetching","content":"Prefer fetching data for components on the server using server components. This approach utilizes the higher CPU power of servers and minimizes bundle sizes for the data fetching operation."},{"heading":"mutations-with-usetoastmutation","content":"For mutations, use the `useToastMutation` hook. This allows for direct server-side mutations while providing user feedback through toast notifications."},{"heading":"realtime-queries-with-supabase","content":"For realtime queries, such as notifications, use Supabase's realtime APIs directly on the client side."},{"heading":"avoiding-usestate-for-data-fetching","content":"Avoid using `useState` for most data fetching operations. Instead, either use server components and fetch data within them, or use `useQuery` if you really need to, but you generally shouldn't need to use `useState`."},{"heading":"avoiding-usestate-for-data-fetching","content":"By following these guidelines, you can optimize data operations in your Nextbase applications for better performance and user experience."}],"headings":[{"id":"server-side-data-fetching","content":"Server-Side Data Fetching"},{"id":"mutations-with-usetoastmutation","content":"Mutations with useToastMutation"},{"id":"realtime-queries-with-supabase","content":"Realtime Queries with Supabase"},{"id":"avoiding-usestate-for-data-fetching","content":"Avoiding useState for Data Fetching"}]},"url":"/docs/v3/guides/optimizing-data-operations-nextjs-supabase","title":"Optimizing Data Operations in Next.js 14 and Supabase","description":"Learn how to efficiently fetch and mutate data in Next.js 14 and Supabase using server components, useToastMutation, and Supabase real-time APIs."},{"_id":"/docs/v3/guides/removing-internationlization","structured":{"contents":[{"content":"Many saas products require internalization. But many don't need it too. Ultimately it is your customer requirements that matter."},{"content":"If you don't need internationalization, you can remove it by following these steps:"},{"content":"Move all the children of the `src/app/[locale]` folder to the `src/app/` folder."},{"content":"Remove the `[locale]` from the `src/app/` folder."},{"content":"Remove the `next-intl` from the `package.json` file."},{"content":"Update your middleware to this pattern. Pay close attention to the"},{"content":"Remove `src/i18n` folder."},{"content":"Replace all `import { Link } from \"@/components/intl-link\";` with `import Link from \"next/link\";`"},{"content":"Remove all `LocaleSwitcherSelect` component and its usage"},{"content":"Remove `import createNextIntlPlugin from \"next-intl/plugin\"` and its usage in `next.config.mjs`"},{"content":"Remove `unstable_setRequestLocale` usage in all static pages and `locale` from 'params'."},{"content":"Remove `useTranslations` usage in all client components."},{"content":"Remove `getMessages` usage in root layout component and remove `locale` and `messages` props from `AppProviders` component. Finally remove `NextIntlProvider` from the `AppProviders` component."}],"headings":[]},"url":"/docs/v3/guides/removing-internationlization","title":"Removing Internationalization","description":"Learn how to remove internationalization from your Nextbase application"},{"_id":"/docs/v3/guides/row-level-security","structured":{"contents":[{"content":"This article in the supabase website is a great start to understand how RLS works.\nhttps\\://supabase.com/docs/v3/guides/auth/row-level-security"},{"heading":"rls-configured-within-nextbase","content":"NextBase ships with RLS policies for tables such as organizations, organization\\_team\\_members, subscriptions, customers and more."},{"heading":"rls-configured-within-nextbase","content":"It also comes with RLS for storage buckets."},{"heading":"setup-rls-for-your-new-tables","content":"TODO"}],"headings":[{"id":"rls-configured-within-nextbase","content":"RLS configured within NextBase"},{"id":"setup-rls-for-your-new-tables","content":"Setup RLS for your new tables"}]},"url":"/docs/v3/guides/row-level-security","title":"Row Level Security","description":"Row Level Security in NextBase is a powerful feature that allows you to control access to rows in your database based on the user who is querying the data."},{"_id":"/docs/v3/guides/stripe-subscriptions","structured":{"contents":[{"content":"This guide will walk you through how Stripe has been set up in different files in Nextbase Starter Kit.."},{"heading":"stripe-configuration","content":"Stripe configuration is stored in the `.env` file. You need to provide your Stripe API keys here."},{"heading":"stripe-client","content":"A Stripe client is created in `src/utils/stripe.ts`. This client is used to interact with the Stripe API."},{"heading":"stripe-webhook","content":"A webhook endpoint is set up in `src/pages/api/stripe/webhook.ts` to handle events from Stripe."},{"heading":"stripe-checkout-and-customer-portal","content":"A checkout session is created in `src/data/user/organizations.ts`."},{"heading":"stripe-subscriptions","content":"Subscriptions are managed in `src/utils/supabase-admin.ts`. Subscriptions are at the organization level.\nWhen a subscription is created, the `manageSubscriptionStatusChange` function is called. This function creates a new subscription record in the database."},{"heading":"organization-admins","content":"Organization admins can manage subscriptions in `/organization/[organizationId]/settings/billing` route.\nAll members can see the current subscription status in `/organization/[organizationId]/settings/billing` route. However,\nonly organization admins can manage subscriptions."}],"headings":[{"id":"stripe-configuration","content":"Stripe Configuration"},{"id":"stripe-client","content":"Stripe Client"},{"id":"stripe-webhook","content":"Stripe Webhook"},{"id":"stripe-checkout-and-customer-portal","content":"Stripe Checkout and Customer Portal"},{"id":"stripe-subscriptions","content":"Stripe Subscriptions"},{"id":"organization-admins","content":"Organization Admins"}]},"url":"/docs/v3/guides/stripe-subscriptions","title":"Stripe Setup in Nextbase Ultimate","description":"This guide walks you through how Stripe has been set up in different files in Nextbase Ultimate."},{"_id":"/docs/v3/guides/writing-integration-tests","structured":{"contents":[{"content":"All versions of Nextbase Starter Kit come with Playwright installed. Playwright is a powerful end-to-end testing framework that allows you to write integration tests for your application."},{"heading":"configuration","content":"Playwright is configured in the `playwright.config.js` file."},{"heading":"configuration","content":"As you can see, the configuration file is quite simple. The playwright projects are configured in this manner:"},{"heading":"configuration","content":"Projects which setup authentication are configured in the `with-auth` project."},{"heading":"configuration","content":"Projects which test logged in users are configured in the `Logged In Users (Desktop Chrome)` project."},{"heading":"configuration","content":"Projects which test logged out users are configured in the `Logged Out Users (Desktop Chrome)` project."},{"heading":"configuration","content":"The `Logged In Users (Desktop Chrome)` project is configured to use the `with-auth` project as a dependency. This means that the `with-auth` project will run first, and then the `Logged In Users (Desktop Chrome)` project will run.\nHowever, the tests themselves run in parallel in each project."},{"heading":"running-tests","content":"Playwright runs in `NODE_ENV=test` mode. It also relies on `.env.test` for environment variables.\nNow, .env.test is configured to use **local supabase** which is setup by running `pnpm supabase start` command.\nThe `pnpm supabase start` command sets up a local supabase instance using docker."},{"heading":"running-tests","content":"The supabase api url is available at &#x2A;*http\\://localhost:54321/** (This is the most important url)"},{"heading":"running-tests","content":"The supabase db url is at &#x2A;*http\\://localhost:54322/**"},{"heading":"running-tests","content":"The supabase studio is available at &#x2A;*http\\://localhost:54323/** (You can use this to use the UI interface )"},{"heading":"running-tests","content":"The supabase inbucket instance is available at &#x2A;*http\\://localhost:54324/** (We use this for testing emails)"},{"heading":"running-tests","content":"Now that supabase is running locally. All we need to do is to run playwright."},{"heading":"running-playwright-in-headless-mode","content":"**Since playwright has been configured to start the local dev server, you don't need to run `pnpm dev` separately.**"},{"heading":"writing-tests","content":"All the test cases are written in the `e2e` folder. The `e2e` folder is divided into two folders:"},{"heading":"writing-tests","content":"Tests which require a user logged in need to be named `test-case.user.spec.ts`"},{"heading":"writing-tests","content":"Tests which don't require a user logged in need to be named `test-case.spec.ts`"},{"heading":"create-organization-test-case-example","content":"This is an example of a test case which requires a user to be logged in."}],"headings":[{"id":"configuration","content":"Configuration"},{"id":"running-tests","content":"Running tests"},{"id":"running-playwright-with-ui","content":"Running playwright with UI"},{"id":"running-playwright-in-headless-mode","content":"Running playwright in headless mode"},{"id":"writing-tests","content":"Writing tests"},{"id":"create-organization-test-case-example","content":"Create Organization Test case example"}]},"url":"/docs/v3/guides/writing-integration-tests","title":"Writing Integration Tests with Playwright","description":"Learn how to write integration tests using Playwright, a powerful end-to-end testing framework"},{"_id":"/docs/v3/guides/writing-unit-tests","structured":{"contents":[{"content":"All versions of Nextbase Starter Kit come with Vitest installed. Vitest is a lightweight and fast testing framework that allows you to write unit tests for your application."},{"heading":"configuration","content":"Vitest is configured in the `vitest.config.ts` file."},{"heading":"configuration","content":"As you can see, the configuration file is quite simple. You can customize it according to your needs."},{"heading":"running-tests","content":"Vitest runs in `NODE_ENV=test` mode. It also relies on `.env.test` for environment variables."},{"heading":"writing-tests","content":"All the test cases are written in the `tests` folder."},{"heading":"helper-function-test-case-example","content":"This is an example of a test case for a helper function."},{"heading":"using-vitest-with-github-actions","content":"You can easily integrate Vitest with GitHub Actions to run your tests automatically on every push or pull request. Here is a basic example of a GitHub Actions workflow:"},{"heading":"using-vitest-with-github-actions","content":"Most SaaS solo developers or small teams write integration tests more, hence we only added Vitest setup and a dummy test case, but you can update the test suite as needed."}],"headings":[{"id":"configuration","content":"Configuration"},{"id":"running-tests","content":"Running tests"},{"id":"running-vitest","content":"Running Vitest"},{"id":"writing-tests","content":"Writing tests"},{"id":"helper-function-test-case-example","content":"Helper Function Test case example"},{"id":"using-vitest-with-github-actions","content":"Using Vitest with GitHub Actions"}]},"url":"/docs/v3/guides/writing-unit-tests","title":"Writing Unit Tests with Vitest","description":"Learn how to write unit tests using Vitest, a lightweight and fast testing framework"},{"_id":"/docs/v3","structured":{"contents":[{"content":"NextBase is the fastest way to start your SaaS project. It comes with incredible set of features pre-built for you. It is built on top of Supabase and NextJS. It is a fullstack SaaS starter kit. Here is the complete stack:"},{"content":"NextJS with App Router"},{"content":"Supabase"},{"content":"React Server Components"},{"content":"Tailwind CSS"},{"content":"Typescript (with strict mode)"},{"content":"Zod (for validation)"},{"content":"React Query"},{"content":"React Hook Form"},{"content":"Sonner (for notifications)"},{"content":"Lucide Icons"},{"content":"Resend and React email (for emails)"},{"content":"Sentry"},{"content":"Posthog (for analytics)"},{"content":"Google Analytics (for analytics)"},{"heading":"feature-list","content":"NextBase is a full-stack SaaS starter kit built on top of Supabase and NextJS. It comes with an incredible set of pre-built features that can help you kickstart your SaaS project. The features are categorized into different tiers - Essential, Pro, Ultimate, and Custom. Here's a detailed breakdown of the features available in each tier:"},{"heading":"all-variants","content":"For a detailed breakdown of all the variants, please check out our Pricing Page."}],"headings":[{"id":"local-dev-setup-video","content":"Local dev setup video"},{"id":"feature-list","content":"Feature list"},{"id":"all-variants","content":"All Variants"}]},"url":"/docs/v3","title":"Welcome to Nextbase v3!"},{"_id":"/docs/v3/installation/dev-environment","structured":{"contents":[{"heading":"setup-env-variables","content":"In your repository directory, you should have a .env.local.example file which looks like this. For dev environment, just copy the .env.local.example file to .env.local. The localhost values are used for local development.So nothing needs to change here."},{"heading":"supabase-env-for-login-with-providers","content":"Note. Supabase also reads from .env.local for local development. Read more about it in this github issue."},{"heading":"supabase-env-for-login-with-providers","content":"https\\://github.com/supabase/cli/issues/1518#issuecomment-2084506041"},{"heading":"supabase-env-for-login-with-providers","content":"It reads from .env.local for local development, or .env in that priority."},{"heading":"supabase-env-for-login-with-providers","content":"Note: If you don't need to login with providers, yet you can avoid setting up the above env variables. However, please ensure that you comment out the authentication provider logic for the\nproviders you don't need in the `supabase/config.toml` file."},{"heading":"run-your-dev-server","content":"We are ready to start our dev server. Run"},{"heading":"run-your-dev-server","content":"`pnpm dev`"},{"heading":"run-your-dev-server","content":"Our local development server is now running at `localhost:3000`."}],"headings":[{"id":"link-your-repository-to-supabase","content":"Link your repository to supabase"},{"id":"setup-env-variables","content":"Setup env variables"},{"id":"supabase-env-for-login-with-providers","content":"Supabase env for login with providers"},{"id":"run-your-dev-server","content":"Run your dev server"}]},"url":"/docs/v3/installation/dev-environment","title":"Dev environment","description":"Setup your dev environment for the Supabase Stripe Starter Kit. Link your repository to Supabase, setup env variables, and run your dev server."},{"_id":"/docs/v3/installation/first-steps","structured":{"contents":[{"content":"Welcome to the Nextbase repository on GitHub!"},{"heading":"prerequisites","content":"Before you get started, make sure you have the following:"},{"heading":"prerequisites","content":"Git installed on your system. If you don't already have it, you can download it from the official Git website (https\\://git-scm.com/)."},{"heading":"prerequisites","content":"A terminal or command prompt. This will be used to run commands and navigate directories."},{"heading":"prerequisites","content":"Node.js installed on your system. If you don't already have it, you can download it from the official Node.js website (https\\://nodejs.org/)."},{"heading":"cloning-the-repository","content":"To get started with the Nextbase project, you will need to clone the repository onto your local machine. Here's how to do it:"},{"heading":"cloning-the-repository","content":"Open a terminal or command prompt and navigate to the directory where you want to store the Nextbase repository."},{"heading":"cloning-the-repository","content":"Run the following command to clone the repository:"},{"heading":"cloning-the-repository","content":"This will create a new directory called \"nextbase\" in your current location, and clone the contents of the repository into it."},{"heading":"cloning-the-repository","content":"Navigate into the repository by running the following command:"},{"heading":"cloning-the-repository","content":"You are now ready to start working with the Nextbase project! To update the repository with the latest changes, run the following command:"},{"heading":"cloning-the-repository","content":"That's it! You should now have a local copy of the Nextbase repository on your machine."}],"headings":[{"id":"prerequisites","content":"Prerequisites"},{"id":"cloning-the-repository","content":"Cloning the Repository"}]},"url":"/docs/v3/installation/first-steps","title":"First Steps","description":"Welcome to the Nextbase repository on GitHub!"},{"_id":"/docs/v3/installation/login","structured":{"contents":[{"heading":"sign-up-using-email","content":"Go to `http://localhost:3000/login` . If you are logging in for the first time, you can sign up and setup your email login credentials."},{"heading":"sign-up-using-email","content":"Once your account is created in supabase local studio, you will receive an email in inbucket, which you can access at http\\://localhost:54324/monitor\\`. Click on the link and login."},{"heading":"sign-up-using-email","content":"Confirm your email and you will be taken to the dashboard! 🎉"},{"heading":"sign-up-using-email-and-password","content":"Go to `http://localhost:3000/login` . For the first time, you can sign up and setup your email and password login credentials."},{"heading":"sign-up-using-email-and-password","content":"Supabase auto-confirms the credentials and does not send any confirmation email."},{"heading":"sign-up-using-email-and-password","content":"You can login now using the same credentials and you will be taken to the dashboard! 🎉"}],"headings":[{"id":"sign-up-using-email","content":"Sign up using email"},{"id":"sign-up-using-email-and-password","content":"Sign up using email and password"}]},"url":"/docs/v3/installation/login","title":"Login to app in development","description":"Login to app in development using email and password."},{"_id":"/docs/v3/installation/setting-up-stripe","structured":{"contents":[{"heading":"for-nextbase-pro-and-nextbase-ultimate-users","content":"There is a much simpler way to set up payments for Nextbase Pro and Nextbase Ultimate users. You can skip the rest of this page.\nPlease follow this guide to set up payments for your Nextbase Pro and Nextbase Ultimate users."},{"heading":"for-nextbase-essential-users","content":"Since there is no admin panel for Essential users, you will need to set up payments manually in Stripe."},{"heading":"create-a-stripe-account","content":"Go to the Stripe website (https\\://stripe.com/) and click on the \"Sign Up\" button in the top right corner.\nFollow the prompts to create a new account. You will need to provide your email address and choose a password.\nOnce you have created your account, log in and complete the onboarding process. You don't need to setup everything while you are setting up."},{"heading":"enter-test-mode","content":"By default your account will be in `Test Mode`. This means you can test your integration without processing real payments. You can switch to `Live Mode` when you are ready to start processing real payments.\nIf you are not in `Test Mode`, you can switch to it by clicking on the `Test Mode` toggle in the top right part of your dashboard."},{"heading":"get-your-api-keys","content":"Once you have setup stripe, head to the Developers section and in the Keys section, grab the Secret key and Publishable key from there. Save them to a secure location. You will need them."},{"heading":"get-your-api-keys","content":"So as of now you have two keys."},{"heading":"get-your-api-keys","content":"If you are using Nextbase Essential, then continue the rest of the guide. If\nyou are using Nextbase Pro or Ultimate, then there is an easier way to setup\nStripe. You can skip the rest of the guide and go to the Setup Stripe\nproducts using admin panel section."},{"heading":"get-your-api-keys","content":"You can still use the rest of the guide to setup Stripe. But checkout the admin panel link. We think\nit is way easier."},{"heading":"setup-webhooks-for-local-testing","content":"Webhooks are a way for you to receive real-time updates from Stripe. You can use webhooks to receive notifications when a payment is made, when a customer is created, and more. This is important for your Supabase project to know when a payment is made so that you can update your database accordingly.\nYou can use webhooks to also track `product` and `price` updates in your app. It has already been configured in Nextbase.\nIn Nextbase, we have configured a webhook endpoint at `/api/stripe/webhooks`. This is the endpoint that Stripe will send the events to. But for Stripe to send events to your local machine, you need to expose your local server to the internet. You can use a tool like ngrok to do this, which can be more flexible, but you can also use Stripe CLI to trigger fixture events and test your webhook locally."},{"heading":"setting-up-using-stripe-cli","content":"Install the Stripe CLI from here."},{"heading":"setting-up-using-stripe-cli","content":"Login to your Stripe account using the CLI."},{"heading":"setting-up-using-stripe-cli","content":"This will ask you to open a link in your browser. Once you have logged in, you can close the browser and return to the terminal."},{"heading":"setting-up-using-stripe-cli","content":"Forward the webhook events to your local server."},{"heading":"setting-up-using-stripe-cli","content":"This will give you a webhook signing secret. Add this to your `.env.local` file."},{"heading":"setting-up-using-stripe-cli","content":"Trigger the following events to create your product and price."},{"heading":"setting-up-using-stripe-cli","content":"Confirm that you see the events in your terminal and the products and prices are created in supabase. Here is how they might look like in your supabase studio. You can visit `http://localhost:54323` and look at your table editor to confirm."},{"heading":"setting-up-using-stripe-cli","content":"Now as your dev server is already running. You can go your app and head over to the organization billing page and you can see the created product with its price there."},{"heading":"setting-up-using-stripe-cli","content":"Test out the `Free Trial` option and the `Choose` option to see if the payment is successful. Use test payment methods and test card numbers from here."},{"heading":"setting-up-using-ngrok","content":"With the dev server running, go to the stripe developers -> webhooks section. The steps are the same as in the previous section. However instead of listening to the events locally and triggering fake events, we will create the products directly in Stripe."},{"heading":"setting-up-using-ngrok","content":"Run a service like ngrok to expose your localhost to the internet. Grab the URL. (e.g. `https://12345678.ngrok.io`)"},{"heading":"setting-up-using-ngrok","content":"Create a webhook endpoint and set the url to your ngrok url. (e.g. `https://12345678.ngrok.io/api/stripe/webhooks`). Select all events."},{"heading":"setting-up-using-ngrok","content":"Make sure that \\` All events \\` are forwared to our webhook listener. (Select the checkbox suggesting to select all events)."},{"heading":"start-creating-products","content":"Now create a couple of `Products` in Stripe, also make sure to create `prices` for each of the products."},{"heading":"start-creating-products","content":"Our database will automatically stay in sync with Stripe and create the necessary rows in the database."}],"headings":[{"id":"for-nextbase-pro-and-nextbase-ultimate-users","content":"For Nextbase Pro and Nextbase Ultimate Users"},{"id":"for-nextbase-essential-users","content":"For Nextbase Essential Users"},{"id":"create-a-stripe-account","content":"Create a Stripe account"},{"id":"enter-test-mode","content":"Enter Test Mode"},{"id":"get-your-api-keys","content":"Get your API keys"},{"id":"setup-webhooks-for-local-testing","content":"Setup Webhooks for local testing"},{"id":"setting-up-using-stripe-cli","content":"Setting up using Stripe CLI"},{"id":"setting-up-using-ngrok","content":"Setting up using Ngrok"},{"id":"start-creating-products","content":"Start creating products"}]},"url":"/docs/v3/installation/setting-up-stripe","title":"Setting up Stripe","description":"Setting up Stripe for your Supabase project. Learn how to create products, webhooks, and more."},{"_id":"/docs/v3/installation/setting-up-supabase-locally","structured":{"contents":[{"content":"To get started with Supabase locally, you will just need to pull the docker image. Here's how to do it:"},{"heading":"setting-up-docker-for-supabase","content":"First, you will need to install Docker on your machine. You can download the installer from the Docker website since it is the recommended way to install Docker on Windows and macOS. If you are using Linux, you can follow the instructions for your specific distribution on the Docker documentation."},{"heading":"setting-up-docker-for-supabase","content":"Next, you will need to setup the env variables."},{"heading":"setting-up-docker-for-supabase","content":"In your repository directory, you should have a `.env.local.example` file which looks like this. For dev environment, just copy the `.env.local.example` file to `.env.local`. The localhost values are used for local development.So nothing needs to change here."},{"heading":"setting-up-docker-for-supabase","content":"Note. Supabase also reads from .env.local for local development. If you don't need to setup social providers like github, google yet, just leave the dummy values in env.local file. Removing the values will however break supabase start, since the config.toml has social providers enabled by default. Either disable them in config.toml or leave the dummy values. Read more here."},{"heading":"setting-up-docker-for-supabase","content":"Once you have Docker installed and running in your machine, you can now pull the Supabase Docker image by running `pnpm supabase start` in your terminal."},{"heading":"setting-up-docker-for-supabase","content":"> Please note that for the first time you run the `pnpm supabase start` command, it will take some time to pull the Docker image. Subsequent runs will be faster since the images will be cached and volumes will be persisted."},{"heading":"setting-up-docker-for-supabase","content":"After running the command, you should see the output simmilar to the following:"},{"heading":"setting-up-docker-for-supabase","content":"You can now access the Supabase Studio by visiting the URL `http://localhost:54323` in your browser."},{"heading":"setting-up-docker-for-supabase","content":"You can also access the API and GraphQL URLs to interact with your local Supabase instance."},{"heading":"setting-up-docker-for-supabase","content":"To connect database clients to your local Supabase instance, you can use the DB URL `postgresql://postgres:postgres@localhost:54322/postgres`"},{"heading":"setting-up-docker-for-supabase","content":"To monitor your inboxes, you can use the Inbucket URL `http://localhost:54324`"},{"heading":"setting-up-docker-for-supabase","content":"You can stop the Supabase server by running the `pnpm supabase stop` command, to save on system resources."}],"headings":[{"id":"setting-up-docker-for-supabase","content":"Setting up Docker for Supabase"}]},"url":"/docs/v3/installation/setting-up-supabase-locally","title":"Setting up Supabase Locally","description":"Learn how to set up Supabase locally for development. This guide will show you how to pull the Docker image and set up the environment variables for your Next.js application."},{"_id":"/docs/v3/installation/supabase-for-production","structured":{"contents":[{"content":"To get started with Supabase, you will need to set up an account and create a project. Here's how to do it:"},{"heading":"creating-an-account","content":"Go to the Supabase website (https\\://supabase.com/) and click on the \"Sign Up\" button in the top right corner."},{"heading":"creating-an-account","content":"Follow the prompts to create a new account. You will need to provide your email address and choose a password."},{"heading":"creating-a-project","content":"Once you have created your account, log in and click on the \"Create Project\" button in the dashboard."},{"heading":"creating-a-project","content":"Give your project a name and choose a region for your data to be stored in. You will also need to create a database password. Make sure to save this password in a secure location, as you will need it later to access your database. Then click on the \"Create Project\" button to finish creating your project."},{"heading":"accessing-your-project","content":"You should now see your new project listed in the dashboard. Click on the project name to open it and start working with Supabase."},{"heading":"auth-configuration","content":"Go to the \"Authentication\" section of your project usually at `https://app.supabase.io/project/<project-id>/auth`."},{"heading":"auth-configuration","content":"Go to the URL Configuration tab."},{"heading":"auth-configuration","content":"Set the Site URL to `http://localhost:3000` or it can also be your production domain e.g. `https://myapp.com`. (This is not as important as the Redirect URL configuration)"},{"heading":"auth-configuration","content":"Set Redirect URLs to\na. `http://localhost:3000/auth/callback` (for local development) or `http://localhost:3000/**` (this means all routes under `http://localhost:3000` will be accepted as a valid redirect URL)\nb. `https://myapp.com/auth/callback` (for production) or `https://myapp.com/**` (this means all routes under `https://myapp.com` will be accepted as a valid redirect URL)"},{"heading":"install-dependencies-and-link-your-project","content":"run `pnpm install`"},{"heading":"install-dependencies-and-link-your-project","content":"Supabase login within your terminal"},{"heading":"install-dependencies-and-link-your-project","content":"Note: If you haven't used supabase before in the terminal, you will need to login with supabase in the terminal."},{"heading":"install-dependencies-and-link-your-project","content":"Create an access token for supabase here https\\://app.supabase.com/account/tokens"},{"heading":"install-dependencies-and-link-your-project","content":"Copy the access token and run `pnpm supabase login`"},{"heading":"install-dependencies-and-link-your-project","content":"It will prompt for an access token. Paste the token there. It should log you in."},{"heading":"install-dependencies-and-link-your-project","content":"Run `pnpm supabase link --project-ref {projectRef}` . Your project ref is available in the project settings section within the supabase dashboard."},{"heading":"install-dependencies-and-link-your-project","content":"Once you run the command, you will prompted for the database password. Enter the password, you saved earlier."},{"heading":"install-dependencies-and-link-your-project","content":"When it completes, your project will be linked to supabase."},{"heading":"install-dependencies-and-link-your-project","content":"Run `pnpm supabase db push` . This will push all the nextbase magic into your own supabase project. Teams, admin panel, projects etc will now be ready for use."},{"heading":"setup-env-variables-for-production","content":"In your repository directory, you should have a .env.local.example file which looks like this. Create a `.env.local` file right beside it but instead of dummy values like `YOUR_SUPABASE_PROJECT_REF`, fill the actual details."},{"heading":"setup-env-variables-for-production","content":"**NEXT\\_PUBLIC\\_SUPABASE\\_URL** is available in the project api settings section in supabase. It's called Project URL over there."},{"heading":"setup-env-variables-for-production","content":"**NEXT\\_PUBLIC\\_SUPABASE\\_PUBLISHABLE\\_KEY** and **SUPABASE\\_SECRET\\_KEY** are also available in the Project API settings section in supabase."},{"heading":"setup-env-variables-for-production","content":"Setup a Sendgrid / Post mark account and once you do, grab your authorised email sender address and add it to **ADMIN\\_EMAIL**. Same goes for **SENDGRID\\_API\\_KEY**. (If you want to use a different email provider, create a different environment variable here and set the relevant value)."},{"heading":"setup-env-variables-for-production","content":"Ensure that **NEXT\\_PUBLIC\\_SITE\\_URL** points to your domain."},{"heading":"setup-env-variables-for-production","content":"The other env variables should be relatively easy to setup."}],"headings":[{"id":"creating-an-account","content":"Creating an Account"},{"id":"creating-a-project","content":"Creating a Project"},{"id":"accessing-your-project","content":"Accessing Your Project"},{"id":"auth-configuration","content":"Auth configuration"},{"id":"install-dependencies-and-link-your-project","content":"Install dependencies and link your project"},{"id":"setup-env-variables-for-production","content":"Setup env variables for production"}]},"url":"/docs/v3/installation/supabase-for-production","title":"Setting up Supabase for Production","description":"Learn how to set up Supabase for production. This guide will walk you through creating an account, creating a project, and setting up environment variables for production."},{"_id":"/docs/v3/integrations/api-key-authentication-with-unkey","structured":{"contents":[{"content":"Nextbase Ultimate comes with unkey configured."},{"content":"When using the latest Supabase with asymmetric keys, the JWT signing approach has changed. New\ndocs are on the way, but please view this GitHub\ncomment in the meantime."},{"heading":"why-use-unkey","content":"Unkey is a service that allows you to create API keys for your users."},{"heading":"why-use-unkey","content":"It allows you to create API keys for your users without having to store them in your database."},{"heading":"why-use-unkey","content":"It allows you to revoke API keys for your users without having to update your database."},{"heading":"why-use-unkey","content":"Keys can be created for limited time periods too."},{"heading":"why-use-unkey","content":"The keys can then be used to authenticate your users to your API endpoints."},{"heading":"why-use-unkey","content":"Usage with Zapier, Make and Integromat are also now easy."},{"heading":"configuring-unkey","content":"Necessary env variables that you need for Unkey after creating an account there"},{"heading":"configuring-unkey","content":"1 UNKEY\\_ROOT\\_KEY:"},{"heading":"configuring-unkey","content":"This variable likely represents the root key associated with Unkey.\nIt may be used for specific administrative or configuration purposes within Unkey."},{"heading":"configuring-unkey","content":"2 UNKEY\\_API\\_ID:"},{"heading":"configuring-unkey","content":"This variable seems to be associated with an API ID in the context of Unkey.\nIt may serve as an identifier for your Unkey API integration."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Import Unkey Middleware\nImport the `withUnkeyAuth` middleware from the Unkey package at the beginning of your route handler file."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Implement the Route Handler\nCreate your route handler logic, ensuring that it includes the withUnkeyAuth middleware. This middleware handles the API key authentication process."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Now, this route handler is protected, and only requests with valid API keys will be allowed to access it."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"By following these steps, you can configure Unkey and create route handlers with API key authentication in your Nextbase project, ensuring secure and authenticated access to your protected routes."},{"heading":"disabling-unkey","content":"To disable Unkey, you can either remove its middleware from your route handlers or adjust your configuration to bypass Unkey authentication."},{"heading":"removing-unkey-middleware","content":"In your route handler files, look for the import statement that includes Unkey middleware and remove or comment out the line."},{"heading":"exposing-routes-which-can-be-used-with-api-key-auth","content":"To expose routes with API key authentication, follow these steps:"},{"heading":"1-import-unkey-middleware","content":"In your route handler files, import the Unkey middleware (`withUnkeyAuth`) at the beginning of the file. This middleware will handle the API key authentication process."},{"heading":"2-use-unkey-middleware-in-route-handlers","content":"Apply the Unkey middleware to the routes you want to expose with API key authentication. Wrap your route handler logic with withUnkeyAuth:"},{"heading":"2-use-unkey-middleware-in-route-handlers","content":"By applying withUnkeyAuth to a route handler, you ensure that the route is protected and requires a valid API key for access."},{"heading":"bonus-usage-with-zapier","content":"Since API key auth is now built in to Nextbase, configuring Zapier, Make or Integromat is now easy."},{"heading":"bonus-usage-with-zapier","content":"Zapier guide coming soon."},{"heading":"bonus-usage-with-zapier","content":"TODO"}],"headings":[{"id":"why-use-unkey","content":"Why use Unkey?"},{"id":"configuring-unkey","content":"Configuring unkey"},{"id":"steps-for-creating-unkey-root-key","content":"Steps for creating Unkey Root key"},{"id":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Creating an route handler which can be used with api key auth"},{"id":"disabling-unkey","content":"Disabling unkey"},{"id":"removing-unkey-middleware","content":"Removing Unkey Middleware"},{"id":"exposing-routes-which-can-be-used-with-api-key-auth","content":"Exposing routes which can be used with API key auth"},{"id":"1-import-unkey-middleware","content":"1\\. Import Unkey Middleware"},{"id":"2-use-unkey-middleware-in-route-handlers","content":"2\\. Use Unkey Middleware in Route Handlers"},{"id":"bonus-usage-with-zapier","content":"(Bonus): Usage with Zapier"}]},"url":"/docs/v3/integrations/api-key-authentication-with-unkey","title":"API Key Authentication with Unkey","description":"Learn how to use Unkey for API key authentication in Nextbase Ultimate and how to expose routes with API key auth."},{"_id":"/docs/v3/integrations/resend-and-react-email-integration","structured":{"contents":[{"heading":"resend-and-react-email-integration","content":"Nextbase comes with Resend and React Email configured. This makes creating/modifying email templates and sending them to your customers very easy."},{"heading":"resendcom","content":"Resend serves as the backbone for handling email delivery in Nextbase.\nIt streamlines the process of sending emails, managing templates, and handling various aspects of email communication.\nWith Resend, you can focus on creating compelling content, knowing that the delivery mechanism is robust and reliable."},{"heading":"resendcom","content":"**You can use a different email service provider if you want to.**"},{"heading":"how-to-use-resend-in-nextjs","content":"Resend is usually configured like this in Next.js."},{"heading":"how-to-use-resend-in-nextjs","content":"Install resend as a dependency and then import it into your application."},{"heading":"how-to-use-resend-in-nextjs","content":"Create an account in Resend and get your API key."},{"heading":"how-to-use-resend-in-nextjs","content":"Add your API key to your environment variables."},{"heading":"how-to-use-resend-in-nextjs","content":"Then, you can use it like this."},{"heading":"react-email-declarative-email-templating","content":"`react-email` is a React library that allows you to compose email templates using familiar React components. Instead of dealing with complex HTML templates, you can leverage the power of React to create modular and dynamic email content. This integration aligns with the React philosophy, making it intuitive for developers already familiar with React development.\nNextbase provides a seamless integration of Resend and `react-email` so that you can harness the benefits of both tools effortlessly."},{"heading":"emails-folder","content":"Nextbase comes with the `emails` folder configured as the repository for your React Email components. You can write your emails here and also preview them with the email development server by running `pnpm email-dev`."},{"heading":"react-email-development-server","content":"In Nextbase, a React Email development server is set up to streamline the process of writing and previewing emails. This development server allows developers to interactively design and visualize emails in a browser, facilitating a more efficient email template creation workflow."},{"heading":"react-email-development-server","content":"**Package.json Script**"},{"heading":"react-email-development-server","content":"The React Email development server is typically configured as a script in the package.json file. The script is named \"email:dev\":"},{"heading":"react-email-development-server","content":"To start the React Email development server, developers can run the following command in the terminal:"},{"heading":"react-email-development-server","content":"This starts the React Email development server and provides a convenient way to preview email templates."},{"heading":"react-email-development-server","content":"**/src/emails:**"},{"heading":"react-email-development-server","content":"Each file in the `emails` folder corresponds to a specific type of email, and it contains a React component responsible for rendering the content of that email.\nWe can then send the emails using `Resend`. Here are some emails we already have in the `emails` folder:"},{"heading":"welcomeemailtsx","content":"In this example, the WelcomeEmail component represents the content of a welcome email. This declarative approach simplifies the process of creating and maintaining various email templates in your application."},{"heading":"invitationtojointsx","content":"This is the organization invitation email template. When a user is invited to join an organization, this email is sent to them."},{"heading":"invitationtojointsx","content":"The email is then sent like this."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"It is recommended to use a third-party SMTP service for production environments in Supabase."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"Reliability: Supabase built-in SMTP server is severely rate limited. It is only suitable for development and testing purposes. Supabase recommends using a third-party SMTP service for production environments. Resend provides a robust and reliable email delivery mechanism."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"Observability: Resend provides a dashboard to monitor email delivery and other metrics."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"In a production environment, it's crucial to ensure reliable email delivery. Ensure that Resend or another third-party SMTP service is configured in your Supabase project."},{"heading":"react-email-and-resend-integration-in-nextbase","content":"See `src/utils/api-routes/utils.ts` for an example of how to we use Resend in Nextbase.\nHere the `sendEmail` function is used to send emails using Resend. It receives the email options as an argument and sends the email using Resend."},{"heading":"configuration","content":"You only require the API key to configure Resend. You can find the API key in the Resend dashboard."}],"headings":[{"id":"resend-and-react-email-integration","content":"Resend and React Email Integration"},{"id":"resendcom","content":"Resend.com"},{"id":"how-to-use-resend-in-nextjs","content":"How to use Resend in Next.js?"},{"id":"react-email-declarative-email-templating","content":"React-email: Declarative Email Templating"},{"id":"emails-folder","content":"`emails` folder"},{"id":"react-email-development-server","content":"React Email Development Server"},{"id":"welcomeemailtsx","content":"WelcomeEmail.tsx:"},{"id":"invitationtojointsx","content":"InvitationToJoin.tsx"},{"id":"optional-configure-resend-with-supabase-smtp-server","content":"(Optional) Configure Resend with Supabase SMTP Server"},{"id":"react-email-and-resend-integration-in-nextbase","content":"React Email and Resend Integration in Nextbase"},{"id":"configuration","content":"Configuration"}]},"url":"/docs/v3/integrations/resend-and-react-email-integration","title":"Resend and React Email Integration","description":"Learn how to use Resend and React Email in Nextbase."},{"_id":"/docs/v3/known-issues/build-error-undici","structured":{"contents":[{"heading":"build-error-with-undici","content":"You may encounter a build error related to Undici when trying to deploy your Next.js application to a production environment. This issue typically occurs when using `localhost` URLs in your production configuration."},{"heading":"the-problem","content":"The error message might look something like this:"}],"headings":[{"id":"build-error-with-undici","content":"Build Error with Undici"},{"id":"the-problem","content":"The Problem"}]},"url":"/docs/v3/known-issues/build-error-undici","title":"Build Error with Undici","description":"How to resolve the build error related to Undici when using localhost URLs in production environments."},{"_id":"/docs/v3/known-issues/fetch-failed-build-error-undici","structured":{"contents":[{"heading":"build-error-with-undici","content":"You may encounter a build error related to Undici when trying to deploy your Next.js application to a production environment. This issue typically occurs when using `localhost` URLs in your production configuration."},{"heading":"the-problem","content":"The error message might look something like this:"},{"heading":"references","content":"This issue on the next.js GitHub repository talks about it:"},{"heading":"references","content":"https\\://github.com/vercel/next.js/issues/44062"},{"heading":"references","content":"https\\://github.com/vercel/next.js/issues/44062#issuecomment-1445183293"},{"heading":"fix","content":"To fix the issue, you need to ensure that the build environment doesn't have environment variables that point to `localhost`. If they are, you need to change them to 127.0.0.1."}],"headings":[{"id":"build-error-with-undici","content":"Build Error with Undici"},{"id":"the-problem","content":"The Problem"},{"id":"references","content":"References"},{"id":"fix","content":"Fix"}]},"url":"/docs/v3/known-issues/fetch-failed-build-error-undici","title":"Build Error with Undici","description":"How to resolve the build error related to Undici when using localhost URLs in production environments."},{"_id":"/docs/v3/known-issues/supabase-get-session-warning","structured":{"contents":[{"heading":"supabase-getsession-warning","content":"You may encounter a warning when using the `getSession()` method from Supabase's auth-js library. This is a known issue that has been identified and is being addressed by the Supabase team.\nIn most cases, the warning can be safely ignored since this warning features most of the time because of usage in middleware.\nHowever, a middleware is a critical part of any Next.js application's speed of route resolution and as several users pointed out in the github issue, this warning is unnecessary as middlewares should avoid network requests as much as possible and only\nauthenticate by validating the cookie. A network for each middleware path will slow down app significantly."},{"heading":"current-status","content":"We are aware of this issue, which is tracked in the following GitHub pull request:"},{"heading":"current-status","content":"https\\://github.com/supabase/auth-js/pull/895"},{"heading":"temporary-workaround","content":"The Supabase team was requested to remove the warning. Once they implement the fix upstream, we will update our package to incorporate the changes. This update will eliminate the warning message.\nWhile waiting for the official fix, you can safely continue using the `getSession()` method. The warning does not affect the functionality of your application, but it may appear in your console logs."},{"heading":"stay-updated","content":"We recommend keeping an eye on the GitHub pull request mentioned above for the most up-to-date information on the progress of this fix. Once the update is available, we will promptly integrate it into our package."}],"headings":[{"id":"supabase-getsession-warning","content":"Supabase getSession() Warning"},{"id":"current-status","content":"Current Status"},{"id":"temporary-workaround","content":"Temporary Workaround"},{"id":"stay-updated","content":"Stay Updated"}]},"url":"/docs/v3/known-issues/supabase-get-session-warning","title":"Supabase getSession() Warning","description":"Explanation of the known warning when using Supabase getSession() and the upcoming fix."},{"_id":"/docs/v3/payments/stripe-configuration","structured":{"contents":[{"content":"Stripe is implemented using an Abstract payment provider. It has the following shape."}],"headings":[]},"url":"/docs/v3/payments/stripe-configuration","title":"Stripe Configuration"},{"_id":"/docs/v3/payments/stripe-webhooks","structured":{"contents":[{"content":"Stripe webhooks are crucial for keeping your application in sync with events that happen in Stripe, such as successful payments, subscription updates, or failed charges. In NextBase, we've set up a webhook endpoint to handle these events. Let's break down how it's configured:"},{"heading":"webhook-endpoint","content":"The webhook endpoint is set up in `src/app/api/stripe/webhooks/route.ts`. This file contains a POST route handler that processes incoming webhook events from Stripe."},{"heading":"webhook-endpoint","content":"The webhook syncs products, prices, and subscriptions with our database."}],"headings":[{"id":"webhook-endpoint","content":"Webhook Endpoint"}]},"url":"/docs/v3/payments/stripe-webhooks","title":"Stripe Webhooks"},{"_id":"/docs/v3/performance/strategies-for-pages","structured":{"contents":[{"heading":"strategies-for-pages","content":"In this Next.js 14 codebase, we employ several strategies to enhance page performance using async/await and Suspense. These techniques are crucial for optimizing data fetching and rendering, ensuring a smooth user experience."},{"heading":"strategies-for-pages","content":"Asynchronous Data Fetching in Server Components"},{"heading":"strategies-for-pages","content":"Server components in Next.js can execute asynchronously, allowing us to fetch data on the server before rendering the component. This approach reduces bundle sizes and leverages server CPU power for faster data fetching."},{"heading":"strategies-for-pages","content":"For example, in the roadmap page, we asynchronously fetch data for different roadmap sections like planned, in progress, and completed tasks using async functions before rendering the components."},{"heading":"strategies-for-pages","content":"Parallel Data Fetching with Suspense"},{"heading":"strategies-for-pages","content":"We use React's Suspense component to fetch multiple data sources in parallel and render child components as soon as the data is available. This technique allows the page to start rendering without waiting for all data to be fetched, improving perceived performance."},{"heading":"strategies-for-pages","content":"For instance, the blog list page fetches blog posts and tags in parallel, wrapped in Suspense, enhancing the user experience by streaming rendering."},{"heading":"strategies-for-pages","content":"Server components have the added benefit of streaming rendering. Consider the page responsible for rendering the blog list page at `/blog`. This page has to fetch two sets of data: the list of blog posts and the list of tags. We can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components: `Tags` and `BlogList`."},{"heading":"strategies-for-pages","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"strategies-for-pages","content":"Suspense for Incremental Rendering"},{"heading":"strategies-for-pages","content":"We also use Suspense to incrementally render parts of the page as data becomes available. This approach is evident in the changelog page, where the changelog list is rendered asynchronously and wrapped in Suspense to display a loading state until the data is fetched."}],"headings":[{"id":"strategies-for-pages","content":"Strategies for pages"}]},"url":"/docs/v3/performance/strategies-for-pages","title":"Performance Strategies"},{"_id":"/docs/v3/performance/streaming-suspense-rendering","structured":{"contents":[{"heading":"streaming-server-rendering","content":"In our Next.js 14 pages, we leverage streaming server rendering primarily through the use of server components and the Suspense component for data fetching and rendering optimization. This approach allows us to start rendering the page even before all the data is fetched, improving the user experience by displaying content as soon as it's available."},{"heading":"example-of-streaming-server-rendering","content":"In the blog list page, we demonstrate streaming server rendering by fetching and rendering two sets of data in parallel: the list of blog posts and the list of tags. This is facilitated by moving the data fetching into two separate components, Tags and BlogList, which are server components. By wrapping these components in Suspense, we can render them in parallel and stream the rendering of the page as soon as the data is available, without waiting for all data to be fetched."},{"heading":"example-of-streaming-server-rendering","content":"Server components have the added benefit of streaming rendering.\nConsider the page responsible for rendering the blog list page at **/blog** route.\nThis page has to fetch two sets of data: the list of blog posts and the list of tags.\nWe can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components:"},{"heading":"example-of-streaming-server-rendering","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"benefits","content":"Improved User Experience: The page starts rendering immediately, displaying the \"All blog posts\" title and placeholders for tags and posts until the actual data is fetched. This approach significantly enhances the perceived performance of the application."},{"heading":"benefits","content":"Efficient Data Fetching: By fetching data in server components, we leverage the server's CPU power, leading to faster data retrieval and smaller bundle sizes since we don't have to ship the data fetching logic to the client.\nImplementation"},{"heading":"benefits","content":"The implementation of streaming server rendering in our Next.js 14 pages is a testament to the framework's capabilities in optimizing performance and enhancing the user experience. By judiciously using server components and Suspense, we ensure that our pages are not only fast but also provide immediate feedback to users, which is crucial for modern web applications."}],"headings":[{"id":"streaming-server-rendering","content":"Streaming server rendering"},{"id":"example-of-streaming-server-rendering","content":"Example of Streaming Server Rendering"},{"id":"benefits","content":"Benefits"}]},"url":"/docs/v3/performance/streaming-suspense-rendering","title":"Streaming Suspense Server Rendering"},{"_id":"/docs/v3/quick-setup","structured":{"contents":[{"heading":"super-quick-dev-setup","content":"This guide covers the basic setup for Supabase and Next.js."},{"heading":"super-quick-dev-setup","content":"For integrating Stripe, refer to the comprehensive setup guide."},{"heading":"1-clone-the-repository-and-install-dependencies","content":"This command installs all necessary dependencies."},{"heading":"2-copy-envlocalexample","content":"Duplicate the `.env.local.example` file at the project root as `.env.local`."},{"heading":"3-launch-supabase-locally","content":"**Note:** Ensure Docker is installed on your system. Visit Docker's documentation for installation details."},{"heading":"4-start-the-app-by-running-the-following-command","content":"You're all set! The app should now be accessible at http\\://localhost:3000.\nStripe and pricing aren't configured yet, but you can start building your app with Supabase and Next.js."},{"heading":"optional-setting-up-stripe-locally","content":"To set up Stripe locally, refer to the comprehensive setup guide."}],"headings":[{"id":"super-quick-dev-setup","content":"Super quick dev setup"},{"id":"nextbase-ultimate-setup-video","content":"Nextbase Ultimate setup video"},{"id":"1-clone-the-repository-and-install-dependencies","content":"1\\. Clone the repository and install dependencies:"},{"id":"2-copy-envlocalexample","content":"2\\. Copy .env.local.example"},{"id":"3-launch-supabase-locally","content":"3\\. Launch Supabase locally:"},{"id":"4-start-the-app-by-running-the-following-command","content":"4\\. Start the App by running the following command:"},{"id":"optional-setting-up-stripe-locally","content":"(Optional) Setting up **Stripe** locally"}]},"url":"/docs/v3/quick-setup","title":"Quick setup","description":"Setting up nextbase is easy. Follow these steps to get started."},{"_id":"/docs/v3/security/access-control-on-supabase-functions","structured":{"contents":[{"content":"Supabase functions can be accessed by anyone, but we can control access to them using Supabase RBAC."}],"headings":[]},"url":"/docs/v3/security/access-control-on-supabase-functions","title":"Access control on Supabase functions","description":"How to control access to your Supabase functions."},{"_id":"/docs/v3/security/protected-nextjs-areas","structured":{"contents":[{"heading":"protected-nextjs-pages-and-api-routes","content":"The `app_admin` folder is only accessible to application admins. Hence all pages which start with the route `app_admin` are only accessible to app admins."},{"heading":"protected-nextjs-pages-and-api-routes","content":"The `dashboard` route , `setting` routes, `organization` routes are all only accessible to logged in users."},{"heading":"protected-nextjs-pages-and-api-routes","content":"Other pages are public."},{"heading":"creating-new-pags","content":"To create pages only accessible to logged in users, create them within the `(authenticated-pages)/(user-pages)` folders. This will make them available to all pages including app\\_admins."},{"heading":"creating-new-pags","content":"To create pages only accessible to app\\_admin users, create them within the `(authenticated-users)/(app_admin)` folder."},{"heading":"creating-new-pags","content":"`(external-pages)` are available to logged out users as well."},{"heading":"creating-new-pags","content":"`(login-pages)` should contain all authentication related pages such as login, signup etc."}],"headings":[{"id":"protected-nextjs-pages-and-api-routes","content":"Protected Next.js pages and api routes"},{"id":"creating-new-pags","content":"Creating new pags"}]},"url":"/docs/v3/security/protected-nextjs-areas","title":"Protected Next.js areas","description":"How to create new pages and routes that are protected."},{"_id":"/docs/v3/security/row-level-security","structured":{"contents":[{"content":"Row Level Security (RLS) is a feature in PostgreSQL that provides the ability to control which users can `SELECT`, `INSERT`, `UPDATE`, or `DELETE` which rows in a table. It adds an additional layer of security in multi-user database environments and is crucial for protecting sensitive data.\nUnderstanding Row Level Security"},{"content":"RLS works by enabling you to define policies that restrict, on a per-user basis, whether certain rows can be returned by a `SELECT` query or modified by an `INSERT`, `UPDATE`, or `DELETE` query. This is particularly useful in multi-tenant environments where you want to ensure that users can only access their own data."},{"content":"In PostgreSQL, RLS is disabled by default for all tables. To enable RLS, you use the `ALTER TABLE` command with the ENABLE ROW LEVEL SECURITY clause. Once RLS is enabled, all users (except the table owner and superusers) are denied access to the table—unless a policy is explicitly added to grant them access."},{"heading":"importance-of-row-level-security","content":"RLS is important for a variety of reasons:"},{"heading":"importance-of-row-level-security","content":"Data Protection: RLS helps to protect sensitive data by ensuring that users can only access data that they are authorized to see. This is particularly important in industries such as healthcare or finance where regulations require strict controls over data access."},{"heading":"importance-of-row-level-security","content":"Multi-tenancy: In multi-tenant environments, RLS can be used to ensure that each tenant can only access their own data. This is much more efficient and secure than having separate tables or databases for each tenant."},{"heading":"importance-of-row-level-security","content":"Flexibility: RLS policies can be defined using any boolean SQL expression, which provides a great deal of flexibility. For example, you could define a policy that only allows users to access data that was created within the last 30 days."}],"headings":[{"id":"importance-of-row-level-security","content":"Importance of Row Level Security"}]},"url":"/docs/v3/security/row-level-security","title":"Row Level Security","description":"Row Level Security (RLS) is a feature in PostgreSQL that provides the ability to control which users can SELECT, INSERT, UPDATE, or DELETE which rows in a table. It adds an additional layer of security in multi-user database environments and is crucial for protecting sensitive data."},{"_id":"/docs/v3/tech-stack/why-nextjs","structured":{"contents":[{"content":"There are several reasons why we chose Next.js over other options when building a server-rendered React application:"},{"content":"Ease of use: Next.js is designed to be easy to learn and use, with a simple and intuitive API and a wide range of built-in features. This can make it a good choice for developers who are new to server-rendered React applications or who want to get up and running quickly."},{"content":"Performance: Next.js is optimized for performance, with features such as automatic code splitting and server rendering that can help to improve the load time and overall performance of our application."},{"content":"Scalability: Next.js is designed to be scalable, with support for static exports and the ability to run on a serverless architecture. This can make it a good choice for applications that are expected to handle a large number of users or a high volume of traffic."},{"content":"Community and ecosystem: Next.js has a large and active community of developers, with a wide range of resources and tools available to support building applications with the framework. This can make it easier to find help and support when working with Next.js."},{"content":"Next 13 is massive leap in terms of performance and developer experience and we experienced it while building apps in our other projects and we think no one should miss out on using Next.js 14+."},{"content":"It really is taking the world by storm.\nhttps\\://mobilecoderz.com/blog/next-js-framework-the-next-big-thing-of-2022/"}],"headings":[]},"url":"/docs/v3/tech-stack/why-nextjs","title":"Why Next.js?","description":"Why we chose Next.js for server-rendered React applications."},{"_id":"/docs/v3/tech-stack/why-shadcn-ui","structured":{"contents":[{"content":"Shadcn UI is a component library that is built on top of Radix UI. It is a set of reusable UI components that are styled with Tailwind CSS. It is designed to be used with Next.js and React."},{"content":"We chose Shadcn UI because it is a modern and easy to use component library that is built on top of Radix UI. It is designed to be used with Next.js and React."},{"content":"It is built with Tailwind CSS and TypeScript. It is designed to be used with Next.js and React."},{"content":"It is designed to be used with Next.js and React."}],"headings":[]},"url":"/docs/v3/tech-stack/why-shadcn-ui","title":"Why Shadcn UI?","description":"Why we chose Shadcn UI for our design system."},{"_id":"/docs/v3/tech-stack/why-stripe","structured":{"contents":[{"content":"While there are many payments providers today, we went with Stripe which is the leading payments provider of choice for most SaaS companies today."},{"content":"Wide range of payment options: Stripe supports a wide range of payment methods, including credit cards, debit cards, and online payment services such as Apple Pay and Google Pay. This can make it easy for businesses to accept payments from a variety of sources."},{"content":"Customizable payment forms: Stripe allows businesses to customize the look and feel of their payment forms to match their brand. This can help to create a seamless and professional payment experience for customers."},{"content":"Advanced fraud protection: Stripe provides advanced fraud protection to help businesses detect and prevent fraudulent activity. This can help to reduce the risk of chargebacks and other fraudulent activity."},{"content":"Easy integration: Stripe offers a wide range of tools and libraries to make it easy to integrate the platform into a business's existing systems and processes. This can help to streamline the payment process and reduce the time and effort required to manage payments."}],"headings":[]},"url":"/docs/v3/tech-stack/why-stripe","title":"Why Stripe?","description":"Why Stripe is the leading payments provider of choice for most SaaS companies today. Wide range of payment options, customizable payment forms, advanced fraud protection, and easy integration."},{"_id":"/docs/v3/tech-stack/why-supabase","structured":{"contents":[{"content":"Supabase has had a breakthrough last couple of years in SaaS products usage. It is the new Firebase but better."},{"content":"We went with Supabase because it makes backend and database development a breeze."},{"content":"It is realtime first, open source and built on top of Postgres. It provides a powerful dashboard, with many many features. Some include"},{"content":"Easy to use Javascript and Node.js clients. They also have Flutter, Swift and Dart clients to easily built mobile apps later."},{"content":"Realtime with just a checkbox. All your database tables can be made realtime with just a click of a button."},{"content":"Database hooks - Allows you to integrate with Zapier, Make etc and connect your app to other apps"},{"content":"Edge functions"},{"content":"Database triggers and functions"},{"content":"Admin Panel"},{"content":"Custom reports and Graphs"},{"content":"Errors dashboard"},{"content":"Foreign Table Wrappers (In development) and so many more."},{"content":"If you want to build robust webapps quickly, Supabase is the way to go in 2023."}],"headings":[]},"url":"/docs/v3/tech-stack/why-supabase","title":"Why Supabase?","description":"Why we chose Supabase for our backend and database needs"},{"_id":"/docs/v3/troubleshooting/login-callback-wrong","structured":{"contents":[{"content":"This is generally because the Supabase Authentication / URL configuration is not setup correctly. Ensure the following:"},{"content":"The Site URL is set to your production domain eg: `https://usenextbase.com`"},{"content":"The Redirect urls include both your production and development hosts"},{"content":"eg : `https://usenextbase/com/**` and `http://localhost:3000/**`"}],"headings":[]},"url":"/docs/v3/troubleshooting/login-callback-wrong","title":"I am unable to login","description":"This is generally because the Supabase Authentication / URL configuration is not setup correctly."},{"_id":"/docs/v3/ui/dark-mode","structured":{"contents":[{"heading":"dark-mode","content":"NextBase supports dark mode for your application."}],"headings":[{"id":"dark-mode","content":"Dark Mode"}]},"url":"/docs/v3/ui/dark-mode","title":"Dark Mode","description":"NextBase supports dark mode for your application."},{"_id":"/docs/v3/ui/nprogress","structured":{"contents":[{"heading":"nprogress","content":"NProgress is a library that provides a progress bar for your application."}],"headings":[{"id":"nprogress","content":"NProgress"}]},"url":"/docs/v3/ui/nprogress","title":"NProgress","description":"NProgress is a library that provides a progress bar for your application."},{"_id":"/docs/v3/ui/shadcn-ui","structured":{"contents":[{"heading":"shadcn-ui","content":"Shadcn UI is a very popular library for building UI components.\nIt is built on top of Radix UI and Tailwind CSS."},{"heading":"shadcn-ui","content":"Checkout their website for more information."}],"headings":[{"id":"shadcn-ui","content":"Shadcn UI"}]},"url":"/docs/v3/ui/shadcn-ui","title":"Shadcn UI","description":"Shadcn UI is a very popular library for building UI components."},{"_id":"/docs/v3/ui/sonner","structured":{"contents":[{"content":"Sonner is quite a simple library to use. We chose sonner because it's styling doesn't get in the way and it's easy to use."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v3/ui/sonner","title":"Sonner","description":"Sonner is a library that provides a toast notification for your application."},{"_id":"/docs/v3/ui/tailwindcss","structured":{"contents":[{"content":"Tailwind has been configured with next.js."},{"content":"We have the following plugins configured:"},{"content":"`tailwindcss-animate`: This plugin adds a set of helpful animation utilities to Tailwind CSS."},{"content":"`@tailwindcss/typography`: This plugin provides a set of `prose` classes for beautiful typographic defaults."},{"content":"Additionally, we've extended the default Tailwind configuration with:"},{"content":"Custom color palette including primary, secondary, destructive, muted, accent, popover, and card colors."},{"content":"Custom border radius values."},{"content":"Custom font families:"},{"content":"`sans`: Uses the Inter font"},{"content":"`display`: Uses the Lexend font"},{"content":"`sans2`: Uses the Manrope font"},{"content":"Custom keyframes and animations:"},{"content":"`accordion-down` and `accordion-up`"},{"content":"`scale-up` and `scale-down`"},{"content":"Extended max-width with an `8xl` size."},{"content":"The configuration also includes a dark mode setup using the `class` strategy, allowing for easy implementation of a dark theme in your application."}],"headings":[]},"url":"/docs/v3/ui/tailwindcss","title":"Tailwind CSS","description":"Tailwind CSS is a utility-first CSS framework."},{"_id":"/docs/v4/admin-panel/blog","structured":{"contents":[{"content":"The blog feature in the admin panel allows you to manage your blog posts efficiently. Here's an overview of its functionality:"},{"content":"Blog posts can be written in Markdown format."},{"content":"You can add images to your blog posts."},{"content":"You can add tags to your blog posts."},{"content":"You can add categories to your blog posts."},{"content":"You can add a summary to your blog posts."},{"content":"You can add a thumbnail to your blog posts."},{"content":"You can add a published date to your blog posts."},{"content":"You can add an author to your blog posts."}],"headings":[]},"url":"/docs/v4/admin-panel/blog","title":"Blog","description":"Blog feature in the admin panel"},{"_id":"/docs/v4/admin-panel/changelog","structured":{"contents":[{"content":"TODO"}],"headings":[]},"url":"/docs/v4/admin-panel/changelog","title":"Changelog","description":"Changelog feature in the admin panel"},{"_id":"/docs/v4/admin-panel/feedback","structured":{"contents":[{"content":"TODO"}],"headings":[]},"url":"/docs/v4/admin-panel/feedback","title":"Feedback","description":"Feedback management in the admin panel"},{"_id":"/docs/v4/admin-panel/make-user-app-admin","structured":{"contents":[{"content":"The Admin Panel is only available in Pro and Ultimate starter kits. Nextbase Pro has features such\nas user management, billing and workspace management. Ultimate starter kit has all the features of\nthe Admin Panel which are in Pro plus Changelog Manager, Feedback Manager, Blog manager etc."},{"content":"Admin panel allows a SAAS owner to manage data of their application at a high level.\nTo make a user an application admin, go to the supabase dashboard."},{"heading":"making-a-user-application-admin","content":"Make sure the user has created an account in your application. Once they have it, the user id of this user can be found in Supabase in the `auth.users` or in the `public.user_profiles` table."},{"heading":"re-login","content":"Note: The user will have to logout and login again for the changes to take effect."},{"heading":"re-login","content":"This is necessary because a new session with elevated privileges is created for\nthe user."},{"heading":"admin-panel","content":"Once the user is an application admin, they should be able to see the Admin Panel sub nav in the sidebar. It looks like this:"},{"heading":"admin-panel","content":"Workspace admins can manage data of their workspace. However, they are still regular users of\nthe application. Application admins are superadmins who can manage data of the entire\napplication."},{"heading":"admin-panel","content":"No. Application admins are assigned only via the Supabase dashboard, which is only accessible to\nthe owner of the application. It is not within the application at all. As long as the Supabase\ndatabase is secure, a user won't be able to make themselves application admins just by tampering\nwith the application code."}],"headings":[{"id":"making-a-user-application-admin","content":"Making a User Application Admin"},{"id":"re-login","content":"Re-Login"},{"id":"admin-panel","content":"Admin Panel"}]},"url":"/docs/v4/admin-panel/make-user-app-admin","title":"Making a User Application Admin","description":"Learn how to grant application admin privileges to users in your Nextbase application"},{"_id":"/docs/v4/admin-panel/roadmap","structured":{"contents":[{"content":"TODO"}],"headings":[]},"url":"/docs/v4/admin-panel/roadmap","title":"Roadmap","description":"Product roadmap management in the admin panel"},{"_id":"/docs/v4/admin-panel/setting-up-payments","structured":{"contents":[{"heading":"setting-up-stripe","content":"First, navigate to the Stripe Developer Dashboard. This is where you'll find your API keys and set up webhooks."},{"heading":"create-products-and-prices","content":"If you don't have any products and prices created in Stripe, you can create them in the Stripe dashboard."},{"heading":"get-environment-variables","content":"Find your `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY` in the developers section and paste them in the `.env.local` file.\nDon't worry about the `STRIPE_WEBHOOK_SECRET` for now. We will setup webhooks later."},{"heading":"sync-stripe-products-and-prices-with-application","content":"Sync payments with your database from the \\`\\`Payments section in the admin panel:"},{"heading":"sync-stripe-products-and-prices-with-application","content":"Stripe products and pricesare now visible in the admin panel:"},{"heading":"ready-to-collect-payments","content":"You are now ready to collect payments. Now when your user logs into the application and see the workspace billing page, they will see the products and prices:"},{"heading":"ready-to-collect-payments","content":"Products and prices are neatly organized into subscriptions and one-time payments. You can change the logic for this in the code."},{"heading":"configuring-webhooks","content":"Set up your webhook endpoint in the Stripe dashboard:"},{"heading":"configuring-webhooks","content":"Select the events you want to listen for:"},{"heading":"configuring-webhooks","content":"Add your endpoint URL:"},{"heading":"workspace-subscriptions-flow","content":"Webhooks need to be set up in Stripe before you can see the subscription purchase flow. That is\nbecause the subscription details are synced from Stripe to the application via webhooks. If\nwebhooks are not setup, then even after billing is done, the subscription details will not be\nsynced to the application."},{"heading":"workspace-subscriptions-flow","content":"Here's how the subscription purchase process looks:"},{"heading":"workspace-subscriptions-flow","content":"And the subscription card after purchase:"},{"heading":"workspace-subscriptions-flow","content":"Users can view invoices:"},{"heading":"workspace-subscriptions-flow","content":"And make one-time payments:"},{"heading":"admin-panel---monitoring-and-metrics","content":"Keep track of your payment metrics in the admin panel:"},{"heading":"admin-panel---monitoring-and-metrics","content":"You can view customers, subscriptions and invoices details from the admin panel."},{"heading":"admin-panel---monitoring-and-metrics","content":"\\[WIP] Once set up in Stripe, your products will be visible in the admin panel:"}],"headings":[{"id":"setting-up-stripe","content":"Setting up Stripe"},{"id":"create-products-and-prices","content":"Create products and prices"},{"id":"get-environment-variables","content":"Get environment variables"},{"id":"sync-stripe-products-and-prices-with-application","content":"Sync Stripe Products and prices with Application"},{"id":"ready-to-collect-payments","content":"Ready to collect payments"},{"id":"configuring-webhooks","content":"Configuring Webhooks"},{"id":"workspace-subscriptions-flow","content":"Workspace subscriptions flow"},{"id":"admin-panel---monitoring-and-metrics","content":"Admin panel - Monitoring and Metrics"}]},"url":"/docs/v4/admin-panel/setting-up-payments","title":"Setting up Payments","description":"Setting up Stripe payments using the admin panel"},{"_id":"/docs/v4/authentication/advanced-methods","structured":{"contents":[{"heading":"possible-advanced-methods","content":"**Two-Factor Authentication (2FA)**: Enhance security by requiring a second form of verification."},{"heading":"possible-advanced-methods","content":"**Biometric Authentication**: Implement fingerprint or face recognition for mobile apps."},{"heading":"possible-advanced-methods","content":"**Single Sign-On (SSO)**: Allow users to access multiple applications with a single set of credentials."},{"heading":"possible-advanced-methods","content":"**JSON Web Tokens (JWT)**: Use JWTs for stateless authentication in your API."},{"heading":"possible-advanced-methods","content":"**OAuth 2.0**: Implement a full OAuth 2.0 flow for more complex authorization scenarios."},{"heading":"implementation-considerations","content":"Implementing advanced authentication methods often requires additional libraries, services, or custom server-side logic. Here are some general steps:"},{"heading":"implementation-considerations","content":"Choose the appropriate method based on your security requirements and user experience goals."},{"heading":"implementation-considerations","content":"Research and select any necessary third-party services or libraries."},{"heading":"implementation-considerations","content":"Implement the chosen method, ensuring it integrates well with Nextbase and Supabase."},{"heading":"implementation-considerations","content":"Thoroughly test the new authentication method, including edge cases and potential security vulnerabilities."},{"heading":"implementation-considerations","content":"Update your application's documentation and user guides to reflect the new authentication options."},{"heading":"implementation-considerations","content":"Remember, while advanced authentication methods can provide enhanced security, they may also introduce complexity to your application. Always balance security needs with user experience considerations."}],"headings":[{"id":"possible-advanced-methods","content":"Possible Advanced Methods"},{"id":"implementation-considerations","content":"Implementation Considerations"}]},"url":"/docs/v4/authentication/advanced-methods","title":"Advanced Authentication Methods","description":"While Nextbase provides built-in support for common authentication methods, you may need more advanced or specialized authentication options for certain use cases."},{"_id":"/docs/v4/authentication/login-with-api-key","structured":{"contents":[{"heading":"implementation","content":"API key authentication is typically implemented using a third-party service like Unkey. The exact implementation details may vary based on your specific requirements and the service you're using."},{"heading":"implementation","content":"Here's a general outline of how you might implement API key authentication:"},{"heading":"implementation","content":"Generate an API key for the user or service that needs access."},{"heading":"implementation","content":"Store the API key securely (never in plain text)."},{"heading":"implementation","content":"When a request comes in, validate the API key."},{"heading":"implementation","content":"If the key is valid, authenticate the request and proceed."},{"heading":"usage-example","content":"Here's a simplified example of how you might validate an API key in a Next.js API route:"},{"heading":"usage-example","content":"Remember to handle API keys securely and follow best practices for API authentication."}],"headings":[{"id":"implementation","content":"Implementation"},{"id":"usage-example","content":"Usage Example"}]},"url":"/docs/v4/authentication/login-with-api-key","title":"Login with API Key","description":"Nextbase Ultimate supports authentication using API keys, which is useful for integrating with external services or for machine-to-machine communication."},{"_id":"/docs/v4/authentication/login-with-magic-link","structured":{"contents":[{"heading":"server-action","content":"Here's the server action used to login with Magic Link:"},{"heading":"usage","content":"Here's an example of how to use the Magic Link authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to receive a login link via email, providing a passwordless authentication experience."}],"headings":[{"id":"server-action","content":"Server Action"},{"id":"usage","content":"Usage"}]},"url":"/docs/v4/authentication/login-with-magic-link","title":"Login with Magic Link","description":"Nextbase supports login with Magic Link, which is a secure way to authenticate users without requiring them to remember a password."},{"_id":"/docs/v4/authentication/login-with-password","structured":{"contents":[{"content":"Here's the server action used for password-based signup. Login is similar, but uses a different schema and action."},{"content":"We use next-safe-action to ensure type safety and error handling for our server actions.\nNext safe action errors are also passed along to react hook form with field level errors to correctly display errors in the UI."},{"heading":"usage","content":"Here's an example of how to use password-based authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to log in using their email and password credentials."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v4/authentication/login-with-password","title":"Login with Password","description":"Nextbase supports traditional email and password authentication, providing a familiar login experience for users."},{"_id":"/docs/v4/authentication/login-with-social-providers","structured":{"contents":[{"content":"Here's the server action used for social provider login:"},{"heading":"usage","content":"Here's an example of how to use social provider authentication in your login component:"},{"heading":"usage","content":"This implementation allows users to log in using their accounts from supported social providers."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v4/authentication/login-with-social-providers","title":"Login with Social Providers","description":"Nextbase supports authentication with various social providers, allowing users to log in using their existing accounts from platforms like Google, GitHub, and Twitter."},{"_id":"/docs/v4/authentication/overview","structured":{"contents":[{"content":"It only has small wrappers around the supabase javascript client."},{"heading":"supported-auth-methods","content":"Authentication Type"},{"heading":"supported-auth-methods","content":"Description"},{"heading":"supported-auth-methods","content":"Email"},{"heading":"supported-auth-methods","content":"Authentication using only the user's email."},{"heading":"supported-auth-methods","content":"Email + Password"},{"heading":"supported-auth-methods","content":"Authentication using the user's email and password."},{"heading":"supported-auth-methods","content":"Third Party Providers"},{"heading":"supported-auth-methods","content":"Authentication using third-party providers. Examples given are Google, GitHub, and Twitter.  Configuration is done via the Supabase dashboard."},{"heading":"supported-auth-methods","content":"Login with API Key"},{"heading":"supported-auth-methods","content":"Authentication using an API key using Unkey (Nextbase Ultimate only)."},{"heading":"providers","content":"Providers are configured using the RenderProviders component. It renders all the providers passed to it as props. For example, the login page component Login.tsx renders providers like so:"},{"heading":"providers","content":"You can configure whichever providers you like using the supabase dashboard. Head over to `https://app.supabase.com/project/{your-project}/auth/providers` and configure the providers you require."},{"heading":"providers","content":"They will automatically work in your frontend. No other configuration required."}],"headings":[{"id":"supported-auth-methods","content":"Supported auth methods"},{"id":"providers","content":"Providers"}]},"url":"/docs/v4/authentication/overview","title":"Overview","description":"NextBase comes with Email, Email + password and Third party providers authentication built-in."},{"_id":"/docs/v4/development-updates/code-and-migration-updates","structured":{"contents":[{"content":"We are constantly working on improving the codebase and migrating to new versions."},{"heading":"codebase","content":"All Nextbase repos, (nextbase essential, nextbase pro, nextbase ultimate) are fullstack nextjs applications.\nThey are all built with the same tech stack and have the same architecture."}],"headings":[{"id":"codebase","content":"Codebase"}]},"url":"/docs/v4/development-updates/code-and-migration-updates","title":"Code and migration updates","description":"Updates to the Nextbase codebase and migration updates."},{"_id":"/docs/v4/development-updates/version-control","structured":{"contents":[{"content":"We use git for version control.\nAll our repositories are hosted on github."}],"headings":[]},"url":"/docs/v4/development-updates/version-control","title":"Version Control","description":"How we version control our Nextbase repositories"},{"_id":"/docs/v4/getting-started","structured":{"contents":[{"heading":"getting-started-with-nextbase","content":"Welcome to NextBase! We're thrilled to have you on board. This guide will walk you through the initial steps to get started with your purchased NextBase repository."},{"heading":"accessing-your-purchase","content":"To access your purchase, navigate to your dashboard. Here, you will find all your purchased items. Look for the NextBase repository (Essential, Pro, or Ultimate) that you've purchased."},{"heading":"entering-your-github-username","content":"You will be prompted to enter your GitHub username. This step is crucial as it grants you access to the purchased repository. Please ensure you enter the correct username."},{"heading":"cloning-the-repository","content":"Once you've gained access, the next step is to clone the repository. Here's how:\nLet us say that you purchased the NextBase Pro repository. It is a private repository which is at https\\://github.com/imbhargav5/nextbase-pro.\nTo clone the repository, you will need to run the following command in your terminal:"},{"heading":"cloning-the-repository","content":"**Important Note:** We strongly discourage forking the repository. If you fork the repository, the code will be visible to others who have forked it. To maintain the privacy and integrity of your code, always opt for cloning.\nNext Steps"},{"heading":"cloning-the-repository","content":"After cloning the repository, you're all set to start exploring and working with NextBase. If you encounter any issues or have any questions, don't hesitate to reach out to our support team."}],"headings":[{"id":"getting-started-with-nextbase","content":"Getting Started with NextBase"},{"id":"accessing-your-purchase","content":"Accessing Your Purchase"},{"id":"entering-your-github-username","content":"Entering Your GitHub Username"},{"id":"cloning-the-repository","content":"Cloning the Repository"}]},"url":"/docs/v4/getting-started","title":"Getting Started with NextBase"},{"_id":"/docs/v4/guides/admin-panel","structured":{"contents":[{"heading":"user-impersonation-debugging","content":"As an application admin, you can debug bugs more quickly for any user by loggin in as them and debugging the issue from their account. It is available in the application admin users section."},{"heading":"user-details-table","content":"The user details table is a table that shows all the users in the application. It is available in the application admin users section."},{"heading":"workspaces-table","content":"The workspaces table is a table that shows all the workspaces in the application. It is available in the application admin workspaces section."},{"heading":"admin-controls-for-feedback-roadmap","content":"Admin users can manage feedback and roadmap items. They can update feedback status, type, priority etc."}],"headings":[{"id":"user-impersonation-debugging","content":"User impersonation debugging"},{"id":"user-details-table","content":"User details table"},{"id":"workspaces-table","content":"Workspaces table"},{"id":"admin-controls-for-feedback-roadmap","content":"Admin controls for feedback, roadmap"}]},"url":"/docs/v4/guides/admin-panel","title":"Admin Panel Guide","description":"The Admin panel has features user impersonation, fix bugs more quickly, and more."},{"_id":"/docs/v4/guides/asynchronous-data-fetching","structured":{"contents":[{"content":"In Nextbase starter kit, we to rely on server components as much as possible to fetch data. The advantages are"},{"content":"smaller bundle sizes: because we don't have to ship the data fetching logic to the client"},{"content":"faster page loads: because we rely on the cpu power of the server instead of the client to fetch data. Server CPUs are usually faster than client CPUs."},{"heading":"data-fetching-in-server-components","content":"Since server components can by async, we can just await on the data which we require for that component. Next.js layouts and pages are server components\nby default unless you explicitly specify them as client components."},{"heading":"data-fetching-in-server-components","content":"For instance, take a look at our the page rendering our `/blog/[slug]` route:"},{"heading":"data-fetching-in-server-components","content":"Notice how we are awaiting on the `anonGetPublishedBlogPostBySlug` function to fetch the blog post data. This function is a server action which is defined in `src/data/anon/internalBlog.ts`. And since the `BlogPostPage` is a server component, we can await on the data fetching function."},{"heading":"streaming-rendering","content":"Server components have the added benefit of streaming rendering. Consider the page responsible for rendering the blog list page at `/blog`. This page has to fetch two sets of data: the list of blog posts and the list of tags. We can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components: `Tags` and `BlogList`."},{"heading":"streaming-rendering","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"streaming-rendering","content":"So the user experience for this page looks like this:"},{"heading":"streaming-rendering","content":"the page starts rendering"},{"heading":"streaming-rendering","content":"\"All blog posts\" title is rendered"},{"heading":"streaming-rendering","content":"\"Loading tags...\" is rendered and when the tags are fetched, the tags are rendered"},{"heading":"streaming-rendering","content":"\"Loading posts...\" is rendered and when the posts are fetched, the posts are rendered"},{"heading":"streaming-rendering","content":"This is a much better user experience than having to wait for the page to render until all the data is fetched."},{"heading":"data-fetching-in-client-components","content":"We don't fetching data much in client components unless in very rare cases. To fetch data in client components simply rely on `useQuery` from\nreact-query. For eg: the login button in the navigation menu is a client component and it fetches the user data using `useQuery`:"}],"headings":[{"id":"data-fetching-in-server-components","content":"Data Fetching in Server components"},{"id":"streaming-rendering","content":"Streaming rendering"},{"id":"data-fetching-in-client-components","content":"Data fetching in client components"}]},"url":"/docs/v4/guides/asynchronous-data-fetching","title":"Asynchronous Data Fetching Guide","description":"Learn how to fetch data asynchronously in Next.js using server actions and react query"},{"_id":"/docs/v4/guides/authentication","structured":{"contents":[{"heading":"authentication","content":"See Authentication Overview here."},{"heading":"authentication","content":"More coming soon..."}],"headings":[{"id":"authentication","content":"Authentication"}]},"url":"/docs/v4/guides/authentication","title":"Authentication","description":"Authenticating users in Nextbase Starter kit"},{"_id":"/docs/v4/guides/creating-application-admin-page","structured":{"contents":[{"content":"Admin Panel pages are internal web app pages that are only accessible to application administrators. These pages usually require the user to be authenticated and have admin privileges."},{"content":"All admin panel pages should be children of the layout located at `src/app/(dynamic-pages)/(authenticated-pages)/app_admin/layout.tsx`. This layout handles checking if the user is authenticated and if they have admin privileges. If the user is not authenticated or does not have admin privileges, they will be redirected to the login page."},{"content":"Here is an example of how to create an admin panel page:"}],"headings":[{"id":"admin-panel-layout","content":"Admin panel layout"}]},"url":"/docs/v4/guides/creating-application-admin-page","title":"Creating Admin Panel Page","description":"Guide on how to create an admin panel page in  Nextbase."},{"_id":"/docs/v4/guides/creating-docs-page","structured":{"contents":[{"content":"Docs are written using fumadocs."},{"content":"Index page for docs is in a different layout mainly because fumadocs doesn't support index layouts. It only has a docs page which renders a single mdx page at a time."},{"content":"All the actual docs are generated from content-collections. All the docs are written as mdx files in `src/content/docs`."},{"content":"They are then converted into json files by content-collections."},{"content":"Fumadocs and content collections work together to render the docs page."},{"content":"<File name=\"page.tsx\" />"},{"content":"<File name=\"layout.tsx\" />"},{"content":"<File name=\"page.tsx\" />"},{"content":"<File name=\"layout.tsx\" />"},{"content":"<File name=\"page.tsx\" />"},{"content":"<File name=\"global.css\" />"},{"content":"<File name=\"button.tsx\" />"},{"content":"<File name=\"tabs.tsx\" />"},{"content":"<File name=\"dialog.tsx\" />"},{"content":"<File name=\"getting-started.mdx\" />"},{"content":"<File name=\"package.json\" />"}],"headings":[]},"url":"/docs/v4/guides/creating-docs-page","title":"Creating Docs Page","description":"Guide on how to create a docs page in Nextbase."},{"_id":"/docs/v4/guides/creating-in-app-notifications","structured":{"contents":[{"content":"In-app notifications are a great way to communicate with your users. They can be used to inform users about new features, promotions, or any other important information.\nNextbase Ultimate comes with a built-in in-app notification system that allows you to create and send notifications to your users."},{"heading":"notifications-table","content":"User notifications are managed in the `user_notifications` table which is created automatically by Nextbase Ultimate supabase migrations during installation. Here is the relevant part of the table schema:"},{"heading":"notifications-table","content":"As you can see, the table has a foreign key to the `user_profiles` table, which means that each notification is associated with a user. This is important because it allows you to send notifications to specific users."},{"heading":"notifications-table","content":"Also, the table has a `payload` column which is a JSONB column that can be used to store any data you want to send to the user."},{"heading":"notifications-table","content":"Additionally, the table has a `is_read` and `is_seen` columns that can be used to track whether the user has read or seen the notification."},{"heading":"notifications-table","content":"Finally the RLS policies ensure that users can only read, update, and delete their own notifications."},{"heading":"creating-a-notification","content":"To create a notification, you can use the `createNotification` function which is available in the `src/data/user/notifications` module:"},{"heading":"creating-a-notification","content":"This function allows adding a notification of any payload to a user."},{"heading":"typesafety","content":"It is important to note that the `payload` column is of type `JSONB` which means that you can store any data you want in it. However, this also means that you will not get any type safety when accessing the data.\nFor that purpose, we use zod schemas to define the payload type. It is present in `src/utils/zod-schemas/notifications.ts`"},{"heading":"typesafety","content":"As you can see, we define a `UserNotification` type which is a union of all the possible notification types. This allows us to get type safety when accessing the notification payload.\nWhen you want to create a new notification type you can simply add it to the union type and define the zod schema for it.\nFor eg. if you want to create a notification type for when a user's profile was seen by another user, you can do the following:"},{"heading":"parsing-notification-payload","content":"We utilize a parse notification function in `src/utils/parseNotification.ts`, where we parse and normalize the notification payload. This is useful when we want to display the notification in the UI."},{"heading":"parsing-notification-payload","content":"So to add our new notification type, we can simply add a new case to the switch statement:"},{"heading":"viewing-notifications","content":"UI to mark a notification as seen and read is already built into Nextbase Ultimate."}],"headings":[{"id":"notifications-table","content":"Notifications Table"},{"id":"creating-a-notification","content":"Creating a notification"},{"id":"typesafety","content":"Typesafety"},{"id":"parsing-notification-payload","content":"Parsing notification payload"},{"id":"viewing-notifications","content":"Viewing notifications"}]},"url":"/docs/v4/guides/creating-in-app-notifications","title":"Creating in-app notifications","description":"Guide on how to create in-app notifications in Nextbase."},{"_id":"/docs/v4/guides/creating-new-supabase-table","structured":{"contents":[{"content":"When creating a new feature in your app, you might need to create a new table in your Supabase database. In this guide, we'll learn how to create a new table in Supabase, manage migrations, and use the new table in your Next.js app."},{"heading":"1-postgres-queries","content":"Let's say you want to create a table to store a list of todos. Here's the PostgreSQL query to create a `todos` table:"},{"heading":"2-approaches-to-creating-tables","content":"There are two main approaches to creating tables in Supabase:"},{"heading":"2-approaches-to-creating-tables","content":"**Direct SQL Execution**: Run the SQL query directly in the Supabase SQL editor (localhost:54323 for local development or in your production Supabase database)."},{"heading":"2-approaches-to-creating-tables","content":"**Using Migrations**: Create and manage database changes through migrations, which is the recommended approach for better tracking and version control."},{"heading":"2-approaches-to-creating-tables","content":"While the first approach is quick, it's not ideal for maintaining and tracking changes over time. Let's focus on the migration approach."},{"heading":"3-managing-migrations","content":"Nextbase provides commands to create and manage migrations:"},{"heading":"creating-a-new-migration","content":"To create a new migration:"},{"heading":"creating-a-new-migration","content":"This command will create a new timestamped SQL file in the `supabase/migrations` directory. Open this file and add your SQL query:"},{"heading":"applying-migrations-locally","content":"To apply new migrations to your local database without losing data:"},{"heading":"resetting-local-database","content":"If you need to reset your local database and apply all migrations from scratch:"},{"heading":"pushing-migrations-to-production","content":"To push your migrations to the production Supabase database:"},{"heading":"pushing-migrations-to-production","content":"This command will apply any new migrations to your production database."},{"heading":"4-generating-types-locally","content":"After creating or modifying tables, you should regenerate TypeScript types for your Supabase tables. Run the following command:"},{"heading":"4-generating-types-locally","content":"This command regenerates the types for your local database. The generated types are located in `apps/database/lib/database.types.ts` and exported from the `database` workspace package as `database/types`."},{"heading":"5-using-generated-types","content":"Types are crucial for type checking and auto-completion in your IDE. You can use the generated types to type-check your database queries and auto-complete column names."},{"heading":"5-using-generated-types","content":"Example usage:"},{"heading":"5-using-generated-types","content":"In this example, `data` will be an array of `Todo` objects, with types generated from your database schema."},{"heading":"conclusion","content":"By following this workflow, you can create new tables, manage database changes through migrations, and ensure type safety in your Nextbase project. This approach provides better version control, easier collaboration, and a more maintainable codebase."}],"headings":[{"id":"1-postgres-queries","content":"1\\. Postgres Queries"},{"id":"2-approaches-to-creating-tables","content":"2\\. Approaches to Creating Tables"},{"id":"3-managing-migrations","content":"3\\. Managing Migrations"},{"id":"creating-a-new-migration","content":"Creating a New Migration"},{"id":"applying-migrations-locally","content":"Applying Migrations Locally"},{"id":"resetting-local-database","content":"Resetting Local Database"},{"id":"pushing-migrations-to-production","content":"Pushing Migrations to Production"},{"id":"4-generating-types-locally","content":"4\\. Generating Types Locally"},{"id":"5-using-generated-types","content":"5\\. Using Generated Types"},{"id":"conclusion","content":"Conclusion"}]},"url":"/docs/v4/guides/creating-new-supabase-table","title":"Creating a new Supabase table","description":"Learn how to create a new table in Supabase, manage migrations, and use the new table in your Next.js app."},{"_id":"/docs/v4/guides/creating-protected-page","structured":{"contents":[{"content":"Nextbase implements a two-tier protection system for routes that require authentication:"},{"content":"**Middleware Protection**: Fast, preliminary check"},{"content":"**Layout-based Protection**: Thorough, database-validated check"},{"heading":"middleware-protection","content":"The middleware provides a quick, preliminary check based on the presence of an authentication cookie. This happens before the page rendering begins, offering a performance-optimized way to redirect unauthenticated users."},{"heading":"middleware-protection","content":"This middleware quickly checks for the presence of a session, providing a fast redirect to the login page if no session is found. This enhances website performance by avoiding unnecessary page renders for unauthenticated users."},{"heading":"layout-based-protection","content":"The layout protection provides a more thorough check, validating the session against the database to ensure it's still valid and not stale."},{"heading":"layout-based-protection","content":"This layout-based check provides a more comprehensive validation, ensuring that even if a user has a cookie, it corresponds to a valid, active session in the database. This prevents access with stale or invalid sessions."},{"heading":"example-creating-a-new-protected-route-my-protected-page","content":"Let's walk through the correct process of creating a new protected route \"/my-protected-page\" in Nextbase, leveraging the two-tier protection system."},{"heading":"step-1-create-the-page-file","content":"Create a new file at the following path:"},{"heading":"step-1-create-the-page-file","content":"Add your page content to this file:"},{"heading":"step-2-update-middleware-configuration","content":"You need to manually add the new route to the `protectedPaths` array in the middleware configuration. Open the `middleware.ts` file and update it as follows:"},{"heading":"step-2-update-middleware-configuration","content":"By manually adding the new route to the `protectedPaths` array, you ensure that it's covered by the middleware protection, providing the fast, preliminary check."},{"heading":"step-3-layout-based-protection-automatic","content":"Your new page is automatically protected by the authenticated layout, as it's placed under the `(authenticated-pages)` directory:"},{"heading":"step-3-layout-based-protection-automatic","content":"This layout performs the thorough, database-validated check for all pages within this directory, ensuring that even if a user passes the middleware check, their session is still valid and active."},{"heading":"step-4-add-navigation-optional","content":"If you want to add navigation to your new protected page, update your navigation component:"},{"heading":"testing-your-new-protected-route","content":"To test your new protected route and verify the two-tier protection:"},{"heading":"testing-your-new-protected-route","content":"Start your Nextbase application"},{"heading":"testing-your-new-protected-route","content":"Try accessing \"/my-protected-page\" without logging in"},{"heading":"testing-your-new-protected-route","content":"You should be quickly redirected to the login page by the middleware"},{"heading":"testing-your-new-protected-route","content":"Log in with a valid user account"},{"heading":"testing-your-new-protected-route","content":"Access \"/my-protected-page\""},{"heading":"testing-your-new-protected-route","content":"The middleware will allow access (fast check)"},{"heading":"testing-your-new-protected-route","content":"The layout protection will perform a thorough check"},{"heading":"testing-your-new-protected-route","content":"You should see your protected page content"},{"heading":"testing-your-new-protected-route","content":"Invalidate your session (e.g., by logging out in another tab)"},{"heading":"testing-your-new-protected-route","content":"Try accessing \"/my-protected-page\" again"},{"heading":"testing-your-new-protected-route","content":"The middleware might initially allow access (if the cookie is still present)"},{"heading":"testing-your-new-protected-route","content":"The layout protection should detect the invalid session and redirect to login"},{"heading":"testing-your-new-protected-route","content":"By following these steps and understanding the two-tier protection system, you've successfully created a new protected route in your Nextbase application. This route is secured by both fast middleware checks and thorough layout-based validation, ensuring optimal performance and robust security for your application."}],"headings":[{"id":"middleware-protection","content":"Middleware Protection"},{"id":"layout-based-protection","content":"Layout-based Protection"},{"id":"example-creating-a-new-protected-route-my-protected-page","content":"Example: Creating a New Protected Route \"/my-protected-page\""},{"id":"step-1-create-the-page-file","content":"Step 1: Create the Page File"},{"id":"step-2-update-middleware-configuration","content":"Step 2: Update Middleware Configuration"},{"id":"step-3-layout-based-protection-automatic","content":"Step 3: Layout-based Protection (Automatic)"},{"id":"step-4-add-navigation-optional","content":"Step 4: Add Navigation (Optional)"},{"id":"testing-your-new-protected-route","content":"Testing Your New Protected Route"}]},"url":"/docs/v4/guides/creating-protected-page","title":"Creating a Protected Page","description":"A complete guide on creating and managing protected routes in Nextbase using a two-tier protection system, including correct middleware configuration and an example scenario."},{"_id":"/docs/v4/guides/creating-public-dynamic-page","structured":{"contents":[{"content":"A public dynamic page in a SaaS site can be something like a product page, a user profile page, etc. These pages are dynamic and are part of the application. They are not just static pages that are part of the marketing site.\nIn Nextbase, we have a `(dynamic-pages)` route segment (folder) within our `src/app` folder which contains all the dynamic pages."},{"content":"So if you want to create a new dynamic page, you can just create a new folder within the `(dynamic-pages)` folder and create a new page within that folder. You can nest\nyour dynamic pages as deep as you want to get the route you are looking for."},{"content":"The key difference here is that this page is not cached and is always served fresh."},{"content":"The layout file for the dynamic pages is at `src/app/(dynamic-pages)/layout.tsx`."}],"headings":[]},"url":"/docs/v4/guides/creating-public-dynamic-page","title":"Creating Public Dynamic Page","description":"Guide on how to create a public dynamic page in a Next.js application such as in Nextbase."},{"_id":"/docs/v4/guides/creating-public-static-page","structured":{"contents":[{"content":"A public static page in a SaaS site can be something like a pricing page, a contact page, a about page, etc. These pages are not dynamic and are not part of the application. They are just static pages that are part of the marketing site.\nIn Nextbase, we have a `(static-pages)` route segment (folder) within our `src/app` folder which contains all the static pages."},{"content":"So if you want to create a new static page, you can just create a new folder within the `(static-pages)` folder and create a new page within that folder. You can nest\nyour static pages as deep as you want to get the route you are looking for."},{"content":"The layout file for the static pages is at `src/app/(static-pages)/layout.tsx`."},{"content":"As you can see the layout has no additional configuration such as `export const dynamic` etc. Which means that this layout defauls to the next.js defaults and is static."}],"headings":[]},"url":"/docs/v4/guides/creating-public-static-page","title":"Creating Public Static Page","description":"Guide on how to create a public static page in a Next.js application such as in Nextbase."},{"_id":"/docs/v4/guides/handling-realtime-data-with-supabase","structured":{"contents":[{"content":"In this guide, we'll learn how to handle realtime data in Supabase using server actions and react query.\nWe implemented notifications as a realtime feature in Nextbase Ultimate and we can use that as a reference."},{"heading":"supabase-realtime","content":"Supabase provides a realtime service which can be used to listen to changes in your database.\nWhen used with a React client component useEffect it looks something like this:"},{"heading":"supabase-realtime","content":"In the above code, we are listening to changes in the `user_notifications` table and refetching the data when a change occurs.\nDepending on the scenario, you can also just use the payload from the realtime data to `setState` or update a `react query` cache directly."}],"headings":[{"id":"supabase-realtime","content":"Supabase Realtime"}]},"url":"/docs/v4/guides/handling-realtime-data-with-supabase","title":"Handling Realtime Data With Supabase","description":"Learn how to handle realtime data in Supabase using server actions and react query"},{"_id":"/docs/v4/guides/managing-users","structured":{"contents":[{"content":"Users are the core of your application. Supabase Auth provides a complete user management system, including sign up, sign in, email verification, password recovery, and more."},{"heading":"how-does-supabase-store-user-data","content":"Supabase stores user data in a table called `auth.users`. `auth` is an internal schema that is created when you initialize Supabase."},{"heading":"how-does-supabase-store-user-data","content":"When new users sign up, they are added to this table."},{"heading":"how-does-supabase-store-user-data","content":"This table and tables related in the auth schema are automatically updated when users update their login information"},{"heading":"storing-more-information-about-users","content":"However this table doesn't contain any more data other than email and identifying information. Since we want to store more information about our users, we use a table called `user_profiles`."},{"heading":"storing-more-information-about-users","content":"`user_profiles` is a table which allows users to update their public information such as name and avatar. (avatar image is stored in supabase storage and the url is stored here)."},{"heading":"storing-more-information-about-users","content":"Users can also store their private preferences in the `user_private_info` table. This table is only accessible by the user whose id matches the \\`user\\_id\\` column. Both the tables mentioned above have auth.users.id as a foreign key."},{"heading":"storing-more-information-about-users","content":"Hence if you want to add more public information about your users, you can add a new column to \\`user\\_profiles\\` and if you want to add more private information, you can add a new column to \\`user\\_private\\_info\\`."}],"headings":[{"id":"how-does-supabase-store-user-data","content":"How does Supabase store user data?"},{"id":"storing-more-information-about-users","content":"Storing more information about users"}]},"url":"/docs/v4/guides/managing-users","title":"Managing Users","description":"Learn how to manage users with Supabase"},{"_id":"/docs/v4/guides/mutating-data-with-supabase","structured":{"contents":[{"content":"In this guide, we'll learn how to mutate data asynchronously in Supabase using server actions and react query.\nUsually, the scenario looks like this."},{"content":"The server and client components for the route are loaded."},{"content":"User performs an action that triggers a mutation, eg. clicking a button, filling out a form, etc."},{"content":"The client sends a request to the server to perform the mutation."},{"content":"In previous versions of Next.js, we would have to write API routes to handle the request.\nBut as of Next.js 13, and now in 14 and beyond, we have a new way to handle this. These are called route handlers and server actions."},{"heading":"what-are-route-handlers","content":"In Next.js 14, Route handlers have replaced API routes.\nThese are simple `route.ts` files that can be placed in the `app` folder at any level."},{"heading":"what-are-route-handlers","content":"Here is an example of a route handler that handles a `POST` request to `/api/hello`."},{"heading":"what-are-route-handlers","content":"As of Next.js 14, Route handlers have replaced API routes. These are simple route.ts files that can be placed in the app folder at any level. They can handle multiple HTTP methods (GET, POST, PUT, DELETE, etc.) in a single file."},{"heading":"what-are-route-handlers","content":"Here's an example of a simple POST request using Route handlers:"},{"heading":"what-are-route-handlers","content":"Now both the above examples are functionally equivalent. But the latter is much more concise and easier to read."},{"heading":"what-are-server-actions","content":"Server actions are convenient wrappers around route handlers that allow you to perform mutations on your data within\nyour client / server components. Instead of writing a route handler, you can simply write a server action and call it from your client component."},{"heading":"what-are-server-actions","content":"Here's an example of a server action that handles a `POST` request similar to the example above."},{"heading":"what-are-server-actions","content":"And here's how you would call it from your client component."},{"heading":"what-are-server-actions","content":"In the above example, we are using the `useMutation` hook from react-query to call our server action."},{"heading":"what-are-server-actions","content":"Note how we are directly invoking the `hello` server action from our client component."},{"heading":"what-are-server-actions","content":"This will internally be converted to a `POST` request to a route handler automatically created by Next.js and mapped to a route."},{"heading":"usetoastmutation-hook","content":"Nextbase starter kit provides a convenience hook called `useToastMutation` that can be used to display a toast notification throughout the lifecycle of a mutation.\nIt is a thin wrapper around `useMutation` from react-query and uses `sonner` to display toast notifications."}],"headings":[{"id":"what-are-route-handlers","content":"What are route handlers?"},{"id":"what-are-server-actions","content":"What are server actions?"},{"id":"usetoastmutation-hook","content":"useToastMutation hook"}]},"url":"/docs/v4/guides/mutating-data-with-supabase","title":"Mutating Data With Supabase","description":"Learn how to mutate data asynchronously in Supabase using server actions and react query"},{"_id":"/docs/v4/guides/optimizing-data-operations-nextjs-supabase","structured":{"contents":[{"content":"In this guide, we'll explore best practices for fetching and mutating data in Nextbase applications, focusing on performance and efficiency."},{"heading":"server-side-data-fetching","content":"Prefer fetching data for components on the server using server components. This approach utilizes the higher CPU power of servers and minimizes bundle sizes for the data fetching operation."},{"heading":"mutations-with-usetoastmutation","content":"For mutations, use the `useToastMutation` hook. This allows for direct server-side mutations while providing user feedback through toast notifications."},{"heading":"realtime-queries-with-supabase","content":"For realtime queries, such as notifications, use Supabase's realtime APIs directly on the client side."},{"heading":"avoiding-usestate-for-data-fetching","content":"Avoid using `useState` for most data fetching operations. Instead, either use server components and fetch data within them, or use `useQuery` if you really need to, but you generally shouldn't need to use `useState`."},{"heading":"avoiding-usestate-for-data-fetching","content":"By following these guidelines, you can optimize data operations in your Nextbase applications for better performance and user experience."}],"headings":[{"id":"server-side-data-fetching","content":"Server-Side Data Fetching"},{"id":"mutations-with-usetoastmutation","content":"Mutations with useToastMutation"},{"id":"realtime-queries-with-supabase","content":"Realtime Queries with Supabase"},{"id":"avoiding-usestate-for-data-fetching","content":"Avoiding useState for Data Fetching"}]},"url":"/docs/v4/guides/optimizing-data-operations-nextjs-supabase","title":"Optimizing Data Operations in Next.js 14 and Supabase","description":"Learn how to efficiently fetch and mutate data in Next.js 14 and Supabase using server components, useToastMutation, and Supabase real-time APIs."},{"_id":"/docs/v4/guides/removing-internationlization","structured":{"contents":[{"content":"Many saas products require internalization. But many don't need it too. Ultimately it is your customer requirements that matter."},{"content":"If you don't need internationalization, you can remove it by following these steps:"},{"content":"Move all the children of the `src/app/[locale]` folder to the `src/app/` folder."},{"content":"Remove the `[locale]` from the `src/app/` folder."},{"content":"Remove the `next-intl` from the `package.json` file."},{"content":"Update your middleware to this pattern. Pay close attention to the"},{"content":"Remove `src/i18n` folder."},{"content":"Replace all `import { Link } from \"@/components/intl-link\";` with `import Link from \"next/link\";`"},{"content":"Remove all `LocaleSwitcherSelect` component and its usage"},{"content":"Remove `import createNextIntlPlugin from \"next-intl/plugin\"` and its usage in `next.config.mjs`"},{"content":"Remove `unstable_setRequestLocale` usage in all static pages and `locale` from 'params'."},{"content":"Remove `useTranslations` usage in all client components."},{"content":"Remove `getMessages` usage in root layout component and remove `locale` and `messages` props from `AppProviders` component. Finally remove `NextIntlProvider` from the `AppProviders` component."}],"headings":[]},"url":"/docs/v4/guides/removing-internationlization","title":"Removing Internationalization","description":"Learn how to remove internationalization from your Nextbase application"},{"_id":"/docs/v4/guides/row-level-security","structured":{"contents":[{"content":"This article in the supabase website is a great start to understand how RLS works.\nhttps\\://supabase.com/docs/v3/guides/auth/row-level-security"},{"heading":"rls-configured-within-nextbase","content":"NextBase ships with RLS policies for tables such as organizations, organization\\_team\\_members, subscriptions, customers and more."},{"heading":"rls-configured-within-nextbase","content":"It also comes with RLS for storage buckets."},{"heading":"setup-rls-for-your-new-tables","content":"TODO"}],"headings":[{"id":"rls-configured-within-nextbase","content":"RLS configured within NextBase"},{"id":"setup-rls-for-your-new-tables","content":"Setup RLS for your new tables"}]},"url":"/docs/v4/guides/row-level-security","title":"Row Level Security","description":"Row Level Security in NextBase is a powerful feature that allows you to control access to rows in your database based on the user who is querying the data."},{"_id":"/docs/v4/guides/stripe-subscriptions","structured":{"contents":[{"content":"This guide will walk you through how Stripe has been set up in different files in Nextbase Starter Kit.."},{"heading":"stripe-configuration","content":"Stripe configuration is stored in the `.env` file. You need to provide your Stripe API keys here."},{"heading":"stripe-client","content":"A Stripe client is created in `src/utils/stripe.ts`. This client is used to interact with the Stripe API."},{"heading":"stripe-webhook","content":"A webhook endpoint is set up in `src/pages/api/stripe/webhook.ts` to handle events from Stripe."},{"heading":"stripe-checkout-and-customer-portal","content":"A checkout session is created in `src/data/user/organizations.ts`."},{"heading":"stripe-subscriptions","content":"Subscriptions are managed in `src/utils/supabase-admin.ts`. Subscriptions are at the organization level.\nWhen a subscription is created, the `manageSubscriptionStatusChange` function is called. This function creates a new subscription record in the database."},{"heading":"organization-admins","content":"Organization admins can manage subscriptions in `/organization/[organizationId]/settings/billing` route.\nAll members can see the current subscription status in `/organization/[organizationId]/settings/billing` route. However,\nonly organization admins can manage subscriptions."}],"headings":[{"id":"stripe-configuration","content":"Stripe Configuration"},{"id":"stripe-client","content":"Stripe Client"},{"id":"stripe-webhook","content":"Stripe Webhook"},{"id":"stripe-checkout-and-customer-portal","content":"Stripe Checkout and Customer Portal"},{"id":"stripe-subscriptions","content":"Stripe Subscriptions"},{"id":"organization-admins","content":"Organization Admins"}]},"url":"/docs/v4/guides/stripe-subscriptions","title":"Stripe Setup in Nextbase Ultimate","description":"This guide walks you through how Stripe has been set up in different files in Nextbase Ultimate."},{"_id":"/docs/v4/guides/writing-integration-tests","structured":{"contents":[{"content":"All versions of Nextbase Starter Kit come with Playwright installed. Playwright is a powerful end-to-end testing framework that allows you to write integration tests for your application."},{"heading":"configuration","content":"Playwright is configured in the `playwright.config.js` file."},{"heading":"configuration","content":"As you can see, the configuration file is quite simple. The playwright projects are configured in this manner:"},{"heading":"configuration","content":"Projects which setup authentication are configured in the `with-auth` project."},{"heading":"configuration","content":"Projects which test logged in users are configured in the `Logged In Users (Desktop Chrome)` project."},{"heading":"configuration","content":"Projects which test logged out users are configured in the `Logged Out Users (Desktop Chrome)` project."},{"heading":"configuration","content":"The `Logged In Users (Desktop Chrome)` project is configured to use the `with-auth` project as a dependency. This means that the `with-auth` project will run first, and then the `Logged In Users (Desktop Chrome)` project will run.\nHowever, the tests themselves run in parallel in each project."},{"heading":"running-tests","content":"Playwright runs in `NODE_ENV=test` mode. It also relies on `.env.test` for environment variables.\nNow, .env.test is configured to use **local supabase** which is setup by running `pnpm supabase start` command.\nThe `pnpm supabase start` command sets up a local supabase instance using docker."},{"heading":"running-tests","content":"The supabase api url is available at &#x2A;*http\\://localhost:54321/** (This is the most important url)"},{"heading":"running-tests","content":"The supabase db url is at &#x2A;*http\\://localhost:54322/**"},{"heading":"running-tests","content":"The supabase studio is available at &#x2A;*http\\://localhost:54323/** (You can use this to use the UI interface )"},{"heading":"running-tests","content":"The supabase inbucket instance is available at &#x2A;*http\\://localhost:54324/** (We use this for testing emails)"},{"heading":"running-tests","content":"Now that supabase is running locally. All we need to do is to run playwright."},{"heading":"running-playwright-in-headless-mode","content":"**Since playwright has been configured to start the local dev server, you don't need to run `pnpm dev` separately.**"},{"heading":"writing-tests","content":"All the test cases are written in the `e2e` folder. The `e2e` folder is divided into two folders:"},{"heading":"writing-tests","content":"Tests which require a user logged in need to be named `test-case.user.spec.ts`"},{"heading":"writing-tests","content":"Tests which don't require a user logged in need to be named `test-case.spec.ts`"},{"heading":"create-organization-test-case-example","content":"This is an example of a test case which requires a user to be logged in."}],"headings":[{"id":"configuration","content":"Configuration"},{"id":"running-tests","content":"Running tests"},{"id":"running-playwright-with-ui","content":"Running playwright with UI"},{"id":"running-playwright-in-headless-mode","content":"Running playwright in headless mode"},{"id":"writing-tests","content":"Writing tests"},{"id":"create-organization-test-case-example","content":"Create Organization Test case example"}]},"url":"/docs/v4/guides/writing-integration-tests","title":"Writing Integration Tests with Playwright","description":"Learn how to write integration tests using Playwright, a powerful end-to-end testing framework"},{"_id":"/docs/v4/guides/writing-unit-tests","structured":{"contents":[{"content":"All versions of Nextbase Starter Kit come with Vitest installed. Vitest is a lightweight and fast testing framework that allows you to write unit tests for your application."},{"heading":"configuration","content":"Vitest is configured in the `vitest.config.ts` file."},{"heading":"configuration","content":"As you can see, the configuration file is quite simple. You can customize it according to your needs."},{"heading":"running-tests","content":"Vitest runs in `NODE_ENV=test` mode. It also relies on `.env.test` for environment variables."},{"heading":"writing-tests","content":"All the test cases are written in the `tests` folder."},{"heading":"helper-function-test-case-example","content":"This is an example of a test case for a helper function."},{"heading":"using-vitest-with-github-actions","content":"You can easily integrate Vitest with GitHub Actions to run your tests automatically on every push or pull request. Here is a basic example of a GitHub Actions workflow:"},{"heading":"using-vitest-with-github-actions","content":"Most SaaS solo developers or small teams write integration tests more, hence we only added Vitest setup and a dummy test case, but you can update the test suite as needed."}],"headings":[{"id":"configuration","content":"Configuration"},{"id":"running-tests","content":"Running tests"},{"id":"running-vitest","content":"Running Vitest"},{"id":"writing-tests","content":"Writing tests"},{"id":"helper-function-test-case-example","content":"Helper Function Test case example"},{"id":"using-vitest-with-github-actions","content":"Using Vitest with GitHub Actions"}]},"url":"/docs/v4/guides/writing-unit-tests","title":"Writing Unit Tests with Vitest","description":"Learn how to write unit tests using Vitest, a lightweight and fast testing framework"},{"_id":"/docs/v4","structured":{"contents":[{"content":"NextBase is the fastest way to start your SaaS project. It comes with incredible set of features pre-built for you. It is built on top of Supabase and NextJS. It is a fullstack SaaS starter kit. Here is the complete stack:"},{"content":"NextJS with App Router"},{"content":"Supabase"},{"content":"React Server Components"},{"content":"Tailwind CSS"},{"content":"Typescript (with strict mode)"},{"content":"Zod (for validation)"},{"content":"React Query"},{"content":"React Hook Form"},{"content":"Sonner (for notifications)"},{"content":"Lucide Icons"},{"content":"Resend and React email (for emails)"},{"content":"Sentry"},{"content":"Posthog (for analytics)"},{"content":"Google Analytics (for analytics)"},{"heading":"feature-list","content":"NextBase is a full-stack SaaS starter kit built on top of Supabase and NextJS. It comes with an incredible set of pre-built features that can help you kickstart your SaaS project. The features are categorized into different tiers - Essential, Pro, Ultimate, and Custom. Here's a detailed breakdown of the features available in each tier:"},{"heading":"all-variants","content":"For a detailed breakdown of all the variants, please check out our Pricing Page."}],"headings":[{"id":"local-dev-setup-video","content":"Local dev setup video"},{"id":"feature-list","content":"Feature list"},{"id":"all-variants","content":"All Variants"}]},"url":"/docs/v4","title":"Welcome to Nextbase v4!"},{"_id":"/docs/v4/installation/dev-environment","structured":{"contents":[{"heading":"setup-env-variables","content":"In your repository directory, you should have a .env.local.example file which looks like this. For dev environment, copy the .env.local.example file to .env.local. The localhost values are used for local development. After running `pnpm supabase start`, run `pnpm supabase status` to get the actual `publishable key` and `secret key` values, then update your `.env.local` file accordingly."},{"heading":"supabase-env-for-login-with-providers","content":"Note. Supabase also reads from .env.local for local development. Read more about it in this github issue."},{"heading":"supabase-env-for-login-with-providers","content":"https\\://github.com/supabase/cli/issues/1518#issuecomment-2084506041"},{"heading":"supabase-env-for-login-with-providers","content":"It reads from .env.local for local development, or .env in that priority."},{"heading":"supabase-env-for-login-with-providers","content":"Note: If you don't need to login with providers, yet you can avoid setting up the above env variables. However, please ensure that you comment out the authentication provider logic for the\nproviders you don't need in the `supabase/config.toml` file."},{"heading":"run-your-dev-server","content":"We are ready to start our dev server. Run"},{"heading":"run-your-dev-server","content":"`pnpm dev`"},{"heading":"run-your-dev-server","content":"Our local development server is now running at `localhost:3000`."}],"headings":[{"id":"link-your-repository-to-supabase","content":"Link your repository to supabase"},{"id":"setup-env-variables","content":"Setup env variables"},{"id":"supabase-env-for-login-with-providers","content":"Supabase env for login with providers"},{"id":"run-your-dev-server","content":"Run your dev server"}]},"url":"/docs/v4/installation/dev-environment","title":"Dev environment","description":"Setup your dev environment for the Supabase Stripe Starter Kit. Link your repository to Supabase, setup env variables, and run your dev server."},{"_id":"/docs/v4/installation/first-steps","structured":{"contents":[{"content":"Welcome to the Nextbase repository on GitHub!"},{"heading":"prerequisites","content":"Before you get started, make sure you have the following:"},{"heading":"prerequisites","content":"Git installed on your system. If you don't already have it, you can download it from the official Git website (https\\://git-scm.com/)."},{"heading":"prerequisites","content":"A terminal or command prompt. This will be used to run commands and navigate directories."},{"heading":"prerequisites","content":"Node.js installed on your system. If you don't already have it, you can download it from the official Node.js website (https\\://nodejs.org/)."},{"heading":"cloning-the-repository","content":"To get started with the Nextbase project, you will need to clone the repository onto your local machine. Here's how to do it:"},{"heading":"cloning-the-repository","content":"Open a terminal or command prompt and navigate to the directory where you want to store the Nextbase repository."},{"heading":"cloning-the-repository","content":"Run the following command to clone the repository:"},{"heading":"cloning-the-repository","content":"This will create a new directory called \"nextbase\" in your current location, and clone the contents of the repository into it."},{"heading":"cloning-the-repository","content":"Navigate into the repository by running the following command:"},{"heading":"cloning-the-repository","content":"You are now ready to start working with the Nextbase project! To update the repository with the latest changes, run the following command:"},{"heading":"cloning-the-repository","content":"That's it! You should now have a local copy of the Nextbase repository on your machine."}],"headings":[{"id":"prerequisites","content":"Prerequisites"},{"id":"cloning-the-repository","content":"Cloning the Repository"}]},"url":"/docs/v4/installation/first-steps","title":"First Steps","description":"Welcome to the Nextbase repository on GitHub!"},{"_id":"/docs/v4/installation/login","structured":{"contents":[{"heading":"sign-up-using-email","content":"Go to `http://localhost:3000/login` . If you are logging in for the first time, you can sign up and setup your email login credentials."},{"heading":"sign-up-using-email","content":"Once your account is created in supabase local studio, you will receive an email in inbucket, which you can access at http\\://localhost:54324/monitor\\`. Click on the link and login."},{"heading":"sign-up-using-email","content":"Confirm your email and you will be taken to the dashboard! 🎉"},{"heading":"sign-up-using-email-and-password","content":"Go to `http://localhost:3000/login` . For the first time, you can sign up and setup your email and password login credentials."},{"heading":"sign-up-using-email-and-password","content":"Supabase auto-confirms the credentials and does not send any confirmation email."},{"heading":"sign-up-using-email-and-password","content":"You can login now using the same credentials and you will be taken to the dashboard! 🎉"}],"headings":[{"id":"sign-up-using-email","content":"Sign up using email"},{"id":"sign-up-using-email-and-password","content":"Sign up using email and password"}]},"url":"/docs/v4/installation/login","title":"Login to app in development","description":"Login to app in development using email and password."},{"_id":"/docs/v4/installation/setting-up-stripe","structured":{"contents":[{"heading":"for-nextbase-pro-and-nextbase-ultimate-users","content":"There is a much simpler way to set up payments for Nextbase Pro and Nextbase Ultimate users. You can skip the rest of this page.\nPlease follow this guide to set up payments for your Nextbase Pro and Nextbase Ultimate users."},{"heading":"for-nextbase-essential-users","content":"Since there is no admin panel for Essential users, you will need to set up payments manually in Stripe."},{"heading":"create-a-stripe-account","content":"Go to the Stripe website (https\\://stripe.com/) and click on the \"Sign Up\" button in the top right corner.\nFollow the prompts to create a new account. You will need to provide your email address and choose a password.\nOnce you have created your account, log in and complete the onboarding process. You don't need to setup everything while you are setting up."},{"heading":"enter-test-mode","content":"By default your account will be in `Test Mode`. This means you can test your integration without processing real payments. You can switch to `Live Mode` when you are ready to start processing real payments.\nIf you are not in `Test Mode`, you can switch to it by clicking on the `Test Mode` toggle in the top right part of your dashboard."},{"heading":"get-your-api-keys","content":"Once you have setup stripe, head to the Developers section and in the Keys section, grab the Secret key and Publishable key from there. Save them to a secure location. You will need them."},{"heading":"get-your-api-keys","content":"So as of now you have two keys."},{"heading":"get-your-api-keys","content":"If you are using Nextbase Essential, then continue the rest of the guide. If\nyou are using Nextbase Pro or Ultimate, then there is an easier way to setup\nStripe. You can skip the rest of the guide and go to the Setup Stripe\nproducts using admin panel section."},{"heading":"get-your-api-keys","content":"You can still use the rest of the guide to setup Stripe. But checkout the admin panel link. We think\nit is way easier."},{"heading":"setup-webhooks-for-local-testing","content":"Webhooks are a way for you to receive real-time updates from Stripe. You can use webhooks to receive notifications when a payment is made, when a customer is created, and more. This is important for your Supabase project to know when a payment is made so that you can update your database accordingly.\nYou can use webhooks to also track `product` and `price` updates in your app. It has already been configured in Nextbase.\nIn Nextbase, we have configured a webhook endpoint at `/api/stripe/webhooks`. This is the endpoint that Stripe will send the events to. But for Stripe to send events to your local machine, you need to expose your local server to the internet. You can use a tool like ngrok to do this, which can be more flexible, but you can also use Stripe CLI to trigger fixture events and test your webhook locally."},{"heading":"setting-up-using-stripe-cli","content":"Install the Stripe CLI from here."},{"heading":"setting-up-using-stripe-cli","content":"Login to your Stripe account using the CLI."},{"heading":"setting-up-using-stripe-cli","content":"This will ask you to open a link in your browser. Once you have logged in, you can close the browser and return to the terminal."},{"heading":"setting-up-using-stripe-cli","content":"Forward the webhook events to your local server."},{"heading":"setting-up-using-stripe-cli","content":"This will give you a webhook signing secret. Add this to your `.env.local` file."},{"heading":"setting-up-using-stripe-cli","content":"Trigger the following events to create your product and price."},{"heading":"setting-up-using-stripe-cli","content":"Confirm that you see the events in your terminal and the products and prices are created in supabase. Here is how they might look like in your supabase studio. You can visit `http://localhost:54323` and look at your table editor to confirm."},{"heading":"setting-up-using-stripe-cli","content":"Now as your dev server is already running. You can go your app and head over to the organization billing page and you can see the created product with its price there."},{"heading":"setting-up-using-stripe-cli","content":"Test out the `Free Trial` option and the `Choose` option to see if the payment is successful. Use test payment methods and test card numbers from here."},{"heading":"setting-up-using-ngrok","content":"With the dev server running, go to the stripe developers -> webhooks section. The steps are the same as in the previous section. However instead of listening to the events locally and triggering fake events, we will create the products directly in Stripe."},{"heading":"setting-up-using-ngrok","content":"Run a service like ngrok to expose your localhost to the internet. Grab the URL. (e.g. `https://12345678.ngrok.io`)"},{"heading":"setting-up-using-ngrok","content":"Create a webhook endpoint and set the url to your ngrok url. (e.g. `https://12345678.ngrok.io/api/stripe/webhooks`). Select all events."},{"heading":"setting-up-using-ngrok","content":"Make sure that \\` All events \\` are forwared to our webhook listener. (Select the checkbox suggesting to select all events)."},{"heading":"start-creating-products","content":"Now create a couple of `Products` in Stripe, also make sure to create `prices` for each of the products."},{"heading":"start-creating-products","content":"Our database will automatically stay in sync with Stripe and create the necessary rows in the database."}],"headings":[{"id":"for-nextbase-pro-and-nextbase-ultimate-users","content":"For Nextbase Pro and Nextbase Ultimate Users"},{"id":"for-nextbase-essential-users","content":"For Nextbase Essential Users"},{"id":"create-a-stripe-account","content":"Create a Stripe account"},{"id":"enter-test-mode","content":"Enter Test Mode"},{"id":"get-your-api-keys","content":"Get your API keys"},{"id":"setup-webhooks-for-local-testing","content":"Setup Webhooks for local testing"},{"id":"setting-up-using-stripe-cli","content":"Setting up using Stripe CLI"},{"id":"setting-up-using-ngrok","content":"Setting up using Ngrok"},{"id":"start-creating-products","content":"Start creating products"}]},"url":"/docs/v4/installation/setting-up-stripe","title":"Setting up Stripe","description":"Setting up Stripe for your Supabase project. Learn how to create products, webhooks, and more."},{"_id":"/docs/v4/installation/setting-up-supabase-locally","structured":{"contents":[{"content":"To get started with Supabase locally, you will just need to pull the docker image. Here's how to do it:"},{"heading":"setting-up-docker-for-supabase","content":"First, you will need to install Docker on your machine. You can download the installer from the Docker website since it is the recommended way to install Docker on Windows and macOS. If you are using Linux, you can follow the instructions for your specific distribution on the Docker documentation."},{"heading":"setting-up-docker-for-supabase","content":"Next, you will need to setup the env variables."},{"heading":"setting-up-docker-for-supabase","content":"In your repository directory, you should have a `.env.local.example` file which looks like this. For dev environment, copy the `.env.local.example` file to `.env.local`. The localhost values are used for local development. However, you will need to update `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY` with the actual values from `pnpm supabase status` after starting Supabase (see step 4 below)."},{"heading":"setting-up-docker-for-supabase","content":"Note. Supabase also reads from .env.local for local development. If you don't need to setup social providers like github, google yet, just leave the dummy values in env.local file. Removing the values will however break supabase start, since the config.toml has social providers enabled by default. Either disable them in config.toml or leave the dummy values. Read more here."},{"heading":"setting-up-docker-for-supabase","content":"Once you have Docker installed and running in your machine, you can now pull the Supabase Docker image by running `pnpm supabase start` in your terminal."},{"heading":"setting-up-docker-for-supabase","content":"> Please note that for the first time you run the `pnpm supabase start` command, it will take some time to pull the Docker image. Subsequent runs will be faster since the images will be cached and volumes will be persisted."},{"heading":"setting-up-docker-for-supabase","content":"After running the command, you should see the output simmilar to the following:"},{"heading":"setting-up-docker-for-supabase","content":"**Important:** Copy the `publishable key` and `secret key` values from the terminal output above and update your `.env.local` file:"},{"heading":"setting-up-docker-for-supabase","content":"Set `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` to the `publishable key` value"},{"heading":"setting-up-docker-for-supabase","content":"Set `SUPABASE_SECRET_KEY` to the `secret key` value"},{"heading":"setting-up-docker-for-supabase","content":"You can also run `pnpm supabase status` at any time to see these values again."},{"heading":"setting-up-docker-for-supabase","content":"You can now access the Supabase Studio by visiting the URL `http://localhost:54323` in your browser."},{"heading":"setting-up-docker-for-supabase","content":"You can also access the API and GraphQL URLs to interact with your local Supabase instance."},{"heading":"setting-up-docker-for-supabase","content":"To connect database clients to your local Supabase instance, you can use the DB URL `postgresql://postgres:postgres@localhost:54322/postgres`"},{"heading":"setting-up-docker-for-supabase","content":"To monitor your inboxes, you can use the Inbucket URL `http://localhost:54324`"},{"heading":"setting-up-docker-for-supabase","content":"You can stop the Supabase server by running the `pnpm supabase stop` command, to save on system resources."}],"headings":[{"id":"setting-up-docker-for-supabase","content":"Setting up Docker for Supabase"}]},"url":"/docs/v4/installation/setting-up-supabase-locally","title":"Setting up Supabase Locally","description":"Learn how to set up Supabase locally for development. This guide will show you how to pull the Docker image and set up the environment variables for your Next.js application."},{"_id":"/docs/v4/installation/supabase-for-production","structured":{"contents":[{"content":"To get started with Supabase, you will need to set up an account and create a project. Here's how to do it:"},{"heading":"creating-an-account","content":"Go to the Supabase website (https\\://supabase.com/) and click on the \"Sign Up\" button in the top right corner."},{"heading":"creating-an-account","content":"Follow the prompts to create a new account. You will need to provide your email address and choose a password."},{"heading":"creating-a-project","content":"Once you have created your account, log in and click on the \"Create Project\" button in the dashboard."},{"heading":"creating-a-project","content":"Give your project a name and choose a region for your data to be stored in. You will also need to create a database password. Make sure to save this password in a secure location, as you will need it later to access your database. Then click on the \"Create Project\" button to finish creating your project."},{"heading":"accessing-your-project","content":"You should now see your new project listed in the dashboard. Click on the project name to open it and start working with Supabase."},{"heading":"auth-configuration","content":"Go to the \"Authentication\" section of your project usually at `https://app.supabase.io/project/<project-id>/auth`."},{"heading":"auth-configuration","content":"Go to the URL Configuration tab."},{"heading":"auth-configuration","content":"Set the Site URL to `http://localhost:3000` or it can also be your production domain e.g. `https://myapp.com`. (This is not as important as the Redirect URL configuration)"},{"heading":"auth-configuration","content":"Set Redirect URLs to\na. `http://localhost:3000/auth/callback` (for local development) or `http://localhost:3000/**` (this means all routes under `http://localhost:3000` will be accepted as a valid redirect URL)\nb. `https://myapp.com/auth/callback` (for production) or `https://myapp.com/**` (this means all routes under `https://myapp.com` will be accepted as a valid redirect URL)"},{"heading":"install-dependencies-and-link-your-project","content":"run `pnpm install`"},{"heading":"install-dependencies-and-link-your-project","content":"Supabase login within your terminal"},{"heading":"install-dependencies-and-link-your-project","content":"Note: If you haven't used supabase before in the terminal, you will need to login with supabase in the terminal."},{"heading":"install-dependencies-and-link-your-project","content":"Create an access token for supabase here https\\://app.supabase.com/account/tokens"},{"heading":"install-dependencies-and-link-your-project","content":"Copy the access token and run `pnpm supabase login`"},{"heading":"install-dependencies-and-link-your-project","content":"It will prompt for an access token. Paste the token there. It should log you in."},{"heading":"install-dependencies-and-link-your-project","content":"Run `pnpm supabase link --project-ref {projectRef}` . Your project ref is available in the project settings section within the supabase dashboard."},{"heading":"install-dependencies-and-link-your-project","content":"Once you run the command, you will prompted for the database password. Enter the password, you saved earlier."},{"heading":"install-dependencies-and-link-your-project","content":"When it completes, your project will be linked to supabase."},{"heading":"install-dependencies-and-link-your-project","content":"Run `pnpm supabase db push` . This will push all the nextbase magic into your own supabase project. Teams, admin panel, projects etc will now be ready for use."},{"heading":"setup-env-variables-for-production","content":"In your repository directory, you should have a .env.local.example file which looks like this. Create a `.env.local` file right beside it but instead of dummy values like `YOUR_SUPABASE_PROJECT_REF`, fill the actual details."},{"heading":"setup-env-variables-for-production","content":"**NEXT\\_PUBLIC\\_SUPABASE\\_URL** is available in the project api settings section in supabase. It's called Project URL over there."},{"heading":"setup-env-variables-for-production","content":"**NEXT\\_PUBLIC\\_SUPABASE\\_PUBLISHABLE\\_KEY** and **SUPABASE\\_SECRET\\_KEY** are also available in the Project API settings section in supabase."},{"heading":"setup-env-variables-for-production","content":"Setup a Sendgrid / Post mark account and once you do, grab your authorised email sender address and add it to **ADMIN\\_EMAIL**. Same goes for **SENDGRID\\_API\\_KEY**. (If you want to use a different email provider, create a different environment variable here and set the relevant value)."},{"heading":"setup-env-variables-for-production","content":"Ensure that **NEXT\\_PUBLIC\\_SITE\\_URL** points to your domain."},{"heading":"setup-env-variables-for-production","content":"The other env variables should be relatively easy to setup."}],"headings":[{"id":"creating-an-account","content":"Creating an Account"},{"id":"creating-a-project","content":"Creating a Project"},{"id":"accessing-your-project","content":"Accessing Your Project"},{"id":"auth-configuration","content":"Auth configuration"},{"id":"install-dependencies-and-link-your-project","content":"Install dependencies and link your project"},{"id":"setup-env-variables-for-production","content":"Setup env variables for production"}]},"url":"/docs/v4/installation/supabase-for-production","title":"Setting up Supabase for Production","description":"Learn how to set up Supabase for production. This guide will walk you through creating an account, creating a project, and setting up environment variables for production."},{"_id":"/docs/v4/integrations/api-key-authentication-with-unkey","structured":{"contents":[{"content":"Nextbase Ultimate comes with unkey configured."},{"content":"The Unkey integration has changed due to how JWTs are now managed differently in Supabase\n(asymmetric keys). Documentation for this section is currently in progress. In the meantime,\nplease view this GitHub\ncomment for more details."},{"heading":"why-use-unkey","content":"Unkey is a service that allows you to create API keys for your users."},{"heading":"why-use-unkey","content":"It allows you to create API keys for your users without having to store them in your database."},{"heading":"why-use-unkey","content":"It allows you to revoke API keys for your users without having to update your database."},{"heading":"why-use-unkey","content":"Keys can be created for limited time periods too."},{"heading":"why-use-unkey","content":"The keys can then be used to authenticate your users to your API endpoints."},{"heading":"why-use-unkey","content":"Usage with Zapier, Make and Integromat are also now easy."},{"heading":"configuring-unkey","content":"Necessary env variables that you need for Unkey after creating an account there"},{"heading":"configuring-unkey","content":"1 UNKEY\\_ROOT\\_KEY:"},{"heading":"configuring-unkey","content":"This variable likely represents the root key associated with Unkey.\nIt may be used for specific administrative or configuration purposes within Unkey."},{"heading":"configuring-unkey","content":"2 UNKEY\\_API\\_ID:"},{"heading":"configuring-unkey","content":"This variable seems to be associated with an API ID in the context of Unkey.\nIt may serve as an identifier for your Unkey API integration."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Import Unkey Middleware\nImport the `withUnkeyAuth` middleware from the Unkey package at the beginning of your route handler file."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Implement the Route Handler\nCreate your route handler logic, ensuring that it includes the withUnkeyAuth middleware. This middleware handles the API key authentication process."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Now, this route handler is protected, and only requests with valid API keys will be allowed to access it."},{"heading":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"By following these steps, you can configure Unkey and create route handlers with API key authentication in your Nextbase project, ensuring secure and authenticated access to your protected routes."},{"heading":"disabling-unkey","content":"To disable Unkey, you can either remove its middleware from your route handlers or adjust your configuration to bypass Unkey authentication."},{"heading":"removing-unkey-middleware","content":"In your route handler files, look for the import statement that includes Unkey middleware and remove or comment out the line."},{"heading":"exposing-routes-which-can-be-used-with-api-key-auth","content":"To expose routes with API key authentication, follow these steps:"},{"heading":"1-import-unkey-middleware","content":"In your route handler files, import the Unkey middleware (`withUnkeyAuth`) at the beginning of the file. This middleware will handle the API key authentication process."},{"heading":"2-use-unkey-middleware-in-route-handlers","content":"Apply the Unkey middleware to the routes you want to expose with API key authentication. Wrap your route handler logic with withUnkeyAuth:"},{"heading":"2-use-unkey-middleware-in-route-handlers","content":"By applying withUnkeyAuth to a route handler, you ensure that the route is protected and requires a valid API key for access."},{"heading":"bonus-usage-with-zapier","content":"Since API key auth is now built in to Nextbase, configuring Zapier, Make or Integromat is now easy."},{"heading":"bonus-usage-with-zapier","content":"Zapier guide coming soon."},{"heading":"bonus-usage-with-zapier","content":"TODO"}],"headings":[{"id":"why-use-unkey","content":"Why use Unkey?"},{"id":"configuring-unkey","content":"Configuring unkey"},{"id":"steps-for-creating-unkey-root-key","content":"Steps for creating Unkey Root key"},{"id":"creating-an-route-handler-which-can-be-used-with-api-key-auth","content":"Creating an route handler which can be used with api key auth"},{"id":"disabling-unkey","content":"Disabling unkey"},{"id":"removing-unkey-middleware","content":"Removing Unkey Middleware"},{"id":"exposing-routes-which-can-be-used-with-api-key-auth","content":"Exposing routes which can be used with API key auth"},{"id":"1-import-unkey-middleware","content":"1\\. Import Unkey Middleware"},{"id":"2-use-unkey-middleware-in-route-handlers","content":"2\\. Use Unkey Middleware in Route Handlers"},{"id":"bonus-usage-with-zapier","content":"(Bonus): Usage with Zapier"}]},"url":"/docs/v4/integrations/api-key-authentication-with-unkey","title":"API Key Authentication with Unkey","description":"Learn how to use Unkey for API key authentication in Nextbase Ultimate and how to expose routes with API key auth."},{"_id":"/docs/v4/integrations/resend-and-react-email-integration","structured":{"contents":[{"heading":"resend-and-react-email-integration","content":"Nextbase comes with Resend and React Email configured. This makes creating/modifying email templates and sending them to your customers very easy."},{"heading":"resendcom","content":"Resend serves as the backbone for handling email delivery in Nextbase.\nIt streamlines the process of sending emails, managing templates, and handling various aspects of email communication.\nWith Resend, you can focus on creating compelling content, knowing that the delivery mechanism is robust and reliable."},{"heading":"resendcom","content":"**You can use a different email service provider if you want to.**"},{"heading":"how-to-use-resend-in-nextjs","content":"Resend is usually configured like this in Next.js."},{"heading":"how-to-use-resend-in-nextjs","content":"Install resend as a dependency and then import it into your application."},{"heading":"how-to-use-resend-in-nextjs","content":"Create an account in Resend and get your API key."},{"heading":"how-to-use-resend-in-nextjs","content":"Add your API key to your environment variables."},{"heading":"how-to-use-resend-in-nextjs","content":"Then, you can use it like this."},{"heading":"react-email-declarative-email-templating","content":"`react-email` is a React library that allows you to compose email templates using familiar React components. Instead of dealing with complex HTML templates, you can leverage the power of React to create modular and dynamic email content. This integration aligns with the React philosophy, making it intuitive for developers already familiar with React development.\nNextbase provides a seamless integration of Resend and `react-email` so that you can harness the benefits of both tools effortlessly."},{"heading":"emails-folder","content":"Nextbase comes with the `emails` folder configured as the repository for your React Email components. You can write your emails here and also preview them with the email development server by running `pnpm email-dev`."},{"heading":"react-email-development-server","content":"In Nextbase, a React Email development server is set up to streamline the process of writing and previewing emails. This development server allows developers to interactively design and visualize emails in a browser, facilitating a more efficient email template creation workflow."},{"heading":"react-email-development-server","content":"**Package.json Script**"},{"heading":"react-email-development-server","content":"The React Email development server is typically configured as a script in the package.json file. The script is named \"email:dev\":"},{"heading":"react-email-development-server","content":"To start the React Email development server, developers can run the following command in the terminal:"},{"heading":"react-email-development-server","content":"This starts the React Email development server and provides a convenient way to preview email templates."},{"heading":"react-email-development-server","content":"**/src/emails:**"},{"heading":"react-email-development-server","content":"Each file in the `emails` folder corresponds to a specific type of email, and it contains a React component responsible for rendering the content of that email.\nWe can then send the emails using `Resend`. Here are some emails we already have in the `emails` folder:"},{"heading":"welcomeemailtsx","content":"In this example, the WelcomeEmail component represents the content of a welcome email. This declarative approach simplifies the process of creating and maintaining various email templates in your application."},{"heading":"invitationtojointsx","content":"This is the organization invitation email template. When a user is invited to join an organization, this email is sent to them."},{"heading":"invitationtojointsx","content":"The email is then sent like this."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"It is recommended to use a third-party SMTP service for production environments in Supabase."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"Reliability: Supabase built-in SMTP server is severely rate limited. It is only suitable for development and testing purposes. Supabase recommends using a third-party SMTP service for production environments. Resend provides a robust and reliable email delivery mechanism."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"Observability: Resend provides a dashboard to monitor email delivery and other metrics."},{"heading":"optional-configure-resend-with-supabase-smtp-server","content":"In a production environment, it's crucial to ensure reliable email delivery. Ensure that Resend or another third-party SMTP service is configured in your Supabase project."},{"heading":"react-email-and-resend-integration-in-nextbase","content":"See `src/utils/api-routes/utils.ts` for an example of how to we use Resend in Nextbase.\nHere the `sendEmail` function is used to send emails using Resend. It receives the email options as an argument and sends the email using Resend."},{"heading":"configuration","content":"You only require the API key to configure Resend. You can find the API key in the Resend dashboard."}],"headings":[{"id":"resend-and-react-email-integration","content":"Resend and React Email Integration"},{"id":"resendcom","content":"Resend.com"},{"id":"how-to-use-resend-in-nextjs","content":"How to use Resend in Next.js?"},{"id":"react-email-declarative-email-templating","content":"React-email: Declarative Email Templating"},{"id":"emails-folder","content":"`emails` folder"},{"id":"react-email-development-server","content":"React Email Development Server"},{"id":"welcomeemailtsx","content":"WelcomeEmail.tsx:"},{"id":"invitationtojointsx","content":"InvitationToJoin.tsx"},{"id":"optional-configure-resend-with-supabase-smtp-server","content":"(Optional) Configure Resend with Supabase SMTP Server"},{"id":"react-email-and-resend-integration-in-nextbase","content":"React Email and Resend Integration in Nextbase"},{"id":"configuration","content":"Configuration"}]},"url":"/docs/v4/integrations/resend-and-react-email-integration","title":"Resend and React Email Integration","description":"Learn how to use Resend and React Email in Nextbase."},{"_id":"/docs/v4/known-issues/build-error-undici","structured":{"contents":[{"heading":"build-error-with-undici","content":"You may encounter a build error related to Undici when trying to deploy your Next.js application to a production environment. This issue typically occurs when using `localhost` URLs in your production configuration."},{"heading":"the-problem","content":"The error message might look something like this:"}],"headings":[{"id":"build-error-with-undici","content":"Build Error with Undici"},{"id":"the-problem","content":"The Problem"}]},"url":"/docs/v4/known-issues/build-error-undici","title":"Build Error with Undici","description":"How to resolve the build error related to Undici when using localhost URLs in production environments."},{"_id":"/docs/v4/known-issues/fetch-failed-build-error-undici","structured":{"contents":[{"heading":"build-error-with-undici","content":"You may encounter a build error related to Undici when trying to deploy your Next.js application to a production environment. This issue typically occurs when using `localhost` URLs in your production configuration."},{"heading":"the-problem","content":"The error message might look something like this:"},{"heading":"references","content":"This issue on the next.js GitHub repository talks about it:"},{"heading":"references","content":"https\\://github.com/vercel/next.js/issues/44062"},{"heading":"references","content":"https\\://github.com/vercel/next.js/issues/44062#issuecomment-1445183293"},{"heading":"fix","content":"To fix the issue, you need to ensure that the build environment doesn't have environment variables that point to `localhost`. If they are, you need to change them to 127.0.0.1."}],"headings":[{"id":"build-error-with-undici","content":"Build Error with Undici"},{"id":"the-problem","content":"The Problem"},{"id":"references","content":"References"},{"id":"fix","content":"Fix"}]},"url":"/docs/v4/known-issues/fetch-failed-build-error-undici","title":"Build Error with Undici","description":"How to resolve the build error related to Undici when using localhost URLs in production environments."},{"_id":"/docs/v4/known-issues/supabase-get-session-warning","structured":{"contents":[{"heading":"supabase-getsession-warning","content":"You may encounter a warning when using the `getSession()` method from Supabase's auth-js library. This is a known issue that has been identified and is being addressed by the Supabase team.\nIn most cases, the warning can be safely ignored since this warning features most of the time because of usage in middleware.\nHowever, a middleware is a critical part of any Next.js application's speed of route resolution and as several users pointed out in the github issue, this warning is unnecessary as middlewares should avoid network requests as much as possible and only\nauthenticate by validating the cookie. A network for each middleware path will slow down app significantly."},{"heading":"current-status","content":"We are aware of this issue, which is tracked in the following GitHub pull request:"},{"heading":"current-status","content":"https\\://github.com/supabase/auth-js/pull/895"},{"heading":"temporary-workaround","content":"The Supabase team was requested to remove the warning. Once they implement the fix upstream, we will update our package to incorporate the changes. This update will eliminate the warning message.\nWhile waiting for the official fix, you can safely continue using the `getSession()` method. The warning does not affect the functionality of your application, but it may appear in your console logs."},{"heading":"stay-updated","content":"We recommend keeping an eye on the GitHub pull request mentioned above for the most up-to-date information on the progress of this fix. Once the update is available, we will promptly integrate it into our package."}],"headings":[{"id":"supabase-getsession-warning","content":"Supabase getSession() Warning"},{"id":"current-status","content":"Current Status"},{"id":"temporary-workaround","content":"Temporary Workaround"},{"id":"stay-updated","content":"Stay Updated"}]},"url":"/docs/v4/known-issues/supabase-get-session-warning","title":"Supabase getSession() Warning","description":"Explanation of the known warning when using Supabase getSession() and the upcoming fix."},{"_id":"/docs/v4/payments/stripe-configuration","structured":{"contents":[{"content":"Stripe is implemented using an Abstract payment provider. It has the following shape."}],"headings":[]},"url":"/docs/v4/payments/stripe-configuration","title":"Stripe Configuration"},{"_id":"/docs/v4/payments/stripe-webhooks","structured":{"contents":[{"content":"Stripe webhooks are crucial for keeping your application in sync with events that happen in Stripe, such as successful payments, subscription updates, or failed charges. In NextBase, we've set up a webhook endpoint to handle these events. Let's break down how it's configured:"},{"heading":"webhook-endpoint","content":"The webhook endpoint is set up in `src/app/api/stripe/webhooks/route.ts`. This file contains a POST route handler that processes incoming webhook events from Stripe."},{"heading":"webhook-endpoint","content":"The webhook syncs products, prices, and subscriptions with our database."}],"headings":[{"id":"webhook-endpoint","content":"Webhook Endpoint"}]},"url":"/docs/v4/payments/stripe-webhooks","title":"Stripe Webhooks"},{"_id":"/docs/v4/performance/strategies-for-pages","structured":{"contents":[{"heading":"strategies-for-pages","content":"In this Next.js 14 codebase, we employ several strategies to enhance page performance using async/await and Suspense. These techniques are crucial for optimizing data fetching and rendering, ensuring a smooth user experience."},{"heading":"strategies-for-pages","content":"Asynchronous Data Fetching in Server Components"},{"heading":"strategies-for-pages","content":"Server components in Next.js can execute asynchronously, allowing us to fetch data on the server before rendering the component. This approach reduces bundle sizes and leverages server CPU power for faster data fetching."},{"heading":"strategies-for-pages","content":"For example, in the roadmap page, we asynchronously fetch data for different roadmap sections like planned, in progress, and completed tasks using async functions before rendering the components."},{"heading":"strategies-for-pages","content":"Parallel Data Fetching with Suspense"},{"heading":"strategies-for-pages","content":"We use React's Suspense component to fetch multiple data sources in parallel and render child components as soon as the data is available. This technique allows the page to start rendering without waiting for all data to be fetched, improving perceived performance."},{"heading":"strategies-for-pages","content":"For instance, the blog list page fetches blog posts and tags in parallel, wrapped in Suspense, enhancing the user experience by streaming rendering."},{"heading":"strategies-for-pages","content":"Server components have the added benefit of streaming rendering. Consider the page responsible for rendering the blog list page at `/blog`. This page has to fetch two sets of data: the list of blog posts and the list of tags. We can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components: `Tags` and `BlogList`."},{"heading":"strategies-for-pages","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"strategies-for-pages","content":"Suspense for Incremental Rendering"},{"heading":"strategies-for-pages","content":"We also use Suspense to incrementally render parts of the page as data becomes available. This approach is evident in the changelog page, where the changelog list is rendered asynchronously and wrapped in Suspense to display a loading state until the data is fetched."}],"headings":[{"id":"strategies-for-pages","content":"Strategies for pages"}]},"url":"/docs/v4/performance/strategies-for-pages","title":"Performance Strategies"},{"_id":"/docs/v4/performance/streaming-suspense-rendering","structured":{"contents":[{"heading":"streaming-server-rendering","content":"In our Next.js 14 pages, we leverage streaming server rendering primarily through the use of server components and the Suspense component for data fetching and rendering optimization. This approach allows us to start rendering the page even before all the data is fetched, improving the user experience by displaying content as soon as it's available."},{"heading":"example-of-streaming-server-rendering","content":"In the blog list page, we demonstrate streaming server rendering by fetching and rendering two sets of data in parallel: the list of blog posts and the list of tags. This is facilitated by moving the data fetching into two separate components, Tags and BlogList, which are server components. By wrapping these components in Suspense, we can render them in parallel and stream the rendering of the page as soon as the data is available, without waiting for all data to be fetched."},{"heading":"example-of-streaming-server-rendering","content":"Server components have the added benefit of streaming rendering.\nConsider the page responsible for rendering the blog list page at **/blog** route.\nThis page has to fetch two sets of data: the list of blog posts and the list of tags.\nWe can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.\nIn this case, we have moved the data fetching into two separate components:"},{"heading":"example-of-streaming-server-rendering","content":"Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel.\nAlso, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available."},{"heading":"benefits","content":"Improved User Experience: The page starts rendering immediately, displaying the \"All blog posts\" title and placeholders for tags and posts until the actual data is fetched. This approach significantly enhances the perceived performance of the application."},{"heading":"benefits","content":"Efficient Data Fetching: By fetching data in server components, we leverage the server's CPU power, leading to faster data retrieval and smaller bundle sizes since we don't have to ship the data fetching logic to the client.\nImplementation"},{"heading":"benefits","content":"The implementation of streaming server rendering in our Next.js 14 pages is a testament to the framework's capabilities in optimizing performance and enhancing the user experience. By judiciously using server components and Suspense, we ensure that our pages are not only fast but also provide immediate feedback to users, which is crucial for modern web applications."}],"headings":[{"id":"streaming-server-rendering","content":"Streaming server rendering"},{"id":"example-of-streaming-server-rendering","content":"Example of Streaming Server Rendering"},{"id":"benefits","content":"Benefits"}]},"url":"/docs/v4/performance/streaming-suspense-rendering","title":"Streaming Suspense Server Rendering"},{"_id":"/docs/v4/quick-setup","structured":{"contents":[{"heading":"super-quick-dev-setup","content":"This guide covers the basic setup for Supabase and Next.js."},{"heading":"super-quick-dev-setup","content":"For integrating Stripe, refer to the comprehensive setup guide."},{"heading":"1-clone-the-repository-and-install-dependencies","content":"This command installs all necessary dependencies."},{"heading":"2-copy-envlocalexample","content":"Duplicate the `.env.local.example` file at the project root as `.env.local`."},{"heading":"3-launch-supabase-locally","content":"**Note:** Ensure Docker is installed on your system. Visit Docker's documentation for installation details."},{"heading":"3-launch-supabase-locally","content":"After Supabase starts, run `pnpm supabase status` to get the actual key values. Copy the `publishable key` and `secret key` from the terminal output and update your `.env.local` file:"},{"heading":"3-launch-supabase-locally","content":"Set `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` to the `publishable key` value"},{"heading":"3-launch-supabase-locally","content":"Set `SUPABASE_SECRET_KEY` to the `secret key` value"},{"heading":"4-start-the-app-by-running-the-following-command","content":"You're all set! The app should now be accessible at http\\://localhost:3000.\nStripe and pricing aren't configured yet, but you can start building your app with Supabase and Next.js."},{"heading":"optional-setting-up-stripe-locally","content":"To set up Stripe locally, refer to the comprehensive setup guide."}],"headings":[{"id":"super-quick-dev-setup","content":"Super quick dev setup"},{"id":"nextbase-ultimate-setup-video","content":"Nextbase Ultimate setup video"},{"id":"1-clone-the-repository-and-install-dependencies","content":"1\\. Clone the repository and install dependencies:"},{"id":"2-copy-envlocalexample","content":"2\\. Copy .env.local.example"},{"id":"3-launch-supabase-locally","content":"3\\. Launch Supabase locally:"},{"id":"4-start-the-app-by-running-the-following-command","content":"4\\. Start the App by running the following command:"},{"id":"optional-setting-up-stripe-locally","content":"(Optional) Setting up **Stripe** locally"}]},"url":"/docs/v4/quick-setup","title":"Quick setup","description":"Setting up nextbase is easy. Follow these steps to get started."},{"_id":"/docs/v4/security/access-control-on-supabase-functions","structured":{"contents":[{"content":"Supabase functions can be accessed by anyone, but we can control access to them using Supabase RBAC."}],"headings":[]},"url":"/docs/v4/security/access-control-on-supabase-functions","title":"Access control on Supabase functions","description":"How to control access to your Supabase functions."},{"_id":"/docs/v4/security/protected-nextjs-areas","structured":{"contents":[{"heading":"protected-nextjs-pages-and-api-routes","content":"The `app_admin` folder is only accessible to application admins. Hence all pages which start with the route `app_admin` are only accessible to app admins."},{"heading":"protected-nextjs-pages-and-api-routes","content":"The `dashboard` route , `setting` routes, `organization` routes are all only accessible to logged in users."},{"heading":"protected-nextjs-pages-and-api-routes","content":"Other pages are public."},{"heading":"creating-new-pags","content":"To create pages only accessible to logged in users, create them within the `(authenticated-pages)/(user-pages)` folders. This will make them available to all pages including app\\_admins."},{"heading":"creating-new-pags","content":"To create pages only accessible to app\\_admin users, create them within the `(authenticated-users)/(app_admin)` folder."},{"heading":"creating-new-pags","content":"`(external-pages)` are available to logged out users as well."},{"heading":"creating-new-pags","content":"`(login-pages)` should contain all authentication related pages such as login, signup etc."}],"headings":[{"id":"protected-nextjs-pages-and-api-routes","content":"Protected Next.js pages and api routes"},{"id":"creating-new-pags","content":"Creating new pags"}]},"url":"/docs/v4/security/protected-nextjs-areas","title":"Protected Next.js areas","description":"How to create new pages and routes that are protected."},{"_id":"/docs/v4/security/row-level-security","structured":{"contents":[{"content":"Row Level Security (RLS) is a feature in PostgreSQL that provides the ability to control which users can `SELECT`, `INSERT`, `UPDATE`, or `DELETE` which rows in a table. It adds an additional layer of security in multi-user database environments and is crucial for protecting sensitive data.\nUnderstanding Row Level Security"},{"content":"RLS works by enabling you to define policies that restrict, on a per-user basis, whether certain rows can be returned by a `SELECT` query or modified by an `INSERT`, `UPDATE`, or `DELETE` query. This is particularly useful in multi-tenant environments where you want to ensure that users can only access their own data."},{"content":"In PostgreSQL, RLS is disabled by default for all tables. To enable RLS, you use the `ALTER TABLE` command with the ENABLE ROW LEVEL SECURITY clause. Once RLS is enabled, all users (except the table owner and superusers) are denied access to the table—unless a policy is explicitly added to grant them access."},{"heading":"importance-of-row-level-security","content":"RLS is important for a variety of reasons:"},{"heading":"importance-of-row-level-security","content":"Data Protection: RLS helps to protect sensitive data by ensuring that users can only access data that they are authorized to see. This is particularly important in industries such as healthcare or finance where regulations require strict controls over data access."},{"heading":"importance-of-row-level-security","content":"Multi-tenancy: In multi-tenant environments, RLS can be used to ensure that each tenant can only access their own data. This is much more efficient and secure than having separate tables or databases for each tenant."},{"heading":"importance-of-row-level-security","content":"Flexibility: RLS policies can be defined using any boolean SQL expression, which provides a great deal of flexibility. For example, you could define a policy that only allows users to access data that was created within the last 30 days."}],"headings":[{"id":"importance-of-row-level-security","content":"Importance of Row Level Security"}]},"url":"/docs/v4/security/row-level-security","title":"Row Level Security","description":"Row Level Security (RLS) is a feature in PostgreSQL that provides the ability to control which users can SELECT, INSERT, UPDATE, or DELETE which rows in a table. It adds an additional layer of security in multi-user database environments and is crucial for protecting sensitive data."},{"_id":"/docs/v4/tech-stack/why-nextjs","structured":{"contents":[{"content":"There are several reasons why we chose Next.js over other options when building a server-rendered React application:"},{"content":"Ease of use: Next.js is designed to be easy to learn and use, with a simple and intuitive API and a wide range of built-in features. This can make it a good choice for developers who are new to server-rendered React applications or who want to get up and running quickly."},{"content":"Performance: Next.js is optimized for performance, with features such as automatic code splitting and server rendering that can help to improve the load time and overall performance of our application."},{"content":"Scalability: Next.js is designed to be scalable, with support for static exports and the ability to run on a serverless architecture. This can make it a good choice for applications that are expected to handle a large number of users or a high volume of traffic."},{"content":"Community and ecosystem: Next.js has a large and active community of developers, with a wide range of resources and tools available to support building applications with the framework. This can make it easier to find help and support when working with Next.js."},{"content":"Next 13 is massive leap in terms of performance and developer experience and we experienced it while building apps in our other projects and we think no one should miss out on using Next.js 14+."},{"content":"It really is taking the world by storm.\nhttps\\://mobilecoderz.com/blog/next-js-framework-the-next-big-thing-of-2022/"}],"headings":[]},"url":"/docs/v4/tech-stack/why-nextjs","title":"Why Next.js?","description":"Why we chose Next.js for server-rendered React applications."},{"_id":"/docs/v4/tech-stack/why-shadcn-ui","structured":{"contents":[{"content":"Shadcn UI is a component library that is built on top of Radix UI. It is a set of reusable UI components that are styled with Tailwind CSS. It is designed to be used with Next.js and React."},{"content":"We chose Shadcn UI because it is a modern and easy to use component library that is built on top of Radix UI. It is designed to be used with Next.js and React."},{"content":"It is built with Tailwind CSS and TypeScript. It is designed to be used with Next.js and React."},{"content":"It is designed to be used with Next.js and React."}],"headings":[]},"url":"/docs/v4/tech-stack/why-shadcn-ui","title":"Why Shadcn UI?","description":"Why we chose Shadcn UI for our design system."},{"_id":"/docs/v4/tech-stack/why-stripe","structured":{"contents":[{"content":"While there are many payments providers today, we went with Stripe which is the leading payments provider of choice for most SaaS companies today."},{"content":"Wide range of payment options: Stripe supports a wide range of payment methods, including credit cards, debit cards, and online payment services such as Apple Pay and Google Pay. This can make it easy for businesses to accept payments from a variety of sources."},{"content":"Customizable payment forms: Stripe allows businesses to customize the look and feel of their payment forms to match their brand. This can help to create a seamless and professional payment experience for customers."},{"content":"Advanced fraud protection: Stripe provides advanced fraud protection to help businesses detect and prevent fraudulent activity. This can help to reduce the risk of chargebacks and other fraudulent activity."},{"content":"Easy integration: Stripe offers a wide range of tools and libraries to make it easy to integrate the platform into a business's existing systems and processes. This can help to streamline the payment process and reduce the time and effort required to manage payments."}],"headings":[]},"url":"/docs/v4/tech-stack/why-stripe","title":"Why Stripe?","description":"Why Stripe is the leading payments provider of choice for most SaaS companies today. Wide range of payment options, customizable payment forms, advanced fraud protection, and easy integration."},{"_id":"/docs/v4/tech-stack/why-supabase","structured":{"contents":[{"content":"Supabase has had a breakthrough last couple of years in SaaS products usage. It is the new Firebase but better."},{"content":"We went with Supabase because it makes backend and database development a breeze."},{"content":"It is realtime first, open source and built on top of Postgres. It provides a powerful dashboard, with many many features. Some include"},{"content":"Easy to use Javascript and Node.js clients. They also have Flutter, Swift and Dart clients to easily built mobile apps later."},{"content":"Realtime with just a checkbox. All your database tables can be made realtime with just a click of a button."},{"content":"Database hooks - Allows you to integrate with Zapier, Make etc and connect your app to other apps"},{"content":"Edge functions"},{"content":"Database triggers and functions"},{"content":"Admin Panel"},{"content":"Custom reports and Graphs"},{"content":"Errors dashboard"},{"content":"Foreign Table Wrappers (In development) and so many more."},{"content":"If you want to build robust webapps quickly, Supabase is the way to go in 2023."}],"headings":[]},"url":"/docs/v4/tech-stack/why-supabase","title":"Why Supabase?","description":"Why we chose Supabase for our backend and database needs"},{"_id":"/docs/v4/troubleshooting/login-callback-wrong","structured":{"contents":[{"content":"This is generally because the Supabase Authentication / URL configuration is not setup correctly. Ensure the following:"},{"content":"The Site URL is set to your production domain eg: `https://usenextbase.com`"},{"content":"The Redirect urls include both your production and development hosts"},{"content":"eg : `https://usenextbase/com/**` and `http://localhost:3000/**`"}],"headings":[]},"url":"/docs/v4/troubleshooting/login-callback-wrong","title":"I am unable to login","description":"This is generally because the Supabase Authentication / URL configuration is not setup correctly."},{"_id":"/docs/v4/ui/dark-mode","structured":{"contents":[{"heading":"dark-mode","content":"NextBase supports dark mode for your application."}],"headings":[{"id":"dark-mode","content":"Dark Mode"}]},"url":"/docs/v4/ui/dark-mode","title":"Dark Mode","description":"NextBase supports dark mode for your application."},{"_id":"/docs/v4/ui/nprogress","structured":{"contents":[{"heading":"nprogress","content":"NProgress is a library that provides a progress bar for your application."}],"headings":[{"id":"nprogress","content":"NProgress"}]},"url":"/docs/v4/ui/nprogress","title":"NProgress","description":"NProgress is a library that provides a progress bar for your application."},{"_id":"/docs/v4/ui/shadcn-ui","structured":{"contents":[{"heading":"shadcn-ui","content":"Shadcn UI is a very popular library for building UI components.\nIt is built on top of Radix UI and Tailwind CSS."},{"heading":"shadcn-ui","content":"Checkout their website for more information."}],"headings":[{"id":"shadcn-ui","content":"Shadcn UI"}]},"url":"/docs/v4/ui/shadcn-ui","title":"Shadcn UI","description":"Shadcn UI is a very popular library for building UI components."},{"_id":"/docs/v4/ui/sonner","structured":{"contents":[{"content":"Sonner is quite a simple library to use. We chose sonner because it's styling doesn't get in the way and it's easy to use."}],"headings":[{"id":"usage","content":"Usage"}]},"url":"/docs/v4/ui/sonner","title":"Sonner","description":"Sonner is a library that provides a toast notification for your application."},{"_id":"/docs/v4/ui/tailwindcss","structured":{"contents":[{"content":"Tailwind has been configured with next.js."},{"content":"We have the following plugins configured:"},{"content":"`tailwindcss-animate`: This plugin adds a set of helpful animation utilities to Tailwind CSS."},{"content":"`@tailwindcss/typography`: This plugin provides a set of `prose` classes for beautiful typographic defaults."},{"content":"Additionally, we've extended the default Tailwind configuration with:"},{"content":"Custom color palette including primary, secondary, destructive, muted, accent, popover, and card colors."},{"content":"Custom border radius values."},{"content":"Custom font families:"},{"content":"`sans`: Uses the Inter font"},{"content":"`display`: Uses the Lexend font"},{"content":"`sans2`: Uses the Manrope font"},{"content":"Custom keyframes and animations:"},{"content":"`accordion-down` and `accordion-up`"},{"content":"`scale-up` and `scale-down`"},{"content":"Extended max-width with an `8xl` size."},{"content":"The configuration also includes a dark mode setup using the `class` strategy, allowing for easy implementation of a dark theme in your application."}],"headings":[]},"url":"/docs/v4/ui/tailwindcss","title":"Tailwind CSS","description":"Tailwind CSS is a utility-first CSS framework."},{"_id":"/docs/v5/agent-connections/connect-an-agent","structured":{"contents":[{"content":"You need an agent client that supports remote Streamable HTTP MCP, OAuth and public dynamic client registration. The exact labels in its settings will depend on the client."},{"heading":"1-open-your-connection-settings","content":"Sign in to the app and open **Agent connections**. Use these paths if you want to go there directly:"},{"heading":"1-open-your-connection-settings","content":"Kits"},{"heading":"1-open-your-connection-settings","content":"User connections"},{"heading":"1-open-your-connection-settings","content":"Admin connections"},{"heading":"1-open-your-connection-settings","content":"Supabase, Neon and D1"},{"heading":"1-open-your-connection-settings","content":"`/mcp/connections/user` on the web app"},{"heading":"1-open-your-connection-settings","content":"`/mcp/connections/admin` on the admin app"},{"heading":"1-open-your-connection-settings","content":"Convex"},{"heading":"1-open-your-connection-settings","content":"`/user/settings/connections` on the web app"},{"heading":"1-open-your-connection-settings","content":"`/app-admin/connections` on the admin app"},{"heading":"1-open-your-connection-settings","content":"The admin page requires an application administrator account. Every kit ships a separate admin app with its own origin (the demos use a `-admin` subdomain), and that is where the admin connections page lives. See your kit's agent connections page for the exact addresses."},{"heading":"2-copy-the-server-address","content":"Copy the URL shown on that page into your agent's remote MCP settings. Select OAuth authentication."},{"heading":"2-copy-the-server-address","content":"For the live demos, the user servers are:"},{"heading":"2-copy-the-server-address","content":"Starter"},{"heading":"2-copy-the-server-address","content":"User MCP"},{"heading":"2-copy-the-server-address","content":"Next.js Supabase"},{"heading":"2-copy-the-server-address","content":"`https://nextjs-supabase.usenextbase.com/mcp/user`"},{"heading":"2-copy-the-server-address","content":"TanStack Supabase"},{"heading":"2-copy-the-server-address","content":"`https://tanstack-supabase.usenextbase.com/mcp/user`"},{"heading":"2-copy-the-server-address","content":"Next.js Convex"},{"heading":"2-copy-the-server-address","content":"`https://dynamic-grasshopper-80.convex.site/mcp/user`"},{"heading":"2-copy-the-server-address","content":"TanStack Convex"},{"heading":"2-copy-the-server-address","content":"`https://ceaseless-lark-846.convex.site/mcp/user`"},{"heading":"2-copy-the-server-address","content":"Next.js Neon"},{"heading":"2-copy-the-server-address","content":"`https://nextjs-neon.usenextbase.com/mcp/user`"},{"heading":"2-copy-the-server-address","content":"TanStack Neon"},{"heading":"2-copy-the-server-address","content":"`https://tanstack-neon.usenextbase.com/mcp/user`"},{"heading":"2-copy-the-server-address","content":"Next.js D1"},{"heading":"2-copy-the-server-address","content":"`https://nextjs-d1.usenextbase.com/mcp/user`"},{"heading":"2-copy-the-server-address","content":"TanStack D1"},{"heading":"2-copy-the-server-address","content":"`https://tanstack-d1.usenextbase.com/mcp/user`"},{"heading":"2-copy-the-server-address","content":"For the admin server, replace the final `/user` with `/admin`. Use your own app's addresses when you deploy your product; the table above points to the demos."},{"heading":"3-review-the-connection","content":"The agent opens the consent page. Sign in with your normal account if needed. Check the callback address and permissions, then select the workspace you want the agent to use."},{"heading":"3-review-the-connection","content":"Client names are supplied by the client itself. The consent screen marks them as unverified, so use the callback address and the client you started from to check the request."},{"heading":"3-review-the-connection","content":"Supabase, Neon and D1 offer read access. Convex offers an additional project creation permission when it is requested and you choose to allow it."},{"heading":"4-try-a-tool","content":"Ask the agent to list projects in the selected workspace. If you are testing an admin connection, ask it to list workspaces."},{"heading":"4-try-a-tool","content":"A blank project list can be valid. Add a project through the website and try again."},{"heading":"5-disconnect-when-you-are-done","content":"Open the connection settings and use **Disconnect** or **Revoke**, depending on the kit. Signing out of the session that approved the connection also ends its access."},{"heading":"5-disconnect-when-you-are-done","content":"Access tokens are short-lived. Connections can refresh for up to 30 days, but may end sooner with the original sign-in session. If access has ended, start a new connection."},{"heading":"5-disconnect-when-you-are-done","content":"If the client never opens consent or loops through login, see troubleshooting."}],"headings":[{"id":"1-open-your-connection-settings","content":"1\\. Open your connection settings"},{"id":"2-copy-the-server-address","content":"2\\. Copy the server address"},{"id":"3-review-the-connection","content":"3\\. Review the connection"},{"id":"4-try-a-tool","content":"4\\. Try a tool"},{"id":"5-disconnect-when-you-are-done","content":"5\\. Disconnect when you are done"}]},"url":"/docs/v5/agent-connections/connect-an-agent","title":"Connect an agent","description":"Copy your server address, sign in and approve access to one workspace."},{"_id":"/docs/v5/agent-connections","structured":{"contents":[{"content":"MCP lets an agent talk to your application through a set of named tools. In Nextbase, those tools use the same accounts and workspace permissions as the website."},{"heading":"two-different-connections","content":"**User MCP** is for a person using the app. The connection is limited to the workspace they choose. It can read account context and list projects. Convex kits can also create a project with optional write permission."},{"heading":"two-different-connections","content":"**Admin MCP** is for an application administrator. It can read user and workspace information across the application. It requires a current admin role and has its own endpoint and credentials."},{"heading":"two-different-connections","content":"Keeping these separate makes the permission request easier to understand. The user approves exactly the kind of access the agent needs."},{"heading":"oauth-is-included","content":"The agent opens the app's consent page, the user signs in, and the app shows the callback address, workspace and permissions. After approval, the client receives credentials for that connection."},{"heading":"oauth-is-included","content":"You can inspect and disconnect connections from the website. You can continue using the app's normal login at the same time."},{"heading":"where-are-the-endpoints","content":"Supabase, Neon and D1 serve the User MCP from the web app's origin, followed by `/mcp/user`, and the Admin MCP from the separate admin app's origin, followed by `/mcp/admin`. Each origin only serves its own audience; the other path returns 404."},{"heading":"where-are-the-endpoints","content":"Convex uses the backend's `.convex.site` origin. Its connection settings show the exact backend address. The two deployed demos are listed in connect an agent."},{"heading":"where-are-the-endpoints","content":"Start with the User MCP unless you need application-wide admin tools. Follow the connection guide for the full flow."}],"headings":[{"id":"two-different-connections","content":"Two different connections"},{"id":"oauth-is-included","content":"OAuth is included"},{"id":"where-are-the-endpoints","content":"Where are the endpoints?"}]},"url":"/docs/v5/agent-connections","title":"User and admin MCPs","description":"Let an agent use your app through a connection the user can approve and revoke."},{"_id":"/docs/v5/agent-connections/tools-and-permissions","structured":{"contents":[{"content":"These are the tools currently available. Use the names below when writing an integration or extending the starter."},{"content":"Server"},{"content":"Tool"},{"content":"Scope"},{"content":"Available in"},{"content":"User"},{"content":"`account_context`"},{"content":"`user:read`"},{"content":"All 8 kits"},{"content":"User"},{"content":"`projects_list`"},{"content":"`user:read`"},{"content":"All 8 kits"},{"content":"User"},{"content":"`projects_create`"},{"content":"`user:write`"},{"content":"Both Convex kits, with explicit write consent"},{"content":"Admin"},{"content":"`admin_users_list`"},{"content":"`admin:read`"},{"content":"All 8 kits"},{"content":"Admin"},{"content":"`admin_workspaces_list`"},{"content":"`admin:read`"},{"content":"All 8 kits"},{"heading":"user-permissions","content":"A User MCP connection selects one workspace. Being an administrator doesn't widen that connection to other workspaces or add admin tools to it."},{"heading":"user-permissions","content":"`projects_create` is an optional Convex tool. The user must approve write access, and their current workspace permissions must still allow project creation. Approval of read access alone doesn't allow a write."},{"heading":"user-permissions","content":"The Supabase, Neon and D1 MCP servers expose read-only tools in v5. Add a permission-aware tool and its tests before using these kits for a write workflow."},{"heading":"admin-permissions","content":"The Admin MCP lists users and workspaces. It doesn't change roles, impersonate users, run arbitrary queries or manage infrastructure credentials."},{"heading":"admin-permissions","content":"Every admin connection requires the application's current administrator role. Removing that role ends access even if the token hasn't expired yet."},{"heading":"oauth-support","content":"The current implementation supports public clients, dynamic client registration, authorization code flow and PKCE S256. The registered callback and requested MCP resource must match exactly."},{"heading":"oauth-support","content":"Client ID metadata documents and confidential clients are not supported in this release. A client that requires one of those methods needs an adapter change before it can connect."},{"heading":"oauth-support","content":"Browser requests are allowed from the configured app origin, Claude and ChatGPT. Desktop and server clients without an Origin header are also supported. This allowlist doesn't mean every client integration has been tested."},{"heading":"extending-the-tools","content":"For Supabase, Neon and D1, start in `apps/web/src/mcp/`. The protocol and provider adapters are separated, and the tests cover consent, token reuse, expiry, revocation and permission changes."},{"heading":"extending-the-tools","content":"For Convex, start in `apps/database/convex/mcp/`. The user and admin gateway components are separate, and tool handlers call the existing authenticated application functions."},{"heading":"extending-the-tools","content":"Keep the authenticated user identity on the server. A tool argument such as `userId` or `workspaceId` is not proof of permission. Resolve the connection and current access before returning data or making a change."}],"headings":[{"id":"user-permissions","content":"User permissions"},{"id":"admin-permissions","content":"Admin permissions"},{"id":"oauth-support","content":"OAuth support"},{"id":"extending-the-tools","content":"Extending the tools"}]},"url":"/docs/v5/agent-connections/tools-and-permissions","title":"Tools and permissions","description":"What each connection can do in the v5 release."},{"_id":"/docs/v5/authentication","structured":{"contents":[{"content":"Each starter keeps the authentication provider it was built with. Adding MCP doesn't create another account system for your users."},{"content":"Kits"},{"content":"Authentication"},{"content":"Where to configure it"},{"content":"Supabase"},{"content":"Supabase Auth"},{"content":"Supabase Auth settings and the kit's web env file"},{"content":"Convex"},{"content":"Convex Auth"},{"content":"Auth providers and environment variables in the selected Convex deployment"},{"content":"Neon"},{"content":"Better Auth"},{"content":"Application auth configuration and server environment variables"},{"content":"D1"},{"content":"Better Auth"},{"content":"Application auth configuration and Worker secrets"},{"heading":"email-and-password","content":"The kits have sign-up and login pages. A user creates an account, verifies their email where required, and signs in to the app. Use your kit's local inbox when testing email verification."},{"heading":"email-and-password","content":"For production, configure your own sender and delivery credentials. The email guide covers how Resend fits into each stack."},{"heading":"social-login-and-magic-links","content":"These depend on the providers configured for your kit. A provider button or server integration still needs that provider's credentials and callback URLs before it can work on your domain."},{"heading":"social-login-and-magic-links","content":"Configure the actual public application address, then check the login, email verification and password reset callbacks. A preview URL and a production URL can have different allowlists."},{"heading":"signing-in-during-an-agent-connection","content":"When an agent requests access, the app opens a consent page. If the user isn't signed in, they go through the existing login page and return to that same consent request."},{"heading":"signing-in-during-an-agent-connection","content":"The user chooses a workspace and approves the permissions. The agent receives its own OAuth credentials. It doesn't get the user's password or the application's database credentials."},{"heading":"when-access-ends","content":"A connection is tied to the native session that approved it. Signing out of that session, disconnecting the agent, or losing the required workspace or admin permissions ends delegated access. Email verification and account restrictions still apply."},{"heading":"when-access-ends","content":"If the agent stops working after a sign out, sign in again and reconnect it. See troubleshooting for the other common cases."}],"headings":[{"id":"email-and-password","content":"Email and password"},{"id":"social-login-and-magic-links","content":"Social login and magic links"},{"id":"signing-in-during-an-agent-connection","content":"Signing in during an agent connection"},{"id":"when-access-ends","content":"When access ends"}]},"url":"/docs/v5/authentication","title":"Authentication","description":"The same login provider for your users, your app and agent connections."},{"_id":"/docs/v5/building-with-agents","structured":{"contents":[{"content":"You can use a coding agent to work on any of the 8 repositories. Start by having it read the project before asking it to change a feature."},{"content":"The repositories have their own setup commands, permission checks and tests. Those are useful context for an agent, just as they are for a developer joining your team."},{"heading":"start-with-a-small-task","content":"Here is a prompt you can adapt:"},{"heading":"start-with-a-small-task","content":"Let the agent follow the existing service, query and mutation patterns. A new page should use the same permissions as the rest of your app."},{"heading":"repository-instructions-and-skills","content":"An `AGENTS.md`, when present, tells a coding agent how to work in that repository. Read the README too; it has setup steps and stack-specific commands that aren't the same across all eight kits."},{"heading":"repository-instructions-and-skills","content":"If your coding tool supports skills, enable the ones that match the kit: Next.js or the router in use, Supabase, Convex, Postgres or Cloudflare, and your deployment platform. Skills come from your coding tool or installed skill packages. v5 doesn't automatically install a shared set of skills into every repository."},{"heading":"let-an-agent-use-the-running-app","content":"For either Supabase kit, give each coding agent a separate git worktree and local Supabase project. Run setup in that worktree and use its assigned URLs and keys. Use Docker or Podman for parallel worktrees on your computer, or native mode for one project in a supported sandbox. Each agent should run migrations and database tests against its own local project."},{"heading":"let-an-agent-use-the-running-app","content":"MCP is useful once the feature exists in your app. A connected user agent can list projects in its selected workspace. An admin agent can look up users and workspaces through a separate connection."},{"heading":"let-an-agent-use-the-running-app","content":"For example:"},{"heading":"let-an-agent-use-the-running-app","content":"On the Convex kits, project creation is also available with explicit write consent. The other six kits currently expose read-only tools."},{"heading":"check-the-result-yourself","content":"Run the app, sign in as a normal user, and try the feature. Test another workspace and check the admin path if you changed admin behavior. Then run the checks in that kit's README."},{"heading":"check-the-result-yourself","content":"When you add a new MCP tool, document its scope, validate its inputs and call the existing permission-aware app code. Add tests for a user who has access and one who doesn't. See tools and permissions."}],"headings":[{"id":"start-with-a-small-task","content":"Start with a small task"},{"id":"repository-instructions-and-skills","content":"Repository instructions and skills"},{"id":"let-an-agent-use-the-running-app","content":"Let an agent use the running app"},{"id":"check-the-result-yourself","content":"Check the result yourself"}]},"url":"/docs/v5/building-with-agents","title":"Building with agents","description":"Use repository instructions, skills and small tasks to build your own product."},{"_id":"/docs/v5/choose-your-starter","structured":{"contents":[{"content":"There isn't one stack you need to use with Nextbase. Start with the one you are comfortable maintaining."},{"content":"Here is the complete v5 lineup. The names in the first column match the kits; the app layer column tells you what is running in the current repository. Each kit links to its overview, and from there to its installation, environment variables, authentication, agent connections, project structure and deployment pages."},{"content":"Starter"},{"content":"App layer"},{"content":"Database"},{"content":"Login provider"},{"content":"Hosting used in the demo"},{"content":"Next.js + Supabase"},{"content":"Next.js App Router"},{"content":"Supabase Postgres"},{"content":"Supabase Auth"},{"content":"Vercel"},{"content":"TanStack + Supabase"},{"content":"React Router 8 framework mode + Express"},{"content":"Supabase Postgres"},{"content":"Supabase Auth"},{"content":"Vercel"},{"content":"Next.js + Convex"},{"content":"Next.js App Router"},{"content":"Convex"},{"content":"Convex Auth"},{"content":"Vercel + Convex"},{"content":"TanStack + Convex"},{"content":"TanStack Start"},{"content":"Convex"},{"content":"Convex Auth"},{"content":"Vercel + Convex"},{"content":"Next.js + Neon"},{"content":"Next.js App Router"},{"content":"Neon Postgres"},{"content":"Better Auth"},{"content":"Vercel"},{"content":"TanStack + Neon"},{"content":"React Router 8 framework mode + Express"},{"content":"Neon Postgres"},{"content":"Better Auth"},{"content":"Vercel"},{"content":"Next.js + Cloudflare D1"},{"content":"Next.js through OpenNext"},{"content":"Cloudflare D1"},{"content":"Better Auth"},{"content":"Cloudflare Workers"},{"content":"TanStack + Cloudflare D1"},{"content":"React Router on Workers, with TanStack Query"},{"content":"Cloudflare D1"},{"content":"Better Auth"},{"content":"Cloudflare Workers"},{"heading":"supabase","content":"Pick a Supabase kit if you want Postgres, authentication and storage from one platform. The kits use Supabase Auth and database policies to protect workspace data."},{"heading":"convex","content":"Pick a Convex kit if you like writing your backend functions in TypeScript and using reactive queries in your app. Convex runs the backend; Vercel hosts the website. The MCP servers live on the Convex HTTP endpoint."},{"heading":"neon","content":"Pick a Neon kit if you want Postgres with your authentication handled in the application. Better Auth manages sign in, and Drizzle owns the database schema and migrations. The repositories still have `planetscale` in their names, but the current database is Neon Postgres."},{"heading":"cloudflare-d1","content":"Pick a D1 kit if you want the app to run on Cloudflare Workers. D1 stores application data, with Cloudflare services used for the other parts of the app. These kits have their own Worker setup and deployment steps."},{"heading":"what-about-agent-access","content":"All 8 kits have separate user and admin connections. The Supabase, Neon and D1 MCP tools are read-only in this release. The Convex user MCP also supports creating a project when the user explicitly grants write access. Every admin MCP is read-only."},{"heading":"what-about-agent-access","content":"See tools and permissions before choosing an agent workflow that needs writes."}],"headings":[{"id":"supabase","content":"Supabase"},{"id":"convex","content":"Convex"},{"id":"neon","content":"Neon"},{"id":"cloudflare-d1","content":"Cloudflare D1"},{"id":"what-about-agent-access","content":"What about agent access?"}]},"url":"/docs/v5/choose-your-starter","title":"Choose your starter","description":"A quick look at all eight kits, their databases, authentication and hosting."},{"_id":"/docs/v5/deployment/cloudflare","structured":{"contents":[{"content":"The D1 kits use Cloudflare Workers for the website and Cloudflare D1 for application data. Create resources for your own app, then update the Worker configuration with their identifiers."},{"heading":"1-configure-production-bindings","content":"Update the production environments in the Wrangler files. Keep the binding names the application expects, and point them at your own D1, R2, queue and other resources."},{"heading":"1-configure-production-bindings","content":"The Next.js kit also has OpenNext cache bindings. Those are separate from the application's `APP_DB` database. The TanStack kit has its own web and jobs Worker configurations."},{"heading":"2-apply-the-database-migrations","content":"For the Next.js kit:"},{"heading":"2-apply-the-database-migrations","content":"Apply its configured cache migrations too when you are creating the OpenNext cache database for the first time."},{"heading":"2-apply-the-database-migrations","content":"For the TanStack kit:"},{"heading":"2-apply-the-database-migrations","content":"Both application databases need the MCP records migration before the web app starts serving OAuth connections."},{"heading":"3-set-the-secrets-and-application-origin","content":"Configure Better Auth, your sender and Resend credentials on the Workers that use them. Keep secrets in Worker secrets rather than public vars."},{"heading":"3-set-the-secrets-and-application-origin","content":"Use your deployed origin in the auth and site settings. The User MCP and Admin MCP addresses will use that same origin."},{"heading":"4-build-for-production","content":"For Next.js:"},{"heading":"4-build-for-production","content":"Deploy the platform Worker as described in the repository when creating the app's background services."},{"heading":"4-build-for-production","content":"For TanStack:"},{"heading":"4-build-for-production","content":"That script builds with `CLOUDFLARE_ENV=production` and deploys the web and jobs Workers. Make sure the generated build contains your production bindings before upload. Building with the default environment can leave local placeholder database IDs in the generated configuration."},{"heading":"5-check-the-live-app","content":"Test email verification, password login, a workspace and both connection gates. The Next.js demo uses Workers Paid because password/auth work exceeded the Free CPU limit during testing. Check your own runtime limits if you see intermittent CPU errors."},{"heading":"5-check-the-live-app","content":"See troubleshooting if the Worker deploys successfully but login or the database fails."}],"headings":[{"id":"1-configure-production-bindings","content":"1\\. Configure production bindings"},{"id":"2-apply-the-database-migrations","content":"2\\. Apply the database migrations"},{"id":"3-set-the-secrets-and-application-origin","content":"3\\. Set the secrets and application origin"},{"id":"4-build-for-production","content":"4\\. Build for production"},{"id":"5-check-the-live-app","content":"5\\. Check the live app"}]},"url":"/docs/v5/deployment/cloudflare","title":"Deploy on Cloudflare","description":"Publish the two D1 kits with their Worker, database and email bindings."},{"_id":"/docs/v5/deployment","structured":{"contents":[{"content":"The demo hosting is already configured, but your product needs its own database, domain and provider credentials."},{"content":"The six Supabase, Convex and Neon websites run on Vercel. The two D1 websites run on Cloudflare Workers. Convex also has a backend deployment of its own."},{"heading":"before-you-publish","content":"Configure the production database or backend for your kit."},{"heading":"before-you-publish","content":"Apply the additive database migrations, or publish the Convex schema and functions."},{"heading":"before-you-publish","content":"Configure the app origin, login callbacks and verification email delivery."},{"heading":"before-you-publish","content":"Build and deploy the website."},{"heading":"before-you-publish","content":"Test sign up, email verification, login and workspace creation on the live domain."},{"heading":"before-you-publish","content":"Connect a test agent and check its workspace access and revocation."},{"heading":"before-you-publish","content":"The new MCP routes depend on their storage migration or Convex gateway components. Publish that backend change before serving the connection pages."},{"heading":"choose-your-deployment-guide","content":"Vercel, Supabase, Convex and Neon"},{"heading":"choose-your-deployment-guide","content":"Cloudflare Workers and D1"},{"heading":"optional-features","content":"Stripe, object storage, analytics, social providers and realtime services have their own configuration. A successful website deployment doesn't configure an account with each provider."},{"heading":"optional-features","content":"Enable the integrations you need, then test those flows on your own deployment. Start with authentication and workspaces so you have a working app to build on."}],"headings":[{"id":"before-you-publish","content":"Before you publish"},{"id":"choose-your-deployment-guide","content":"Choose your deployment guide"},{"id":"optional-features","content":"Optional features"}]},"url":"/docs/v5/deployment","title":"Deploy your app","description":"Publish your kit with its database, login callbacks and agent connections configured."},{"_id":"/docs/v5/deployment/vercel","structured":{"contents":[{"content":"Use this guide for the Supabase, Convex and Neon kits. Start from the environment example and README in the repository you chose."},{"heading":"1-configure-the-backend","content":"**Supabase:** create or select your project, link the CLI, and apply the repository's migrations from `apps/database`. Include the MCP migration. Configure Auth, email delivery and callback URLs for your own domain."},{"heading":"1-configure-the-backend","content":"**Convex:** select your production deployment and publish the schema, functions and both gateway components. Set `MCP_APP_URL` on the backend to the website's canonical origin. Keep the existing Convex Auth providers and signing keys when updating an existing app."},{"heading":"1-configure-the-backend","content":"**Neon:** create or select your Postgres database and apply the migrations using the migration connection. Use the runtime connection strings for the website. The TanStack Neon kit has separate auth, application and worker roles; preserve their RLS boundaries."},{"heading":"1-configure-the-backend","content":"The two Neon repository names still include `planetscale`. You don't need a PlanetScale database for these versions."},{"heading":"2-import-the-repository","content":"Create a Vercel project from your own repository. Check its root directory, package manager and build command against that kit's README and Vercel configuration."},{"heading":"2-import-the-repository","content":"These are monorepos. Some projects build from the repository root and others use `apps/web`. Keep the configuration shipped with your kit rather than applying one setting to all six."},{"heading":"3-add-production-environment-variables","content":"Set the database/backend credentials, auth configuration, app origin and email configuration. Use the exact variable names from the kit's env example."},{"heading":"3-add-production-environment-variables","content":"The app origin is `NEXT_PUBLIC_SITE_URL` for Next.js Supabase and Convex, `NEXT_PUBLIC_APP_URL` for Next.js Neon, `APP_URL` for TanStack Supabase, and `APP_BASE_URL` for TanStack Neon. TanStack Convex uses the app/backend configuration documented in its README."},{"heading":"3-add-production-environment-variables","content":"For Supabase, the browser gets the public key. The secret/service-role key belongs only on the server. For Neon, the migration owner's connection is used by migrations, not by the normal application process."},{"heading":"4-deploy-and-test","content":"Build using the repository's normal command. Test a preview with its own callback configuration, then publish production."},{"heading":"4-deploy-and-test","content":"Open `/login` and `/sign-up`, create a test account, verify its email, and create a workspace. On the connection page, confirm that the displayed server URL belongs to the deployment you intended."},{"heading":"4-deploy-and-test","content":"Convex MCP addresses use the backend's `.convex.site` origin. The other four Vercel-hosted MCPs use the website origin. Follow connect an agent to test consent and a tool call."}],"headings":[{"id":"1-configure-the-backend","content":"1\\. Configure the backend"},{"id":"2-import-the-repository","content":"2\\. Import the repository"},{"id":"3-add-production-environment-variables","content":"3\\. Add production environment variables"},{"id":"4-deploy-and-test","content":"4\\. Deploy and test"}]},"url":"/docs/v5/deployment/vercel","title":"Deploy on Vercel","description":"Set up the six Node-hosted starters and their production databases."},{"_id":"/docs/v5/emails","structured":{"contents":[{"content":"The app needs to send verification emails before users can finish signing up. It may also send password resets, magic links and workspace invitations."},{"content":"The demos use Resend with a verified sending domain. For your product, configure your own domain and sender, then add the credentials in the right place for your kit."},{"heading":"supabase-kits","content":"Configure Resend SMTP in the Supabase project's Auth settings. Set the sender, SMTP host, port and credentials, then check the site URL and allowed redirect URLs."},{"heading":"supabase-kits","content":"The web app's optional email API configuration is separate from Supabase Auth delivery. Adding a Resend key only to the web app won't configure Supabase's verification emails."},{"heading":"convex-kits","content":"Configure the selected Convex deployment's email provider variables. The auth flow uses `AUTH_RESEND_KEY` and `AUTH_EMAIL_FROM`. App emails can use the separate Resend variables defined in that kit's env example."},{"heading":"convex-kits","content":"A frontend env file alone doesn't configure a cloud Convex backend. Verify which deployment you are changing before testing a sign up."},{"heading":"neon-kits","content":"Better Auth sends through the application's email setup. Use the example env file to configure the sender and Resend credentials."},{"heading":"neon-kits","content":"The TanStack Neon kit uses an outbox for delivery. Its local worker runs separately; the Vercel deployment dispatches bounded batches and retries pending work on its configured cron."},{"heading":"d1-kits","content":"Configure email secrets on the relevant Workers, including the background email Worker where used. The kits use Cloudflare jobs/outbox handling for email delivery."},{"heading":"d1-kits","content":"Follow the kit's local email instructions when testing. You don't need to send local test emails to real users."},{"heading":"check-the-complete-flow","content":"Create a test account, open the delivered verification email, and sign in again after verification. Then test a password reset and an invitation if you use those flows."},{"heading":"check-the-complete-flow","content":"If the link opens the wrong website, check the configured application origin and the auth provider's callback allowlist first."}],"headings":[{"id":"supabase-kits","content":"Supabase kits"},{"id":"convex-kits","content":"Convex kits"},{"id":"neon-kits","content":"Neon kits"},{"id":"d1-kits","content":"D1 kits"},{"id":"check-the-complete-flow","content":"Check the complete flow"}]},"url":"/docs/v5/emails","title":"Emails with Resend","description":"Configure verification and app emails for the stack you picked."},{"_id":"/docs/v5/getting-started","structured":{"contents":[{"content":"Welcome to Nextbase! Let's get your app running first. You can configure billing and the other integrations after that."},{"heading":"1-get-your-repository","content":"Use the GitHub repository you have access to for your kit. The exact names and links are on the starter pages."},{"heading":"1-get-your-repository","content":"For example, for the Next.js Supabase kit:"},{"heading":"1-get-your-repository","content":"If GitHub says the repository doesn't exist, check that you are signed in with the account that has access."},{"heading":"2-install-dependencies","content":"Use Node.js 24 and the pnpm version declared in your kit's `package.json`. Some kits use pnpm 10 and others use pnpm 11. Let the repository's `packageManager` field choose the version."},{"heading":"2-install-dependencies","content":"For Supabase, use one local project per agent or worktree: Docker or Podman for parallel work on your computer, or the native runtime in a supported sandbox without Docker. The other Postgres setups use Docker. The D1 kits use local Worker databases, and Convex has its own local backend."},{"heading":"3-follow-your-kits-setup","content":"The env file location and database commands differ between kits. Each kit has its own section in the sidebar under **The 8 starter kits**, with an installation page, an environment variables reference, and pages for authentication, agent connections, project structure and deployment. Open your kit from choose your starter and follow its **Installation** page."},{"heading":"3-follow-your-kits-setup","content":"Copy example env files only when you haven't already created your own. Keep the database and web server running while you work."},{"heading":"4-sign-up-and-create-a-workspace","content":"Open the local address printed by the dev server, then sign up. Verify your email when the kit asks for it. Local emails are available in the kit's inbox or email development service."},{"heading":"4-sign-up-and-create-a-workspace","content":"Complete the account setup and create a workspace. Try adding a project through the website before connecting an agent. This gives you something useful to test with."},{"heading":"5-connect-your-agent","content":"Open your account's connection settings and copy the User MCP address. Follow connect an agent to approve access to your workspace."},{"heading":"5-connect-your-agent","content":"That's it. You can start working on your own feature now. You can definately come back to billing, analytics and social login once the basic app is running."}],"headings":[{"id":"1-get-your-repository","content":"1\\. Get your repository"},{"id":"2-install-dependencies","content":"2\\. Install dependencies"},{"id":"3-follow-your-kits-setup","content":"3\\. Follow your kit's setup"},{"id":"4-sign-up-and-create-a-workspace","content":"4\\. Sign up and create a workspace"},{"id":"5-connect-your-agent","content":"5\\. Connect your agent"}]},"url":"/docs/v5/getting-started","title":"Getting started","description":"Get the repository, start your kit locally and create your first workspace."},{"_id":"/docs/v5","structured":{"contents":[{"content":"Nextbase v5 gives you 8 starter kits to choose from. Pick your frontend, pick your database, and start building your product."},{"content":"The app already has sign up, login, workspaces, projects and an admin area. Your users can work through the website, or connect an agent to get things done for them. Both use the same account and the same workspace permissions."},{"heading":"whats-new-in-v5","content":"We now have Next.js and TanStack kits for Supabase, Convex, Neon and Cloudflare D1. Each kit has its own user MCP and admin MCP, with OAuth and a seperate place to manage connections."},{"heading":"whats-new-in-v5","content":"An MCP is the interface an agent uses to talk to your running app. For example, a user can ask their agent to list projects in their workspace. An application administrator can connect to the admin MCP to look up users and workspaces."},{"heading":"whats-new-in-v5","content":"You don't need to change your login provider to use this. Supabase kits keep Supabase Auth, Convex kits keep Convex Auth, and Neon and D1 kits keep Better Auth."},{"heading":"where-should-i-start","content":"Choose your starter."},{"heading":"where-should-i-start","content":"Follow getting started, then the setup steps for your kit."},{"heading":"where-should-i-start","content":"Create an account and a workspace in the app."},{"heading":"where-should-i-start","content":"Connect an agent when you are ready."},{"heading":"where-should-i-start","content":"If you already know which stack you want, open its section under **The 8 starter kits** in the sidebar. Each kit has its own overview, installation guide, environment variables reference, and pages for authentication, agent connections, project structure and deployment."},{"heading":"for-humans-and-agents","content":"You get a website your users can actually use, along with tools their agents can call. The admin connection is separate because running the whole application needs different permissions from using one workspace."},{"heading":"for-humans-and-agents","content":"If you are using a coding agent to build your product, see building with agents. That's where we cover repository instructions, skills and a practical way to work on a feature."},{"heading":"for-humans-and-agents","content":"See the v5 release notes for the details of this release. Older docs are still available in the version menu."}],"headings":[{"id":"whats-new-in-v5","content":"What's new in v5?"},{"id":"where-should-i-start","content":"Where should I start?"},{"id":"for-humans-and-agents","content":"For humans and agents"}]},"url":"/docs/v5","title":"Welcome to Nextbase v5!","description":"Eight starter kits. Build with your preferred stack, and let people and agents use the same app."},{"_id":"/docs/v5/local-supabase","structured":{"contents":[{"content":"For the Next.js and TanStack Supabase kits, use one local Supabase project per git worktree. Each agent can develop and test its database changes independently. This follows Supabase's project-per-agent approach."},{"heading":"1-enable-the-stack-engine","content":"The engine is experimental and requires CLI 2.119.0 or later. These instructions target **2.121.0**. In `apps/database/package.json`, set the `supabase` development dependency to the exact version `\"2.121.0\"`, then install from the repository root:"},{"heading":"1-enable-the-stack-engine","content":"In `apps/database/supabase/config.toml`, add the following to the existing experimental table, or create that table if it is absent:"},{"heading":"1-enable-the-stack-engine","content":"Remove explicit service ports so the CLI can allocate them. This includes `port` in the API, database, pooler, Studio, mail and Analytics sections; database `shadow_port`; mail `smtp_port` and `pop3_port`; Analytics `vector_port`; and Edge Runtime `inspector_port`. Older kits call the mail section `[inbucket]`; rename it to `[local_smtp]` and remove its ports too. Remove any equivalent `SUPABASE_*_PORT` overrides from your shell."},{"heading":"1-enable-the-stack-engine","content":"Keep the schema, Auth settings and migrations. Commit the config, package manifest and lockfile so new worktrees inherit the setup. Check Supabase's port-removal checklist and limitations if you have customized the config."},{"heading":"2-start-a-project-in-each-worktree","content":"Create a worktree for each agent. For example, from your kit's repository:"},{"heading":"2-start-a-project-in-each-worktree","content":"Use Docker or Podman when several agents work on the same computer. Each worktree has its own `apps/database/supabase/` directory, so it gets its own project and assigned ports. Two agents in the same directory on the same branch still share a database."},{"heading":"2-start-a-project-in-each-worktree","content":"For one project in a sandbox without a container engine, replace the start command with:"},{"heading":"2-start-a-project-in-each-worktree","content":"Native mode supports macOS 14+ on Apple silicon and Linux amd64/arm64 with glibc 2.35+. See the runtime guide for download access and non-root Postgres requirements. A project keeps the runtime selected when it was created."},{"heading":"3-connect-the-web-app-to-its-project","content":"Create the kit's env file only if it is missing, using the command on its installation page (Next.js + Supabase or TanStack + Supabase). Then inspect this worktree's connection details:"},{"heading":"3-connect-the-web-app-to-its-project","content":"Copy the values from the JSON `env` object into the matching file. Update only these entries, preserving your other integrations:"},{"heading":"3-connect-the-web-app-to-its-project","content":"Kit"},{"heading":"3-connect-the-web-app-to-its-project","content":"Env file"},{"heading":"3-connect-the-web-app-to-its-project","content":"Mapping from the CLI's `env` object"},{"heading":"3-connect-the-web-app-to-its-project","content":"Next.js + Supabase"},{"heading":"3-connect-the-web-app-to-its-project","content":"`apps/web/.env.local`"},{"heading":"3-connect-the-web-app-to-its-project","content":"`API_URL` → `NEXT_PUBLIC_SUPABASE_URL`, `PUBLISHABLE_KEY` → `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SECRET_KEY` → `SUPABASE_SECRET_KEY`"},{"heading":"3-connect-the-web-app-to-its-project","content":"TanStack + Supabase"},{"heading":"3-connect-the-web-app-to-its-project","content":"`apps/web/.env`"},{"heading":"3-connect-the-web-app-to-its-project","content":"`API_URL` → `SUPABASE_URL`, `ANON_KEY` → `SUPABASE_ANON_KEY`, `SERVICE_ROLE_KEY` → `SUPABASE_SERVICE_ROLE_KEY`"},{"heading":"3-connect-the-web-app-to-its-project","content":"Keep secret and service-role keys server-only. The output contains credentials; don't commit it. Use the returned `STUDIO_URL`, `MAILPIT_URL` and `MCP_URL` for local tools instead of assuming their ports."},{"heading":"3-connect-the-web-app-to-its-project","content":"Older starter copies have a `supabase:sync-env` script that uses `--output json` and expects top-level keys. Before using it with this engine, update it to use `--output-format json`, read `env`, and sync the API URL as well as the keys. Until then, use the mapping above. The `nextbase-website-new` repository already includes the updated sync script."},{"heading":"3-connect-the-web-app-to-its-project","content":"In a Next.js kit, replace any image `remotePatterns` entry fixed to port `54321` with an entry derived from `NEXT_PUBLIC_SUPABASE_URL` for `/storage/v1/**`. Preserve query-string support for signed and transformed images. Otherwise Storage images from the assigned port will fail image optimization."},{"heading":"3-connect-the-web-app-to-its-project","content":"Start the web app with `pnpm dev`. When running multiple web servers, select a different web port in each worktree and match the app origin and Supabase Auth redirect settings to it. Supabase allocates its own service ports; your web server's port remains your responsibility."},{"heading":"4-work-on-the-local-database","content":"Run local database commands through the database package so the CLI finds the right config:"},{"heading":"4-work-on-the-local-database","content":"The last command prints types; use your kit's generation script to write them to its expected file. Follow its migration workflow and access-control tests. Commands such as `db pull`, `db push` and `db dump` require `--local` for local work."},{"heading":"4-work-on-the-local-database","content":"Use the default project in each worktree. Named `--stack` projects need extra routing because database commands don't accept that flag. The local Supabase MCP manages the development backend; Nextbase's User and Admin MCP connections operate through the application's permissions."},{"heading":"5-stop-or-switch-projects","content":"Stopping preserves this project's data. Branch changes select a different local project: start it, refresh the env values and restart the web server. Stop the previous project before switching branches if you no longer need it."},{"heading":"5-stop-or-switch-projects","content":"Enabling the engine starts with a fresh local database built from migrations and seed data. It doesn't import or delete your previous local database. Keep that database until you have exported anything you need. Avoid global stop commands while other agents are working, and use `supabase stack destroy` only when you intend to permanently remove the selected project's database."}],"headings":[{"id":"1-enable-the-stack-engine","content":"1\\. Enable the stack engine"},{"id":"2-start-a-project-in-each-worktree","content":"2\\. Start a project in each worktree"},{"id":"3-connect-the-web-app-to-its-project","content":"3\\. Connect the web app to its project"},{"id":"4-work-on-the-local-database","content":"4\\. Work on the local database"},{"id":"5-stop-or-switch-projects","content":"5\\. Stop or switch projects"}]},"url":"/docs/v5/local-supabase","title":"Local Supabase per agent","description":"Give each coding agent an isolated Supabase project, with Docker on your computer or native processes in a sandbox."},{"_id":"/docs/v5/release-notes","structured":{"contents":[{"content":"Released on **14 October 2026**."},{"content":"v5 brings all 8 starter kits into one documented lineup. Each kit has a user MCP and an admin MCP, while keeping its existing authentication provider."},{"heading":"the-kits","content":"Next.js + Supabase"},{"heading":"the-kits","content":"TanStack + Supabase"},{"heading":"the-kits","content":"Next.js + Convex"},{"heading":"the-kits","content":"TanStack + Convex"},{"heading":"the-kits","content":"Next.js + Neon"},{"heading":"the-kits","content":"TanStack + Neon"},{"heading":"the-kits","content":"Next.js + Cloudflare D1"},{"heading":"the-kits","content":"TanStack + Cloudflare D1"},{"heading":"the-kits","content":"Each has its own docs section with installation, environment variables, authentication, agent connections, project structure and deployment pages, plus a live demo. The two repositories with PlanetScale in their names now use Neon Postgres."},{"heading":"agent-connections","content":"Users can connect an agent to one workspace. Administrators use a separate server to look up users and workspaces across the application."},{"heading":"agent-connections","content":"OAuth handles sign in, consent and credentials for the connection. The website has settings to inspect and revoke access. The original auth provider, account and workspace rules are preserved."},{"heading":"agent-connections","content":"All kits support reading account context and projects through the User MCP. Both Convex kits additionally support project creation with explicit write consent. The remaining six User MCPs and all eight Admin MCPs are read-only."},{"heading":"what-was-checked","content":"The release checks covered native authentication, consent, user/admin separation, permissions, token refresh and revocation. Full authorized MCP flows were exercised against isolated real backends with the official MCP SDK. The published sites were also checked for discovery, auth challenges and their connection/login routes."},{"heading":"what-was-checked","content":"For Supabase, Neon and D1, production test accounts also completed sign in, connection settings, workspace consent and cancellation. These checks did not create delegated production access grants."},{"heading":"if-you-are-comming-from-v4","content":"Choose the v5 repository that matches your stack first. Treat moving an existing app as a migration: review its auth, data model and integrations before moving production data."},{"heading":"things-to-keep-in-mind","content":"Provider credentials are still needed for optional billing, storage, analytics, email and social login integrations in your own deployment. These docs describe what is included and how to configure it; they don't turn on every external service for you."},{"heading":"things-to-keep-in-mind","content":"The Next.js Convex kit's main auth, onboarding, workspace and project paths use Convex. Some older advanced application pages still have Supabase-based actions and need migration before you rely on those paths. The kit page calls this out too."},{"heading":"things-to-keep-in-mind","content":"We'll keep adding to the docs as the kits grow. If something is missing, start with the repository README and the setup steps for the kit you picked."}],"headings":[{"id":"the-kits","content":"The kits"},{"id":"agent-connections","content":"Agent connections"},{"id":"what-was-checked","content":"What was checked?"},{"id":"if-you-are-comming-from-v4","content":"If you are comming from v4"},{"id":"things-to-keep-in-mind","content":"Things to keep in mind"}]},"url":"/docs/v5/release-notes","title":"Nextbase v5","description":"The eight starter kits and the agent connections included in this release."},{"_id":"/docs/v5/starters/nextjs-convex/agent-connections","structured":{"contents":[{"content":"This kit serves two MCP servers from the Convex backend, not from either Next.js app. Each is its own `convex-mcp-gateway` component instance (`mcpUser` and `mcpAdmin` in `apps/database/convex/convex.config.ts`) with its own tool registry and OAuth resource. Read user and admin MCPs for the shared concepts and connect an agent for the client-side steps."},{"heading":"where-users-manage-connections","content":"Connection"},{"heading":"where-users-manage-connections","content":"Settings page"},{"heading":"where-users-manage-connections","content":"Live demo settings"},{"heading":"where-users-manage-connections","content":"Live demo server"},{"heading":"where-users-manage-connections","content":"User"},{"heading":"where-users-manage-connections","content":"`/user/settings/connections` in the web app"},{"heading":"where-users-manage-connections","content":"`https://nextjs-convex.usenextbase.com/user/settings/connections`"},{"heading":"where-users-manage-connections","content":"`https://dynamic-grasshopper-80.convex.site/mcp/user`"},{"heading":"where-users-manage-connections","content":"Admin"},{"heading":"where-users-manage-connections","content":"`/app-admin/connections` in the admin app"},{"heading":"where-users-manage-connections","content":"`https://nextjs-convex-admin.usenextbase.com/app-admin/connections`"},{"heading":"where-users-manage-connections","content":"`https://dynamic-grasshopper-80.convex.site/mcp/admin`"},{"heading":"where-users-manage-connections","content":"Both pages render `McpConnections` (one copy in each app's `src/components/`) and read `api.mcp.connections.settings`, which returns the exact endpoint for the current deployment, the account's connections and recent activity. Revoke a connection from the same page. The admin page requires a current `platformRoles` admin role and lives on the admin origin, not in `apps/web`."},{"heading":"where-users-manage-connections","content":"Your own deployment has its own address: `CONVEX_SITE_URL` followed by `/mcp/user` or `/mcp/admin`. Neither app origin is the MCP address. Copy the URL shown in connection settings rather than guessing it."},{"heading":"endpoints-on-the-backend","content":"All of these live on the `.convex.site` origin and are mounted by `mountMcp` in `apps/database/convex/mcp/http.ts`:"},{"heading":"endpoints-on-the-backend","content":"Endpoint"},{"heading":"endpoints-on-the-backend","content":"Purpose"},{"heading":"endpoints-on-the-backend","content":"`/mcp/user`, `/mcp/admin`"},{"heading":"endpoints-on-the-backend","content":"Streamable HTTP MCP servers (`POST`, `GET`, `DELETE`, `OPTIONS`)"},{"heading":"endpoints-on-the-backend","content":"`/.well-known/oauth-protected-resource/mcp/user` and `.../mcp/admin`"},{"heading":"endpoints-on-the-backend","content":"Resource metadata, pointing at the issuer"},{"heading":"endpoints-on-the-backend","content":"`/.well-known/oauth-authorization-server/oauth`"},{"heading":"endpoints-on-the-backend","content":"Issuer metadata (the issuer is `CONVEX_SITE_URL/oauth`)"},{"heading":"endpoints-on-the-backend","content":"`/oauth/register`"},{"heading":"endpoints-on-the-backend","content":"Dynamic public-client registration"},{"heading":"endpoints-on-the-backend","content":"`/oauth/authorize`"},{"heading":"endpoints-on-the-backend","content":"Starts a consent request and redirects to the consent page of the web app (user) or the admin app (admin)"},{"heading":"endpoints-on-the-backend","content":"`/oauth/token`"},{"heading":"endpoints-on-the-backend","content":"Authorization code and refresh token exchange"},{"heading":"endpoints-on-the-backend","content":"`/oauth/revoke`"},{"heading":"endpoints-on-the-backend","content":"Token revocation"},{"heading":"endpoints-on-the-backend","content":"Opening an MCP URL without a token returns an authentication challenge. That is expected."},{"heading":"tools-and-permissions","content":"From `apps/database/convex/mcp/tools.ts`:"},{"heading":"tools-and-permissions","content":"Server"},{"heading":"tools-and-permissions","content":"Tool"},{"heading":"tools-and-permissions","content":"Scope"},{"heading":"tools-and-permissions","content":"Access"},{"heading":"tools-and-permissions","content":"What it does"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`account_context`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"Read"},{"heading":"tools-and-permissions","content":"Signed-in account, the one authorized workspace and the granted scopes"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`projects_list`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"Read"},{"heading":"tools-and-permissions","content":"Active projects in the authorized workspace, paginated with `cursor` and `limit` (1 to 50, default 20)"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`projects_create`"},{"heading":"tools-and-permissions","content":"`user:write`"},{"heading":"tools-and-permissions","content":"Write"},{"heading":"tools-and-permissions","content":"Creates a project through `createProjectForActor`, the same helper the website uses. Needs at least the `member` role"},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_users_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"Read"},{"heading":"tools-and-permissions","content":"User profiles (id, email, name, disabled and deleted flags). Never credentials or sessions"},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_workspaces_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"Read"},{"heading":"tools-and-permissions","content":"Workspaces (id, name, slug)"},{"heading":"tools-and-permissions","content":"The gateway only lets a tool run when the token's scopes include the tool's `metadata.scope`. Each handler then calls `verifiedCaller`, which rechecks on every call that the connection is unrevoked and unexpired, that the approving Convex Auth session still exists, that the account is active, and that the user still has the required workspace role or admin role. A token issued for `/mcp/user` is rejected by `/mcp/admin` and the other way round, even for an administrator."},{"heading":"tools-and-permissions","content":"The `caller` argument is injected by the gateway and hidden from tool schemas; clients cannot supply it. Successful write calls are recorded in `mcpActivity`."},{"heading":"consent-behaviour-in-this-kit","content":"The agent registers itself at `/oauth/register` (public client, PKCE S256, `authorization_code` and `refresh_token` grants only) and requests `/oauth/authorize` with the exact resource URL."},{"heading":"consent-behaviour-in-this-kit","content":"The backend stores a pending request (valid 10 minutes) and redirects to `/mcp/authorize?request_id=...` on the consent origin. For the user server that origin is `MCP_APP_URL` (falling back to `APP_URL`, then `SITE_URL`), which is the web app. For the admin server it is `ADMIN_SITE_URL` first, so administrator consent happens in the admin app. If you are not signed in, the consent page sends you to that app's `/login?next=/mcp/authorize?...` and the middleware brings you back."},{"heading":"consent-behaviour-in-this-kit","content":"The consent page (`src/components/mcp-consent.tsx` in each app) shows the client name as unverified, the callback URL and the requested scopes. For the user server you pick one workspace; `user:write` (create projects) is an optional checkbox and is reset when you change workspaces. The admin server offers only `admin:read` and shows an error if you are not an administrator. If an admin request ever reaches the web app's consent page, it shows a link that continues the request in the admin app at `NEXT_PUBLIC_ADMIN_APP_URL`."},{"heading":"consent-behaviour-in-this-kit","content":"Approval creates an `mcpConnections` row bound to your user and session, valid for 30 days, and redirects to the client with a one-time code (5 minutes). Each account can hold 20 active connections."},{"heading":"consent-behaviour-in-this-kit","content":"Access tokens (`nb_mcp_...`) last 15 minutes. Refresh tokens rotate once; reusing one revokes the connection. Raw codes and tokens are stored only as hashes."},{"heading":"consent-behaviour-in-this-kit","content":"Browser clients are accepted from `MCP_APP_URL` (or its `APP_URL` and `SITE_URL` fallbacks), `https://claude.ai` and `https://chatgpt.com`. Desktop and server clients without an `Origin` header also work. Confidential clients and client ID metadata documents are not supported. An hourly cron (`convex/crons.ts`) prunes expired credentials and 90-day-old activity."},{"heading":"consent-behaviour-in-this-kit","content":"For the end-to-end walkthrough with an agent client, follow connect an agent. For the tool catalogue across kits, see tools and permissions."},{"heading":"where-the-code-lives","content":"File"},{"heading":"where-the-code-lives","content":"Role"},{"heading":"where-the-code-lives","content":"`apps/database/convex/mcp/tools.ts`"},{"heading":"where-the-code-lives","content":"Tool handlers and the `userTools` and `adminTools` registries"},{"heading":"where-the-code-lives","content":"`apps/database/convex/mcp/http.ts`"},{"heading":"where-the-code-lives","content":"Mounts both gateways, OAuth routes and discovery documents"},{"heading":"where-the-code-lives","content":"`apps/database/convex/mcp/oauth.ts`"},{"heading":"where-the-code-lives","content":"Registration, authorize, exchange and revoke mutations"},{"heading":"where-the-code-lives","content":"`apps/database/convex/mcp/connections.ts`"},{"heading":"where-the-code-lives","content":"`verifiedCaller`, `record`, consent and the settings query"},{"heading":"where-the-code-lives","content":"`apps/database/convex/mcp/actor.ts`"},{"heading":"where-the-code-lives","content":"Shared read helpers (`listProjects`, `listUsers`, `listWorkspaces`)"},{"heading":"where-the-code-lives","content":"`apps/database/convex/mcp/protocol.ts`"},{"heading":"where-the-code-lives","content":"Scopes, resource and issuer URLs, PKCE and hashing"},{"heading":"where-the-code-lives","content":"`apps/database/convex/mcp/schema.ts`"},{"heading":"where-the-code-lives","content":"`mcpClients`, `mcpRequests`, `mcpConnections`, `mcpCodes`, `mcpTokens`, `mcpActivity`"},{"heading":"where-the-code-lives","content":"`apps/database/convex/mcp/cleanup.ts`"},{"heading":"where-the-code-lives","content":"The hourly prune"},{"heading":"where-the-code-lives","content":"`apps/database/convex/mcp.test.ts`"},{"heading":"where-the-code-lives","content":"Scope, consent, replay, audience and role tests"},{"heading":"where-the-code-lives","content":"`apps/web/src/app/mcp/authorize/` and `apps/web/src/components/mcp-consent.tsx`"},{"heading":"where-the-code-lives","content":"User consent page"},{"heading":"where-the-code-lives","content":"`apps/admin/src/app/mcp/authorize/` and `apps/admin/src/components/mcp-consent.tsx`"},{"heading":"where-the-code-lives","content":"Admin consent page"},{"heading":"where-the-code-lives","content":"`apps/web/src/components/mcp-connections.tsx`, `apps/admin/src/components/mcp-connections.tsx`"},{"heading":"where-the-code-lives","content":"Settings pages"},{"heading":"where-the-code-lives","content":"`docs/mcp-connections.md`"},{"heading":"where-the-code-lives","content":"The repository's own reference"},{"heading":"adding-a-tool-safely","content":"The repository ships an `mcp-tool` skill in `.agents/skills/mcp-tool/SKILL.md` that walks through this. In short:"},{"heading":"adding-a-tool-safely","content":"Write the handler in `mcp/tools.ts` as an `internalQuery` or `internalMutation`, never a public function. The first line is `verifiedCaller(ctx, caller, \"user\", \"user:read\")` for reads, or `verifiedCaller(ctx, caller, \"user\", \"user:write\", true)` for writes (the `true` requires at least `member`). Admin tools use `\"admin\", \"admin:read\"`."},{"heading":"adding-a-tool-safely","content":"Scope data to `grant.workspaceId`. Reuse `pageArgs` and `bounded(limit)` for lists, validate free-text sizes, and call the same exported helper the website uses for writes, then `await record(ctx, grant, \"<tool_name>\")`."},{"heading":"adding-a-tool-safely","content":"Register it in `userTools` or `adminTools` with `defineMcpQuery` or `defineMcpMutation`, `identityArg: \"caller\"` and a `metadata.scope` that matches the handler's check."},{"heading":"adding-a-tool-safely","content":"Add tests in `mcp.test.ts` for the happy path, the missing scope, the downgraded role, cross-workspace access and a forged caller. Run `pnpm --filter database test` and `pnpm --filter database typecheck`."},{"heading":"adding-a-tool-safely","content":"Update `docs/mcp-connections.md`. A new scope also needs `scopesFor` in `protocol.ts`, `scopes_supported` in `http.ts` and the labels in both copies of `mcp-consent.tsx`."},{"heading":"adding-a-tool-safely","content":"Never publish MCP test fixtures or synthetic identity helpers as public Convex functions."}],"headings":[{"id":"where-users-manage-connections","content":"Where users manage connections"},{"id":"endpoints-on-the-backend","content":"Endpoints on the backend"},{"id":"tools-and-permissions","content":"Tools and permissions"},{"id":"consent-behaviour-in-this-kit","content":"Consent behaviour in this kit"},{"id":"where-the-code-lives","content":"Where the code lives"},{"id":"adding-a-tool-safely","content":"Adding a tool safely"}]},"url":"/docs/v5/starters/nextjs-convex/agent-connections","title":"Agent connections","description":"The User MCP and Admin MCP of the Next.js + Convex starter: endpoints, tools, OAuth consent and how to add a tool."},{"_id":"/docs/v5/starters/nextjs-convex/authentication","structured":{"contents":[{"content":"Convex Auth owns users and sessions in this kit. The same login is used by the web app, the admin app, server components and agent connections. For the concepts shared across all kits, read the v5 authentication guide first."},{"heading":"where-the-configuration-lives","content":"File"},{"heading":"where-the-configuration-lives","content":"What it does"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/auth.ts`"},{"heading":"where-the-configuration-lives","content":"Declares the providers and the `convexAuth` callbacks"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/auth.config.ts`"},{"heading":"where-the-configuration-lives","content":"Registers `CONVEX_SITE_URL` as the issuer domain with application id `convex`"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/http.ts`"},{"heading":"where-the-configuration-lives","content":"Mounts the Convex Auth HTTP routes and the MCP routes"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/lib/authz.ts`"},{"heading":"where-the-configuration-lives","content":"`requireUser`, `requireWorkspaceRole`, `requirePlatformAdmin` and their trusted `*Id` variants"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/lib/auth_redirect.ts`"},{"heading":"where-the-configuration-lives","content":"Allows redirects only to the `SITE_URL` and `ADMIN_SITE_URL` origins"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/lib/user_profiles.ts`"},{"heading":"where-the-configuration-lives","content":"Creates the `userProfiles`, `userSettings` and `platformRoles` rows for a new user"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/platform.ts`"},{"heading":"where-the-configuration-lives","content":"The internal `grantAdmin` mutation that promotes an account to application administrator"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/proxy.ts`"},{"heading":"where-the-configuration-lives","content":"Web middleware built on `convexAuthNextjsMiddleware`"},{"heading":"where-the-configuration-lives","content":"`apps/admin/src/proxy.ts`"},{"heading":"where-the-configuration-lives","content":"Admin middleware: sends signed-out visitors of `/app-admin` to the admin `/login`"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/app/convex-client-provider.tsx`"},{"heading":"where-the-configuration-lives","content":"Browser Convex client with auth"},{"heading":"where-the-configuration-lives","content":"Provider credentials and signing keys are deployment environment variables. See environment variables for the list."},{"heading":"providers","content":"`auth.ts` registers these providers:"},{"heading":"providers","content":"**Password** with `verify` set to the email provider and `reset` set to the password reset provider. Emails are trimmed and lower-cased; passwords must have at least 10 characters."},{"heading":"providers","content":"**Email magic link** (`id: \"email\"`, valid for 15 minutes). The login and sign-up pages call `signIn(\"password\")` and `signIn(\"email\")`."},{"heading":"providers","content":"**Password reset** (`id: \"password-reset\"`, 15 minutes). The email links to `/update-password?mode=reset&email=...&reset_code=...` on `SITE_URL`."},{"heading":"providers","content":"**GitHub** and **Google**, only when both `AUTH_GITHUB_ID` and `AUTH_GITHUB_SECRET` (or `AUTH_GOOGLE_ID` and `AUTH_GOOGLE_SECRET`) exist on the deployment. The login and sign-up pages ship without provider buttons. Add a button that calls `signIn(\"github\")` or `signIn(\"google\")` when you configure a provider, and register the callback URL on the provider's side using your `.convex.site` origin."},{"heading":"providers","content":"After a user is created or updated, the `afterUserCreatedOrUpdated` callback calls `ensureUserProfile`. The `redirect` callback passes the target through `authRedirect`, which allows only the `SITE_URL` origin and, when set, the `ADMIN_SITE_URL` origin, and falls back to `/dashboard` on the web origin."},{"heading":"email-verification-and-the-local-inbox","content":"Sign-up sends a verification code and link through the email provider. In production the provider posts to the Resend API with `AUTH_RESEND_KEY` and `AUTH_EMAIL_FROM`. When the backend has `APP_ENV=local` or `APP_ENV=test`, nothing is sent; the message is logged as `[local-auth-email]` with the code and URL, and `convex dev` prints it in the `pnpm dev` terminal."},{"heading":"email-verification-and-the-local-inbox","content":"The password reset provider has no local branch and always needs `SITE_URL`, `AUTH_RESEND_KEY` and `AUTH_EMAIL_FROM`. Set them on your local deployment with `pnpm exec convex env set` if you want to test resets locally. See emails with Resend for the production setup."},{"heading":"pages-and-sessions","content":"Web app (`apps/web`, port 3000):"},{"heading":"pages-and-sessions","content":"Path"},{"heading":"pages-and-sessions","content":"Purpose"},{"heading":"pages-and-sessions","content":"`/sign-up`, `/login`"},{"heading":"pages-and-sessions","content":"Create an account or sign in. Signed-in visitors are redirected to `/dashboard`, or to a pending `/mcp/authorize?...` request"},{"heading":"pages-and-sessions","content":"`/forgot-password`, `/update-password`"},{"heading":"pages-and-sessions","content":"Request and complete a password reset"},{"heading":"pages-and-sessions","content":"`/onboarding`"},{"heading":"pages-and-sessions","content":"Accept terms, complete the profile, create the first workspace (`convex/onboarding.ts`)"},{"heading":"pages-and-sessions","content":"`/dashboard`"},{"heading":"pages-and-sessions","content":"Entry point after login"},{"heading":"pages-and-sessions","content":"`/auth/impersonate`"},{"heading":"pages-and-sessions","content":"Receives a single-use impersonation handoff from the admin app and starts a 15-minute support session"},{"heading":"pages-and-sessions","content":"`/logout`"},{"heading":"pages-and-sessions","content":"Ends the session"},{"heading":"pages-and-sessions","content":"Admin app (`apps/admin`, port 3002):"},{"heading":"pages-and-sessions","content":"Path"},{"heading":"pages-and-sessions","content":"Purpose"},{"heading":"pages-and-sessions","content":"`/login`"},{"heading":"pages-and-sessions","content":"Password or magic-link sign-in for administrators. Signed-in visitors go to `/app-admin`, or to a pending `/mcp/authorize?...` request"},{"heading":"pages-and-sessions","content":"`/auth/callback`"},{"heading":"pages-and-sessions","content":"Returns magic-link and OAuth callbacks to `/app-admin`"},{"heading":"pages-and-sessions","content":"`/app-admin`"},{"heading":"pages-and-sessions","content":"The admin console. Anyone without the admin platform role gets the unauthorized page"},{"heading":"pages-and-sessions","content":"`/mcp/authorize`"},{"heading":"pages-and-sessions","content":"Consent page for the Admin MCP"},{"heading":"pages-and-sessions","content":"`apps/web/src/proxy.ts` redirects unauthenticated requests to `/login` for `/dashboard`, `/settings`, `/profile`, `/workspace`, `/project`, `/home`, `/user`, `/logout` and `/onboarding`. It also adds a request id header and sets `X-Frame-Options: DENY`, a `frame-ancestors 'none'` policy and `Referrer-Policy: no-referrer` on `/mcp/*` pages. `apps/admin/src/proxy.ts` does the same for `/app-admin`. Both middlewares are coarse gates; pages and Convex functions still check access."},{"heading":"pages-and-sessions","content":"Server components read the session with `convexAuthNextjsToken()` and pass it to `fetchQuery` or `fetchMutation`. Client components use `useQuery` and `useMutation` from `convex/react`. Sessions are rows in the Convex Auth `authSessions` table. The admin app has its own origin-scoped session, so signing in on `localhost:3002` does not sign you in on `localhost:3000`. MCP connections are bound to the session that approved them, so signing out of that session also ends those connections."},{"heading":"pages-and-sessions","content":"`/auth/callback`, `/auth/confirm` and `/confirm-delete-user/[token]` in the web app still call the Supabase client, as do `apps/web/src/middlewares/` (unused; `proxy.ts` is the real middleware) and the Supabase-era e2e helpers. Convex Auth does not use these routes. Remove or port them before shipping."},{"heading":"becoming-an-application-administrator","content":"Application-wide administration is a row in the `platformRoles` table with `role: \"admin\"`. `ensureUserProfile` gives every new user `role: \"user\"`. Nothing in the sign-up flow promotes an account; the operator does it from the CLI."},{"heading":"becoming-an-application-administrator","content":"Sign up normally in the web app, then run the internal mutation from the repository root against the selected deployment:"},{"heading":"becoming-an-application-administrator","content":"`platform:grantAdmin` (`apps/database/convex/platform.ts`) looks the account up by normalised email, refuses unknown, disabled or deleted accounts, inserts or patches the `platformRoles` row, and returns `{ userId, changed }`. It is idempotent and an `internalMutation`, so it is never callable from a client. For a production deployment add `--prod`: `pnpm exec convex run --prod platform:grantAdmin '{\"email\":\"...\"}'`. `platform.test.ts` covers the promote and idempotent path and the unknown and disabled account rejections."},{"heading":"becoming-an-application-administrator","content":"Then sign in to the admin app at `/login` on the admin origin. The web app reads the role through `api.users.viewer` (`isAppAdmin`); the admin app's `/app-admin` layout calls `serverGetLoggedInUserClaims()` and `unauthorized()` for everyone else."},{"heading":"the-admin-app","content":"Platform administration no longer lives in `apps/web`. The admin console is `/app-admin` in `apps/admin`, with users, workspaces, marketing content (blog, changelog, authors, tags), feedback moderation, the payment gateway, settings and `/app-admin/connections` for the Admin MCP. The demo admin is at `https://nextjs-convex-admin.usenextbase.com`. Being a workspace owner never grants application administration."},{"heading":"the-admin-app","content":"The two apps share the Convex deployment and every privileged operation is authorised in Convex, not in the browser. `ADMIN_SITE_URL` on the deployment tells the backend which second origin to trust for auth redirects and admin consent. The admin app's `NEXT_PUBLIC_WEB_APP_URL` and the web app's `NEXT_PUBLIC_ADMIN_APP_URL` let each app link to the other."},{"heading":"the-admin-app","content":"**Impersonation.** From the admin user list choose *Impersonate in web app* and enter a support reason. Convex requires an active administrator session, creates a one-use token, stores only its SHA-256 hash and writes the reason to the audit log. The handoff expires in 60 seconds and travels in the URL fragment to `NEXT_PUBLIC_WEB_APP_URL` + `/auth/impersonate`, where the web app exchanges it for a 15-minute target-user session. Expired or replayed links, demoted administrators and administrator targets are rejected; the admin session stays signed in on its own origin. *End support session* in the web app signs the temporary session out. The repository's `docs/separate-admin-app.md` has the full description."},{"heading":"what-is-protected-and-how","content":"Convex has no row-level security; authorization is code in each function. The rules the kit follows:"},{"heading":"what-is-protected-and-how","content":"Every public `query` or `mutation` starts with a check from `convex/lib/authz.ts`. `requireUser` resolves the Convex Auth user and rejects disabled or deleted profiles. `requireWorkspaceRole(ctx, workspaceId, minRole)` checks `workspaceMembers` against the role order `readonly < member < admin < owner`. `requirePlatformAdmin` checks `platformRoles`."},{"heading":"what-is-protected-and-how","content":"Functions never accept a `userId` from the client. The `requireUserId`, `requireWorkspaceRoleId` and `requirePlatformAdminId` variants exist for trusted internal callers such as MCP tools."},{"heading":"what-is-protected-and-how","content":"Shared domain helpers, for example `createProjectForActor` in `convex/projects.ts`, enforce the same rules for the website and for agents. Creating a project needs at least the `member` role."},{"heading":"what-is-protected-and-how","content":"Anything not called from the browser is an `internalQuery` or `internalMutation`. MCP tool handlers and `platform:grantAdmin` are internal functions."},{"heading":"what-is-protected-and-how","content":"Errors are thrown with `appError(code, message)` from `convex/lib/errors.ts`."},{"heading":"what-is-protected-and-how","content":"Backend tests in `apps/database/convex/*.test.ts` use `convexTest` with `t.withIdentity({ subject: \"<userId>|<sessionId>\" })` and cover the unauthorized and wrong-role paths."},{"heading":"what-is-protected-and-how","content":"Server actions in both apps go through `authActionClient` and `adminActionClient` from each app's `src/lib/safe-action.ts`. Admin layouts use `serverGetLoggedInUserClaims()` and `unauthorized()`. UI visibility is a convenience, not the security boundary."}],"headings":[{"id":"where-the-configuration-lives","content":"Where the configuration lives"},{"id":"providers","content":"Providers"},{"id":"email-verification-and-the-local-inbox","content":"Email verification and the local inbox"},{"id":"pages-and-sessions","content":"Pages and sessions"},{"id":"becoming-an-application-administrator","content":"Becoming an application administrator"},{"id":"the-admin-app","content":"The admin app"},{"id":"what-is-protected-and-how","content":"What is protected and how"}]},"url":"/docs/v5/starters/nextjs-convex/authentication","title":"Authentication","description":"How Convex Auth works in the Next.js + Convex starter: providers, sessions, the admin app and role, and how functions are protected."},{"_id":"/docs/v5/starters/nextjs-convex/deployment","structured":{"contents":[{"content":"Production has three deployments: the Convex backend, the customer web app (`apps/web`) and the admin app (`apps/admin`). Publish the backend first, because both builds need the backend URLs and the MCP gateways must exist before the apps serve connection pages. The platform-generic Vercel steps are in the Vercel deployment guide; this page covers what is specific to this kit. The demo runs at `https://nextjs-convex.usenextbase.com` (web) and `https://nextjs-convex-admin.usenextbase.com` (admin) against one shared Convex deployment."},{"heading":"create-and-select-the-convex-deployment","content":"Create a project in your Convex account, then select its production deployment with the Convex CLI from the repository root (`convex.json` points at `apps/database/convex/`). Never point the local bootstrap at it: `pnpm bootstrap:local` refuses non-local deployment names on purpose."},{"heading":"set-the-backend-variables","content":"Backend secrets are deployment environment variables, not Next.js env files. Set them against the selected deployment:"},{"heading":"set-the-backend-variables","content":"`JWT_PRIVATE_KEY` and `JWKS` are required by Convex Auth. The local bootstrap generates an RS256 pair for the local backend (see `scripts/bootstrap-convex-local.ts`, which uses `jose`); for production generate your own pair the same way and keep the private key out of the repository. Add `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET`, `AUTH_GOOGLE_ID` and `AUTH_GOOGLE_SECRET` only if you use those providers. Do not set `APP_ENV=local` in production; it would log auth emails instead of sending them."},{"heading":"set-the-backend-variables","content":"`SITE_URL` must be the canonical web origin: it validates auth redirects and builds password reset links. `ADMIN_SITE_URL` is the admin origin: the redirect allowlist permits exactly those two origins, and Admin MCP consent is routed there. `MCP_APP_URL` is where `/oauth/authorize` sends users for User MCP consent and the browser origin the MCP servers accept. `CONVEX_SITE_URL` is provided by Convex."},{"heading":"publish-the-backend","content":"Deploy the schema, functions, HTTP routes, the cron and both gateway component instances (`mcpUser` and `mcpAdmin`) with `pnpm exec convex deploy` from the repository root with a deploy key, or through your CI integration. `pnpm convex:once` is `convex dev --once` and targets the development deployment you have selected, so use it only for a dev deployment."},{"heading":"publish-the-backend","content":"Keep the existing Convex Auth providers, signing keys and auth tables when updating an existing app. If you change the schema in a way existing documents do not satisfy, the push fails; follow the stepwise approach on the project structure page."},{"heading":"publish-the-backend","content":"Once the backend is live and you have signed up, promote the first administrator against production:"},{"heading":"configure-the-web-host","content":"Set these on the web app's host (Vercel project settings or Netlify):"},{"heading":"configure-the-web-host","content":"Variable"},{"heading":"configure-the-web-host","content":"Value"},{"heading":"configure-the-web-host","content":"`NEXT_PUBLIC_CONVEX_URL`"},{"heading":"configure-the-web-host","content":"The deployment URL, `https://<name>.convex.cloud`"},{"heading":"configure-the-web-host","content":"`CONVEX_SITE_URL`"},{"heading":"configure-the-web-host","content":"The HTTP actions origin, `https://<name>.convex.site`"},{"heading":"configure-the-web-host","content":"`CONVEX_DEPLOYMENT`"},{"heading":"configure-the-web-host","content":"The deployment name, if you run Convex CLI commands during the build"},{"heading":"configure-the-web-host","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"configure-the-web-host","content":"The web origin, matching `SITE_URL` and `MCP_APP_URL` on the backend"},{"heading":"configure-the-web-host","content":"`NEXT_PUBLIC_ADMIN_APP_URL`"},{"heading":"configure-the-web-host","content":"The admin origin, matching `ADMIN_SITE_URL` on the backend"},{"heading":"configure-the-web-host","content":"`REVALIDATE_SECRET`"},{"heading":"configure-the-web-host","content":"A random secret; the admin project must use the same value"},{"heading":"configure-the-web-host","content":"`ADMIN_EMAIL`, `RESEND_API_KEY`"},{"heading":"configure-the-web-host","content":"Sender and key for emails the Next.js server sends. Leave `USE_LOCAL_EMAIL` unset"},{"heading":"configure-the-web-host","content":"Integrations"},{"heading":"configure-the-web-host","content":"`STRIPE_*`, `NEXT_PUBLIC_POSTHOG_*`, `NEXT_PUBLIC_SENTRY_DSN`, `UNKEY_*`, `OTEL_*` as needed"},{"heading":"configure-the-web-host","content":"The web build also reads the legacy `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY`. `scripts/verify-local.sh` exports placeholders for them during the compile-mode build; set the same placeholders on the host if your build fails without them, or remove the legacy code paths. The full table is on the environment variables page."},{"heading":"configure-the-admin-host","content":"Create a second Vercel project for the admin app against the same repository, and set:"},{"heading":"configure-the-admin-host","content":"Variable"},{"heading":"configure-the-admin-host","content":"Value"},{"heading":"configure-the-admin-host","content":"`NEXT_PUBLIC_CONVEX_URL`"},{"heading":"configure-the-admin-host","content":"The same deployment URL as the web project"},{"heading":"configure-the-admin-host","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"configure-the-admin-host","content":"The admin origin, matching `ADMIN_SITE_URL` on the backend"},{"heading":"configure-the-admin-host","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"configure-the-admin-host","content":"The web origin. Impersonation handoffs and `/api/revalidate` calls go here"},{"heading":"configure-the-admin-host","content":"`REVALIDATE_SECRET`"},{"heading":"configure-the-admin-host","content":"The same value as the web project"},{"heading":"configure-the-admin-host","content":"`apps/admin/.env.local.example` also lists `NEXT_PUBLIC_CONVEX_SITE_URL`. The admin code reads `ADMIN_EMAIL`, `RESEND_API_KEY`, the `STRIPE_*` keys and the same legacy Supabase placeholders as the web app; add them if a feature or the build needs them. The admin app has its own origin-scoped session, so administrators sign in again at the admin `/login`."},{"heading":"build-and-deploy-both-apps","content":"Use `apps/web` and `apps/admin` as the root directories of two Vercel projects; each runs its own `next build`. The root `vercel.admin.json` holds the admin project's configuration (`pnpm --filter admin build`, output `apps/admin/.next`). `apps/web/netlify.toml` runs `pnpm run build` for Netlify; there is no Netlify configuration for the admin app. From the repository root the equivalents are:"},{"heading":"build-and-deploy-both-apps","content":"`pnpm build` runs Turborepo's `build` task for every package. Follow the Vercel guide for importing the repository and preview environments. Preview deployments need their own `SITE_URL`, `ADMIN_SITE_URL` and `MCP_APP_URL` on a matching Convex deployment; a backend configured for production will redirect auth and consent flows to the production apps."},{"heading":"configure-external-callbacks","content":"**Resend:** verify your sending domain and use a sender on it for `AUTH_EMAIL_FROM` and `ADMIN_EMAIL`. See emails with Resend."},{"heading":"configure-external-callbacks","content":"**GitHub and Google:** register the OAuth callback with your `.convex.site` origin as Convex Auth requires, and add buttons that call `signIn(\"github\")` or `signIn(\"google\")`; the login pages ship without them."},{"heading":"configure-external-callbacks","content":"**Stripe:** point the webhook at `https://app.example.com/api/stripe/webhooks` and set `STRIPE_WEBHOOK_SECRET`. The gateway in `apps/web/src/payments/stripe-payment-gateway.ts` still persists billing through the Supabase client, and the Convex billing tables have no functions yet, so finish that migration before enabling billing."},{"heading":"smoke-test","content":"`GET /api/health` on the web origin returns `{ \"ok\": true }`."},{"heading":"smoke-test","content":"Open `/sign-up`, create an account, and confirm the verification email arrives from Resend. Sign in again at `/login`."},{"heading":"smoke-test","content":"Complete onboarding, create a workspace, and add a project."},{"heading":"smoke-test","content":"Request a password reset from `/forgot-password` and finish it at `/update-password`."},{"heading":"smoke-test","content":"Open `/user/settings/connections` and check that the displayed server URL is `https://<name>.convex.site/mcp/user` for the deployment you intended. Connect an agent and call `account_context` (connect an agent)."},{"heading":"smoke-test","content":"Run `pnpm exec convex run --prod platform:grantAdmin '{\"email\":\"...\"}'` for your account, sign in at the admin origin's `/login`, open `/app-admin` and `/app-admin/connections`, and confirm a non-admin account gets the unauthorized page."},{"heading":"smoke-test","content":"From `/app-admin/users`, impersonate a test user and confirm you land on the web origin with the support banner, then end the support session."},{"heading":"smoke-test","content":"Check the Convex dashboard logs for the hourly `prune MCP credentials` cron."},{"heading":"backups-and-data","content":"`pnpm db:export` runs `convex export --include-file-storage` for the selected deployment. `pnpm db:restore <file>` runs `convex import --replace-all` and replaces every table. Verify which deployment is selected before restoring."},{"heading":"what-this-kit-does-not-include","content":"There is no Cloudflare Workers target for this kit; the Cloudflare guide is for the D1 kits. There is no Vercel or Convex CI configuration in the repository either: `.github/workflows/` has a nightly `integration-tests.yml` (still Supabase-based), `release.yml` for Changesets and `agent-config.yml`. The repository uses local verification with `pnpm signoff` (`verify:local` plus `gh signoff`) instead of pull request checks."},{"heading":"what-this-kit-does-not-include","content":"If login or consent lands on the wrong page after deploying, compare `SITE_URL`, `ADMIN_SITE_URL`, `MCP_APP_URL`, `NEXT_PUBLIC_SITE_URL`, `NEXT_PUBLIC_ADMIN_APP_URL` and `NEXT_PUBLIC_WEB_APP_URL` first, then see troubleshooting."}],"headings":[{"id":"create-and-select-the-convex-deployment","content":"Create and select the Convex deployment"},{"id":"set-the-backend-variables","content":"Set the backend variables"},{"id":"publish-the-backend","content":"Publish the backend"},{"id":"configure-the-web-host","content":"Configure the web host"},{"id":"configure-the-admin-host","content":"Configure the admin host"},{"id":"build-and-deploy-both-apps","content":"Build and deploy both apps"},{"id":"configure-external-callbacks","content":"Configure external callbacks"},{"id":"smoke-test","content":"Smoke test"},{"id":"backups-and-data","content":"Backups and data"},{"id":"what-this-kit-does-not-include","content":"What this kit does not include"}]},"url":"/docs/v5/starters/nextjs-convex/deployment","title":"Deployment","description":"Publish the Convex backend, configure its variables, and deploy the web and admin Next.js apps for the Next.js + Convex starter."},{"_id":"/docs/v5/starters/nextjs-convex/environment-variables","structured":{"contents":[{"content":"This kit keeps configuration in three places. The web app reads `apps/web/.env.local`, the admin app reads `apps/admin/.env.local`, and everything that Convex functions need lives as deployment environment variables, set with `pnpm exec convex env set NAME value` from the repository root. A frontend env file never configures the backend."},{"content":"Variables prefixed `NEXT_PUBLIC_` are bundled into the browser. Everything else is server-only. Use placeholders below; never commit real values."},{"heading":"values-written-by-the-local-bootstrap","content":"`pnpm dev` runs `scripts/bootstrap-convex-local.ts`, which writes these into the two app env files and keeps your other entries. In production you set them on each host yourself."},{"heading":"values-written-by-the-local-bootstrap","content":"Variable"},{"heading":"values-written-by-the-local-bootstrap","content":"File"},{"heading":"values-written-by-the-local-bootstrap","content":"Required"},{"heading":"values-written-by-the-local-bootstrap","content":"Purpose"},{"heading":"values-written-by-the-local-bootstrap","content":"Where the value comes from"},{"heading":"values-written-by-the-local-bootstrap","content":"`NEXT_PUBLIC_CONVEX_URL`"},{"heading":"values-written-by-the-local-bootstrap","content":"both apps"},{"heading":"values-written-by-the-local-bootstrap","content":"Yes"},{"heading":"values-written-by-the-local-bootstrap","content":"Browser and server connection to Convex. The Convex client provider throws without it"},{"heading":"values-written-by-the-local-bootstrap","content":"Local: `CONVEX_URL` from the CLI-generated root `.env.local`. Cloud: the deployment URL (`https://<name>.convex.cloud`)"},{"heading":"values-written-by-the-local-bootstrap","content":"`CONVEX_SITE_URL`"},{"heading":"values-written-by-the-local-bootstrap","content":"both apps"},{"heading":"values-written-by-the-local-bootstrap","content":"Yes for web"},{"heading":"values-written-by-the-local-bootstrap","content":"HTTP action origin of the deployment, also used as the Convex Auth issuer domain in `auth.config.ts`"},{"heading":"values-written-by-the-local-bootstrap","content":"Local: root `.env.local`. Cloud: `https://<name>.convex.site`"},{"heading":"values-written-by-the-local-bootstrap","content":"`CONVEX_DEPLOYMENT`"},{"heading":"values-written-by-the-local-bootstrap","content":"`apps/web/.env.local`"},{"heading":"values-written-by-the-local-bootstrap","content":"CLI only"},{"heading":"values-written-by-the-local-bootstrap","content":"Tells the Convex CLI which deployment `apps/database` scripts target"},{"heading":"values-written-by-the-local-bootstrap","content":"Local: `local:...` or `anonymous:...`. Cloud: your deployment name"},{"heading":"values-written-by-the-local-bootstrap","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"values-written-by-the-local-bootstrap","content":"both apps"},{"heading":"values-written-by-the-local-bootstrap","content":"Yes"},{"heading":"values-written-by-the-local-bootstrap","content":"Public origin of each app"},{"heading":"values-written-by-the-local-bootstrap","content":"`http://localhost:3000` for web, `http://localhost:3002` for admin; your domains in production"},{"heading":"values-written-by-the-local-bootstrap","content":"`APP_ENV`"},{"heading":"values-written-by-the-local-bootstrap","content":"both apps"},{"heading":"values-written-by-the-local-bootstrap","content":"Yes locally"},{"heading":"values-written-by-the-local-bootstrap","content":"Marks the local environment; the bootstrap sets `local`"},{"heading":"values-written-by-the-local-bootstrap","content":"Set by the bootstrap"},{"heading":"values-written-by-the-local-bootstrap","content":"`NEXT_PUBLIC_ADMIN_APP_URL`"},{"heading":"values-written-by-the-local-bootstrap","content":"`apps/web/.env.local`"},{"heading":"values-written-by-the-local-bootstrap","content":"Yes"},{"heading":"values-written-by-the-local-bootstrap","content":"Admin origin that the web app links to (impersonation banner, admin consent hand-off)"},{"heading":"values-written-by-the-local-bootstrap","content":"`http://localhost:3002` locally; your admin domain in production"},{"heading":"values-written-by-the-local-bootstrap","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"values-written-by-the-local-bootstrap","content":"`apps/admin/.env.local`"},{"heading":"values-written-by-the-local-bootstrap","content":"Yes"},{"heading":"values-written-by-the-local-bootstrap","content":"Customer web origin the admin app links to, sends impersonation handoffs to and calls `/api/revalidate` on"},{"heading":"values-written-by-the-local-bootstrap","content":"`http://localhost:3000` locally; your web domain in production"},{"heading":"values-written-by-the-local-bootstrap","content":"`REVALIDATE_SECRET`"},{"heading":"values-written-by-the-local-bootstrap","content":"both apps"},{"heading":"values-written-by-the-local-bootstrap","content":"Yes"},{"heading":"values-written-by-the-local-bootstrap","content":"Shared secret for `POST /api/revalidate` on the web app. Must be identical in both apps"},{"heading":"values-written-by-the-local-bootstrap","content":"The bootstrap generates one and writes the same value to both files"},{"heading":"values-written-by-the-local-bootstrap","content":"The root `.env.local` (`CONVEX_DEPLOYMENT`, `CONVEX_URL`, `CONVEX_SITE_URL`) is generated by `convex dev`, is git-ignored, and is only read by the bootstrap script."},{"heading":"appswebenvlocalexample","content":"These are the entries in the example file, in order. Copy it only when `apps/web/.env.local` is missing. The first four are commented out in the example because the bootstrap writes them; fill them in by hand only for a cloud deployment."},{"heading":"appswebenvlocalexample","content":"Variable"},{"heading":"appswebenvlocalexample","content":"Required"},{"heading":"appswebenvlocalexample","content":"Purpose"},{"heading":"appswebenvlocalexample","content":"Where the value comes from"},{"heading":"appswebenvlocalexample","content":"`APP_ENV`"},{"heading":"appswebenvlocalexample","content":"Generated"},{"heading":"appswebenvlocalexample","content":"`local` marks the local environment"},{"heading":"appswebenvlocalexample","content":"Written by the bootstrap; unset in production"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_CONVEX_URL`"},{"heading":"appswebenvlocalexample","content":"Generated"},{"heading":"appswebenvlocalexample","content":"Convex deployment URL"},{"heading":"appswebenvlocalexample","content":"Written by the bootstrap; `https://<name>.convex.cloud` in production"},{"heading":"appswebenvlocalexample","content":"`CONVEX_SITE_URL`"},{"heading":"appswebenvlocalexample","content":"Generated"},{"heading":"appswebenvlocalexample","content":"Convex HTTP actions origin"},{"heading":"appswebenvlocalexample","content":"Written by the bootstrap; `https://<name>.convex.site` in production"},{"heading":"appswebenvlocalexample","content":"`CONVEX_DEPLOYMENT`"},{"heading":"appswebenvlocalexample","content":"Generated, CLI only"},{"heading":"appswebenvlocalexample","content":"Deployment the Convex CLI targets"},{"heading":"appswebenvlocalexample","content":"Written by the bootstrap; `local:...` or `anonymous:...` locally"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"appswebenvlocalexample","content":"Yes"},{"heading":"appswebenvlocalexample","content":"Website origin (also written by the bootstrap)"},{"heading":"appswebenvlocalexample","content":"`http://localhost:3000`"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_SUPABASE_URL`"},{"heading":"appswebenvlocalexample","content":"Legacy"},{"heading":"appswebenvlocalexample","content":"Placeholder read by remaining Supabase-era code paths under `apps/web/src` and by `next build`. Convex does not use it"},{"heading":"appswebenvlocalexample","content":"Keep the placeholder from the example; do not point it at a project"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`"},{"heading":"appswebenvlocalexample","content":"Legacy"},{"heading":"appswebenvlocalexample","content":"Same as above"},{"heading":"appswebenvlocalexample","content":"Placeholder"},{"heading":"appswebenvlocalexample","content":"`SUPABASE_SECRET_KEY`"},{"heading":"appswebenvlocalexample","content":"Legacy"},{"heading":"appswebenvlocalexample","content":"Same as above, server-only"},{"heading":"appswebenvlocalexample","content":"Placeholder"},{"heading":"appswebenvlocalexample","content":"`SUPABASE_DATABASE_PASSWORD`"},{"heading":"appswebenvlocalexample","content":"Legacy"},{"heading":"appswebenvlocalexample","content":"Unused by Convex"},{"heading":"appswebenvlocalexample","content":"Placeholder"},{"heading":"appswebenvlocalexample","content":"`SUPABASE_PROJECT_REF`"},{"heading":"appswebenvlocalexample","content":"Legacy, commented out"},{"heading":"appswebenvlocalexample","content":"Unused by Convex"},{"heading":"appswebenvlocalexample","content":"Leave commented"},{"heading":"appswebenvlocalexample","content":"`STRIPE_SECRET_KEY`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Stripe API access in `apps/web/src/payments/` and `/api/stripe/webhooks`"},{"heading":"appswebenvlocalexample","content":"Stripe dashboard"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Stripe.js in the browser"},{"heading":"appswebenvlocalexample","content":"Stripe dashboard"},{"heading":"appswebenvlocalexample","content":"`STRIPE_WEBHOOK_SECRET`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Verifies webhook signatures"},{"heading":"appswebenvlocalexample","content":"`stripe listen` locally, webhook settings in production"},{"heading":"appswebenvlocalexample","content":"`ADMIN_EMAIL`"},{"heading":"appswebenvlocalexample","content":"Yes for app emails"},{"heading":"appswebenvlocalexample","content":"`from` address for emails the Next.js server sends (invitations, account emails)"},{"heading":"appswebenvlocalexample","content":"An address verified with your email provider"},{"heading":"appswebenvlocalexample","content":"`RESEND_API_KEY`"},{"heading":"appswebenvlocalexample","content":"Production"},{"heading":"appswebenvlocalexample","content":"Resend key for emails sent by the Next.js server. Separate from the backend's `AUTH_RESEND_KEY`"},{"heading":"appswebenvlocalexample","content":"Resend dashboard"},{"heading":"appswebenvlocalexample","content":"`USE_LOCAL_EMAIL`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"`true` posts to a Mailpit server at `http://localhost:54324`, which this kit does not start"},{"heading":"appswebenvlocalexample","content":"`true` or unset"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_POSTHOG_API_KEY`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"PostHog analytics"},{"heading":"appswebenvlocalexample","content":"PostHog project settings"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_POSTHOG_APP_ID`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"PostHog"},{"heading":"appswebenvlocalexample","content":"PostHog project settings"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_POSTHOG_HOST`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"PostHog ingestion host"},{"heading":"appswebenvlocalexample","content":"PostHog project settings"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_GA_ID`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Google Analytics"},{"heading":"appswebenvlocalexample","content":"Google Analytics property"},{"heading":"appswebenvlocalexample","content":"`UNKEY_ROOT_KEY`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Unkey API key management (`src/data/user/unkey.ts`)"},{"heading":"appswebenvlocalexample","content":"Unkey dashboard"},{"heading":"appswebenvlocalexample","content":"`UNKEY_API_ID`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Unkey API id"},{"heading":"appswebenvlocalexample","content":"Unkey dashboard"},{"heading":"appswebenvlocalexample","content":"`TWITTER_API_KEY`, `TWITTER_API_SECRET`"},{"heading":"appswebenvlocalexample","content":"Legacy"},{"heading":"appswebenvlocalexample","content":"Supabase-era social provider keys. Convex Auth never reads them"},{"heading":"appswebenvlocalexample","content":"Leave as placeholders"},{"heading":"appswebenvlocalexample","content":"`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`"},{"heading":"appswebenvlocalexample","content":"Legacy"},{"heading":"appswebenvlocalexample","content":"Not read by Convex Auth; use `AUTH_GOOGLE_ID` and `AUTH_GOOGLE_SECRET` on the backend instead"},{"heading":"appswebenvlocalexample","content":"Leave as placeholders"},{"heading":"appswebenvlocalexample","content":"`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`"},{"heading":"appswebenvlocalexample","content":"Legacy"},{"heading":"appswebenvlocalexample","content":"Not read by Convex Auth; use `AUTH_GITHUB_ID` and `AUTH_GITHUB_SECRET` on the backend instead"},{"heading":"appswebenvlocalexample","content":"Leave as placeholders"},{"heading":"appswebenvlocalexample","content":"`REVALIDATE_SECRET`"},{"heading":"appswebenvlocalexample","content":"Yes"},{"heading":"appswebenvlocalexample","content":"Required in the `x-revalidate-secret` header for `POST /api/revalidate`; the admin app sends it after publishing content"},{"heading":"appswebenvlocalexample","content":"The bootstrap generates one and keeps the same value in `apps/admin/.env.local`"},{"heading":"appswebenvlocalexample","content":"`OTEL_ENABLED`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Turns on OpenTelemetry in `instrumentation.ts`"},{"heading":"appswebenvlocalexample","content":"`true` or `false`"},{"heading":"appswebenvlocalexample","content":"`OTEL_SERVICE_NAME`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Service name for traces"},{"heading":"appswebenvlocalexample","content":"Default `nextbase-ultimate-web`"},{"heading":"appswebenvlocalexample","content":"`OTEL_EXPORTER_OTLP_ENDPOINT`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"OTLP collector endpoint"},{"heading":"appswebenvlocalexample","content":"Your collector, default `http://localhost:4318`"},{"heading":"appswebenvlocalexample","content":"`OTEL_EXPORTER_OTLP_HEADERS`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Extra OTLP headers"},{"heading":"appswebenvlocalexample","content":"Your collector"},{"heading":"appswebenvlocalexample","content":"`LOG_LEVEL`"},{"heading":"appswebenvlocalexample","content":"Optional"},{"heading":"appswebenvlocalexample","content":"Structured logger verbosity"},{"heading":"appswebenvlocalexample","content":"`info` by default"},{"heading":"appswebenvlocalexample","content":"`NEXT_PUBLIC_ADMIN_APP_URL`"},{"heading":"appswebenvlocalexample","content":"Yes"},{"heading":"appswebenvlocalexample","content":"Origin of the separate admin app (also written by the bootstrap)"},{"heading":"appswebenvlocalexample","content":"`http://localhost:3002`"},{"heading":"appswebenvlocalexample","content":"Two variables are read by the code but are not in the example: `NEXT_PUBLIC_SENTRY_DSN` (Sentry client, server and edge configs) and `OPENAI_API_KEY` (the `/api/chat` route and image generation; it appears in `apps/web/.env.test`). Add them to `apps/web/.env.local` if you enable those features."},{"heading":"appsadminenvlocalexample","content":"The admin app has its own, much shorter example. The bootstrap writes `apps/admin/.env.local` for you locally."},{"heading":"appsadminenvlocalexample","content":"Variable"},{"heading":"appsadminenvlocalexample","content":"Required"},{"heading":"appsadminenvlocalexample","content":"Purpose"},{"heading":"appsadminenvlocalexample","content":"Where the value comes from"},{"heading":"appsadminenvlocalexample","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"appsadminenvlocalexample","content":"Yes"},{"heading":"appsadminenvlocalexample","content":"The admin app's own origin"},{"heading":"appsadminenvlocalexample","content":"`http://localhost:3002` locally; your admin domain in production"},{"heading":"appsadminenvlocalexample","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"appsadminenvlocalexample","content":"Yes"},{"heading":"appsadminenvlocalexample","content":"The customer web origin: links back to the web app, the impersonation destination and the `/api/revalidate` target"},{"heading":"appsadminenvlocalexample","content":"`http://localhost:3000` locally; your web domain in production"},{"heading":"appsadminenvlocalexample","content":"`NEXT_PUBLIC_CONVEX_URL`"},{"heading":"appsadminenvlocalexample","content":"Yes"},{"heading":"appsadminenvlocalexample","content":"Convex deployment URL, the same value as the web app"},{"heading":"appsadminenvlocalexample","content":"`https://<name>.convex.cloud`"},{"heading":"appsadminenvlocalexample","content":"`NEXT_PUBLIC_CONVEX_SITE_URL`"},{"heading":"appsadminenvlocalexample","content":"Listed"},{"heading":"appsadminenvlocalexample","content":"Convex HTTP actions origin. Listed in the example; the admin code does not currently read it, and the bootstrap writes `CONVEX_SITE_URL` instead"},{"heading":"appsadminenvlocalexample","content":"`https://<name>.convex.site`"},{"heading":"appsadminenvlocalexample","content":"`REVALIDATE_SECRET`"},{"heading":"appsadminenvlocalexample","content":"Yes"},{"heading":"appsadminenvlocalexample","content":"Must match the web app's value; `safe-action.ts` refuses to revalidate without it and `NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"appsadminenvlocalexample","content":"Same value as `apps/web/.env.local`"},{"heading":"appsadminenvlocalexample","content":"The admin code also reads `ADMIN_EMAIL`, `RESEND_API_KEY`, `USE_LOCAL_EMAIL`, the `STRIPE_*` keys and the legacy Supabase placeholders from its environment; they are not in its example file. Add them to `apps/admin/.env.local` if a feature or build needs them."},{"heading":"convex-deployment-variables","content":"Set these with the CLI against the deployment you have selected. The local bootstrap sets `JWT_PRIVATE_KEY`, `JWKS`, `SITE_URL`, `ADMIN_SITE_URL` and `APP_ENV` for you."},{"heading":"convex-deployment-variables","content":"Variable"},{"heading":"convex-deployment-variables","content":"Required"},{"heading":"convex-deployment-variables","content":"Purpose"},{"heading":"convex-deployment-variables","content":"Where the value comes from"},{"heading":"convex-deployment-variables","content":"`JWT_PRIVATE_KEY`"},{"heading":"convex-deployment-variables","content":"Yes"},{"heading":"convex-deployment-variables","content":"Convex Auth signing key (RS256, PKCS8)"},{"heading":"convex-deployment-variables","content":"Generated by the bootstrap locally. For a cloud deployment, generate your own pair"},{"heading":"convex-deployment-variables","content":"`JWKS`"},{"heading":"convex-deployment-variables","content":"Yes"},{"heading":"convex-deployment-variables","content":"Public JWKS matching `JWT_PRIVATE_KEY`"},{"heading":"convex-deployment-variables","content":"Same as above"},{"heading":"convex-deployment-variables","content":"`SITE_URL`"},{"heading":"convex-deployment-variables","content":"Yes"},{"heading":"convex-deployment-variables","content":"Website origin. Validates auth redirects (`lib/auth_redirect.ts`), builds password reset links, and is the last fallback for the MCP consent URL"},{"heading":"convex-deployment-variables","content":"Your website origin"},{"heading":"convex-deployment-variables","content":"`ADMIN_SITE_URL`"},{"heading":"convex-deployment-variables","content":"Yes with the admin app"},{"heading":"convex-deployment-variables","content":"Admin origin. Added to the auth redirect allowlist so admin login and magic-link callbacks return to the admin app, and used as the consent page origin for the Admin MCP"},{"heading":"convex-deployment-variables","content":"Your admin origin"},{"heading":"convex-deployment-variables","content":"`APP_ENV`"},{"heading":"convex-deployment-variables","content":"Yes locally"},{"heading":"convex-deployment-variables","content":"`local` or `test` makes auth emails log to the console instead of sending"},{"heading":"convex-deployment-variables","content":"`local` from the bootstrap; unset in production"},{"heading":"convex-deployment-variables","content":"`AUTH_RESEND_KEY`"},{"heading":"convex-deployment-variables","content":"Production"},{"heading":"convex-deployment-variables","content":"Resend key used by `convex/auth.ts` for verification, magic link and password reset emails"},{"heading":"convex-deployment-variables","content":"Resend dashboard"},{"heading":"convex-deployment-variables","content":"`AUTH_EMAIL_FROM`"},{"heading":"convex-deployment-variables","content":"Production"},{"heading":"convex-deployment-variables","content":"Sender for those emails"},{"heading":"convex-deployment-variables","content":"A sender on your verified Resend domain"},{"heading":"convex-deployment-variables","content":"`AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET`"},{"heading":"convex-deployment-variables","content":"Optional"},{"heading":"convex-deployment-variables","content":"Registers the GitHub provider when both exist"},{"heading":"convex-deployment-variables","content":"GitHub OAuth app"},{"heading":"convex-deployment-variables","content":"`AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET`"},{"heading":"convex-deployment-variables","content":"Optional"},{"heading":"convex-deployment-variables","content":"Registers the Google provider when both exist"},{"heading":"convex-deployment-variables","content":"Google Cloud OAuth client"},{"heading":"convex-deployment-variables","content":"`MCP_APP_URL`"},{"heading":"convex-deployment-variables","content":"Production"},{"heading":"convex-deployment-variables","content":"Canonical website origin used for the User MCP consent redirect and the browser origin allowlist. Falls back to `APP_URL`, then `SITE_URL`"},{"heading":"convex-deployment-variables","content":"Your website origin"},{"heading":"convex-deployment-variables","content":"`CONVEX_SITE_URL`"},{"heading":"convex-deployment-variables","content":"Supplied"},{"heading":"convex-deployment-variables","content":"The deployment's `.convex.site` origin. Convex provides it; the MCP code throws if it is missing"},{"heading":"convex-deployment-variables","content":"Provided by Convex"},{"heading":"convex-deployment-variables","content":"The bootstrap refuses to run against a deployment whose name does not start with `local:` or `anonymous:`, so it cannot write these to a shared environment by accident."},{"heading":"convex-deployment-variables","content":"For how the auth variables are used, see authentication. For production, see deployment."}],"headings":[{"id":"values-written-by-the-local-bootstrap","content":"Values written by the local bootstrap"},{"id":"appswebenvlocalexample","content":"`apps/web/.env.local.example`"},{"id":"appsadminenvlocalexample","content":"`apps/admin/.env.local.example`"},{"id":"convex-deployment-variables","content":"Convex deployment variables"}]},"url":"/docs/v5/starters/nextjs-convex/environment-variables","title":"Environment variables","description":"Every variable in the Next.js + Convex starter, split between the web env file, the admin env file and the Convex deployment."},{"_id":"/docs/v5/starters/nextjs-convex","structured":{"contents":[{"content":"This kit uses Next.js App Router, a Convex backend and Convex Auth. You get the account, workspace and project foundation, a separate admin application, and separate agent connections for users and application administrators."},{"content":"Open the live demo · Repository"},{"heading":"the-stack","content":"Layer"},{"heading":"the-stack","content":"What the kit uses"},{"heading":"the-stack","content":"Frontend"},{"heading":"the-stack","content":"Next.js 16 App Router, React 19, Cache Components, Tailwind CSS 4, shadcn/ui"},{"heading":"the-stack","content":"Admin"},{"heading":"the-stack","content":"A second Next.js application in `apps/admin` on its own origin, sharing the same Convex backend"},{"heading":"the-stack","content":"Backend and database"},{"heading":"the-stack","content":"Convex: document database, reactive queries, mutations, HTTP actions, file storage, crons"},{"heading":"the-stack","content":"Authentication"},{"heading":"the-stack","content":"Convex Auth (`@convex-dev/auth`) with password, magic link, and optional GitHub and Google"},{"heading":"the-stack","content":"Agent connections"},{"heading":"the-stack","content":"Two `convex-mcp-gateway` component instances served from the Convex backend"},{"heading":"the-stack","content":"Emails"},{"heading":"the-stack","content":"React Email templates in `apps/email`; Resend for production auth emails"},{"heading":"the-stack","content":"Hosting used by the demo"},{"heading":"the-stack","content":"Web and admin apps on Vercel, backend on Convex. A `netlify.toml` is also included for the web app"},{"heading":"the-stack","content":"Convex replaces the separate database, realtime and storage services you would otherwise run. Locally, the Convex CLI manages a local backend for you, so there is no Docker in this kit."},{"heading":"what-you-get-out-of-the-box","content":"Sign-up, login, email verification, magic links and password reset through Convex Auth."},{"heading":"what-you-get-out-of-the-box","content":"Onboarding that completes a profile and creates the first workspace."},{"heading":"what-you-get-out-of-the-box","content":"Workspaces with `owner`, `admin`, `member` and `readonly` roles, invitations and member management."},{"heading":"what-you-get-out-of-the-box","content":"Workspace-scoped projects with comments and reactive updates."},{"heading":"what-you-get-out-of-the-box","content":"In-app notifications, feedback boards, a public roadmap, an MDX blog, a changelog and a Fumadocs documentation site."},{"heading":"what-you-get-out-of-the-box","content":"A dedicated admin application (`apps/admin`, port 3002) serving `/app-admin`: users, workspaces, marketing content, feedback moderation, the payment gateway, admin agent connections and audited user impersonation. The demo admin runs at `https://nextjs-convex-admin.usenextbase.com`."},{"heading":"what-you-get-out-of-the-box","content":"A `platform:grantAdmin` internal mutation to promote your first application administrator from the CLI."},{"heading":"what-you-get-out-of-the-box","content":"A User MCP at `/mcp/user` and a read-only Admin MCP at `/mcp/admin`, both with OAuth consent."},{"heading":"what-you-get-out-of-the-box","content":"Convex file storage with application-owned metadata."},{"heading":"what-you-get-out-of-the-box","content":"41 React Email templates with a preview server."},{"heading":"what-you-get-out-of-the-box","content":"Stripe, PostHog, Sentry and OpenTelemetry integrations that you enable through environment variables."},{"heading":"what-you-get-out-of-the-box","content":"The core login, onboarding, workspace and project paths use Convex Auth and Convex data. Some older advanced settings, billing and application routes still contain Supabase actions: at the time of writing, 36 files under `apps/web/src` and 21 under `apps/admin/src` still import from `src/supabase-clients/`, including `payments/stripe-payment-gateway.ts`. The billing tables exist in the Convex schema but have no Convex functions yet. Review and migrate those paths before relying on them in your product."},{"heading":"prerequisites","content":"Requirement"},{"heading":"prerequisites","content":"Detail"},{"heading":"prerequisites","content":"Node.js"},{"heading":"prerequisites","content":"24 (`.nvmrc` says `v24`; `setup.sh` refuses other majors)"},{"heading":"prerequisites","content":"pnpm"},{"heading":"prerequisites","content":"11.1.2 through Corepack (`packageManager` in `package.json`)"},{"heading":"prerequisites","content":"Docker"},{"heading":"prerequisites","content":"Not needed. Convex runs a local backend through its CLI"},{"heading":"prerequisites","content":"Convex account"},{"heading":"prerequisites","content":"Only for cloud deployments. Local development uses the CLI-managed local backend"},{"heading":"prerequisites","content":"Resend account"},{"heading":"prerequisites","content":"For production auth emails (`AUTH_RESEND_KEY`, `AUTH_EMAIL_FROM` on the Convex deployment)"},{"heading":"prerequisites","content":"Stripe, PostHog, Sentry"},{"heading":"prerequisites","content":"Optional, only for the integrations you enable"},{"heading":"next-steps","content":"Installation: clone, install, run `pnpm dev`, create your first account, workspace and administrator."},{"heading":"next-steps","content":"Environment variables: what lives in the web and admin env files and what lives on the Convex deployment."},{"heading":"next-steps","content":"Authentication: providers, sessions, the admin app and how functions are protected."},{"heading":"next-steps","content":"Agent connections: the User and Admin MCP servers, their tools and OAuth behaviour."},{"heading":"next-steps","content":"Project structure: the monorepo and where to make changes."},{"heading":"next-steps","content":"Deployment: publish the Convex backend and deploy the web and admin apps."},{"heading":"next-steps","content":"For concepts shared across all eight kits, start at getting started."}],"headings":[{"id":"the-stack","content":"The stack"},{"id":"what-you-get-out-of-the-box","content":"What you get out of the box"},{"id":"prerequisites","content":"Prerequisites"},{"id":"next-steps","content":"Next steps"}]},"url":"/docs/v5/starters/nextjs-convex","title":"Next.js + Convex","description":"Overview of the Next.js + Convex starter: what it ships, what you need, and where to go next."},{"_id":"/docs/v5/starters/nextjs-convex/installation","structured":{"contents":[{"content":"This page gets the kit running on your computer. You need Node.js 24 and pnpm 11.1.2 through Corepack. There is no Docker: the Convex CLI starts and manages a local backend for you."},{"heading":"clone-the-repository","content":"If GitHub says the repository doesn't exist, sign in with the account that has access."},{"heading":"install-dependencies","content":"The root `package.json` pins `pnpm@11.1.2`, so Corepack installs the right version. `./setup.sh` does the same two steps and first checks that `node --version` is a 24.x release."},{"heading":"install-dependencies","content":"`pnpm install` also runs `fumadocs-mdx` in `apps/web` to generate `apps/web/.source/`; don't edit that folder. The `.claude/skills` entry is a symlink to `.agents/skills/`, so on Windows run `git config core.symlinks true` before cloning."},{"heading":"create-the-web-env-file","content":"Copy the web example only if you don't already have a local file:"},{"heading":"create-the-web-env-file","content":"Never overwrite an existing `apps/web/.env.local`. The Convex connection values at the top of the example (`APP_ENV`, `NEXT_PUBLIC_CONVEX_URL`, `CONVEX_SITE_URL`, `CONVEX_DEPLOYMENT`) are commented out and marked as generated: the next step writes them into this file and keeps everything else you have in it. Fill in the optional integrations (Stripe, Resend, PostHog, Unkey) as you enable them."},{"heading":"create-the-web-env-file","content":"You do not need to create `apps/admin/.env.local` by hand; the bootstrap writes it. `apps/admin/.env.local.example` shows the admin app's own values if you want to prepare one."},{"heading":"create-the-web-env-file","content":"The web example still lists Supabase keys, marked as legacy, for code paths that have not been ported yet; Convex itself does not read them. See environment variables for the full list."},{"heading":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"On the first launch the Convex CLI asks which deployment to use. Choose local development. You can also pre-select it with `pnpm db:select-local`, which runs `convex deployment select local`."},{"heading":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"`pnpm dev` is `convex dev --start '...'`. It keeps the local Convex backend running, pushes the schema and functions, and after the first successful sync runs `pnpm bootstrap:local` followed by `turbo run dev` for the `web`, `admin` and `@nextbase/email` packages. The bootstrap script (`scripts/bootstrap-convex-local.ts`):"},{"heading":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"Reads the CLI-generated root `.env.local` (`CONVEX_DEPLOYMENT`, `CONVEX_URL`, `CONVEX_SITE_URL`) and refuses to continue unless the deployment name starts with `local:` or `anonymous:`."},{"heading":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"Writes `APP_ENV`, `NEXT_PUBLIC_SITE_URL`, `CONVEX_DEPLOYMENT`, `NEXT_PUBLIC_CONVEX_URL`, `CONVEX_SITE_URL`, `NEXT_PUBLIC_ADMIN_APP_URL` and a generated `REVALIDATE_SECRET` into `apps/web/.env.local`."},{"heading":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"Writes `APP_ENV`, `NEXT_PUBLIC_SITE_URL=http://localhost:3002`, `NEXT_PUBLIC_WEB_APP_URL`, `NEXT_PUBLIC_CONVEX_URL`, `CONVEX_SITE_URL` and the same `REVALIDATE_SECRET` into `apps/admin/.env.local`."},{"heading":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"Generates `JWT_PRIVATE_KEY` and `JWKS` on the local backend if they are missing, and sets `SITE_URL=http://localhost:3000`, `ADMIN_SITE_URL=http://localhost:3002` and `APP_ENV=local` there."},{"heading":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"Runs `convex run seed:seed`, which is idempotent (tracked in the `seedVersions` table)."},{"heading":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"Keep this process running while you work. The web app is at http\\://localhost:3000, the admin app at http\\://localhost:3002 and the React Email preview at http\\://localhost:3001."},{"heading":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"To run the pieces separately once a backend exists: `pnpm convex:dev`, `pnpm dev:web`, `pnpm dev:admin`, `pnpm dev:email`. `pnpm convex:once` pushes functions once and exits."},{"heading":"find-local-emails","content":"Auth emails are not delivered locally. With `APP_ENV=local` on the backend, `apps/database/convex/auth.ts` logs each verification or magic-link message as `[local-auth-email]` with the recipient, the code and the URL. `convex dev` tails backend logs, so the message appears in the `pnpm dev` terminal. Copy the code from there."},{"heading":"find-local-emails","content":"Two things to know:"},{"heading":"find-local-emails","content":"The password reset provider has no local branch. It always needs `SITE_URL`, `AUTH_RESEND_KEY` and `AUTH_EMAIL_FROM` on the backend, even locally (`pnpm exec convex env set AUTH_RESEND_KEY re_xxx`)."},{"heading":"find-local-emails","content":"Application emails sent by the Next.js server (for example invitations) use `apps/web/src/utils/api-routes/utils.ts`. In development they are posted to a Mailpit server at `http://localhost:54324`, which this kit does not start. Those sends fail until you run a Mailpit instance there or set `USE_LOCAL_EMAIL` to anything other than `true` together with a `RESEND_API_KEY` in `apps/web/.env.local`."},{"heading":"check-that-everything-works","content":"This runs lint, typecheck, the unit tests and a compile-mode `next build` of the web app. It copies any missing `.env.local` files from their examples first and does not need a cloud deployment. For a quicker loop use `pnpm --filter database test` (Vitest with `convex-test`), `pnpm --filter web test` or `pnpm --filter admin test`."},{"heading":"check-that-everything-works","content":"`pnpm test-integration` runs Playwright against a built app (`pnpm build-and-test-integration` builds first). Its helpers are still Supabase-era in places, so expect to adapt them."},{"heading":"create-an-account-and-a-workspace","content":"Open http\\://localhost:3000/sign-up. Passwords need at least 10 characters. Enter the verification code from the `[local-auth-email]` log line, then complete onboarding: accept the terms, fill in your profile and create a workspace."},{"heading":"create-an-account-and-a-workspace","content":"Add a project through the website so there is data to try with an agent."},{"heading":"make-yourself-an-application-administrator","content":"Every new account gets the `user` platform role. Promote your account with the internal `platform:grantAdmin` mutation, run from the repository root against the selected deployment:"},{"heading":"make-yourself-an-application-administrator","content":"It is idempotent, refuses unknown or disabled accounts, and is an `internalMutation`, so it is never exposed over the client API. Then open http\\://localhost:3002/login, sign in with the same credentials and you land on `/app-admin`. Continue to agent connections to try the User and Admin MCP servers."},{"heading":"stop-keep-and-reset","content":"Press `Ctrl+C` in the `pnpm dev` terminal to stop everything. Local Convex state is persistent under `.convex/` (and `apps/database/.convex/`). Never delete those folders as part of setup or testing; they are your local database."},{"heading":"stop-keep-and-reset","content":"Task"},{"heading":"stop-keep-and-reset","content":"Command"},{"heading":"stop-keep-and-reset","content":"Re-run the seed"},{"heading":"stop-keep-and-reset","content":"`pnpm db:seed` (no-op if the current `SEED_VERSION` is already applied)"},{"heading":"stop-keep-and-reset","content":"Inspect data"},{"heading":"stop-keep-and-reset","content":"`pnpm db:status` (`convex data`)"},{"heading":"stop-keep-and-reset","content":"Back up data and files"},{"heading":"stop-keep-and-reset","content":"`pnpm db:export` (`convex export --include-file-storage`)"},{"heading":"stop-keep-and-reset","content":"Restore a backup"},{"heading":"stop-keep-and-reset","content":"`pnpm db:restore <file>` (`convex import --replace-all`, replaces everything in the selected deployment)"},{"heading":"stop-keep-and-reset","content":"Regenerate the typed API"},{"heading":"stop-keep-and-reset","content":"`pnpm convex:codegen`"},{"heading":"stop-keep-and-reset","content":"One-shot local push, codegen and `lib/database.types.ts`"},{"heading":"stop-keep-and-reset","content":"`pnpm gen-types-local`"},{"heading":"stop-keep-and-reset","content":"Promote an administrator"},{"heading":"stop-keep-and-reset","content":"`pnpm exec convex run platform:grantAdmin '{\"email\":\"...\"}'`"},{"heading":"stop-keep-and-reset","content":"Check which deployment is selected before `db:restore`; it replaces all data in that deployment."},{"heading":"stop-keep-and-reset","content":"If something looks stuck, see troubleshooting."}],"headings":[{"id":"clone-the-repository","content":"Clone the repository"},{"id":"install-dependencies","content":"Install dependencies"},{"id":"create-the-web-env-file","content":"Create the web env file"},{"id":"start-the-backend-the-web-app-the-admin-app-and-the-email-preview","content":"Start the backend, the web app, the admin app and the email preview"},{"id":"find-local-emails","content":"Find local emails"},{"id":"check-that-everything-works","content":"Check that everything works"},{"id":"create-an-account-and-a-workspace","content":"Create an account and a workspace"},{"id":"make-yourself-an-application-administrator","content":"Make yourself an application administrator"},{"id":"stop-keep-and-reset","content":"Stop, keep and reset"}]},"url":"/docs/v5/starters/nextjs-convex/installation","title":"Installation","description":"Clone the Next.js + Convex starter, start the local Convex backend with pnpm dev, and create your first account, workspace and administrator."},{"_id":"/docs/v5/starters/nextjs-convex/project-structure","structured":{"contents":[{"content":"The kit is a pnpm and Turborepo monorepo with four apps and one shared package. `convex.json` at the root points the Convex CLI at `apps/database/convex/`, so run all Convex commands from the repository root."},{"content":"<File name=\"proxy.ts\" />"},{"content":"<File name=\"environment.d.ts\" />"},{"content":"<File name=\".env.local.example\" />"},{"content":"<File name=\"next.config.ts\" />"},{"content":"<File name=\"netlify.toml\" />"},{"content":"<File name=\"vitest.config.mts\" />"},{"content":"<File name=\"proxy.ts\" />"},{"content":"<File name=\"environment.d.ts\" />"},{"content":"<File name=\".env.local.example\" />"},{"content":"<File name=\"next.config.ts\" />"},{"content":"<File name=\"vitest.config.mts\" />"},{"content":"<File name=\"schema.ts\" />"},{"content":"<File name=\"auth.ts\" />"},{"content":"<File name=\"auth.config.ts\" />"},{"content":"<File name=\"convex.config.ts\" />"},{"content":"<File name=\"http.ts\" />"},{"content":"<File name=\"crons.ts\" />"},{"content":"<File name=\"seed.ts\" />"},{"content":"<File name=\"workspaces.ts\" />"},{"content":"<File name=\"projects.ts\" />"},{"content":"<File name=\"users.ts\" />"},{"content":"<File name=\"onboarding.ts\" />"},{"content":"<File name=\"admin.ts\" />"},{"content":"<File name=\"impersonation.ts\" />"},{"content":"<File name=\"platform.ts\" />"},{"content":"<File name=\"vitest.config.ts\" />"},{"content":"<File name=\"bootstrap-convex-local.ts\" />"},{"content":"<File name=\"verify-local.sh\" />"},{"content":"<File name=\"signoff.sh\" />"},{"content":"<File name=\"mcp-connections.md\" />"},{"content":"<File name=\"separate-admin-app.md\" />"},{"content":"<File name=\"convex.json\" />"},{"content":"<File name=\"turbo.json\" />"},{"content":"<File name=\"vercel.admin.json\" />"},{"content":"<File name=\"pnpm-workspace.yaml\" />"},{"content":"<File name=\"package.json\" />"},{"content":"<File name=\"setup.sh\" />"},{"content":"<File name=\"AGENTS.md\" />"},{"content":"<File name=\"CHANGELOG.md\" />"},{"heading":"the-four-apps","content":"Package"},{"heading":"the-four-apps","content":"Path"},{"heading":"the-four-apps","content":"Role"},{"heading":"the-four-apps","content":"`web`"},{"heading":"the-four-apps","content":"`apps/web`"},{"heading":"the-four-apps","content":"Next.js 16 customer application and Fumadocs content. Dev server on port 3000"},{"heading":"the-four-apps","content":"`admin`"},{"heading":"the-four-apps","content":"`apps/admin`"},{"heading":"the-four-apps","content":"Next.js 16 admin application: `/app-admin`, admin login, admin MCP consent, feedback and roadmap moderation. Dev server on port 3002"},{"heading":"the-four-apps","content":"`database`"},{"heading":"the-four-apps","content":"`apps/database`"},{"heading":"the-four-apps","content":"Convex backend: schema, functions, MCP servers, tests. Exports `database/api` and `database/data-model`"},{"heading":"the-four-apps","content":"`@nextbase/email`"},{"heading":"the-four-apps","content":"`apps/email`"},{"heading":"the-four-apps","content":"41 React Email templates and the preview server on port 3001"},{"heading":"the-four-apps","content":"`packages/typescript-config` holds the shared `tsconfig.base.json`. The root `scripts/` folder has the local bootstrap, `verify-local.sh` and the `gh signoff` wrapper. `vercel.admin.json` at the root is the Vercel configuration for the admin project. `docs/separate-admin-app.md` describes the admin split and impersonation; `docs/mcp-connections.md` is the MCP reference. `AGENTS.md` at the root and in each app is the hand-edited source for all generated AI tool configs (`CLAUDE.md`, `GEMINI.md`, `.cursor/`, `.codex/` and so on); run `pnpm agents:sync` after editing it."},{"heading":"routes-and-pages","content":"Web routes live in `apps/web/src/app`, grouped by access level:"},{"heading":"routes-and-pages","content":"`(external-pages)/` public marketing site: landing, `blog/`, `changelog/`, `docs/`, `terms/`."},{"heading":"routes-and-pages","content":"`(dynamic-pages)/(login-pages)/` for `/login` and `/sign-up`; `(misc-login-pages)/` for password reset and the legacy auth callbacks."},{"heading":"routes-and-pages","content":"`(dynamic-pages)/(authenticated-pages)/` for the signed-in app: `dashboard/`, `onboarding/`, `user/` (settings, connections, notifications, invitations) and `(application-pages)/workspace/[workspaceSlug]/` and `project/[projectSlug]/`. Layouts use `@navbar/` and `@sidebar/` parallel route slots."},{"heading":"routes-and-pages","content":"`(dynamic-pages)/(updates-pages)/` for feedback boards and the roadmap."},{"heading":"routes-and-pages","content":"`api/` route handlers: `stripe/webhooks`, `invitations`, `chat`, `search`, `revalidate`, `health`, OG images and `v1/`."},{"heading":"routes-and-pages","content":"`auth/impersonate/` receives impersonation handoffs from the admin app; `mcp/authorize/` is the user MCP consent page."},{"heading":"routes-and-pages","content":"Admin routes live in `apps/admin/src/app`:"},{"heading":"routes-and-pages","content":"`app-admin/(admin-pages)/` with `users/`, `workspaces/`, `marketing/` (`blog`, `changelog`, `authors`, `tags`), `payment-gateway/`, `settings/`, `connections/` and the `@navbar/` and `@sidebar/` slots. The layout calls `unauthorized()` for anyone without the admin platform role."},{"heading":"routes-and-pages","content":"`(dynamic-pages)/(updates-pages)/` for the admin views of feedback and the roadmap."},{"heading":"routes-and-pages","content":"`login/`, `auth/callback/` and `mcp/authorize/` for the admin's own sign-in and the Admin MCP consent page."},{"heading":"routes-and-pages","content":"`api/admin/marketing/media/` for media uploads."},{"heading":"routes-and-pages","content":"Cache Components are on in both apps. Code that reads cookies, auth tokens or Convex data at request time must sit inside `<Suspense>`, and route params arrive as `Promise<unknown>`; parse them with the schemas in `src/utils/zod-schemas/params.ts`. The `new-page` skill in `.agents/skills/` has the per-area checklist."},{"heading":"server-and-data-access","content":"All data access goes through Convex functions. On the server, call `fetchQuery` or `fetchMutation` from `convex/nextjs` with `{ token: await convexAuthNextjsToken() }`. In client components use `useQuery`, `usePaginatedQuery` and `useMutation` from `convex/react`. Import `api` from `database/api` and `Id` or `Doc` from `database/data-model`."},{"heading":"server-and-data-access","content":"`src/data/{user,admin,anon}/` in each app holds `\"use server\"` functions and server actions built with `next-cool-action` clients from that app's `src/lib/safe-action.ts`. `apps/web/src/rsc-data/user/` holds React `cache()` wrappers for server components. Many files in `src/data/` still import the Supabase clients; port them to Convex functions when you touch them and do not add new code on `src/supabase-clients/` or `apps/web/src/middlewares/`."},{"heading":"server-and-data-access","content":"Admin publication actions call the web app's `POST /api/revalidate` with the shared `REVALIDATE_SECRET` so the customer app refreshes its public content caches."},{"heading":"schema-and-migrations","content":"Convex has no SQL migrations. Edit `apps/database/convex/schema.ts`, then `pnpm convex:dev` or `pnpm convex:once` pushes it. The push fails if existing documents violate the new schema, so make breaking changes in steps: add fields as `v.optional(...)`, backfill with an internal mutation, then tighten. Run `pnpm convex:codegen` and commit `apps/database/convex/_generated/`. When seed data changes, update `convex/seed.ts` (bump `SEED_VERSION`) and `seed.test.ts`. The `db-migration` skill describes this flow."},{"heading":"schema-and-migrations","content":"`apps/database/supabase/` (SQL migrations, pgTAP tests) and `apps/database/lib/database.types.ts` are legacy. Don't add to them."},{"heading":"authentication-and-authorization","content":"Providers and callbacks are in `convex/auth.ts`; the issuer is `convex/auth.config.ts`; the redirect allowlist is `convex/lib/auth_redirect.ts`. Authorization helpers are in `convex/lib/authz.ts` and must be the first line of every public function. Operator-only functions such as `platform:grantAdmin` (`convex/platform.ts`) are `internalMutation`s. The Next.js gates are `apps/web/src/proxy.ts` and `apps/admin/src/proxy.ts`. Details are on the authentication page."},{"heading":"emails","content":"Templates are React components in `apps/email/emails/`; preview them at `http://localhost:3001` with `pnpm dev:email`. Auth emails are sent by the backend in `convex/auth.ts` through the Resend API. Application emails from the Next.js server go through `apps/web/src/utils/api-routes/utils.ts`, which uses Mailpit in development and Resend otherwise."},{"heading":"background-jobs-and-crons","content":"`convex/crons.ts` registers the hourly MCP credential prune. Add more jobs there with `crons.interval` or `crons.cron`, pointing at internal functions. The schema also has `integrationJobs`, `webhookEvents` and `idempotencyKeys` tables for retryable work. `@convex-dev/migrations`, `@convex-dev/aggregate` and `@convex-dev/rate-limiter` are installed but not registered in `convex.config.ts`; add `app.use(...)` before importing them."},{"heading":"mcp","content":"Everything is under `apps/database/convex/mcp/`, with the consent UI in `src/components/mcp-consent.tsx` of each app (user consent in web, admin consent in admin). See agent connections."},{"heading":"tests","content":"Kind"},{"heading":"tests","content":"Where"},{"heading":"tests","content":"Command"},{"heading":"tests","content":"Convex functions"},{"heading":"tests","content":"`apps/database/convex/*.test.ts`, Vitest with `convex-test` on the edge runtime"},{"heading":"tests","content":"`pnpm --filter database test`"},{"heading":"tests","content":"Web units"},{"heading":"tests","content":"colocated `*.test.ts(x)` under `apps/web/src`, Vitest with jsdom and Testing Library"},{"heading":"tests","content":"`pnpm --filter web test`"},{"heading":"tests","content":"Admin units"},{"heading":"tests","content":"colocated `*.test.ts(x)` under `apps/admin/src`"},{"heading":"tests","content":"`pnpm --filter admin test`"},{"heading":"tests","content":"End to end"},{"heading":"tests","content":"`apps/web/e2e/{anon,user,admin,...}/*.spec.ts`, Playwright config in `apps/web/playwright/`"},{"heading":"tests","content":"`pnpm test-integration` after a build"},{"heading":"tests","content":"Everything before handoff"},{"heading":"tests","content":"lint, typecheck, unit tests, compile-mode build"},{"heading":"tests","content":"`pnpm verify:local`"},{"heading":"tests","content":"Code style is Oxlint and Oxfmt (`pnpm lint:fix`), not ESLint, Prettier or Biome."}],"headings":[{"id":"the-four-apps","content":"The four apps"},{"id":"where-to-make-changes","content":"Where to make changes"},{"id":"routes-and-pages","content":"Routes and pages"},{"id":"server-and-data-access","content":"Server and data access"},{"id":"schema-and-migrations","content":"Schema and migrations"},{"id":"authentication-and-authorization","content":"Authentication and authorization"},{"id":"emails","content":"Emails"},{"id":"background-jobs-and-crons","content":"Background jobs and crons"},{"id":"mcp","content":"MCP"},{"id":"tests","content":"Tests"}]},"url":"/docs/v5/starters/nextjs-convex/project-structure","title":"Project structure","description":"The Next.js + Convex monorepo layout and where to make changes to routes, data, schema, auth, the admin app, emails, jobs, MCP and tests."},{"_id":"/docs/v5/starters/nextjs-d1/agent-connections","structured":{"contents":[{"content":"This kit ships two remote MCP servers. The web Worker serves the User MCP and the admin Worker serves the Admin MCP. Both use the existing Better Auth login, store their OAuth records in D1 and are read-only in v5. The shared pages cover the general flow: user and admin MCPs, connect an agent and tools and permissions."},{"content":"The `mcp_records` migration (`apps/database/migrations/0010_famous_groot.sql`) is part of the normal migration set and must be applied before the `/mcp` routes serve traffic. `pnpm db:migrate` applies it locally."},{"heading":"endpoints","content":"Connection"},{"heading":"endpoints","content":"Served by"},{"heading":"endpoints","content":"Settings page"},{"heading":"endpoints","content":"Server URL"},{"heading":"endpoints","content":"Live demo"},{"heading":"endpoints","content":"User"},{"heading":"endpoints","content":"web Worker (`apps/web`)"},{"heading":"endpoints","content":"`<web origin>/mcp/connections/user`"},{"heading":"endpoints","content":"`<web origin>/mcp/user`"},{"heading":"endpoints","content":"`https://nextjs-d1.usenextbase.com/mcp/user`"},{"heading":"endpoints","content":"Admin"},{"heading":"endpoints","content":"admin Worker (`apps/admin`)"},{"heading":"endpoints","content":"`<admin origin>/mcp/connections/admin`"},{"heading":"endpoints","content":"`<admin origin>/mcp/admin`"},{"heading":"endpoints","content":"`https://nextjs-d1-admin.usenextbase.com/mcp/admin`"},{"heading":"endpoints","content":"Each origin is that Worker's `NEXT_PUBLIC_SITE_URL`: `http://localhost:3000` for the web app and `http://localhost:3001` for the admin app during development. The web sidebar has an **Agent connections** link for every user, and the admin sidebar has an **Admin agent connections** link. Both open in a new tab because the connection pages are rendered by the MCP handler, not by the Next.js app shell."},{"heading":"endpoints","content":"The same handler runs in both Workers. `ADMIN_APP=true` makes it register only the `admin` audience, so the web origin never answers `/mcp/admin` and the admin origin never answers `/mcp/user`."},{"heading":"endpoints","content":"Opening a server URL in a browser without a token returns a `401` with a `WWW-Authenticate` challenge pointing at the resource metadata. That is expected."},{"heading":"tools","content":"Server"},{"heading":"tools","content":"Tool"},{"heading":"tools","content":"Scope"},{"heading":"tools","content":"What it returns"},{"heading":"tools","content":"User"},{"heading":"tools","content":"`account_context`"},{"heading":"tools","content":"`user:read`"},{"heading":"tools","content":"The signed-in user's ID and email plus the approved workspace"},{"heading":"tools","content":"User"},{"heading":"tools","content":"`projects_list`"},{"heading":"tools","content":"`user:read`"},{"heading":"tools","content":"Projects in the approved workspace only, newest first"},{"heading":"tools","content":"Admin"},{"heading":"tools","content":"`admin_users_list`"},{"heading":"tools","content":"`admin:read`"},{"heading":"tools","content":"ID, name, email and role for users across the app"},{"heading":"tools","content":"Admin"},{"heading":"tools","content":"`admin_workspaces_list`"},{"heading":"tools","content":"`admin:read`"},{"heading":"tools","content":"ID, name and slug for workspaces across the app"},{"heading":"tools","content":"List tools accept a `limit` between 1 and 50, defaulting to 20. Every tool is annotated read-only, non-destructive and idempotent. There are no write tools in this kit; `projects_create` exists only in the Convex kits."},{"heading":"tools","content":"Each call reloads the live actor from D1: the session must still exist and be unexpired, the user must be verified and not banned, and the session must not be an impersonation session. `projects_list` additionally re-checks the caller's membership and `projects.read` permission in the approved workspace. The admin tools re-check `user.role = 'admin'` on every request and refuse to run unless `ADMIN_APP` is `true`, so removing the role ends access immediately."},{"heading":"consent-and-workspace-selection","content":"The consent page lists only workspaces where the account has `projects.read`. If there are none, the page says to create or join a workspace first and offers no approve button. Administrators who connect to the User MCP get a normal user connection; their token doesn't work at the admin endpoint, which is on a different origin anyway."},{"heading":"consent-and-workspace-selection","content":"Consent and disconnect forms require same-origin submission and a session-bound CSRF value. The consent page disallows framing. Client names are self-reported by the client and are marked unverified; check the callback address and the server shown on the page."},{"heading":"consent-and-workspace-selection","content":"Each account can have at most 20 active connections. Disconnect from the settings page when you no longer need one."},{"heading":"oauth-details-for-this-kit","content":"Discovery, per origin: `/.well-known/oauth-protected-resource/mcp/user` on the web origin, `/.well-known/oauth-protected-resource/mcp/admin` on the admin origin, and `/.well-known/oauth-authorization-server/oauth` on both. The issuer is `<origin>/oauth` of the Worker you connect to."},{"heading":"oauth-details-for-this-kit","content":"Endpoints on each origin: `/oauth/register`, `/oauth/authorize`, `/oauth/token`, `/oauth/revoke`. Consent is at `/mcp/authorize`."},{"heading":"oauth-details-for-this-kit","content":"Public clients with dynamic registration, authorization code, mandatory PKCE S256, exact callback and resource matching. Confidential clients and client ID metadata documents are not supported."},{"heading":"oauth-details-for-this-kit","content":"Callback URLs must be HTTPS, except HTTP loopback on `localhost`, `127.0.0.1` and `::1`."},{"heading":"oauth-details-for-this-kit","content":"Lifetimes: pending consent 10 minutes, authorization codes 5 minutes, access tokens 15 minutes, refresh up to 30 days but never longer than the native session that approved the connection."},{"heading":"oauth-details-for-this-kit","content":"Refresh tokens rotate. Reusing an old one revokes the whole connection family."},{"heading":"oauth-details-for-this-kit","content":"Browser origins allowed: that Worker's `NEXT_PUBLIC_SITE_URL`, `https://claude.ai` and `https://chatgpt.com`. Clients without an `Origin` header are accepted."},{"heading":"oauth-details-for-this-kit","content":"If a user isn't signed in when consent opens, they are sent to `/login?next=/mcp/authorize?request_id=...` on the same origin and returned to the same request after signing in. Both login pages do a full-document navigation for `/mcp/` destinations so the consent page loads outside the app shell."},{"heading":"where-the-code-lives","content":"<File name=\"core.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"adapter.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"d1-store.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"core.test.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"adapter.integration.test.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"route.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"route.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"oauth-protected-resource/mcp/[audience]/route.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"oauth-authorization-server/oauth/route.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"route.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"route.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"oauth-protected-resource/mcp/[audience]/route.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"oauth-authorization-server/oauth/route.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"0010_famous_groot.sql\" />"},{"heading":"where-the-code-lives","content":"<File name=\"mcp-connections.md\" />"},{"heading":"where-the-code-lives","content":"`core.ts` is the provider-agnostic protocol: OAuth endpoints, consent HTML, token handling, rate limits, the connection pages and tool registration with the official `@modelcontextprotocol/sdk` (Streamable HTTP, stateless JSON responses)."},{"heading":"where-the-code-lives","content":"`adapter.ts` plugs in this kit: it resolves the native Better Auth session, loads the live actor and workspaces from D1 with the shared `can()` permission check, picks the audience from `ADMIN_APP`, and implements the four tool queries."},{"heading":"where-the-code-lives","content":"`d1-store.ts` stores registrations, pending consents and credentials in the `mcp_records` table using insert-or-nothing and compare-and-set updates, so concurrent consent, code and refresh transitions can't be duplicated."},{"heading":"where-the-code-lives","content":"The route files in both apps export the same `handleMcp` handler for `GET`, `POST`, `DELETE` and `OPTIONS`. The admin app has no copy of the MCP code; its `tsconfig.json` maps `@/*` to `./src/*` and then `../web/src/*`, so it imports `apps/web/src/mcp/adapter.ts` directly."},{"heading":"adding-a-tool-safely","content":"Add the data access in `adapter.ts` and keep the permission check inside it. Load the actor with `liveActor` and check the workspace membership or admin role in the query itself, as `projects` and `adminUsers` do. Admin queries should also require `env.ADMIN_APP === \"true\"`."},{"heading":"adding-a-tool-safely","content":"Register the tool in `core.ts` inside the correct audience branch. User tokens never see admin tools and vice versa."},{"heading":"adding-a-tool-safely","content":"Treat tool arguments as untrusted. A `workspaceId` in the input is not proof of access; use `auth.grant.workspace` for user tools."},{"heading":"adding-a-tool-safely","content":"If the tool writes data, don't attach it to `user:read` or `admin:read`. Introduce an explicit write scope, add consent wording for it and keep the read-only annotations accurate."},{"heading":"adding-a-tool-safely","content":"Add cases to `core.test.ts` and `adapter.integration.test.ts`. The integration test creates isolated D1 databases with Miniflare and exercises real Better Auth sign-up and login."},{"heading":"adding-a-tool-safely","content":"Run `pnpm verify:local` before deploying, and apply any new migration with `pnpm db:migrate:production` before shipping the Workers."}],"headings":[{"id":"endpoints","content":"Endpoints"},{"id":"tools","content":"Tools"},{"id":"consent-and-workspace-selection","content":"Consent and workspace selection"},{"id":"oauth-details-for-this-kit","content":"OAuth details for this kit"},{"id":"where-the-code-lives","content":"Where the code lives"},{"id":"adding-a-tool-safely","content":"Adding a tool safely"}]},"url":"/docs/v5/starters/nextjs-d1/agent-connections","title":"Agent connections","description":"The User and Admin MCP servers in the Next.js + Cloudflare D1 starter: endpoints, tools, OAuth behaviour and where the code lives."},{"_id":"/docs/v5/starters/nextjs-d1/authentication","structured":{"contents":[{"content":"This kit uses Better Auth with the Drizzle adapter on top of the same D1 database that holds your workspaces and projects. There is no separate auth service to run. The shared authentication page explains the concepts that apply to all kits; this page is about where things live in this repository."},{"heading":"where-the-configuration-lives","content":"What"},{"heading":"where-the-configuration-lives","content":"Path"},{"heading":"where-the-configuration-lives","content":"Better Auth server instance"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/auth/server.ts` (`createAuth(env)`), reused by the admin app through its `@/*` path alias"},{"heading":"where-the-configuration-lives","content":"Browser client"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/auth/client.ts`"},{"heading":"where-the-configuration-lives","content":"Social provider wiring"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/auth/social-providers.ts`"},{"heading":"where-the-configuration-lives","content":"Auth tables (generated Drizzle schema)"},{"heading":"where-the-configuration-lives","content":"`apps/database/src/schema/auth.generated.ts`, regenerate with `pnpm auth:generate`"},{"heading":"where-the-configuration-lives","content":"Route handlers"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/app/api/auth/[...all]/route.ts` and `apps/admin/src/app/api/auth/[...all]/route.ts`"},{"heading":"where-the-configuration-lives","content":"Web login, sign-up and recovery pages"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/app/(dynamic-pages)/(login-pages)/` and `(misc-login-pages)/`"},{"heading":"where-the-configuration-lives","content":"Admin login and recovery pages"},{"heading":"where-the-configuration-lives","content":"`apps/admin/src/app/login/`, `forgot-password/`, `update-password/`, `two-factor/`"},{"heading":"where-the-configuration-lives","content":"Web middleware chain"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/middleware.ts` and `apps/web/src/middlewares/`"},{"heading":"where-the-configuration-lives","content":"Admin middleware"},{"heading":"where-the-configuration-lives","content":"`apps/admin/src/middleware.ts`"},{"heading":"where-the-configuration-lives","content":"`createAuth` is built per request from the Worker `env`, so it reads `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_SITE_URL`, `ADMIN_EMAIL` and the provider credentials from the bindings described in environment variables. Each Worker's own `NEXT_PUBLIC_SITE_URL` is its only trusted origin, and cookies are marked secure when that URL starts with `https://`. When `ADMIN_APP` is `true`, the cookie prefix changes from `better-auth` to `nextbase-admin`, so the web and admin sessions never share a cookie even on the same parent domain."},{"heading":"email-password-and-verification","content":"Email and password sign-up is enabled and `requireEmailVerification` is on. A verification email is sent on sign-up and on sign-in attempts by unverified users, and the user is signed in automatically after clicking the link. Password resets revoke all sessions and clear trusted devices."},{"heading":"email-password-and-verification","content":"Auth emails are written to the D1 transactional outbox first. When `USE_LOCAL_EMAIL` is `true` and `MAILPIT_API_URL` is set, the Worker also posts the message to Mailpit and marks the outbox row complete. Otherwise the platform Worker's scheduled dispatcher delivers it through Resend. See installation for starting Mailpit, and emails for production delivery."},{"heading":"email-password-and-verification","content":"The local seed creates already-verified accounts so you can skip the inbox entirely during development."},{"heading":"magic-links-two-factor-and-api-keys","content":"**Magic links** are enabled through the `magicLink` plugin with a 15-minute expiry. Sign-up through a magic link is allowed on the web app; the admin login page also offers a magic-link form."},{"heading":"magic-links-two-factor-and-api-keys","content":"**Two-factor** uses the `twoFactor` plugin with TOTP and email OTP. Users manage it under `/user/settings/security`; the challenge page is `/two-factor` on both apps."},{"heading":"magic-links-two-factor-and-api-keys","content":"**API keys** use `@better-auth/api-key` with the `st_` prefix, a required name, metadata, and a rate limit of 120 requests per minute. Keys are accepted as `Bearer` tokens or in the `x-api-key` header and are hashed in the database. Users create them under `/user/settings/developer` on the web app."},{"heading":"social-providers","content":"Google, GitHub and X are supported. A provider appears on a login page only when both of its credentials are present in that Worker:"},{"heading":"social-providers","content":"Provider"},{"heading":"social-providers","content":"Variables"},{"heading":"social-providers","content":"Google"},{"heading":"social-providers","content":"`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`"},{"heading":"social-providers","content":"GitHub"},{"heading":"social-providers","content":"`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`"},{"heading":"social-providers","content":"X"},{"heading":"social-providers","content":"`TWITTER_API_KEY`, `TWITTER_API_SECRET`"},{"heading":"social-providers","content":"Register the callback URL as `<origin>/api/auth/callback/<provider>` in each provider's console, where `<provider>` is `google`, `github` or `twitter`. Register the web origin, and the admin origin too if administrators should sign in with a provider there. Put the credentials in `apps/web/.dev.vars` and `apps/admin/.dev.vars` locally and in each Worker's secrets in production."},{"heading":"sessions","content":"Sessions live in the D1 `session` table and expire after 30 days, refreshed once a day. A short cookie cache (60 seconds, JWE) keeps page loads fast without a database read on every request. Users can revoke sessions from their security settings."},{"heading":"sessions","content":"Better Auth's built-in rate limiting is on (100 requests per 60 seconds), and the kit adds its own `AUTH_RATE_LIMITER` Workers rate-limit binding (10 requests per 60 seconds per key) in a `before` hook. If the limiter binding fails, auth returns `503` rather than running unprotected."},{"heading":"becoming-an-application-admin","content":"Admin status is the `role` column on the Better Auth `user` table, managed by the `admin` plugin with `adminRoles: [\"admin\"]`. Two trusted paths assign it:"},{"heading":"becoming-an-application-admin","content":"**`ADMIN_EMAIL`**: a `databaseHooks.user.create.before` hook sets `role` to `admin` when the new account's email matches `ADMIN_EMAIL` (case-insensitive). Set the same address in the `vars` block of `apps/web/wrangler.jsonc` and `apps/admin/wrangler.jsonc` before the first sign-up with that address."},{"heading":"becoming-an-application-admin","content":"**Local seed**: `pnpm db:seed` inserts `admin@example.com` with the admin role."},{"heading":"becoming-an-application-admin","content":"The repository doesn't ship a UI or script to promote an existing account afterwards. Change the `role` column through a trusted server-side path, for example `wrangler d1 execute`, rather than from the app."},{"heading":"the-admin-app","content":"Platform administration lives in `apps/admin`, a separate Next.js Worker on its own origin: `http://localhost:3001` locally and `https://nextjs-d1-admin.usenextbase.com` for the demo. The customer web app no longer has `/app-admin` routes or admin navigation. On the admin origin:"},{"heading":"the-admin-app","content":"`/login` signs the administrator in with the shared Better Auth tables but an independent `BETTER_AUTH_SECRET` and the `nextbase-admin` cookie prefix."},{"heading":"the-admin-app","content":"`apps/admin/src/middleware.ts` redirects anonymous visitors to `/login?next=/app-admin`, returns `403` for accounts without the `admin` role and for impersonation sessions, and marks every page `no-store` and `noindex`."},{"heading":"the-admin-app","content":"The `/app-admin/(admin-pages)` layout re-checks the role with `serverGetUserType()` and renders `unauthorized()` otherwise."},{"heading":"the-admin-app","content":"Users are at `/app-admin/users`, workspaces at `/app-admin/workspaces`, billing at `/app-admin/payment-gateway`, marketing content under `/app-admin/marketing/...`, settings at `/app-admin/settings`, and feedback and roadmap moderation at `/feedback` and `/roadmap`."},{"heading":"the-admin-app","content":"**Impersonation** crosses the two origins. From `/app-admin/users` the admin app writes an audit row, asks Better Auth for a one-minute single-use token and redirects to `<NEXT_PUBLIC_WEB_APP_URL>/auth/impersonate?token=...`. The web route verifies the token, re-checks that the impersonating account is still a verified, unbanned admin and that the target is not an admin, and only then sets the impersonation session (up to one hour). The web impersonation banner links back to `<NEXT_PUBLIC_ADMIN_APP_URL>/app-admin/users`. Impersonation sessions are rejected by the MCP endpoints and by the admin middleware. The `/admin/remove-user` Better Auth path is disabled so account deletion goes through the application's token flow, which also cleans up R2 media."},{"heading":"what-is-protected-and-how","content":"**Middleware is optimistic.** `apps/web/src/middleware.ts` runs guest, auth and onboarding checks for the matched route; the admin middleware gates the admin origin. In both apps, layouts, server actions and repositories perform the authoritative checks again."},{"heading":"what-is-protected-and-how","content":"**Every tenant operation takes an `Actor`.** The `Actor` type in `apps/database/src/authorization.ts` is `anonymous`, `user` (with optional `isAppAdmin`) or `service` (`platform-worker` or `migration`). Repositories check a permission such as `projects.read` or `members.manage` against the member's workspace role (`owner`, `admin`, `member`, `readonly`) plus any per-member overrides stored in `workspace_members.permissions`."},{"heading":"what-is-protected-and-how","content":"**No raw bindings in UI code.** Request code obtains `APP_DB` through `getCloudflareContext`; browser clients never receive a database binding. The `apps/web/src/d1-clients/` and `apps/web/src/data/` folders are the server-only data layer; the admin app adds `apps/admin/src/data/admin/`."},{"heading":"what-is-protected-and-how","content":"**No interactive transactions.** D1 doesn't support them, so multi-statement workflows use `D1Database.batch()` or conditional compare-and-set updates. Side effects go through the transactional outbox and are delivered idempotently by the platform Worker."},{"heading":"what-is-protected-and-how","content":"**Companion rows are reconciled.** Better Auth's user insert and the application's profile rows can't share one transaction on D1, so an idempotent reconciliation runs after user creation, user updates and session reads until it succeeds."},{"heading":"what-is-protected-and-how","content":"For the agent side of authentication (consent, token lifetimes, what ends access), see agent connections."}],"headings":[{"id":"where-the-configuration-lives","content":"Where the configuration lives"},{"id":"email-password-and-verification","content":"Email, password and verification"},{"id":"magic-links-two-factor-and-api-keys","content":"Magic links, two-factor and API keys"},{"id":"social-providers","content":"Social providers"},{"id":"sessions","content":"Sessions"},{"id":"becoming-an-application-admin","content":"Becoming an application admin"},{"id":"the-admin-app","content":"The admin app"},{"id":"what-is-protected-and-how","content":"What is protected and how"}]},"url":"/docs/v5/starters/nextjs-d1/authentication","title":"Authentication","description":"How Better Auth is configured in the Next.js + Cloudflare D1 starter, how admins are assigned, where the admin app lives, and what protects application data."},{"_id":"/docs/v5/starters/nextjs-d1/deployment","structured":{"contents":[{"content":"The kit deploys as three Cloudflare Workers: the OpenNext web Worker, the OpenNext admin Worker and the platform Worker. All three need the same application D1 database, and the two Next.js Workers also need the OpenNext cache resources. The shared Cloudflare deployment guide has the platform-generic steps; this page lists what is specific to this kit."},{"content":"The live demo runs on Workers Paid. During testing, the Better Auth password work exceeded the Free plan's CPU limit and produced intermittent errors. Check your plan if login fails only sometimes after deploying. See troubleshooting."},{"heading":"1-create-the-cloudflare-resources","content":"Create these in your account, using the names you want (the checked-in names are the local defaults):"},{"heading":"1-create-the-cloudflare-resources","content":"Resource"},{"heading":"1-create-the-cloudflare-resources","content":"Default name"},{"heading":"1-create-the-cloudflare-resources","content":"Used by"},{"heading":"1-create-the-cloudflare-resources","content":"D1 database (application)"},{"heading":"1-create-the-cloudflare-resources","content":"`nextbase-app`"},{"heading":"1-create-the-cloudflare-resources","content":"all three Workers, binding `APP_DB`"},{"heading":"1-create-the-cloudflare-resources","content":"D1 database (OpenNext tag cache)"},{"heading":"1-create-the-cloudflare-resources","content":"`nextbase-next-cache`"},{"heading":"1-create-the-cloudflare-resources","content":"web and admin, binding `NEXT_TAG_CACHE_D1`"},{"heading":"1-create-the-cloudflare-resources","content":"R2 bucket (media)"},{"heading":"1-create-the-cloudflare-resources","content":"`nextbase-media`"},{"heading":"1-create-the-cloudflare-resources","content":"all three Workers, binding `MEDIA_BUCKET`"},{"heading":"1-create-the-cloudflare-resources","content":"R2 bucket (OpenNext incremental cache)"},{"heading":"1-create-the-cloudflare-resources","content":"`nextbase-next-cache`"},{"heading":"1-create-the-cloudflare-resources","content":"web and admin, binding `NEXT_INC_CACHE_R2_BUCKET`"},{"heading":"1-create-the-cloudflare-resources","content":"Queue"},{"heading":"1-create-the-cloudflare-resources","content":"`nextbase-jobs`"},{"heading":"1-create-the-cloudflare-resources","content":"platform producer and consumer"},{"heading":"1-create-the-cloudflare-resources","content":"Queue (dead letter)"},{"heading":"1-create-the-cloudflare-resources","content":"`nextbase-jobs-dlq`"},{"heading":"1-create-the-cloudflare-resources","content":"platform consumer"},{"heading":"1-create-the-cloudflare-resources","content":"Queue (terminal)"},{"heading":"1-create-the-cloudflare-resources","content":"`nextbase-jobs-dlq-persistence-failures`"},{"heading":"1-create-the-cloudflare-resources","content":"no consumer; see the README runbook"},{"heading":"1-create-the-cloudflare-resources","content":"The queue commands from the repository are:"},{"heading":"1-create-the-cloudflare-resources","content":"Durable Objects (`DOQueueHandler`, `RealtimeChannel`) and the two rate limiters are created from the `wrangler.jsonc` declarations on first deploy. The repository doesn't include commands for creating the D1 databases or R2 buckets; use `wrangler d1 create` and `wrangler r2 bucket create`, or the dashboard."},{"heading":"2-edit-the-production-environment-in-the-three-wrangler-files","content":"The top-level D1 IDs (`00000000-...`) are placeholders for local work. Each `wrangler.jsonc` (`apps/web`, `apps/admin`, `apps/platform-worker`) also has an `env.production` block that repeats every binding with the demo's real resource IDs, names and `account_id`. Replace those values with your own; keep the binding names unchanged. The two Next.js production blocks also:"},{"heading":"2-edit-the-production-environment-in-the-three-wrangler-files","content":"give each Worker a distinct name (`nextbase-cloudflare-web-production`, `nextbase-cloudflare-admin-production`) and point `WORKER_SELF_REFERENCE` at that name,"},{"heading":"2-edit-the-production-environment-in-the-three-wrangler-files","content":"set `BETTER_AUTH_URL` and `NEXT_PUBLIC_SITE_URL` to that Worker's public origin (the web origin on web, the admin origin on admin),"},{"heading":"2-edit-the-production-environment-in-the-three-wrangler-files","content":"cross-reference the other app: `NEXT_PUBLIC_ADMIN_APP_URL` on web, `NEXT_PUBLIC_WEB_APP_URL` and `ADMIN_APP: \"true\"` on admin,"},{"heading":"2-edit-the-production-environment-in-the-three-wrangler-files","content":"set `ADMIN_EMAIL` to the address that should become the first admin,"},{"heading":"2-edit-the-production-environment-in-the-three-wrangler-files","content":"set `USE_LOCAL_EMAIL` to `\"false\"` and `MAILPIT_API_URL` to `\"\"`."},{"heading":"2-edit-the-production-environment-in-the-three-wrangler-files","content":"The platform Worker's production block mirrors its bindings and vars, and adds `MAIL_FROM` with a verified sender such as `Your App <no-reply@your-domain>`."},{"heading":"2-edit-the-production-environment-in-the-three-wrangler-files","content":"Run `pnpm cf:typegen:check` afterwards so the generated binding types still match."},{"heading":"3-set-the-worker-secrets","content":"From each app directory, add the secrets for the production environment:"},{"heading":"3-set-the-worker-secrets","content":"`REALTIME_TOKEN_SECRET` must be identical on all three Workers and at least 32 characters. `INTERNAL_SERVICE_SECRET` must be a separate value of at least 32 characters. The admin Worker should get its own `BETTER_AUTH_SECRET`; the admin uses a different cookie prefix, so nothing needs to be shared. The full list is in environment variables."},{"heading":"4-apply-the-migrations","content":"Both D1 databases need their migrations before the first request:"},{"heading":"4-apply-the-migrations","content":"This runs `wrangler d1 migrations apply --remote --env production` for `APP_DB` and `NEXT_TAG_CACHE_D1` using `apps/web/wrangler.jsonc`. The admin Worker shares those databases and owns no migrations. `pnpm db:migrate:remote` runs the same two commands without `--env production`, so it only works if your top-level bindings already point at the production databases. The application migrations include `0010_famous_groot.sql`, which creates `mcp_records`; both Next.js Workers need it before serving OAuth connections."},{"heading":"5-build-and-deploy","content":"Deploy the platform Worker first so the `PLATFORM` service binding exists when the Next.js Workers go live. The root script does that in order:"},{"heading":"5-build-and-deploy","content":"`cf:deploy:production` runs `wrangler deploy --env production` in `apps/platform-worker`, then `cf:deploy:production` in `apps/web`, which builds with `opennextjs-cloudflare build --env production` and deploys. `NEXT_PUBLIC_SITE_URL` has to be present in the shell because it is compiled into the browser bundle; the web build script stops with `Set NEXT_PUBLIC_SITE_URL to the public origin of this deployment` when it is missing, and no default is baked in. A build made with the default environment contains the placeholder D1 IDs and the localhost URL, so always rebuild with `--env production`."},{"heading":"5-build-and-deploy","content":"Then deploy the admin Worker:"},{"heading":"5-build-and-deploy","content":"The admin build script in `apps/admin/package.json` sets `NEXT_PUBLIC_SITE_URL` and `NEXT_PUBLIC_WEB_APP_URL` inline, and the checked-in values are the demo's `https://nextjs-d1-admin.usenextbase.com` and `https://nextjs-d1.usenextbase.com`. Edit that `cf:build:production` script to your own admin and web origins before running it."},{"heading":"5-build-and-deploy","content":"`pnpm cf:deploy` and `pnpm cf:deploy:admin` are the same flows without `--env production`; use them only when your top-level configuration is the production one. `pnpm cf:build` builds all three Workers without deploying."},{"heading":"5-build-and-deploy","content":"**GitHub Actions.** `.github/workflows/deploy.yml` is a manual `workflow_dispatch` workflow with an `app` input (`web` or `admin`). It installs, runs typecheck, tests and lint, then runs `pnpm --filter <app> cf:deploy:production` with `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` from repository secrets and `NEXT_PUBLIC_SITE_URL` set in the workflow file to the demo URL. Change that value to your web origin. It does not deploy the platform Worker; run that from your machine or add a step."},{"heading":"6-configure-the-external-services","content":"**Auth callbacks**: register `https://your-domain/api/auth/callback/google`, `/github` and `/twitter` for the providers you enabled, and the same paths on the admin origin if administrators should use a provider there. Each Worker trusts only its own `NEXT_PUBLIC_SITE_URL`, so the public origins and the Worker vars must match exactly."},{"heading":"6-configure-the-external-services","content":"**Stripe**: point the webhook endpoint at `https://your-domain/api/stripe/webhooks` on the web Worker and store its signing secret in `STRIPE_WEBHOOK_SECRET`. The admin Worker reads billing records from D1 and queues product synchronization to the platform Worker, which keeps the Stripe secret."},{"heading":"6-configure-the-external-services","content":"**Email**: set `RESEND_API_KEY` on the platform Worker and `MAIL_FROM` to a sender on a verified Resend domain. Auth emails are queued in the D1 outbox and dispatched by the platform Worker's cron every minute. See emails."},{"heading":"6-configure-the-external-services","content":"**Custom domains**: the demo serves `https://nextjs-d1.usenextbase.com` and `https://nextjs-d1-admin.usenextbase.com` through two small Cloudflare Pages projects, `infrastructure/demo-domain` and `infra/admin-domain`, that forward to the production Workers with an `APP` service binding. Deploy the web proxy with the command in `infrastructure/demo-domain/README.md` and the admin proxy with `pnpm --filter admin cf:deploy:domain`, then point each DNS CNAME at the Pages hostname (`nextjs-d1-starter-domain.pages.dev` and `nextjs-d1-admin-domain.pages.dev` for the demo). Workers custom domains on the Workers themselves work as well; whichever you pick, keep each Worker's `BETTER_AUTH_URL` and `NEXT_PUBLIC_SITE_URL` equal to its public address."},{"heading":"6-configure-the-external-services","content":"**Agent connections**: no extra variable. The User MCP is `https://your-domain/mcp/user` on the web Worker and the Admin MCP is `https://your-admin-domain/mcp/admin` on the admin Worker, each derived from that Worker's `NEXT_PUBLIC_SITE_URL`."},{"heading":"7-smoke-checks","content":"Open the web site, sign up with the `ADMIN_EMAIL` address and open the verification email. Then open the admin origin, sign in with that account and confirm you land in `/app-admin`."},{"heading":"7-smoke-checks","content":"Sign up with a second address on the web site, verify, create a workspace and a project."},{"heading":"7-smoke-checks","content":"Request `https://your-domain/mcp/user` and `https://your-admin-domain/mcp/admin` without a token and confirm a `401` with a `WWW-Authenticate` header, then check `/.well-known/oauth-authorization-server/oauth` returns JSON on both origins."},{"heading":"7-smoke-checks","content":"Connect an agent to `/mcp/user` on the web origin, approve one workspace and list projects. Try `/mcp/admin` on the admin origin with the admin account."},{"heading":"7-smoke-checks","content":"From `/app-admin/users`, impersonate the second account and confirm you are redirected to the web origin with the impersonation banner, and that the banner's link returns you to the admin app."},{"heading":"7-smoke-checks","content":"Check `https://<platform-worker>.workers.dev/health` and watch the Worker logs for `outbox` or CPU-limit errors during the first few sign-ups."},{"heading":"7-smoke-checks","content":"If you use Stripe, trigger a test checkout and confirm the webhook projection lands in `/app-admin/payment-gateway` on the admin origin."},{"heading":"7-smoke-checks","content":"If something fails, troubleshooting covers the common D1, login and MCP cases."}],"headings":[{"id":"1-create-the-cloudflare-resources","content":"1\\. Create the Cloudflare resources"},{"id":"2-edit-the-production-environment-in-the-three-wrangler-files","content":"2\\. Edit the production environment in the three Wrangler files"},{"id":"3-set-the-worker-secrets","content":"3\\. Set the Worker secrets"},{"id":"4-apply-the-migrations","content":"4\\. Apply the migrations"},{"id":"5-build-and-deploy","content":"5\\. Build and deploy"},{"id":"6-configure-the-external-services","content":"6\\. Configure the external services"},{"id":"7-smoke-checks","content":"7\\. Smoke checks"}]},"url":"/docs/v5/starters/nextjs-d1/deployment","title":"Deployment","description":"Provision Cloudflare resources, configure production bindings and secrets, migrate D1 and deploy the web, admin and platform Workers of the Next.js + Cloudflare D1 starter."},{"_id":"/docs/v5/starters/nextjs-d1/environment-variables","structured":{"contents":[{"content":"This kit splits configuration in three places:"},{"content":"`apps/web/.dev.vars`, `apps/admin/.dev.vars` and `apps/platform-worker/.dev.vars` hold Worker secrets for local development. In production the same names become Wrangler secrets. All three files are gitignored."},{"content":"The `vars` block in each `wrangler.jsonc` holds non-secret defaults that are safe to commit."},{"content":"`apps/web/.env.local` holds values the Next.js toolchain reads outside the Worker, such as `NEXT_PUBLIC_*` build variables and the Sentry upload settings. It repeats some of the Worker values so plain `next dev` tooling has them too. `setup.sh` creates it from the example when it is missing."},{"content":"Never put a real key in a `vars` block or in the example files. Run `pnpm cf:typegen` after adding or renaming a secret in a `.dev.vars.example` file; CI checks the generated `worker-configuration.d.ts` with `pnpm cf:typegen:check`."},{"heading":"appswebdevvars","content":"Secrets for the web Worker. Copy from `apps/web/.dev.vars.example`."},{"heading":"appswebdevvars","content":"Variable"},{"heading":"appswebdevvars","content":"Required"},{"heading":"appswebdevvars","content":"Purpose"},{"heading":"appswebdevvars","content":"Where to get it"},{"heading":"appswebdevvars","content":"`BETTER_AUTH_SECRET`"},{"heading":"appswebdevvars","content":"Required"},{"heading":"appswebdevvars","content":"Signs Better Auth sessions and tokens"},{"heading":"appswebdevvars","content":"Generate a random string of at least 32 characters"},{"heading":"appswebdevvars","content":"`STRIPE_SECRET_KEY`"},{"heading":"appswebdevvars","content":"Required for billing"},{"heading":"appswebdevvars","content":"Creates checkout and portal sessions"},{"heading":"appswebdevvars","content":"Stripe dashboard, API keys"},{"heading":"appswebdevvars","content":"`STRIPE_WEBHOOK_SECRET`"},{"heading":"appswebdevvars","content":"Required for billing"},{"heading":"appswebdevvars","content":"Verifies incoming webhook signatures"},{"heading":"appswebdevvars","content":"`stripe listen` locally, or the webhook endpoint in Stripe"},{"heading":"appswebdevvars","content":"`REALTIME_TOKEN_SECRET`"},{"heading":"appswebdevvars","content":"Required"},{"heading":"appswebdevvars","content":"Signs channel-scoped realtime tokens; must match the admin and platform Workers exactly"},{"heading":"appswebdevvars","content":"Generate at least 32 random characters"},{"heading":"appswebdevvars","content":"`RESEND_API_KEY`"},{"heading":"appswebdevvars","content":"Optional"},{"heading":"appswebdevvars","content":"Sends email when `USE_LOCAL_EMAIL` is not `true`"},{"heading":"appswebdevvars","content":"Resend dashboard"},{"heading":"appswebdevvars","content":"`OPENAI_API_KEY`"},{"heading":"appswebdevvars","content":"Optional"},{"heading":"appswebdevvars","content":"Image generation and AI helpers"},{"heading":"appswebdevvars","content":"OpenAI dashboard"},{"heading":"appswebdevvars","content":"`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`"},{"heading":"appswebdevvars","content":"Optional"},{"heading":"appswebdevvars","content":"Enables Google sign-in when both are set"},{"heading":"appswebdevvars","content":"Google Cloud OAuth client"},{"heading":"appswebdevvars","content":"`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`"},{"heading":"appswebdevvars","content":"Optional"},{"heading":"appswebdevvars","content":"Enables GitHub sign-in when both are set"},{"heading":"appswebdevvars","content":"GitHub OAuth app"},{"heading":"appswebdevvars","content":"`TWITTER_API_KEY`, `TWITTER_API_SECRET`"},{"heading":"appswebdevvars","content":"Optional"},{"heading":"appswebdevvars","content":"Enables X sign-in when both are set"},{"heading":"appswebdevvars","content":"X developer portal"},{"heading":"appswebdevvars","content":"`REVALIDATE_SECRET`"},{"heading":"appswebdevvars","content":"Required"},{"heading":"appswebdevvars","content":"Protects the `/api/revalidate` route"},{"heading":"appswebdevvars","content":"Generate a random string"},{"heading":"appswebdevvars","content":"All of these are server-only. Values shorter than 32 characters for `REALTIME_TOKEN_SECRET` are rejected at runtime."},{"heading":"appsadmindevvars","content":"Secrets and local overrides for the admin Worker. Copy from `apps/admin/.dev.vars.example`. The first block repeats the web Worker's secret names; the admin app runs the same Better Auth, Stripe, email and provider code against the same D1 database."},{"heading":"appsadmindevvars","content":"Variable"},{"heading":"appsadmindevvars","content":"Required"},{"heading":"appsadmindevvars","content":"Purpose"},{"heading":"appsadmindevvars","content":"Where to get it"},{"heading":"appsadmindevvars","content":"`BETTER_AUTH_SECRET`"},{"heading":"appsadmindevvars","content":"Required"},{"heading":"appsadmindevvars","content":"Signs the admin app's own sessions; use a value independent from the web Worker"},{"heading":"appsadmindevvars","content":"Generate a random string of at least 32 characters"},{"heading":"appsadmindevvars","content":"`STRIPE_SECRET_KEY`"},{"heading":"appsadmindevvars","content":"Required for billing"},{"heading":"appsadmindevvars","content":"Reads billing data for the payment gateway pages"},{"heading":"appsadmindevvars","content":"Same key as the web Worker"},{"heading":"appsadmindevvars","content":"`STRIPE_WEBHOOK_SECRET`"},{"heading":"appsadmindevvars","content":"Required for billing"},{"heading":"appsadmindevvars","content":"Present for the shared Stripe module"},{"heading":"appsadmindevvars","content":"Same value as the web Worker"},{"heading":"appsadmindevvars","content":"`REALTIME_TOKEN_SECRET`"},{"heading":"appsadmindevvars","content":"Required"},{"heading":"appsadmindevvars","content":"Same value as the web and platform Workers"},{"heading":"appsadmindevvars","content":"Copy from `apps/web/.dev.vars`"},{"heading":"appsadmindevvars","content":"`RESEND_API_KEY`"},{"heading":"appsadmindevvars","content":"Optional"},{"heading":"appsadmindevvars","content":"Sends email when `USE_LOCAL_EMAIL` is not `true`"},{"heading":"appsadmindevvars","content":"Resend dashboard"},{"heading":"appsadmindevvars","content":"`OPENAI_API_KEY`"},{"heading":"appsadmindevvars","content":"Optional"},{"heading":"appsadmindevvars","content":"AI helpers in the admin content tools"},{"heading":"appsadmindevvars","content":"OpenAI dashboard"},{"heading":"appsadmindevvars","content":"`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`"},{"heading":"appsadmindevvars","content":"Optional"},{"heading":"appsadmindevvars","content":"Google sign-in on the admin login page"},{"heading":"appsadmindevvars","content":"Google Cloud OAuth client"},{"heading":"appsadmindevvars","content":"`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`"},{"heading":"appsadmindevvars","content":"Optional"},{"heading":"appsadmindevvars","content":"GitHub sign-in on the admin login page"},{"heading":"appsadmindevvars","content":"GitHub OAuth app"},{"heading":"appsadmindevvars","content":"`TWITTER_API_KEY`, `TWITTER_API_SECRET`"},{"heading":"appsadmindevvars","content":"Optional"},{"heading":"appsadmindevvars","content":"X sign-in on the admin login page"},{"heading":"appsadmindevvars","content":"X developer portal"},{"heading":"appsadmindevvars","content":"`REVALIDATE_SECRET`"},{"heading":"appsadmindevvars","content":"Required"},{"heading":"appsadmindevvars","content":"Shared revalidation secret"},{"heading":"appsadmindevvars","content":"Same value as the web Worker"},{"heading":"appsadmindevvars","content":"`ADMIN_APP`"},{"heading":"appsadmindevvars","content":"Required"},{"heading":"appsadmindevvars","content":"`true` marks this Worker as the admin app: admin cookie prefix, admin MCP audience, admin-only data paths"},{"heading":"appsadmindevvars","content":"Keep `true`"},{"heading":"appsadmindevvars","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"appsadmindevvars","content":"Required"},{"heading":"appsadmindevvars","content":"The admin app's own origin"},{"heading":"appsadmindevvars","content":"`http://localhost:3001` locally"},{"heading":"appsadmindevvars","content":"`BETTER_AUTH_URL`"},{"heading":"appsadmindevvars","content":"Required"},{"heading":"appsadmindevvars","content":"Better Auth base URL for the admin app"},{"heading":"appsadmindevvars","content":"`http://localhost:3001` locally"},{"heading":"appsadmindevvars","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"appsadmindevvars","content":"Required"},{"heading":"appsadmindevvars","content":"The customer web app's origin, used for the impersonation callback and media URLs"},{"heading":"appsadmindevvars","content":"`http://localhost:3000` locally"},{"heading":"appsadmindevvars","content":"The last four are also set in the `vars` block of `apps/admin/wrangler.jsonc`; the `.dev.vars` copies exist so `next dev` has them before the Wrangler bindings load."},{"heading":"appsplatform-workerdevvars","content":"Secrets for the background Worker. Copy from `apps/platform-worker/.dev.vars.example`."},{"heading":"appsplatform-workerdevvars","content":"Variable"},{"heading":"appsplatform-workerdevvars","content":"Required"},{"heading":"appsplatform-workerdevvars","content":"Purpose"},{"heading":"appsplatform-workerdevvars","content":"Where to get it"},{"heading":"appsplatform-workerdevvars","content":"`REALTIME_TOKEN_SECRET`"},{"heading":"appsplatform-workerdevvars","content":"Required"},{"heading":"appsplatform-workerdevvars","content":"Same value as the web and admin Workers"},{"heading":"appsplatform-workerdevvars","content":"Copy from `apps/web/.dev.vars`"},{"heading":"appsplatform-workerdevvars","content":"`INTERNAL_SERVICE_SECRET`"},{"heading":"appsplatform-workerdevvars","content":"Required"},{"heading":"appsplatform-workerdevvars","content":"Protects the internal and operator recovery routes; missing, short or wrong values get a `404`"},{"heading":"appsplatform-workerdevvars","content":"Generate an independent string of at least 32 characters"},{"heading":"appsplatform-workerdevvars","content":"`RESEND_API_KEY`"},{"heading":"appsplatform-workerdevvars","content":"Optional"},{"heading":"appsplatform-workerdevvars","content":"Delivers outbox emails in production"},{"heading":"appsplatform-workerdevvars","content":"Resend dashboard"},{"heading":"appsplatform-workerdevvars","content":"`STRIPE_SECRET_KEY`"},{"heading":"appsplatform-workerdevvars","content":"Required for billing"},{"heading":"appsplatform-workerdevvars","content":"Reconciles invoices and charges during webhook projection, and runs product synchronization queued by the admin app"},{"heading":"appsplatform-workerdevvars","content":"Same key as the web Worker"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"These are committed. Edit them in the `vars` block, and in the `env.production.vars` block for your deployed Workers."},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Variable"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Worker"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Local default"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Purpose"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`BETTER_AUTH_URL`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"web, admin"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`http://localhost:3000` (web), `http://localhost:3001` (admin)"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Better Auth base URL; set to each Worker's public origin in production"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"web, admin"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`http://localhost:3000` (web), `http://localhost:3001` (admin)"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Each Worker's own origin, trusted origin for auth and the MCP issuer; public"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`NEXT_PUBLIC_ADMIN_APP_URL`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"web"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`http://localhost:3001`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Where the impersonation banner sends admins back to (`/app-admin/users` on the admin origin)"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"admin"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`http://localhost:3000`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"The customer app's origin; impersonation redeems its token at `<web origin>/auth/impersonate`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`ADMIN_APP`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"admin"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`\"true\"`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Switches the shared code into admin mode"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`ADMIN_EMAIL`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"web, admin, platform"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`admin@example.com`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"The first account registered with this email gets the admin role; also the default sender address"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`USE_LOCAL_EMAIL`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"web, admin, platform"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`true`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"When `true`, auth emails are also posted to Mailpit; set to `false` in production"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`MAILPIT_API_URL`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"web, admin, platform"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`http://localhost:8025`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Mailpit API used for local delivery; empty in production"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`INTERNAL_SERVICE_PRINCIPAL`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"platform"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`nextbase-operator-api`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Non-secret identity recorded for operator actions"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`MAIL_FROM`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"platform"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"not set locally"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"Optional verified sender for production emails; falls back to the payload sender or `ADMIN_EMAIL`"},{"heading":"non-secret-vars-in-wranglerjsonc","content":"`NEXT_PUBLIC_SITE_URL` is also needed at build time because it ends up in the browser bundle. The web Worker's `cf:build:production` script refuses to run until it is set in the shell; the admin Worker's `cf:build:production` script sets its own `NEXT_PUBLIC_SITE_URL` and `NEXT_PUBLIC_WEB_APP_URL` inline in `apps/admin/package.json`. See deployment."},{"heading":"appswebenvlocal","content":"Copy from `apps/web/.env.local.example`. It repeats the Worker values above and adds the ones below. Variables starting with `NEXT_PUBLIC_` are exposed to the browser."},{"heading":"appswebenvlocal","content":"Variable"},{"heading":"appswebenvlocal","content":"Required"},{"heading":"appswebenvlocal","content":"Public"},{"heading":"appswebenvlocal","content":"Purpose"},{"heading":"appswebenvlocal","content":"Where to get it"},{"heading":"appswebenvlocal","content":"`NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`"},{"heading":"appswebenvlocal","content":"Required for billing"},{"heading":"appswebenvlocal","content":"Yes"},{"heading":"appswebenvlocal","content":"Stripe.js on the client"},{"heading":"appswebenvlocal","content":"Stripe dashboard"},{"heading":"appswebenvlocal","content":"`NEXT_PUBLIC_POSTHOG_API_KEY`"},{"heading":"appswebenvlocal","content":"Optional"},{"heading":"appswebenvlocal","content":"Yes"},{"heading":"appswebenvlocal","content":"PostHog analytics"},{"heading":"appswebenvlocal","content":"PostHog project settings"},{"heading":"appswebenvlocal","content":"`NEXT_PUBLIC_POSTHOG_HOST`"},{"heading":"appswebenvlocal","content":"Optional"},{"heading":"appswebenvlocal","content":"Yes"},{"heading":"appswebenvlocal","content":"PostHog ingestion host"},{"heading":"appswebenvlocal","content":"PostHog project settings"},{"heading":"appswebenvlocal","content":"`NEXT_PUBLIC_GA_ID`"},{"heading":"appswebenvlocal","content":"Optional"},{"heading":"appswebenvlocal","content":"Yes"},{"heading":"appswebenvlocal","content":"Google Analytics measurement ID"},{"heading":"appswebenvlocal","content":"Google Analytics"},{"heading":"appswebenvlocal","content":"`NEXT_PUBLIC_SENTRY_DSN`"},{"heading":"appswebenvlocal","content":"Optional"},{"heading":"appswebenvlocal","content":"Yes"},{"heading":"appswebenvlocal","content":"Sentry error reporting"},{"heading":"appswebenvlocal","content":"Sentry project settings"},{"heading":"appswebenvlocal","content":"`SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN`"},{"heading":"appswebenvlocal","content":"Optional"},{"heading":"appswebenvlocal","content":"No"},{"heading":"appswebenvlocal","content":"Source map upload during `next build`"},{"heading":"appswebenvlocal","content":"Sentry organization settings"},{"heading":"appswebenvlocal","content":"`LOG_LEVEL`"},{"heading":"appswebenvlocal","content":"Optional"},{"heading":"appswebenvlocal","content":"No"},{"heading":"appswebenvlocal","content":"Pino log level, default `info`"},{"heading":"appswebenvlocal","content":"Your choice"},{"heading":"appswebenvlocal","content":"The repeated entries (`NEXT_PUBLIC_SITE_URL`, `BETTER_AUTH_URL`, `BETTER_AUTH_SECRET`, `ADMIN_EMAIL`, `REALTIME_TOKEN_SECRET`, the Stripe keys, `USE_LOCAL_EMAIL`, `MAILPIT_API_URL`, `RESEND_API_KEY`, `OPENAI_API_KEY`, the social provider keys and `REVALIDATE_SECRET`) should match what you put in `.dev.vars` and `wrangler.jsonc`. At runtime inside the Worker, the binding values win. The admin app has no `.env.local.example`; its Sentry build settings come from the shell when you build it."},{"heading":"shell-variables-for-scripts","content":"Variable"},{"heading":"shell-variables-for-scripts","content":"Used by"},{"heading":"shell-variables-for-scripts","content":"Default"},{"heading":"shell-variables-for-scripts","content":"`MAILPIT_SMTP_PORT`, `MAILPIT_HTTP_PORT`"},{"heading":"shell-variables-for-scripts","content":"`docker-compose.yml`"},{"heading":"shell-variables-for-scripts","content":"`1025`, `8025`"},{"heading":"shell-variables-for-scripts","content":"`PLATFORM_WORKER_PORT`"},{"heading":"shell-variables-for-scripts","content":"`scripts/dev-platform-worker.sh`, `scripts/preview-cloudflare.sh`"},{"heading":"shell-variables-for-scripts","content":"`8788`"},{"heading":"shell-variables-for-scripts","content":"`PLATFORM_PERSIST_TO`"},{"heading":"shell-variables-for-scripts","content":"`scripts/dev-platform-worker.sh`"},{"heading":"shell-variables-for-scripts","content":"`apps/web/.wrangler/state`"},{"heading":"shell-variables-for-scripts","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"shell-variables-for-scripts","content":"`pnpm --filter web cf:build:production` and `cf:deploy:production`"},{"heading":"shell-variables-for-scripts","content":"none; the build fails fast when it is missing"},{"heading":"shell-variables-for-scripts","content":"`D1_HTTP_URL`"},{"heading":"shell-variables-for-scripts","content":"`apps/database/drizzle.config.ts`"},{"heading":"shell-variables-for-scripts","content":"the local SQLite file under `.wrangler/state/v3/d1`"},{"heading":"bindings-not-variables","content":"D1, R2, queues, Durable Objects, service bindings and rate limiters are declared in the three `wrangler.jsonc` files and arrive in `env` next to the variables above. The web and admin Workers each expect `APP_DB`, `NEXT_TAG_CACHE_D1`, `MEDIA_BUCKET`, `NEXT_INC_CACHE_R2_BUCKET`, `PLATFORM`, `WORKER_SELF_REFERENCE`, `NEXT_CACHE_DO_QUEUE`, `AUTH_RATE_LIMITER` and `API_RATE_LIMITER`. The platform Worker expects `APP_DB`, `MEDIA_BUCKET`, `JOBS_QUEUE` and `REALTIME`. Keep those names; change only the resource names and IDs behind them."}],"headings":[{"id":"appswebdevvars","content":"apps/web/.dev.vars"},{"id":"appsadmindevvars","content":"apps/admin/.dev.vars"},{"id":"appsplatform-workerdevvars","content":"apps/platform-worker/.dev.vars"},{"id":"non-secret-vars-in-wranglerjsonc","content":"Non-secret vars in wrangler.jsonc"},{"id":"appswebenvlocal","content":"apps/web/.env.local"},{"id":"shell-variables-for-scripts","content":"Shell variables for scripts"},{"id":"bindings-not-variables","content":"Bindings, not variables"}]},"url":"/docs/v5/starters/nextjs-d1/environment-variables","title":"Environment variables","description":"Every variable in the Next.js + Cloudflare D1 starter's example files, which Worker reads it, and where the value comes from."},{"_id":"/docs/v5/starters/nextjs-d1","structured":{"contents":[{"content":"This kit uses Next.js through OpenNext on Cloudflare Workers, with D1, Drizzle and Better Auth. You get the account, workspace and project foundation, a separate administrator app, and remote MCP connections for users and application administrators."},{"content":"Open the live demo · Repository"},{"heading":"the-stack","content":"Layer"},{"heading":"the-stack","content":"What the kit uses"},{"heading":"the-stack","content":"Frontend"},{"heading":"the-stack","content":"Next.js 16 (App Router), compiled to a Cloudflare Worker by OpenNext"},{"heading":"the-stack","content":"Admin app"},{"heading":"the-stack","content":"A second Next.js 16 application (`apps/admin`) compiled to its own Worker and served from its own origin"},{"heading":"the-stack","content":"Database"},{"heading":"the-stack","content":"Cloudflare D1 with Drizzle ORM; D1 is the system of record"},{"heading":"the-stack","content":"Authentication"},{"heading":"the-stack","content":"Better Auth, stored in the same D1 database"},{"heading":"the-stack","content":"Background work"},{"heading":"the-stack","content":"A third Worker (`apps/platform-worker`) with Cloudflare Queues, a cron trigger and a Durable Object for realtime channels"},{"heading":"the-stack","content":"Files"},{"heading":"the-stack","content":"Cloudflare R2 for uploads and public media"},{"heading":"the-stack","content":"Cache"},{"heading":"the-stack","content":"OpenNext R2 incremental cache, D1 tag cache and a Durable Object cache queue"},{"heading":"the-stack","content":"Emails"},{"heading":"the-stack","content":"React Email templates, Mailpit locally, Resend in production"},{"heading":"the-stack","content":"Hosting used by the demo"},{"heading":"the-stack","content":"Cloudflare Workers on the Workers Paid plan"},{"heading":"the-stack","content":"Everything runs inside the Cloudflare runtime. The repository rules forbid adding PostgreSQL, Supabase, S3-compatible storage, third-party realtime brokers or Vercel-specific runtime dependencies."},{"heading":"what-you-get-out-of-the-box","content":"Email and password sign-up with email verification, password reset, magic links, and Google, GitHub and X sign-in when you add credentials"},{"heading":"what-you-get-out-of-the-box","content":"Better Auth sessions, session revocation, two-factor TOTP with backup codes, `st_` API keys and admin impersonation"},{"heading":"what-you-get-out-of-the-box","content":"Onboarding, profile, avatar, notification center and security settings for each user"},{"heading":"what-you-get-out-of-the-box","content":"Multi-tenant workspaces with owner, admin, member and readonly roles, invitations and member management"},{"heading":"what-you-get-out-of-the-box","content":"Projects with comments, workspace settings, and workspace deletion and transfer rules"},{"heading":"what-you-get-out-of-the-box","content":"Stripe products, prices, customers, subscriptions, invoices, checkout, portal and asynchronous webhook projections"},{"heading":"what-you-get-out-of-the-box","content":"A separate administrator app (`apps/admin`, `http://localhost:3001` in development) with users, workspaces, settings, payment gateway, marketing content, changelog, feedback moderation and media management under `/app-admin`"},{"heading":"what-you-get-out-of-the-box","content":"A User MCP served by the web app and an Admin MCP served by the admin app, both with OAuth, so agents can read account context, projects, users and workspaces"},{"heading":"what-you-get-out-of-the-box","content":"R2-backed uploads, Durable Object WebSocket channels, a D1 transactional outbox with Queue retries and dead-letter recovery"},{"heading":"what-you-get-out-of-the-box","content":"Sentry, PostHog, Google Analytics, OpenAI, Vitest and Playwright integration points"},{"heading":"what-you-get-out-of-the-box","content":"Workspaces and admin roles are explained in workspaces and the admin area. In this kit the admin area is not part of the customer-facing web app; it is the `apps/admin` Worker, deployed on its own host (`https://nextjs-d1-admin.usenextbase.com` for the demo)."},{"heading":"prerequisites","content":"Node.js 24. The repository has a `.nvmrc` with `v24`, and `apps/web` and `apps/admin` require `>=24.0.0`."},{"heading":"prerequisites","content":"pnpm 11. The root `package.json` declares `pnpm@11.1.2` in `packageManager`, so `corepack enable` picks the right version."},{"heading":"prerequisites","content":"Docker, only if you want to inspect local emails in Mailpit. Without Docker, auth emails stay in the local D1 outbox and sign-up still works."},{"heading":"prerequisites","content":"A Cloudflare account for deployment. The demo runs on Workers Paid; see deployment."},{"heading":"prerequisites","content":"Optional: Stripe, Resend, OpenAI, Google, GitHub and X developer accounts for the integrations you plan to use."},{"heading":"prerequisites","content":"You don't need a Cloudflare account to develop locally. Wrangler creates local D1 databases, R2 buckets and queues on your machine."},{"heading":"next-steps","content":"Installation: clone, install, migrate the local D1 databases and start the dev servers."},{"heading":"next-steps","content":"Environment variables: every variable in the `.dev.vars` and `.env.local` example files of the three Workers."},{"heading":"next-steps","content":"Authentication: how Better Auth is configured, how an account becomes an application admin and where the admin app lives."},{"heading":"next-steps","content":"Agent connections: the User and Admin MCP endpoints, tools and OAuth details."},{"heading":"next-steps","content":"Project structure: the five apps in the monorepo and where to make changes."},{"heading":"next-steps","content":"Deployment: Cloudflare resources, production bindings, secrets and the deploy commands for all three Workers."},{"heading":"next-steps","content":"The shared getting started page covers the flow that is the same across all eight kits."}],"headings":[{"id":"the-stack","content":"The stack"},{"id":"what-you-get-out-of-the-box","content":"What you get out of the box"},{"id":"prerequisites","content":"Prerequisites"},{"id":"next-steps","content":"Next steps"}]},"url":"/docs/v5/starters/nextjs-d1","title":"Next.js + Cloudflare D1","description":"What the Next.js + Cloudflare D1 starter includes, what you need before you start, and where to go next."},{"_id":"/docs/v5/starters/nextjs-d1/installation","structured":{"contents":[{"content":"Local development doesn't need a Cloudflare account. Wrangler keeps local D1 databases, R2 buckets, queues and Durable Objects under `apps/web/.wrangler/state`, and the platform Worker reads from that same directory."},{"content":"Use Node.js 24 and the pnpm version in the repository's `package.json` (`pnpm@11.1.2`). Docker is optional and only used for the Mailpit inbox."},{"heading":"clone-the-kit","content":"If GitHub says the repository doesn't exist, check that you are signed in with the account that has access."},{"heading":"install-dependencies","content":"`pnpm install` runs `fumadocs-mdx` in `apps/web` as a postinstall step. The lockfile also applies a patch to `@opennextjs/aws` from `patches/`; that happens automatically."},{"heading":"create-the-env-files","content":"Create each file only when it is missing. Never overwrite a file you already customized."},{"heading":"create-the-env-files","content":"The `.dev.vars` files hold Worker secrets. Non-secret local defaults such as `BETTER_AUTH_URL`, `NEXT_PUBLIC_SITE_URL`, `ADMIN_EMAIL`, `USE_LOCAL_EMAIL` and `MAILPIT_API_URL` are already set in the `vars` block of `apps/web/wrangler.jsonc`, `apps/admin/wrangler.jsonc` and `apps/platform-worker/wrangler.jsonc`. `apps/web/.env.local` only holds the optional public analytics and Sentry build variables."},{"heading":"create-the-env-files","content":"Two values need attention before the app runs well:"},{"heading":"create-the-env-files","content":"`REALTIME_TOKEN_SECRET` must be the exact same value of at least 32 characters in the web, admin and platform `.dev.vars` files."},{"heading":"create-the-env-files","content":"`INTERNAL_SERVICE_SECRET` in the platform Worker must be an independent value of at least 32 characters."},{"heading":"create-the-env-files","content":"The admin app can keep its own `BETTER_AUTH_SECRET`; its cookies use a different prefix, so the two sign-ins stay independent. The full list is in environment variables."},{"heading":"start-mailpit-optional","content":"This runs `docker compose up -d mailpit` and waits for `http://localhost:8025/livez`. The inbox is at http\\://localhost:8025 and SMTP listens on port 1025. Override the ports with `MAILPIT_SMTP_PORT` and `MAILPIT_HTTP_PORT` in your shell if they clash."},{"heading":"start-mailpit-optional","content":"Without Mailpit, auth emails are written to the D1 outbox and stay pending. Sign-up doesn't fail; you just can't open the verification link. Use the seeded accounts in the next step instead."},{"heading":"migrate-and-seed-the-local-databases","content":"`pnpm db:migrate` applies the application migrations from `apps/database/migrations` to the `APP_DB` binding and the OpenNext cache migration from `apps/web/cache-migrations` to `NEXT_TAG_CACHE_D1`. Both run against local Wrangler databases using `apps/web/wrangler.jsonc`. The admin app points at the same local database names, so it needs no separate migration step."},{"heading":"migrate-and-seed-the-local-databases","content":"`pnpm db:seed` executes `apps/database/seed.sql` against the local `APP_DB`. It is idempotent and creates verified local-only accounts, so you can sign in without an email provider. The app-admin account is `admin@example.com` with password `password123`. Alice, Bob and Carol are at `alice@demo.nextbase.local`, `bob@demo.nextbase.local` and `carol@demo.nextbase.local` with the same password. Never reuse these credentials outside local development."},{"heading":"start-the-dev-servers","content":"Turborepo runs every workspace `dev` script in parallel:"},{"heading":"start-the-dev-servers","content":"Process"},{"heading":"start-the-dev-servers","content":"Address"},{"heading":"start-the-dev-servers","content":"Next.js dev server (`apps/web`)"},{"heading":"start-the-dev-servers","content":"http\\://localhost:3000"},{"heading":"start-the-dev-servers","content":"Admin app (`apps/admin`, `next dev -p 3001`)"},{"heading":"start-the-dev-servers","content":"http\\://localhost:3001"},{"heading":"start-the-dev-servers","content":"Platform Worker through `wrangler dev` (`apps/platform-worker`)"},{"heading":"start-the-dev-servers","content":"`http://127.0.0.1:8788`, health check at `/health`"},{"heading":"start-the-dev-servers","content":"React Email preview (`apps/email`)"},{"heading":"start-the-dev-servers","content":"`http://localhost:3001`"},{"heading":"start-the-dev-servers","content":"`apps/admin` and `apps/email` each declare port 3001, so under `pnpm dev` only one of them gets it. When you need the admin app, run the processes separately: `pnpm web#dev`, `pnpm worker:dev` and `pnpm admin:dev` in three terminals, and start the email preview with `pnpm --filter @nextbase/email dev` only when you work on templates. `NEXT_PUBLIC_ADMIN_APP_URL` in `apps/web/wrangler.jsonc` and `NEXT_PUBLIC_WEB_APP_URL` in `apps/admin/wrangler.jsonc` already point the two apps at each other on ports 3000 and 3001."},{"heading":"start-the-dev-servers","content":"Both Next.js apps read their Worker bindings and `.dev.vars` through `initOpenNextCloudflareForDev()` in their `next.config.ts`, so `getCloudflareContext()` works in development. The platform Worker script also polls `/__scheduled` every two seconds so the outbox cron behaves like production. Change the Worker port with `PLATFORM_WORKER_PORT`."},{"heading":"sign-up-and-create-a-workspace","content":"Open http\\://localhost:3000. Either sign in with a seeded account or sign up with a new email. New accounts must verify their email; open the message in Mailpit when it is running."},{"heading":"sign-up-and-create-a-workspace","content":"Complete onboarding and create a workspace. Add a project through the website so you have data to try with an agent later. If you sign up with the `ADMIN_EMAIL` configured in the Wrangler `vars` (`admin@example.com` by default), that account receives the application-admin role."},{"heading":"sign-up-and-create-a-workspace","content":"To use the admin area, open http\\://localhost:3001 and sign in again with the admin account. The admin app has its own login page and its own session cookie; a non-admin account gets a `403` there."},{"heading":"verify-the-setup","content":"This runs the legacy-reference scan, the Drizzle journal and schema drift checks, generated-type checks, local migrations, lint, typecheck, unit tests and an OpenNext Worker build. The Playwright suite builds the admin and web Workers and prepares local D1 on its own:"},{"heading":"verify-the-setup","content":"For a production-runtime check in workerd, run `pnpm cf:preview`. It builds all three Workers with `pnpm cf:build` and starts the platform Worker and the web Worker locally with `opennextjs-cloudflare preview`."},{"heading":"one-command-setup","content":"`./setup.sh` runs the same flow: install, copy `apps/web/.dev.vars`, `apps/platform-worker/.dev.vars` and `apps/web/.env.local` from their examples when missing, start Mailpit if Docker is available, migrate, seed and `pnpm verify:local`. It doesn't create `apps/admin/.dev.vars`; copy that one yourself before starting the admin app."},{"heading":"stop-and-reset","content":"Press `Ctrl-C` to stop `pnpm dev`. Stop Mailpit with `pnpm infra:stop`; `pnpm infra:status` shows whether it is running."},{"heading":"stop-and-reset","content":"To clear the local D1 databases and re-apply migrations:"},{"heading":"stop-and-reset","content":"`db:reset` deletes only `apps/web/.wrangler/state/v3/d1`, so local R2 objects and Durable Object state are kept. Delete `apps/web/.wrangler` entirely if you want a completely fresh local Cloudflare state."},{"heading":"other-local-tools","content":"`pnpm db:studio` opens Drizzle Studio against the local SQLite file."},{"heading":"other-local-tools","content":"`pnpm admin:dev` runs only the admin app; `pnpm web#dev` runs only the web app; `pnpm worker:dev` runs only the platform Worker."},{"heading":"other-local-tools","content":"`pnpm --filter @nextbase/email dev` runs only the email preview."},{"heading":"other-local-tools","content":"`stripe listen --forward-to localhost:3000/api/stripe/webhooks` forwards Stripe webhooks; copy the printed `whsec_` value into `STRIPE_WEBHOOK_SECRET` in `apps/web/.dev.vars` and restart the web app."}],"headings":[{"id":"clone-the-kit","content":"Clone the kit"},{"id":"install-dependencies","content":"Install dependencies"},{"id":"create-the-env-files","content":"Create the env files"},{"id":"start-mailpit-optional","content":"Start Mailpit (optional)"},{"id":"migrate-and-seed-the-local-databases","content":"Migrate and seed the local databases"},{"id":"start-the-dev-servers","content":"Start the dev servers"},{"id":"sign-up-and-create-a-workspace","content":"Sign up and create a workspace"},{"id":"verify-the-setup","content":"Verify the setup"},{"id":"one-command-setup","content":"One-command setup"},{"id":"stop-and-reset","content":"Stop and reset"},{"id":"other-local-tools","content":"Other local tools"}]},"url":"/docs/v5/starters/nextjs-d1/installation","title":"Installation","description":"Clone the Next.js + Cloudflare D1 starter, create the local D1 databases and run the web app, the admin app and the platform Worker on your machine."},{"_id":"/docs/v5/starters/nextjs-d1/project-structure","structured":{"contents":[{"content":"The repository is a pnpm workspace driven by Turborepo. Five apps share one Drizzle schema package; there is no separate `packages/` library besides the TypeScript config."},{"content":"<File name=\"middleware.ts\" />"},{"content":"<File name=\"wrangler.jsonc\" />"},{"content":"<File name=\"open-next.config.ts\" />"},{"content":"<File name=\"next.config.ts\" />"},{"content":"<File name=\"worker-configuration.d.ts\" />"},{"content":"<File name=\".dev.vars.example\" />"},{"content":"<File name=\".env.local.example\" />"},{"content":"<File name=\"middleware.ts\" />"},{"content":"<File name=\"wrangler.jsonc\" />"},{"content":"<File name=\"open-next.config.ts\" />"},{"content":"<File name=\"next.config.ts\" />"},{"content":"<File name=\"worker-configuration.d.ts\" />"},{"content":"<File name=\".dev.vars.example\" />"},{"content":"<File name=\"authorization.ts\" />"},{"content":"<File name=\"outbox.ts\" />"},{"content":"<File name=\"recovery.ts\" />"},{"content":"<File name=\"seed.sql\" />"},{"content":"<File name=\"drizzle.config.ts\" />"},{"content":"<File name=\"wrangler.jsonc\" />"},{"content":"<File name=\".dev.vars.example\" />"},{"content":"<File name=\"docker-compose.yml\" />"},{"content":"<File name=\"setup.sh\" />"},{"content":"<File name=\"turbo.json\" />"},{"content":"<File name=\"pnpm-workspace.yaml\" />"},{"content":"<File name=\"package.json\" />"},{"heading":"the-five-apps","content":"App"},{"heading":"the-five-apps","content":"Package name"},{"heading":"the-five-apps","content":"Role"},{"heading":"the-five-apps","content":"`apps/web`"},{"heading":"the-five-apps","content":"`web`"},{"heading":"the-five-apps","content":"Next.js 16 customer application compiled to a Cloudflare Worker by OpenNext. Owns public pages, user and workspace routes, server actions, auth, payments, the realtime client, the User MCP and the impersonation callback."},{"heading":"the-five-apps","content":"`apps/admin`"},{"heading":"the-five-apps","content":"`admin`"},{"heading":"the-five-apps","content":"Next.js 16 administrator application compiled to its own Worker. Owns `/app-admin`, feedback and roadmap moderation, the admin login and the Admin MCP. Reuses web modules through its `@/*` path alias and declares `web` as a workspace dependency."},{"heading":"the-five-apps","content":"`apps/database`"},{"heading":"the-five-apps","content":"`database`"},{"heading":"the-five-apps","content":"Drizzle D1 schema, migrations, repositories, tenant authorization, the compatibility query facade and the transactional outbox. Imported by all three Workers."},{"heading":"the-five-apps","content":"`apps/platform-worker`"},{"heading":"the-five-apps","content":"`@nextbase/platform-worker`"},{"heading":"the-five-apps","content":"Queue consumer, scheduled outbox dispatcher (cron `*/1 * * * *`), Stripe projection and product synchronization, deferred R2 deletion, email delivery and the `RealtimeChannel` Durable Object."},{"heading":"the-five-apps","content":"`apps/email`"},{"heading":"the-five-apps","content":"`@nextbase/email`"},{"heading":"the-five-apps","content":"React Email templates with a local preview server on port 3001."},{"heading":"the-five-apps","content":"Root-level helpers: `scripts/` holds the shell and TypeScript scripts behind the root `pnpm` commands (seed, reset, verify, preview, typegen), `patches/` holds the pnpm patch for `@opennextjs/aws`, `docker-compose.yml` defines only Mailpit, and `docs/mcp-connections.md` documents the MCP servers. `infrastructure/demo-domain` and `infra/admin-domain` are two tiny Cloudflare Pages projects that forward the demo's public hostnames to the production web and admin Workers through a service binding. `.github/workflows/` has the CI suite, the nightly Playwright run (`e2e.yml`), the changesets release flow and a manual `deploy.yml` that deploys either the web or the admin Worker."},{"heading":"wrangler-configuration","content":"Each Worker has its own `wrangler.jsonc`. The web and admin Workers each declare two D1 databases, two R2 buckets, two service bindings, a Durable Object and two rate limiters; the admin file adds `ADMIN_APP` and `NEXT_PUBLIC_WEB_APP_URL` to its `vars`, and the web file adds `NEXT_PUBLIC_ADMIN_APP_URL`. Keep the application bindings and the OpenNext cache bindings apart:"},{"heading":"wrangler-configuration","content":"Purpose"},{"heading":"wrangler-configuration","content":"Binding"},{"heading":"wrangler-configuration","content":"Local resource name"},{"heading":"wrangler-configuration","content":"Application database"},{"heading":"wrangler-configuration","content":"`APP_DB`"},{"heading":"wrangler-configuration","content":"`nextbase-app`, migrations in `apps/database/migrations`"},{"heading":"wrangler-configuration","content":"OpenNext tag cache"},{"heading":"wrangler-configuration","content":"`NEXT_TAG_CACHE_D1`"},{"heading":"wrangler-configuration","content":"`nextbase-next-cache`, migrations in `apps/web/cache-migrations`"},{"heading":"wrangler-configuration","content":"Uploads and public media"},{"heading":"wrangler-configuration","content":"`MEDIA_BUCKET`"},{"heading":"wrangler-configuration","content":"`nextbase-media`"},{"heading":"wrangler-configuration","content":"OpenNext incremental cache"},{"heading":"wrangler-configuration","content":"`NEXT_INC_CACHE_R2_BUCKET`"},{"heading":"wrangler-configuration","content":"`nextbase-next-cache`"},{"heading":"wrangler-configuration","content":"OpenNext cache queue"},{"heading":"wrangler-configuration","content":"`NEXT_CACHE_DO_QUEUE`"},{"heading":"wrangler-configuration","content":"`DOQueueHandler` Durable Object"},{"heading":"wrangler-configuration","content":"Platform Worker"},{"heading":"wrangler-configuration","content":"`PLATFORM`"},{"heading":"wrangler-configuration","content":"service binding to `nextbase-platform-worker`"},{"heading":"wrangler-configuration","content":"Self reference"},{"heading":"wrangler-configuration","content":"`WORKER_SELF_REFERENCE`"},{"heading":"wrangler-configuration","content":"`nextbase-cloudflare-web` or `nextbase-cloudflare-admin`"},{"heading":"wrangler-configuration","content":"Each app's `open-next.config.ts` wires the R2 incremental cache, D1 tag cache and Durable Object queue. The platform Worker shares `APP_DB` and `MEDIA_BUCKET`, and adds the `JOBS_QUEUE` producer, two queue consumers (`nextbase-jobs` and `nextbase-jobs-dlq`) and the `REALTIME` Durable Object. The generated `worker-configuration.d.ts` in each app is the type source for `env`; regenerate all of them with `pnpm cf:typegen`. All three files carry an `env.production` block with the demo's remote resources; see deployment."},{"heading":"where-to-make-changes","content":"**Customer routes and pages.** `apps/web/src/app/`. Authenticated pages live under `(dynamic-pages)/(authenticated-pages)/`: `(application-pages)/workspace/[workspaceSlug]`, `(application-pages)/project/[projectSlug]`, `dashboard`, `onboarding` and `user/...`. Login and recovery pages are under `(login-pages)` and `(misc-login-pages)`, including the `auth/impersonate` route that redeems admin impersonation tokens. Public marketing, blog, changelog and docs pages are under `(external-pages)` and `(fumadocs)`. API routes are in `app/api/`, including `api/auth/[...all]`, `api/stripe/webhooks`, `api/revalidate`, `api/realtime` and `api/media`."},{"heading":"where-to-make-changes","content":"**Admin routes and pages.** `apps/admin/src/app/`. The admin panel is `app-admin/(admin-pages)/` with `users`, `workspaces`, `settings`, `payment-gateway` and `marketing/{authors,blog,changelog,tags}`, plus `@sidebar` and `@navbar` parallel routes. Feedback and roadmap moderation are under `(updates-pages)/`. The admin login flow is `login/`, `forgot-password/`, `update-password/`, `two-factor/` and `auth/`. Admin-only server code sits in `apps/admin/src/data/admin/`; anything not found under `apps/admin/src` resolves to `apps/web/src` through the `@/*` alias, so shared components and utilities stay in `apps/web`."},{"heading":"where-to-make-changes","content":"**Server data access.** `apps/web/src/data/` holds server-only functions grouped as `anon`, `auth` and `user`; the admin app keeps its own under `apps/admin/src/data/admin/`. They call the D1 clients in `apps/web/src/d1-clients/` and the repositories exported by the `database` package (`database/operations`, `database/authorization`, `database/data-client`). Bindings come from `getCloudflareContext` through `apps/web/src/cloudflare/context.ts`; never create a global database client."},{"heading":"where-to-make-changes","content":"**Schema and migrations.** Edit the Drizzle schema in `apps/database/src/schema/` (`core.ts`, `domain.ts`, `projects.ts`, `users.ts`, `auth.ts`), then run `pnpm db:generate` to write a migration to `apps/database/migrations`, `pnpm db:check` to validate the journal, `pnpm gen-types` to refresh `apps/database/lib/database.types.ts`, and `pnpm db:migrate` to apply locally. Better Auth tables are generated separately with `pnpm auth:generate` into `auth.generated.ts`. `pnpm verify:local` fails if the schema snapshot or generated types drift. The admin app owns no migrations."},{"heading":"where-to-make-changes","content":"**Authorization.** `apps/database/src/authorization.ts` defines workspace roles, permissions, legacy permission keys and the `Actor` type. Add new permissions there and check them in the repository, not in the component."},{"heading":"where-to-make-changes","content":"**Authentication.** `apps/web/src/auth/server.ts` and the files beside it, shared by both Next.js apps; see authentication."},{"heading":"where-to-make-changes","content":"**Emails.** Templates in `apps/email/emails/`. Sending goes through `enqueueOutboxItem` from `database/outbox` with the `email` topic; the platform Worker delivers through Mailpit locally or Resend in production. Auth emails are composed inline in `apps/web/src/auth/server.ts`."},{"heading":"where-to-make-changes","content":"**Background jobs and realtime.** `apps/platform-worker/src/index.ts` contains the queue handler, the scheduled dispatcher, the operator recovery routes and the Durable Object. Realtime token signing lives in `apps/web/src/realtime/`. The dead-letter recovery runbook is in the repository `README.md`."},{"heading":"where-to-make-changes","content":"**Payments.** `apps/web/src/payments/` holds the abstract gateway, the Stripe implementation and the billing catalog. Webhook projection is done asynchronously by the platform Worker. The admin payment pages read the projected D1 billing records and queue product synchronization to the platform Worker."},{"heading":"where-to-make-changes","content":"**MCP.** `apps/web/src/mcp/`, with route files in both `apps/web/src/app` and `apps/admin/src/app`; see agent connections."},{"heading":"where-to-make-changes","content":"**Tests.** Unit tests sit next to the code as `*.test.ts` and run with `pnpm test` (Vitest in `apps/web` and `apps/admin`, Vitest with the Workers pool in `apps/platform-worker`, `node --test` through `tsx` in `apps/database`). Playwright specs are in `apps/web/e2e/` (`anon`, `user`, `admin`, invitations and password updates) with the config in `apps/web/playwright/`; run them with `pnpm test-integration`, which builds the admin and web OpenNext Workers first. CI runs the Playwright suite nightly from `.github/workflows/e2e.yml`."}],"headings":[{"id":"the-five-apps","content":"The five apps"},{"id":"wrangler-configuration","content":"Wrangler configuration"},{"id":"where-to-make-changes","content":"Where to make changes"},{"id":"commands-you-will-use-most","content":"Commands you will use most"}]},"url":"/docs/v5/starters/nextjs-d1/project-structure","title":"Project structure","description":"The five apps in the Next.js + Cloudflare D1 monorepo, the key config files, and where to make each kind of change."},{"_id":"/docs/v5/starters/nextjs-neon/agent-connections","structured":{"contents":[{"content":"This kit ships two remote MCP servers, one in each Next.js app. They use your existing Better Auth login, so an agent never gets a password or a database credential. For the general flow, read user and admin MCPs and connect an agent."},{"heading":"two-connections-on-two-origins","content":"The User MCP reads account context and projects in one selected workspace and is served by the web app. The Admin MCP lists users and workspaces across the app and is served by the admin app. Both are read-only in v5."},{"heading":"two-connections-on-two-origins","content":"Connection"},{"heading":"two-connections-on-two-origins","content":"Served by"},{"heading":"two-connections-on-two-origins","content":"Settings page"},{"heading":"two-connections-on-two-origins","content":"Live demo server"},{"heading":"two-connections-on-two-origins","content":"User"},{"heading":"two-connections-on-two-origins","content":"`apps/web`"},{"heading":"two-connections-on-two-origins","content":"`/mcp/connections/user` on the web origin"},{"heading":"two-connections-on-two-origins","content":"`https://nextjs-neon.usenextbase.com/mcp/user`"},{"heading":"two-connections-on-two-origins","content":"Admin"},{"heading":"two-connections-on-two-origins","content":"`apps/admin`"},{"heading":"two-connections-on-two-origins","content":"`/mcp/connections/admin` on the admin origin"},{"heading":"two-connections-on-two-origins","content":"`https://nextjs-neon-admin.usenextbase.com/mcp/admin`"},{"heading":"two-connections-on-two-origins","content":"The settings pages are linked as **Agent connections** in the web app sidebar and **Admin agent connections** in the admin app sidebar. Each page shows the server address to copy, lists that account's connections and offers **Disconnect**. The admin page only opens for administrators signed in on the admin host."},{"heading":"two-connections-on-two-origins","content":"Each app answers only its own audience. The web routes call `handleMcp(request)`, which defaults to the `user` audience, and the admin routes call `handleMcp(request, \"admin\")`; a request for the other audience returns 404. So `/mcp/admin` and `/mcp/connections/admin` don't exist on the web origin, and `/mcp/user` doesn't exist on the admin origin."},{"heading":"two-connections-on-two-origins","content":"Your own deployment has its own addresses. Each server's origin is the `NEXT_PUBLIC_APP_URL` of the app serving it: the User MCP is `<web NEXT_PUBLIC_APP_URL>/mcp/user` and the Admin MCP is `<admin NEXT_PUBLIC_APP_URL>/mcp/admin`. Locally that is `http://localhost:3000/mcp/user` and `http://localhost:3001/mcp/admin`."},{"heading":"connecting","content":"Copy the server address into your agent's remote MCP settings and choose OAuth. The client must support Streamable HTTP, public dynamic client registration, the authorization code flow and PKCE S256."},{"heading":"connecting","content":"The agent is sent to `/oauth/authorize` on the same origin, which redirects to the consent page at `/mcp/authorize?request_id=...`. If you aren't signed in, you go to `/login?next=...` on that origin and return to the same request; for the admin server that is the admin sign-in page with email and password. The consent page shows the client name (marked as unverified, because the client supplies it), the callback address, the server and the permission. For a user connection you pick one workspace; if you have none, create or join one first. Choose **Allow read access** or **Cancel**."},{"heading":"connecting","content":"Ask the agent to list projects in that workspace to confirm it works. An empty list is valid until you add a project."},{"heading":"tools-and-permissions","content":"Scopes are exactly `user:read` for the user server and `admin:read` for the admin server. Every tool is registered with `readOnlyHint: true`. List tools accept a `limit` between 1 and 50, default 20."},{"heading":"tools-and-permissions","content":"Server"},{"heading":"tools-and-permissions","content":"Tool"},{"heading":"tools-and-permissions","content":"Scope"},{"heading":"tools-and-permissions","content":"What it returns"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`account_context`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"The signed-in account and the approved workspace"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`projects_list`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"Projects in the approved workspace: id, name, slug, description, status, updated\\_at"},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_users_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"Users across the app: id, name, email, role"},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_workspaces_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"Workspaces across the app: id, name, slug"},{"heading":"tools-and-permissions","content":"Permission is re-checked on every call. `liveActor` in `apps/web/src/mcp/adapter.ts` reloads the session and requires that it hasn't expired, isn't an impersonation session, and that the user is verified and not banned. `projects_list` filters by the granted workspace and re-checks membership in SQL. Admin tools re-check `users.role = 'admin'` before every query. A user token is rejected at the admin endpoint and the other way round, and because the two servers live on different origins their tokens are also bound to different resources."},{"heading":"tools-and-permissions","content":"Being an administrator doesn't widen a user connection to other workspaces. Connect to the admin server on the admin origin separately when you need application-wide data."},{"heading":"oauth-details-specific-to-this-kit","content":"Issuer: `<NEXT_PUBLIC_APP_URL>/oauth` of the app you connect to, with metadata at `/.well-known/oauth-authorization-server/oauth` on that origin. The web app advertises only `user:read` and the admin app only `admin:read` in `scopes_supported`."},{"heading":"oauth-details-specific-to-this-kit","content":"Resource metadata: `/.well-known/oauth-protected-resource/mcp/user` on the web origin and `/.well-known/oauth-protected-resource/mcp/admin` on the admin origin."},{"heading":"oauth-details-specific-to-this-kit","content":"Endpoints on each origin: `/oauth/register`, `/oauth/authorize`, `/oauth/token`, `/oauth/revoke`."},{"heading":"oauth-details-specific-to-this-kit","content":"Public clients only (`token_endpoint_auth_method=none`). Confidential clients and client ID metadata documents aren't supported."},{"heading":"oauth-details-specific-to-this-kit","content":"Callback URLs must be HTTPS, except HTTP loopback callbacks on `localhost`, `127.0.0.1` or `::1`."},{"heading":"oauth-details-specific-to-this-kit","content":"Lifetimes: pending consent 10 minutes, authorization codes 5 minutes, access tokens 15 minutes, refresh up to 30 days but never longer than the sign-in session that approved the connection."},{"heading":"oauth-details-specific-to-this-kit","content":"Refresh tokens rotate. Reusing an old one revokes the whole connection."},{"heading":"oauth-details-specific-to-this-kit","content":"Up to 20 active connections per account."},{"heading":"oauth-details-specific-to-this-kit","content":"Browser requests are accepted from the serving app's own origin, `https://claude.ai` and `https://chatgpt.com`. Desktop and server clients without an `Origin` header are accepted too."},{"heading":"oauth-details-specific-to-this-kit","content":"Opening `/mcp/user` on the web origin or `/mcp/admin` on the admin origin without a token returns a 401 with a `WWW-Authenticate` challenge pointing at the resource metadata. That is expected."},{"heading":"oauth-details-specific-to-this-kit","content":"Signing out, disconnecting, losing workspace membership, being banned or losing the admin role ends delegated access. Reconnect from a new sign-in when you need to."},{"heading":"where-the-code-lives","content":"The implementation is in the web app and shared with the admin app through its `@/*` alias, which falls back to `apps/web/src`."},{"heading":"where-the-code-lives","content":"Path"},{"heading":"where-the-code-lives","content":"Role"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.ts`"},{"heading":"where-the-code-lives","content":"Protocol, OAuth, consent and connection pages, audience isolation, the `Adapter` interface and the tool registry"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/adapter.ts`"},{"heading":"where-the-code-lives","content":"`handleMcp(request, audience)` and the Postgres adapter: parameterized SQL on the shared `pool`, session revalidation, workspace lookups"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/pg-store.ts`"},{"heading":"where-the-code-lives","content":"Credential storage in the `mcp_records` table with compare-and-set updates"},{"heading":"where-the-code-lives","content":"`apps/web/src/app/mcp/[...path]/route.ts`, `apps/web/src/app/oauth/[...path]/route.ts` and `apps/web/src/app/.well-known/`"},{"heading":"where-the-code-lives","content":"Mount the handler for the user audience on the web origin"},{"heading":"where-the-code-lives","content":"`apps/admin/src/app/mcp/[...path]/route.ts`, `apps/admin/src/app/oauth/[...path]/route.ts` and `apps/admin/src/app/.well-known/`"},{"heading":"where-the-code-lives","content":"Mount the same handler with the `admin` audience on the admin origin"},{"heading":"where-the-code-lives","content":"`apps/web/src/components/sidebar-platform-nav.tsx` and `apps/admin/src/components/sidebar-admin-panel-nav.tsx`"},{"heading":"where-the-code-lives","content":"The sidebar links to the two connection pages"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.test.ts`"},{"heading":"where-the-code-lives","content":"Vitest suite with an in-memory store covering consent, CSRF, PKCE, replay, expiry, membership loss and demotion"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/pg-store.integration.test.ts`"},{"heading":"where-the-code-lives","content":"Store test against a loopback Postgres given by `MCP_TEST_PG_URL`"},{"heading":"where-the-code-lives","content":"`apps/database/drizzle-postgres/0002_*.sql` and `0003_*.sql`"},{"heading":"where-the-code-lives","content":"The `mcp_records` table, its indexes and the guarded grant to an `app_runtime` role"},{"heading":"where-the-code-lives","content":"`docs/mcp-connections.md`"},{"heading":"where-the-code-lives","content":"Design notes in the repository"},{"heading":"where-the-code-lives","content":"Secrets in `mcp_records` are stored as SHA-256 hashes. Expired rows are cleaned up in bounded batches as traffic arrives, so there is no always-on poller."},{"heading":"adding-a-tool-safely","content":"The repository ships an `mcp-tool` skill in `.agents/skills/mcp-tool/SKILL.md` that walks a coding agent through this. The short version:"},{"heading":"adding-a-tool-safely","content":"Add a method to the `Adapter` interface in `core.ts`. User tools take the actor and the granted workspace; admin tools take the actor only."},{"heading":"adding-a-tool-safely","content":"Implement it in `adapter.ts`. User queries filter by `workspace.id` and re-check membership in SQL, like `projects` does. Admin queries start by confirming `liveActor(actor)?.admin`. Select explicit columns, never secrets or `mcp_records`, and bound results with a `limit`."},{"heading":"adding-a-tool-safely","content":"Register it with `server.registerTool` inside the right audience branch only, with a zod `inputSchema` and the shared `annotations`. Use `limitSchema` for list tools. Name user tools `<noun>_<verb>` and prefix admin tools with `admin_`. Admin tools are served by the admin app automatically, because its routes pass the `admin` audience to the same handler."},{"heading":"adding-a-tool-safely","content":"Extend `core.test.ts`: the fake adapter, the `tools/list` expectations, a `tools/call` test, and negative tests for the wrong audience, invalid input, membership loss and admin demotion."},{"heading":"adding-a-tool-safely","content":"Add the tool to the table in `docs/mcp-connections.md`."},{"heading":"adding-a-tool-safely","content":"Write tools are a design change, not a new method. They need a new scope, consent copy, `readOnlyHint: false`, the existing policy asserts from `apps/web/src/lib/domain/`, and audit and outbox rows via `domainEventValues`. Keep the authenticated identity on the server: a `workspaceId` argument from the client is never proof of access."},{"heading":"adding-a-tool-safely","content":"Verify with `pnpm --filter web exec vitest run src/mcp/core.test.ts`, then `pnpm lint && pnpm typecheck && pnpm test`. For a manual check, run `pnpm local:start` and connect an OAuth MCP client to `http://localhost:3000/mcp/user`; for the admin server, also run `pnpm --filter admin dev` and connect to `http://localhost:3001/mcp/admin`."}],"headings":[{"id":"two-connections-on-two-origins","content":"Two connections on two origins"},{"id":"connecting","content":"Connecting"},{"id":"tools-and-permissions","content":"Tools and permissions"},{"id":"oauth-details-specific-to-this-kit","content":"OAuth details specific to this kit"},{"id":"where-the-code-lives","content":"Where the code lives"},{"id":"adding-a-tool-safely","content":"Adding a tool safely"}]},"url":"/docs/v5/starters/nextjs-neon/agent-connections","title":"Agent connections","description":"The User MCP and Admin MCP servers in the Next.js + Neon starter: endpoints on the web and admin origins, tools, OAuth behaviour and how to add a tool."},{"_id":"/docs/v5/starters/nextjs-neon/authentication","structured":{"contents":[{"content":"This kit uses Better Auth. Users, sessions and accounts live in your own Postgres database, and the same login is used by the website, the admin app and agent connections. For the concepts shared by all kits, read the v5 authentication guide."},{"heading":"where-the-configuration-lives","content":"What"},{"heading":"where-the-configuration-lives","content":"Path"},{"heading":"where-the-configuration-lives","content":"Server configuration (plugins, email hooks, session settings)"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/lib/auth/server.ts`"},{"heading":"where-the-configuration-lives","content":"Browser client"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/lib/auth/client.ts`"},{"heading":"where-the-configuration-lives","content":"Better Auth API route, web app"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/app/api/auth/[...all]/route.ts`"},{"heading":"where-the-configuration-lives","content":"Better Auth API route, admin app"},{"heading":"where-the-configuration-lives","content":"`apps/admin/src/app/api/auth/[...all]/route.ts`"},{"heading":"where-the-configuration-lives","content":"Session helpers (`getCurrentSession`, `requireSession`, `requireAdminSession`)"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/lib/auth/session.ts`"},{"heading":"where-the-configuration-lives","content":"Actor contracts (`requireActor`, `requireWorkspaceActor`)"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/lib/auth/actors.ts`"},{"heading":"where-the-configuration-lives","content":"Workspace roles and permissions"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/lib/auth/organization-access.ts`"},{"heading":"where-the-configuration-lives","content":"Secondary storage, rate limits and locks"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/lib/auth/secondary-storage.ts`"},{"heading":"where-the-configuration-lives","content":"Impersonation handoff bookkeeping"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/lib/auth/support-handoff.ts` and `apps/web/src/lib/cross-app-url.ts`"},{"heading":"where-the-configuration-lives","content":"Auth tables"},{"heading":"where-the-configuration-lives","content":"`apps/database/src/schema.ts`"},{"heading":"where-the-configuration-lives","content":"The admin app has no auth code of its own. Its `@/*` alias falls back to `apps/web/src`, so both apps run the same `createAuth()` from `server.ts` against the same database, each with its own origin."},{"heading":"where-the-configuration-lives","content":"Better Auth's models keep their names in code but map to the app tables: `user` is `users`, `session` is `sessions`, `organization` is `workspaces`, `member` is `workspace_members` and `subscription` is `subscriptions`."},{"heading":"where-the-configuration-lives","content":"The environment variables that matter are `BETTER_AUTH_URL`, `BETTER_AUTH_SECRET`, `NEXT_PUBLIC_APP_URL` and, optionally, `BETTER_AUTH_TRUSTED_ORIGINS`. Trusted origins are `BETTER_AUTH_URL`, `NEXT_PUBLIC_APP_URL` and anything in the comma-separated extra list. Each app sets `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` to its own origin, and both must share `BETTER_AUTH_SECRET`. Cookies are host-only, so a session on the web host is not a session on the admin host."},{"heading":"sign-up-verification-and-the-local-inbox","content":"Email and password is enabled with `requireEmailVerification: true`. A new account can't sign in until the verification link is opened. The pages on the web app are `/sign-up` (also `/signup`), `/login`, `/forgot-password` and `/update-password`."},{"heading":"sign-up-verification-and-the-local-inbox","content":"Verification, password reset, email change and account deletion emails are rendered from `apps/email` and sent through `apps/web/src/lib/email/send-email.ts`. Locally, `EMAIL_PROVIDER=smtp` points at Mailpit, so open http\\://127.0.0.1:8025 to find the message. In production, set `EMAIL_PROVIDER=resend` and `RESEND_API_KEY`. See emails with Resend."},{"heading":"sign-up-verification-and-the-local-inbox","content":"Account deletion sends a confirmation link that expires after one hour. Email change sends a confirmation to the current address."},{"heading":"magic-links-and-social-providers","content":"The `magicLink` plugin is enabled. The login and sign-up pages include magic link forms, and the email is sent through the same email setup. Administrators can also send a customer a login link from the admin app; that action posts to the web app's `/api/auth/sign-in/magic-link` so the link lands on the web origin."},{"heading":"magic-links-and-social-providers","content":"Google, GitHub and X (Twitter) are configured in `server.ts`, but only when both the client ID and client secret for a provider are set. Out of the box, the login form in `apps/web/src/components/auth-form.tsx` offers email and password plus the magic link form; it renders no social buttons, and the README says the same. If you want social login, set the provider variables and add the buttons that call the Better Auth client's social sign-in. The provider icons already exist in `apps/web/src/components/authentication/icons-list.tsx`. Register the callback URL with your provider for the origin in `BETTER_AUTH_URL`."},{"heading":"sessions","content":"Sessions are stored in the database (`storeSessionInDatabase: true`) and expire after 7 days, extended once a day while in use. The cookie cache is disabled, so every request validates against the `sessions` table."},{"heading":"sessions","content":"Rate limiting is on (100 requests per 60-second window) and uses Better Auth's secondary storage. With `VALKEY_URL` set that is Valkey; without it the kit uses the Postgres tables `auth_secondary_storage` and `auth_rate_windows` and transaction-scoped advisory locks, which work through Neon's connection pooling."},{"heading":"sessions","content":"Users can also create API keys under `/user/settings/developer`. Keys use the `nb_` prefix, require a name, and have their own rate limit."},{"heading":"workspaces-and-roles","content":"Workspaces are Better Auth organizations. Any user can create one, up to `BETTER_AUTH_ORGANIZATION_LIMIT` (default 10). Roles are `owner`, `admin`, `member` and `readonly`, and `permissionsForRole` in `actors.ts` maps them to permissions such as `workspace:write`, `members:write`, `billing:write` and `projects:write`."},{"heading":"workspaces-and-roles","content":"Invitations expire after 7 days. The email links to `/api/invitations/view/<id>`, and the user accepts under `/user/invitations` or `/accept-invitation`. Creating a workspace also creates its `workspace_settings` and `workspace_application_settings` rows and writes an audit and outbox event."},{"heading":"the-admin-app-and-application-administrators","content":"Application admin is separate from owning a workspace. It is the Better Auth `admin` plugin's `role` column on the `users` table."},{"heading":"the-admin-app-and-application-administrators","content":"The admin area lives in `apps/admin`, served on its own origin (port 3001 locally, `NEXT_PUBLIC_ADMIN_APP_URL` in production). Administrators sign in at `/login` on the admin host with email and password; that page has no magic link, social or sign-up option, and it redirects straight to `/app-admin` when an admin session already exists. Every route under `apps/admin/src/app/(dynamic-pages)/(authenticated-pages)/` runs `requireAdminSession`, which sends anyone without `role = 'admin'`, or anyone inside an impersonation session, to `/login?error=forbidden`. The web app's sidebar shows an **Open admin panel** link to `<NEXT_PUBLIC_ADMIN_APP_URL>/app-admin` for administrators; the web app itself no longer serves `/app-admin`."},{"heading":"the-admin-app-and-application-administrators","content":"How an account gets the role:"},{"heading":"the-admin-app-and-application-administrators","content":"Locally, the seed creates `admin@example.com` with the admin role (password `password123`)."},{"heading":"the-admin-app-and-application-administrators","content":"An existing administrator can change another user's role from the users page in the admin app. The action in `apps/admin/src/data/admin/user.tsx` calls Better Auth's `setRole` and writes an `admin.user_role_updated` audit entry."},{"heading":"the-admin-app-and-application-administrators","content":"For a fresh production database, the repository has no script for the first administrator. The code reads `users.role`, so set that column to `admin` for your own account using trusted database access (for example the Neon SQL editor), then use the admin app for everyone else. Don't expose this through the app."},{"heading":"the-admin-app-and-application-administrators","content":"Admins can also ban and unban users, create users and send login links."},{"heading":"impersonation-across-the-two-apps","content":"Impersonation starts in the admin app and lands in the web app:"},{"heading":"impersonation-across-the-two-apps","content":"The admin action calls Better Auth's `impersonateUser`, which creates a support session carrying `impersonatedBy`, and the `oneTimeToken` plugin issues a one-use token that lives for one minute. The action records an `admin.user_impersonated` audit entry and registers the handoff with the issuing admin session; if either write fails, the new support session is revoked."},{"heading":"impersonation-across-the-two-apps","content":"The browser is sent to `<NEXT_PUBLIC_WEB_APP_URL>/auth/impersonation` with the token in the URL fragment, so it never reaches a server log."},{"heading":"impersonation-across-the-two-apps","content":"That page posts the token to `/api/impersonation` on the web app, which only accepts same-origin requests, verifies the token, checks that the session is an impersonation session for a user who isn't banned, and consumes the handoff. Consuming re-checks the issuing administrator's role, ban state and exact session in the database; if any check fails, the support session is deleted and no cookie is set."},{"heading":"impersonation-across-the-two-apps","content":"On success the web app sets its own host-only session cookie and opens `/dashboard`. An impersonation banner offers to stop; stopping signs the support session out and returns to `<NEXT_PUBLIC_ADMIN_APP_URL>/app-admin/users`, where the admin's own session is still valid."},{"heading":"impersonation-across-the-two-apps","content":"The MCP servers refuse impersonation sessions, and `requireAdminSession` refuses them too."},{"heading":"what-is-protected-and-how","content":"There is no row-level security for application data in this kit (the only exception is the `mcp_records` table, which is restricted to the server's database role). Protection is explicit in server code:"},{"heading":"what-is-protected-and-how","content":"`apps/web/src/proxy.ts` redirects requests to `/dashboard`, `/onboarding`, `/project`, `/user` and `/workspace` to `/login?next=...` when there is no session cookie. This is only a convenience; it doesn't authorize anything."},{"heading":"what-is-protected-and-how","content":"Layouts under `app/(dynamic-pages)/(authenticated-pages)` in the web app call `unauthorized()` when there are no claims. In the admin app, the equivalent layout calls `requireAdminSession`."},{"heading":"what-is-protected-and-how","content":"Server actions use `authActionClient` or `adminActionClient` from `apps/web/src/lib/safe-action.ts`, validate input with zod, and then check workspace membership with `requireWorkspaceActor` or `getWorkspaceMembership`. Administrative queries and actions live only in `apps/admin/src/data/admin/`."},{"heading":"what-is-protected-and-how","content":"Policies in `apps/web/src/lib/domain/*-policy.ts` (`assertProjectWrite`, `requireWorkspaceManager` and friends) hold the rules, with unit tests next to them."},{"heading":"what-is-protected-and-how","content":"Mutations insert audit log and outbox rows in the same `db.transaction` through `domainEventValues`, so every change has a trail and a realtime event. Admin mutations also call `refreshWebAfterAdminMutation`, which posts to the web app's `/api/internal/admin-revalidate` with `REVALIDATE_SECRET` so public pages pick up CMS changes."},{"heading":"what-is-protected-and-how","content":"Banned users are redirected to `/login?error=banned`, and the MCP servers re-check verification, ban state and role on every call."},{"heading":"what-is-protected-and-how","content":"When you add a feature, keep the pattern: parse input, resolve the actor, check the workspace role, then query or write with the workspace ID from the server, never from the client. The `AGENTS.md` files in the repository spell this out for coding agents too."}],"headings":[{"id":"where-the-configuration-lives","content":"Where the configuration lives"},{"id":"sign-up-verification-and-the-local-inbox","content":"Sign-up, verification and the local inbox"},{"id":"magic-links-and-social-providers","content":"Magic links and social providers"},{"id":"sessions","content":"Sessions"},{"id":"workspaces-and-roles","content":"Workspaces and roles"},{"id":"the-admin-app-and-application-administrators","content":"The admin app and application administrators"},{"id":"impersonation-across-the-two-apps","content":"Impersonation across the two apps"},{"id":"what-is-protected-and-how","content":"What is protected and how"}]},"url":"/docs/v5/starters/nextjs-neon/authentication","title":"Authentication","description":"How Better Auth is configured in the Next.js + Neon starter, how sessions, the separate admin app and impersonation work, and what protects your data."},{"_id":"/docs/v5/starters/nextjs-neon/deployment","structured":{"contents":[{"content":"The demo runs the website and the admin app on Vercel as two projects, and the database on Neon. This page covers what is specific to this kit. Platform steps that are the same for every Node-hosted starter are in the Vercel deployment guide. The repository's own notes on the admin deployment are in `docs/admin-app.md`."},{"content":"Neither app applies migrations when it boots. You run `pnpm db:migrate` against the direct endpoint yourself, then deploy. The `mcp_records` migration must be in place before either app serves OAuth connections."},{"heading":"create-the-neon-database","content":"Create a Neon project. The README asks for two connection strings:"},{"heading":"create-the-neon-database","content":"`DATABASE_URL`: the pooled endpoint, used by both apps at runtime, ideally with an application-only role."},{"heading":"create-the-neon-database","content":"`DATABASE_DIRECT_URL`: the direct (non-pooled) endpoint, used only for migrations, ideally with a role that owns the schema."},{"heading":"create-the-neon-database","content":"If you create a role named `app_runtime` for the apps, migration `0003` grants it access to `mcp_records` and adds a row-level policy for it. If that role doesn't exist, the migration skips that block and the table owner is used. Fresh installs don't need any extra roles."},{"heading":"create-the-neon-database","content":"Keep Neon's default of suspending compute after five idle minutes. The kit avoids always-on pollers for that reason."},{"heading":"apply-the-migrations","content":"From your machine or a CI job with both URLs set:"},{"heading":"apply-the-migrations","content":"`apps/database/src/migrate.ts` opens a single connection to `DATABASE_DIRECT_URL` and applies `apps/database/drizzle-postgres/`. Run it again for every release that adds a migration, before the new builds go live. Don't run the seeds against production; they create demo accounts with a known password."},{"heading":"apply-the-migrations","content":"The optional `PostgreSQL schema validation` workflow in `.github/workflows/postgres-schema.yml` replays the migrations on a clean container when you trigger it by hand."},{"heading":"prepare-email-and-storage","content":"Email: set `EMAIL_PROVIDER=resend`, `RESEND_API_KEY` and a `MAIL_FROM` on a domain you have verified in Resend. See emails with Resend. Verification emails must work before anyone can finish signing up."},{"heading":"prepare-email-and-storage","content":"Storage: create two buckets on an S3-compatible service and set the `S3_*` variables on both apps. MinIO is only for local development. `apps/web/next.config.ts` derives a `next/image` `remotePatterns` entry from `S3_ENDPOINT`, so uploaded images render from your bucket without editing the config; the admin app's `next.config.ts` allows any HTTPS image host."},{"heading":"create-two-vercel-projects","content":"Import the same repository into Vercel twice. The repository has no `vercel.json`; `docs/admin-app.md` records the settings the demo uses:"},{"heading":"create-two-vercel-projects","content":"Project"},{"heading":"create-two-vercel-projects","content":"Root directory"},{"heading":"create-two-vercel-projects","content":"Framework"},{"heading":"create-two-vercel-projects","content":"Demo hostname"},{"heading":"create-two-vercel-projects","content":"Web"},{"heading":"create-two-vercel-projects","content":"`apps/web`"},{"heading":"create-two-vercel-projects","content":"Next.js"},{"heading":"create-two-vercel-projects","content":"`nextjs-neon.usenextbase.com`"},{"heading":"create-two-vercel-projects","content":"Admin"},{"heading":"create-two-vercel-projects","content":"`apps/admin`"},{"heading":"create-two-vercel-projects","content":"Next.js"},{"heading":"create-two-vercel-projects","content":"`nextjs-neon-admin.usenextbase.com`"},{"heading":"create-two-vercel-projects","content":"Each project deploys independently from the same commits. Vercel picks pnpm from the root `packageManager` field; the equivalent local commands are `pnpm --filter web build` and `pnpm --filter admin build`. Both apps need Node 24."},{"heading":"create-two-vercel-projects","content":"Add the production environment variables to each project. Use the exact names from the environment variables page."},{"heading":"create-two-vercel-projects","content":"Required on the web project:"},{"heading":"create-two-vercel-projects","content":"Variable"},{"heading":"create-two-vercel-projects","content":"Value"},{"heading":"create-two-vercel-projects","content":"`DATABASE_URL`"},{"heading":"create-two-vercel-projects","content":"Neon pooled connection string"},{"heading":"create-two-vercel-projects","content":"`BETTER_AUTH_URL`"},{"heading":"create-two-vercel-projects","content":"The web origin, for example `https://app.example.com`"},{"heading":"create-two-vercel-projects","content":"`NEXT_PUBLIC_APP_URL`"},{"heading":"create-two-vercel-projects","content":"The same web origin"},{"heading":"create-two-vercel-projects","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"create-two-vercel-projects","content":"The same web origin"},{"heading":"create-two-vercel-projects","content":"`NEXT_PUBLIC_ADMIN_APP_URL`"},{"heading":"create-two-vercel-projects","content":"The admin origin, for example `https://admin.example.com`"},{"heading":"create-two-vercel-projects","content":"`BETTER_AUTH_SECRET`"},{"heading":"create-two-vercel-projects","content":"A new random string, 32 characters or more; the same value on both projects"},{"heading":"create-two-vercel-projects","content":"`REVALIDATE_SECRET`"},{"heading":"create-two-vercel-projects","content":"A random string; the same value on both projects"},{"heading":"create-two-vercel-projects","content":"`ENFORCE_PRODUCTION_SECRETS`"},{"heading":"create-two-vercel-projects","content":"`true`"},{"heading":"create-two-vercel-projects","content":"`EMAIL_PROVIDER`, `RESEND_API_KEY`, `MAIL_FROM`"},{"heading":"create-two-vercel-projects","content":"Your Resend setup"},{"heading":"create-two-vercel-projects","content":"`S3_ENDPOINT`, `S3_REGION`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_BUCKET_PUBLIC`, `S3_BUCKET_PRIVATE`, `S3_FORCE_PATH_STYLE`"},{"heading":"create-two-vercel-projects","content":"Your bucket"},{"heading":"create-two-vercel-projects","content":"Required on the admin project:"},{"heading":"create-two-vercel-projects","content":"Variable"},{"heading":"create-two-vercel-projects","content":"Value"},{"heading":"create-two-vercel-projects","content":"`DATABASE_URL`"},{"heading":"create-two-vercel-projects","content":"The same Neon pooled connection string"},{"heading":"create-two-vercel-projects","content":"`BETTER_AUTH_URL`"},{"heading":"create-two-vercel-projects","content":"The admin origin"},{"heading":"create-two-vercel-projects","content":"`NEXT_PUBLIC_APP_URL`"},{"heading":"create-two-vercel-projects","content":"The same admin origin"},{"heading":"create-two-vercel-projects","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"create-two-vercel-projects","content":"The same admin origin"},{"heading":"create-two-vercel-projects","content":"`NEXT_PUBLIC_ADMIN_APP_URL`"},{"heading":"create-two-vercel-projects","content":"The same admin origin"},{"heading":"create-two-vercel-projects","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"create-two-vercel-projects","content":"The web origin"},{"heading":"create-two-vercel-projects","content":"`BETTER_AUTH_SECRET`"},{"heading":"create-two-vercel-projects","content":"The same value as the web project"},{"heading":"create-two-vercel-projects","content":"`REVALIDATE_SECRET`"},{"heading":"create-two-vercel-projects","content":"The same value as the web project"},{"heading":"create-two-vercel-projects","content":"`ENFORCE_PRODUCTION_SECRETS`"},{"heading":"create-two-vercel-projects","content":"`true`"},{"heading":"create-two-vercel-projects","content":"`S3_*`"},{"heading":"create-two-vercel-projects","content":"The same bucket as the web project, for marketing CMS uploads"},{"heading":"create-two-vercel-projects","content":"Optional on either project:"},{"heading":"create-two-vercel-projects","content":"`BETTER_AUTH_TRUSTED_ORIGINS` for preview domains that should be able to sign in."},{"heading":"create-two-vercel-projects","content":"`VALKEY_URL` if you run Valkey or Redis. Without it the kit uses Postgres for the auth cache, rate limits and locks, which works on Neon."},{"heading":"create-two-vercel-projects","content":"Realtime on the web project: `NEXT_PUBLIC_CENTRIFUGO_URL` as a `wss://` URL, plus `CENTRIFUGO_URL`, `CENTRIFUGO_API_URL`, `CENTRIFUGO_API_KEY` and `CENTRIFUGO_TOKEN_HMAC_SECRET`. Leave `NEXT_PUBLIC_CENTRIFUGO_URL` unset to disable browser realtime; the app works without it."},{"heading":"create-two-vercel-projects","content":"Stripe: `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` and the price IDs on the web project, with a webhook pointing at `/api/stripe/webhooks` on the web origin. Give the admin project `STRIPE_SECRET_KEY` too if you want the **Sync products** action on the payment gateway page to pull the Stripe catalog."},{"heading":"create-two-vercel-projects","content":"`OPENAI_API_KEY`, PostHog, Google Analytics, Sentry and Vercel Analytics as needed."},{"heading":"create-two-vercel-projects","content":"`DATABASE_DIRECT_URL` is not needed by either app process. Keep it in the place you run migrations from."},{"heading":"auth-callbacks-and-origins","content":"Better Auth builds its URLs from `BETTER_AUTH_URL`, and the trusted origins are `BETTER_AUTH_URL`, `NEXT_PUBLIC_APP_URL` and `BETTER_AUTH_TRUSTED_ORIGINS`. On each project all of these must agree with the domain users actually open on that app. A preview deployment with a different origin needs its own values or an entry in `BETTER_AUTH_TRUSTED_ORIGINS`."},{"heading":"auth-callbacks-and-origins","content":"Sessions are host-only cookies. Administrators sign in on the admin host at `/login`; a web session does not carry over. Impersonation crosses the boundary with a one-minute, single-use token that the admin app sends to `<NEXT_PUBLIC_WEB_APP_URL>/auth/impersonation` and the web app redeems at `/api/impersonation`, so `NEXT_PUBLIC_WEB_APP_URL` on admin and `NEXT_PUBLIC_ADMIN_APP_URL` on web must both be right or the handoff and the return trip fail."},{"heading":"auth-callbacks-and-origins","content":"Email links (verification, reset, invitations) are built from the web app's `NEXT_PUBLIC_APP_URL`. If you enable a social provider, register the callback for your web `BETTER_AUTH_URL` origin with that provider."},{"heading":"auth-callbacks-and-origins","content":"The MCP servers use each app's `NEXT_PUBLIC_APP_URL` as their origin, so there is no separate `MCP_APP_URL` in this kit. Your User MCP is `<web origin>/mcp/user` and your Admin MCP is `<admin origin>/mcp/admin`."},{"heading":"realtime-dispatcher-in-production","content":"Realtime events are written to the `outbox_events` table and published to Centrifugo by `apps/web/src/cli/dispatch-outbox.ts`. Locally `pnpm local:start` keeps it running. The repository has no cron route or scheduled job for production, so if you want realtime in production, run `pnpm --filter web realtime:dispatch:once` on a schedule from a job runner you control, or run the dispatcher as a long-lived process somewhere that can reach both Neon and Centrifugo. Without a dispatcher, the app still works; the browser just doesn't receive live invalidation events."},{"heading":"deploy-and-check","content":"Run `pnpm verify:local` before you push. Then deploy both projects, open the site and check:"},{"heading":"deploy-and-check","content":"`/login` and `/sign-up` load on the web origin. Create a test account, open the verification email from Resend, and sign in."},{"heading":"deploy-and-check","content":"Complete onboarding, create a workspace and add a project."},{"heading":"deploy-and-check","content":"`/mcp/connections/user` on the web origin shows a server address on your domain. `/.well-known/oauth-protected-resource/mcp/user` returns JSON. Opening `/mcp/user` in a browser returns 401 with a `WWW-Authenticate` challenge, which is correct."},{"heading":"deploy-and-check","content":"Follow connect an agent to approve a connection and call `projects_list`."},{"heading":"deploy-and-check","content":"Promote your own account to application administrator as described in authentication. Sign in at `/login` on the admin origin, open `/app-admin`, and check `/mcp/connections/admin` there. The web app's sidebar should now show **Open admin panel**."},{"heading":"deploy-and-check","content":"From the admin users page, impersonate the test account. You should land on the web app's `/dashboard` with the impersonation banner, and stopping should return you to the admin users page. Edit a blog post in the admin CMS and confirm the public page on the web origin refreshes; if it doesn't, `REVALIDATE_SECRET` or `NEXT_PUBLIC_WEB_APP_URL` on the admin project is wrong."},{"heading":"deploy-and-check","content":"If you enabled Stripe, send a test webhook and confirm the billing page updates."},{"heading":"deploy-and-check","content":"If sign-in loops or emails link to the wrong host, compare `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` of the app you are on with the address in the browser first. More cases are in troubleshooting."}],"headings":[{"id":"create-the-neon-database","content":"Create the Neon database"},{"id":"apply-the-migrations","content":"Apply the migrations"},{"id":"prepare-email-and-storage","content":"Prepare email and storage"},{"id":"create-two-vercel-projects","content":"Create two Vercel projects"},{"id":"auth-callbacks-and-origins","content":"Auth callbacks and origins"},{"id":"realtime-dispatcher-in-production","content":"Realtime dispatcher in production"},{"id":"deploy-and-check","content":"Deploy and check"}]},"url":"/docs/v5/starters/nextjs-neon/deployment","title":"Deployment","description":"Put the Next.js + Neon starter in production: a Neon database, migrations, two Vercel projects for the web and admin apps, and post-deploy checks."},{"_id":"/docs/v5/starters/nextjs-neon/environment-variables","structured":{"contents":[{"content":"The kit has two example env files. `.env.example` at the repository root is the one you copy to `.env.local`; the web app, the database package and the scripts read it. `apps/admin/.env.local.example` is the admin app's template, copied to `apps/admin/.env.local`; it only lists the origin variables that differ between the two apps. Both apps validate values with the same zod schema in `apps/web/src/lib/env.ts` (the admin app resolves `@/lib/env` to that file), and almost every variable has a local default there, so both apps start with the example files unchanged. `apps/web/src/environment.d.ts` mirrors the schema for `process.env` typing."},{"content":"Variables prefixed with `NEXT_PUBLIC_` are bundled into the browser. Everything else is server-only. Never put a secret in a `NEXT_PUBLIC_` variable."},{"content":"\"Required\" below means the app can't do its job in production without it. Locally the example values work as they are."},{"heading":"database","content":"Variable"},{"heading":"database","content":"Required"},{"heading":"database","content":"Public"},{"heading":"database","content":"Purpose"},{"heading":"database","content":"Where to get it"},{"heading":"database","content":"`DATABASE_URL`"},{"heading":"database","content":"Yes"},{"heading":"database","content":"No"},{"heading":"database","content":"Connection string used by both apps at runtime. In production, Neon's pooled endpoint with an application role."},{"heading":"database","content":"Neon project connection details; locally `postgresql://nextbase:nextbase@127.0.0.1:35577/nextbase`"},{"heading":"database","content":"`DATABASE_DIRECT_URL`"},{"heading":"database","content":"For migrations"},{"heading":"database","content":"No"},{"heading":"database","content":"Connection string used by `pnpm db:migrate` and drizzle-kit. In production, Neon's direct endpoint with a migration role. Falls back to `DATABASE_URL` when unset."},{"heading":"database","content":"Neon project connection details; locally the same as `DATABASE_URL`"},{"heading":"database","content":"`DATABASE_POOL_SIZE`"},{"heading":"database","content":"No"},{"heading":"database","content":"No"},{"heading":"database","content":"Maximum connections in the shared `pg.Pool`. Defaults to `5`. Not in `.env.example`."},{"heading":"database","content":"Your choice"},{"heading":"auth-and-app-origin","content":"Variable"},{"heading":"auth-and-app-origin","content":"Required"},{"heading":"auth-and-app-origin","content":"Public"},{"heading":"auth-and-app-origin","content":"Purpose"},{"heading":"auth-and-app-origin","content":"Where to get it"},{"heading":"auth-and-app-origin","content":"`BETTER_AUTH_URL`"},{"heading":"auth-and-app-origin","content":"Yes"},{"heading":"auth-and-app-origin","content":"No"},{"heading":"auth-and-app-origin","content":"Better Auth base URL. Must be the public origin of the app that reads it: the web origin on web, the admin origin on admin."},{"heading":"auth-and-app-origin","content":"Your deployment URL; locally `http://localhost:3000` for web and `http://localhost:3001` for admin"},{"heading":"auth-and-app-origin","content":"`BETTER_AUTH_SECRET`"},{"heading":"auth-and-app-origin","content":"Yes"},{"heading":"auth-and-app-origin","content":"No"},{"heading":"auth-and-app-origin","content":"Signs sessions and tokens. At least 32 characters. Both apps must use the same value, because the impersonation handoff verifies a token issued by the admin app on the web app."},{"heading":"auth-and-app-origin","content":"Generate a random string; never reuse the example"},{"heading":"auth-and-app-origin","content":"`BETTER_AUTH_TRUSTED_ORIGINS`"},{"heading":"auth-and-app-origin","content":"No"},{"heading":"auth-and-app-origin","content":"No"},{"heading":"auth-and-app-origin","content":"Comma-separated extra origins Better Auth accepts, on top of `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL`. Useful for preview domains."},{"heading":"auth-and-app-origin","content":"Your extra origins"},{"heading":"auth-and-app-origin","content":"`BETTER_AUTH_ORGANIZATION_LIMIT`"},{"heading":"auth-and-app-origin","content":"No"},{"heading":"auth-and-app-origin","content":"No"},{"heading":"auth-and-app-origin","content":"How many workspaces one user can create. Defaults to `10`. Not in `.env.example`; `.env.test` sets `100`."},{"heading":"auth-and-app-origin","content":"Your choice"},{"heading":"auth-and-app-origin","content":"`ENFORCE_PRODUCTION_SECRETS`"},{"heading":"auth-and-app-origin","content":"Yes in production"},{"heading":"auth-and-app-origin","content":"No"},{"heading":"auth-and-app-origin","content":"Set to `true` so the app fails fast when `BETTER_AUTH_SECRET` is still the development default."},{"heading":"auth-and-app-origin","content":"Set `true` in production"},{"heading":"auth-and-app-origin","content":"`NEXT_PUBLIC_APP_URL`"},{"heading":"auth-and-app-origin","content":"Yes"},{"heading":"auth-and-app-origin","content":"Yes"},{"heading":"auth-and-app-origin","content":"Canonical origin of the app that reads it. Used for links in emails, invitation URLs, trusted origins and as the origin of that app's MCP server: `/mcp/user` on the web origin, `/mcp/admin` on the admin origin."},{"heading":"auth-and-app-origin","content":"Your deployment URL for each app"},{"heading":"independent-app-origins","content":"Both apps need to know each other's address. These two are at the end of `.env.example` and in `apps/admin/.env.local.example`."},{"heading":"independent-app-origins","content":"Variable"},{"heading":"independent-app-origins","content":"Required"},{"heading":"independent-app-origins","content":"Public"},{"heading":"independent-app-origins","content":"Purpose"},{"heading":"independent-app-origins","content":"Where to get it"},{"heading":"independent-app-origins","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"independent-app-origins","content":"Yes"},{"heading":"independent-app-origins","content":"Yes"},{"heading":"independent-app-origins","content":"The customer web origin. The admin app sends impersonation handoffs and customer login links there, and calls its `/api/internal/admin-revalidate` endpoint after admin mutations."},{"heading":"independent-app-origins","content":"Your web deployment URL; locally `http://localhost:3000`"},{"heading":"independent-app-origins","content":"`NEXT_PUBLIC_ADMIN_APP_URL`"},{"heading":"independent-app-origins","content":"Yes"},{"heading":"independent-app-origins","content":"Yes"},{"heading":"independent-app-origins","content":"The admin origin. The web app links administrators to `<admin origin>/app-admin` from the sidebar and returns there when an impersonation session ends."},{"heading":"independent-app-origins","content":"Your admin deployment URL; locally `http://localhost:3001`"},{"heading":"admin-app-appsadminenvlocalexample","content":"The admin app is a separate Next.js project, so it reads `apps/admin/.env.local` rather than the root file. Its template sets the origin variables to the admin address and leaves everything else to the shared defaults."},{"heading":"admin-app-appsadminenvlocalexample","content":"Variable"},{"heading":"admin-app-appsadminenvlocalexample","content":"Value on the admin app"},{"heading":"admin-app-appsadminenvlocalexample","content":"Purpose"},{"heading":"admin-app-appsadminenvlocalexample","content":"`BETTER_AUTH_URL`"},{"heading":"admin-app-appsadminenvlocalexample","content":"The admin origin, locally `http://localhost:3001`"},{"heading":"admin-app-appsadminenvlocalexample","content":"Better Auth base URL for the admin app. Its authentication cookies are host-only, so administrators sign in on the admin host."},{"heading":"admin-app-appsadminenvlocalexample","content":"`NEXT_PUBLIC_APP_URL`"},{"heading":"admin-app-appsadminenvlocalexample","content":"The admin origin"},{"heading":"admin-app-appsadminenvlocalexample","content":"Canonical admin origin; also the origin of the Admin MCP."},{"heading":"admin-app-appsadminenvlocalexample","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"admin-app-appsadminenvlocalexample","content":"The admin origin"},{"heading":"admin-app-appsadminenvlocalexample","content":"Site URL for the admin app, as `apps/admin/AGENTS.md` asks."},{"heading":"admin-app-appsadminenvlocalexample","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"admin-app-appsadminenvlocalexample","content":"The web origin, locally `http://localhost:3000`"},{"heading":"admin-app-appsadminenvlocalexample","content":"Where impersonation handoffs and login links go."},{"heading":"admin-app-appsadminenvlocalexample","content":"`NEXT_PUBLIC_ADMIN_APP_URL`"},{"heading":"admin-app-appsadminenvlocalexample","content":"The admin origin"},{"heading":"admin-app-appsadminenvlocalexample","content":"Same value as on web."},{"heading":"admin-app-appsadminenvlocalexample","content":"In production, also give the admin app the same `DATABASE_URL`, `BETTER_AUTH_SECRET`, `REVALIDATE_SECRET`, `S3_*` and Stripe variables as the web app; it shares the web app's server modules and uses them for marketing CMS uploads and the Stripe catalog sync on the payment gateway page. Locally the defaults in `env.ts` cover the shared values."},{"heading":"email","content":"Variable"},{"heading":"email","content":"Required"},{"heading":"email","content":"Public"},{"heading":"email","content":"Purpose"},{"heading":"email","content":"Where to get it"},{"heading":"email","content":"`MAIL_FROM`"},{"heading":"email","content":"Yes"},{"heading":"email","content":"No"},{"heading":"email","content":"Default sender, for example `Nextbase <hello@yourdomain.com>`."},{"heading":"email","content":"A verified sender on your domain"},{"heading":"email","content":"`EMAIL_PROVIDER`"},{"heading":"email","content":"Yes"},{"heading":"email","content":"No"},{"heading":"email","content":"`smtp` or `resend`. Selects the transport in `apps/web/src/lib/email/send-email.ts`."},{"heading":"email","content":"`smtp` locally with Mailpit, `resend` in production"},{"heading":"email","content":"`SMTP_HOST`"},{"heading":"email","content":"With `smtp`"},{"heading":"email","content":"No"},{"heading":"email","content":"SMTP host."},{"heading":"email","content":"Locally `127.0.0.1` (Mailpit)"},{"heading":"email","content":"`SMTP_PORT`"},{"heading":"email","content":"With `smtp`"},{"heading":"email","content":"No"},{"heading":"email","content":"SMTP port. Defaults to `1025`."},{"heading":"email","content":"Locally `1025`"},{"heading":"email","content":"`SMTP_USER`"},{"heading":"email","content":"No"},{"heading":"email","content":"No"},{"heading":"email","content":"SMTP username. Auth is skipped when empty."},{"heading":"email","content":"Your SMTP provider"},{"heading":"email","content":"`SMTP_PASSWORD`"},{"heading":"email","content":"No"},{"heading":"email","content":"No"},{"heading":"email","content":"SMTP password."},{"heading":"email","content":"Your SMTP provider"},{"heading":"email","content":"`SMTP_SECURE`"},{"heading":"email","content":"No"},{"heading":"email","content":"No"},{"heading":"email","content":"`true` to force TLS. Otherwise TLS is used only on port 465."},{"heading":"email","content":"Your SMTP provider"},{"heading":"email","content":"`RESEND_API_KEY`"},{"heading":"email","content":"With `resend`"},{"heading":"email","content":"No"},{"heading":"email","content":"Resend API key. The app throws at send time if `EMAIL_PROVIDER=resend` and this is empty."},{"heading":"email","content":"Resend dashboard"},{"heading":"email","content":"See emails with Resend for the delivery setup."},{"heading":"storage","content":"The kit talks to any S3-compatible service through the AWS SDK. Locally that is MinIO from the Compose stack. `apps/web/next.config.ts` derives a `next/image` `remotePatterns` entry from `S3_ENDPOINT`, so uploaded images render from whatever endpoint you configure."},{"heading":"storage","content":"Variable"},{"heading":"storage","content":"Required"},{"heading":"storage","content":"Public"},{"heading":"storage","content":"Purpose"},{"heading":"storage","content":"Where to get it"},{"heading":"storage","content":"`S3_ENDPOINT`"},{"heading":"storage","content":"Yes"},{"heading":"storage","content":"No"},{"heading":"storage","content":"S3 API endpoint. Also used for the image `remotePatterns` entry."},{"heading":"storage","content":"Your provider; locally `http://127.0.0.1:9000`"},{"heading":"storage","content":"`S3_REGION`"},{"heading":"storage","content":"Yes"},{"heading":"storage","content":"No"},{"heading":"storage","content":"Bucket region."},{"heading":"storage","content":"Your provider; locally `us-east-1`"},{"heading":"storage","content":"`S3_ACCESS_KEY_ID`"},{"heading":"storage","content":"Yes"},{"heading":"storage","content":"No"},{"heading":"storage","content":"Access key."},{"heading":"storage","content":"Your provider; locally `minio`"},{"heading":"storage","content":"`S3_SECRET_ACCESS_KEY`"},{"heading":"storage","content":"Yes"},{"heading":"storage","content":"No"},{"heading":"storage","content":"Secret key."},{"heading":"storage","content":"Your provider; locally `minio123`"},{"heading":"storage","content":"`S3_BUCKET_PUBLIC`"},{"heading":"storage","content":"Yes"},{"heading":"storage","content":"No"},{"heading":"storage","content":"Bucket for public objects such as avatars."},{"heading":"storage","content":"Create it; locally `nextbase-public` is created by `minio-init`"},{"heading":"storage","content":"`S3_BUCKET_PRIVATE`"},{"heading":"storage","content":"Yes"},{"heading":"storage","content":"No"},{"heading":"storage","content":"Bucket for private uploads served through presigned URLs."},{"heading":"storage","content":"Create it; locally `nextbase-private`"},{"heading":"storage","content":"`S3_FORCE_PATH_STYLE`"},{"heading":"storage","content":"No"},{"heading":"storage","content":"No"},{"heading":"storage","content":"`true` for path-style URLs (needed by MinIO)."},{"heading":"storage","content":"`true` locally; usually `false` for AWS S3"},{"heading":"realtime-and-cache","content":"Realtime in the browser is optional in production. `apps/web/src/lib/realtime/endpoint.ts` ignores `NEXT_PUBLIC_CENTRIFUGO_URL` in production unless it is a `wss://` URL that doesn't point at localhost."},{"heading":"realtime-and-cache","content":"Variable"},{"heading":"realtime-and-cache","content":"Required"},{"heading":"realtime-and-cache","content":"Public"},{"heading":"realtime-and-cache","content":"Purpose"},{"heading":"realtime-and-cache","content":"Where to get it"},{"heading":"realtime-and-cache","content":"`NEXT_PUBLIC_CENTRIFUGO_URL`"},{"heading":"realtime-and-cache","content":"No"},{"heading":"realtime-and-cache","content":"Yes"},{"heading":"realtime-and-cache","content":"WebSocket URL the browser connects to."},{"heading":"realtime-and-cache","content":"Your Centrifugo deployment; locally `ws://127.0.0.1:8000/connection/websocket`"},{"heading":"realtime-and-cache","content":"`CENTRIFUGO_URL`"},{"heading":"realtime-and-cache","content":"No"},{"heading":"realtime-and-cache","content":"No"},{"heading":"realtime-and-cache","content":"Server-side copy of the WebSocket URL."},{"heading":"realtime-and-cache","content":"Same as above"},{"heading":"realtime-and-cache","content":"`VALKEY_URL`"},{"heading":"realtime-and-cache","content":"No"},{"heading":"realtime-and-cache","content":"No"},{"heading":"realtime-and-cache","content":"Valkey or Redis URL for Better Auth's secondary storage, rate limits and locks. When unset, the kit uses the Postgres tables `auth_secondary_storage` and `auth_rate_windows` and advisory locks instead."},{"heading":"realtime-and-cache","content":"Your Valkey host; locally `redis://127.0.0.1:6379`"},{"heading":"realtime-and-cache","content":"`CENTRIFUGO_API_URL`"},{"heading":"realtime-and-cache","content":"With realtime"},{"heading":"realtime-and-cache","content":"No"},{"heading":"realtime-and-cache","content":"Centrifugo HTTP API used by the outbox dispatcher to publish."},{"heading":"realtime-and-cache","content":"Your Centrifugo deployment; locally `http://127.0.0.1:8000/api`"},{"heading":"realtime-and-cache","content":"`CENTRIFUGO_API_KEY`"},{"heading":"realtime-and-cache","content":"With realtime"},{"heading":"realtime-and-cache","content":"No"},{"heading":"realtime-and-cache","content":"API key for that HTTP API."},{"heading":"realtime-and-cache","content":"Centrifugo config; the local one is in `apps/database/local/centrifugo.json`"},{"heading":"realtime-and-cache","content":"`CENTRIFUGO_TOKEN_HMAC_SECRET`"},{"heading":"realtime-and-cache","content":"With realtime"},{"heading":"realtime-and-cache","content":"No"},{"heading":"realtime-and-cache","content":"HMAC secret used to sign 15-minute connection tokens for the browser. At least 16 characters."},{"heading":"realtime-and-cache","content":"Centrifugo config; must match the server"},{"heading":"billing-optional","content":"Billing uses the Better Auth Stripe plugin. Without these, billing pages render a disabled state."},{"heading":"billing-optional","content":"Variable"},{"heading":"billing-optional","content":"Required"},{"heading":"billing-optional","content":"Public"},{"heading":"billing-optional","content":"Purpose"},{"heading":"billing-optional","content":"Where to get it"},{"heading":"billing-optional","content":"`STRIPE_SECRET_KEY`"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"Stripe secret key."},{"heading":"billing-optional","content":"Stripe dashboard"},{"heading":"billing-optional","content":"`STRIPE_WEBHOOK_SECRET`"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"Signing secret for the webhook at `/api/stripe/webhooks` on the web app."},{"heading":"billing-optional","content":"Stripe webhook settings"},{"heading":"billing-optional","content":"`STRIPE_PRO_PRICE_ID`"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"Monthly price for the `pro` plan."},{"heading":"billing-optional","content":"Stripe product catalog"},{"heading":"billing-optional","content":"`STRIPE_PRO_ANNUAL_PRICE_ID`"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"Annual price for `pro`."},{"heading":"billing-optional","content":"Stripe product catalog"},{"heading":"billing-optional","content":"`STRIPE_ENTERPRISE_PRICE_ID`"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"Monthly price for the `enterprise` plan. The plan is registered only when this is set."},{"heading":"billing-optional","content":"Stripe product catalog"},{"heading":"billing-optional","content":"`STRIPE_ENTERPRISE_ANNUAL_PRICE_ID`"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"No"},{"heading":"billing-optional","content":"Annual price for `enterprise`."},{"heading":"billing-optional","content":"Stripe product catalog"},{"heading":"ai-optional","content":"Variable"},{"heading":"ai-optional","content":"Required"},{"heading":"ai-optional","content":"Public"},{"heading":"ai-optional","content":"Purpose"},{"heading":"ai-optional","content":"Where to get it"},{"heading":"ai-optional","content":"`OPENAI_API_KEY`"},{"heading":"ai-optional","content":"No"},{"heading":"ai-optional","content":"No"},{"heading":"ai-optional","content":"Enables the chat and image features."},{"heading":"ai-optional","content":"OpenAI dashboard"},{"heading":"ai-optional","content":"`OPENAI_CHAT_MODEL`"},{"heading":"ai-optional","content":"No"},{"heading":"ai-optional","content":"No"},{"heading":"ai-optional","content":"Chat model name. Defaults to `gpt-4o-mini`."},{"heading":"ai-optional","content":"Your choice"},{"heading":"ai-optional","content":"`OPENAI_IMAGE_MODEL`"},{"heading":"ai-optional","content":"No"},{"heading":"ai-optional","content":"No"},{"heading":"ai-optional","content":"Image model name. Defaults to `dall-e-2`."},{"heading":"ai-optional","content":"Your choice"},{"heading":"analytics-and-monitoring-optional","content":"Variable"},{"heading":"analytics-and-monitoring-optional","content":"Required"},{"heading":"analytics-and-monitoring-optional","content":"Public"},{"heading":"analytics-and-monitoring-optional","content":"Purpose"},{"heading":"analytics-and-monitoring-optional","content":"Where to get it"},{"heading":"analytics-and-monitoring-optional","content":"`NEXT_PUBLIC_POSTHOG_KEY`"},{"heading":"analytics-and-monitoring-optional","content":"No"},{"heading":"analytics-and-monitoring-optional","content":"Yes"},{"heading":"analytics-and-monitoring-optional","content":"PostHog project key."},{"heading":"analytics-and-monitoring-optional","content":"PostHog project settings"},{"heading":"analytics-and-monitoring-optional","content":"`NEXT_PUBLIC_POSTHOG_HOST`"},{"heading":"analytics-and-monitoring-optional","content":"No"},{"heading":"analytics-and-monitoring-optional","content":"Yes"},{"heading":"analytics-and-monitoring-optional","content":"PostHog host URL."},{"heading":"analytics-and-monitoring-optional","content":"PostHog project settings"},{"heading":"analytics-and-monitoring-optional","content":"`NEXT_PUBLIC_GA_MEASUREMENT_ID`"},{"heading":"analytics-and-monitoring-optional","content":"No"},{"heading":"analytics-and-monitoring-optional","content":"Yes"},{"heading":"analytics-and-monitoring-optional","content":"Google Analytics measurement ID."},{"heading":"analytics-and-monitoring-optional","content":"Google Analytics"},{"heading":"analytics-and-monitoring-optional","content":"`NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED`"},{"heading":"analytics-and-monitoring-optional","content":"No"},{"heading":"analytics-and-monitoring-optional","content":"Yes"},{"heading":"analytics-and-monitoring-optional","content":"`true` to load Vercel Analytics."},{"heading":"analytics-and-monitoring-optional","content":"Set `true` on Vercel if you want it"},{"heading":"analytics-and-monitoring-optional","content":"`NEXT_PUBLIC_SENTRY_DSN`"},{"heading":"analytics-and-monitoring-optional","content":"No"},{"heading":"analytics-and-monitoring-optional","content":"Yes"},{"heading":"analytics-and-monitoring-optional","content":"Sentry DSN. The Sentry wrapper in `apps/web/next.config.ts` is commented out; enable it there as well."},{"heading":"analytics-and-monitoring-optional","content":"Sentry project settings"},{"heading":"social-login-optional","content":"The server registers a provider only when both of its variables are set. See authentication for what the login form shows."},{"heading":"social-login-optional","content":"Variable"},{"heading":"social-login-optional","content":"Required"},{"heading":"social-login-optional","content":"Public"},{"heading":"social-login-optional","content":"Purpose"},{"heading":"social-login-optional","content":"Where to get it"},{"heading":"social-login-optional","content":"`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`"},{"heading":"social-login-optional","content":"No"},{"heading":"social-login-optional","content":"No"},{"heading":"social-login-optional","content":"Google OAuth app."},{"heading":"social-login-optional","content":"Google Cloud console"},{"heading":"social-login-optional","content":"`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`"},{"heading":"social-login-optional","content":"No"},{"heading":"social-login-optional","content":"No"},{"heading":"social-login-optional","content":"GitHub OAuth app."},{"heading":"social-login-optional","content":"GitHub developer settings"},{"heading":"social-login-optional","content":"`TWITTER_CLIENT_ID`, `TWITTER_CLIENT_SECRET`"},{"heading":"social-login-optional","content":"No"},{"heading":"social-login-optional","content":"No"},{"heading":"social-login-optional","content":"X (Twitter) OAuth app."},{"heading":"social-login-optional","content":"X developer portal"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"These are read by seeds, tests or single routes and are not in either example file."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Variable"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Purpose"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"`REVALIDATE_SECRET`"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Declared in `env.ts` as optional. Shared secret for `POST /api/revalidate` on the web app (sent in the `x-revalidate-secret` header) and for `POST /api/internal/admin-revalidate` (sent as a `Bearer` token), which the admin app calls after mutations to refresh the public web caches. Set the same value on both apps; both routes fail closed when it is unset."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"`SEED_PASSWORD`"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Password for the fixture accounts created by `pnpm --filter web db:seed`. Defaults to `password123`."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"`SEED_ADMIN_EMAIL`"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Email of the admin row created by the database seed. Defaults to `admin@example.com`."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"`NEXTBASE_SEED_MODE`"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Set to `true` by the clean-migration script so seeding skips verification emails, rate limits and secondary storage."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"`RUN_INTEGRATION_TESTS`"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Set to `true` by `pnpm test:integration` to enable the integration suites."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"`MCP_TEST_PG_URL`"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Loopback Postgres URL for `apps/web/src/mcp/pg-store.integration.test.ts`. Non-loopback hosts are rejected."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"`SITE_URL`, `NEXT_PUBLIC_SITE_URL`"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Set by `apps/web/.env.test` and the Playwright web server to the test origin on port 3100; the admin app's template sets `NEXT_PUBLIC_SITE_URL` to the admin origin."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"`PORT`"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Port for the Playwright web server. Defaults to `3100`."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"`ADMIN_BASE_URL`"},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"Origin the Playwright config builds and starts the admin app on. Defaults to `http://localhost:3101`."},{"heading":"variables-used-by-scripts-tests-and-internal-routes","content":"For production, see the list of what to set on each Vercel project in deployment."}],"headings":[{"id":"database","content":"Database"},{"id":"auth-and-app-origin","content":"Auth and app origin"},{"id":"independent-app-origins","content":"Independent app origins"},{"id":"admin-app-appsadminenvlocalexample","content":"Admin app (`apps/admin/.env.local.example`)"},{"id":"email","content":"Email"},{"id":"storage","content":"Storage"},{"id":"realtime-and-cache","content":"Realtime and cache"},{"id":"billing-optional","content":"Billing (optional)"},{"id":"ai-optional","content":"AI (optional)"},{"id":"analytics-and-monitoring-optional","content":"Analytics and monitoring (optional)"},{"id":"social-login-optional","content":"Social login (optional)"},{"id":"variables-used-by-scripts-tests-and-internal-routes","content":"Variables used by scripts, tests and internal routes"}]},"url":"/docs/v5/starters/nextjs-neon/environment-variables","title":"Environment variables","description":"Every variable in the Next.js + Neon starter's .env.example and apps/admin/.env.local.example, what it does and where the value comes from."},{"_id":"/docs/v5/starters/nextjs-neon","structured":{"contents":[{"content":"This kit uses Next.js App Router, Neon Postgres, Drizzle and Better Auth. You get the account, workspace and project foundation, a separately deployed admin app, and separate agent connections for users and application administrators."},{"content":"Open the live demo · Repository"},{"content":"The repository is `nextjs-planetscale-starter` because the kit started on PlanetScale. The current `main` branch is a Neon Postgres implementation: Drizzle with node-postgres, a Postgres 17 container for local work, and Postgres migrations in `apps/database/drizzle-postgres/`. The old MySQL migrations in `apps/database/drizzle/` are kept for reference only and are never applied. You don't need a PlanetScale account."},{"heading":"the-stack","content":"Layer"},{"heading":"the-stack","content":"What the kit uses"},{"heading":"the-stack","content":"Frontend"},{"heading":"the-stack","content":"Next.js 16 App Router with React 19, Tailwind CSS 4 and shadcn/ui"},{"heading":"the-stack","content":"Apps"},{"heading":"the-stack","content":"`apps/web` for customers on port 3000 and `apps/admin` for administrators on port 3001, sharing one database and the web app's server modules"},{"heading":"the-stack","content":"Database"},{"heading":"the-stack","content":"Neon Postgres in production, Postgres 17 in Docker locally, Drizzle ORM with node-postgres"},{"heading":"the-stack","content":"Authentication"},{"heading":"the-stack","content":"Better Auth with the admin, organization, magic link, API key, one-time token and Stripe plugins"},{"heading":"the-stack","content":"Storage"},{"heading":"the-stack","content":"Any S3-compatible bucket; MinIO locally"},{"heading":"the-stack","content":"Realtime"},{"heading":"the-stack","content":"Centrifugo fed by a transactional outbox and a dispatcher process"},{"heading":"the-stack","content":"Email"},{"heading":"the-stack","content":"React Email templates sent through SMTP (Mailpit locally) or Resend"},{"heading":"the-stack","content":"Hosting used by the demo"},{"heading":"the-stack","content":"Vercel for the website and the admin app (two projects), Neon for the database"},{"heading":"what-you-get-out-of-the-box","content":"Accounts with email and password, email verification, password reset, magic links, email change and account deletion."},{"heading":"what-you-get-out-of-the-box","content":"Workspaces built on Better Auth organizations, with `owner`, `admin`, `member` and `readonly` roles, invitations and member management."},{"heading":"what-you-get-out-of-the-box","content":"Projects inside a workspace, with comments and notifications."},{"heading":"what-you-get-out-of-the-box","content":"An onboarding flow at `/onboarding` and a user area at `/user/settings`, including security settings and API keys under `/user/settings/developer`."},{"heading":"what-you-get-out-of-the-box","content":"A separate admin app in `apps/admin` with its own sign-in page, an admin panel at `/app-admin` (users, workspaces, a marketing CMS for blog, authors, tags and changelog, payment gateway views and settings) and moderation views for feedback and the roadmap. Admins can ban users, change application roles, create users, send login links and impersonate customers through an audited handoff to the web app."},{"heading":"what-you-get-out-of-the-box","content":"A public feedback board and roadmap, plus marketing pages, blog and changelog in the web app."},{"heading":"what-you-get-out-of-the-box","content":"A User MCP at `/mcp/user` on the web app and an Admin MCP at `/mcp/admin` on the admin app, both read-only, with OAuth consent built in. See agent connections."},{"heading":"what-you-get-out-of-the-box","content":"Optional integrations that render a disabled state until configured: Stripe billing, OpenAI chat and image generation, PostHog, Google Analytics, Sentry and Vercel Analytics."},{"heading":"what-you-get-out-of-the-box","content":"Audit logs and outbox events written in the same transaction as every mutation."},{"heading":"what-you-get-out-of-the-box","content":"Shared instructions and skills for AI coding tools (Claude Code, Codex, Cursor and others), generated from `AGENTS.md` and `.agents/`."},{"heading":"what-you-get-out-of-the-box","content":"Authorization lives in server code. There is no row-level security for application data in this kit; every query and action checks the signed-in actor and their workspace membership."},{"heading":"prerequisites","content":"Node.js 24 or newer. The web and admin apps declare `\"node\": \">=24.0.0\"` and CI runs on Node 24."},{"heading":"prerequisites","content":"pnpm 10.33.0, declared in the root `package.json` `packageManager` field. Run `corepack enable` and let Corepack pick it."},{"heading":"prerequisites","content":"Docker with Compose. The local stack starts Postgres, MinIO, Mailpit, Valkey and Centrifugo from `apps/database/docker-compose.yml`."},{"heading":"prerequisites","content":"Access to the GitHub repository with the account that has your license."},{"heading":"prerequisites","content":"For production: a Neon account, a Vercel account (two projects, one per app), an S3-compatible bucket and a Resend account (or another SMTP sender). Stripe is optional."},{"heading":"next-steps","content":"Installation: clone, start the local stack and the two apps, and sign in with a seeded account."},{"heading":"next-steps","content":"Environment variables: every variable in `.env.example` and `apps/admin/.env.local.example` and what it does."},{"heading":"next-steps","content":"Authentication: how Better Auth is configured and how admin access works across the two apps."},{"heading":"next-steps","content":"Agent connections: the User and Admin MCP servers in this kit."},{"heading":"next-steps","content":"Project structure: the monorepo and where to make changes."},{"heading":"next-steps","content":"Deployment: Neon, migrations and the two Vercel projects."},{"heading":"next-steps","content":"For the shared concepts, read getting started and workspaces and the admin area."}],"headings":[{"id":"the-stack","content":"The stack"},{"id":"what-you-get-out-of-the-box","content":"What you get out of the box"},{"id":"prerequisites","content":"Prerequisites"},{"id":"next-steps","content":"Next steps"}]},"url":"/docs/v5/starters/nextjs-neon","title":"Next.js + Neon","description":"Overview of the Next.js + Neon starter: stack, what ships out of the box, prerequisites and where to go next."},{"_id":"/docs/v5/starters/nextjs-neon/installation","structured":{"contents":[{"content":"This page gets the kit running on your computer. You need Node.js 24, pnpm 10.33.0 (through Corepack) and Docker. The database is Neon Postgres in production; locally the kit runs Postgres 17 in a container, so you don't need a Neon account yet."},{"heading":"clone-the-repository","content":"The repository name still says PlanetScale. The `main` branch is the Neon Postgres version. If GitHub says the repository doesn't exist, sign in with the account that has access."},{"heading":"install-dependencies","content":"The root `package.json` pins `pnpm@10.33.0`, so Corepack installs the right version. `pnpm install` also runs `fumadocs-mdx` in `apps/web` to generate the `.source/` folder; don't edit that folder."},{"heading":"install-dependencies","content":"On Windows, run `git config core.symlinks true` before cloning. The `.claude/skills` entry is a symlink to `.agents/skills/`."},{"heading":"create-the-env-files","content":"The web app, the database package and the scripts share one env file at the repository root:"},{"heading":"create-the-env-files","content":"The admin app has its own template with the origin variables it needs:"},{"heading":"create-the-env-files","content":"The defaults point at the local Docker services and at `http://localhost:3000` for the web app and `http://localhost:3001` for the admin app, so you don't need to change anything to start. Keep your existing files if you already have them. The full list of variables is on the environment variables page."},{"heading":"start-the-stack-and-the-web-app","content":"This single command runs, in order:"},{"heading":"start-the-stack-and-the-web-app","content":"`pnpm db:start`: `docker compose up` for Postgres, MinIO, Mailpit, Valkey and Centrifugo, then a one-off `minio-init` container that creates the `nextbase-public` and `nextbase-private` buckets."},{"heading":"start-the-stack-and-the-web-app","content":"`pnpm db:migrate`: applies `apps/database/drizzle-postgres/` to the local database."},{"heading":"start-the-stack-and-the-web-app","content":"`pnpm db:seed`: the minimal database seed, then the full fixture seed from `apps/web/src/cli/seed.ts`. Both are idempotent."},{"heading":"start-the-stack-and-the-web-app","content":"The Next.js dev server for the web app (`pnpm web#dev`) and the outbox dispatcher (`pnpm realtime:dispatch`) together through `concurrently`."},{"heading":"start-the-stack-and-the-web-app","content":"Open http\\://localhost:3000 when the `web` process prints that it is ready."},{"heading":"start-the-stack-and-the-web-app","content":"Local services and their addresses:"},{"heading":"start-the-stack-and-the-web-app","content":"Service"},{"heading":"start-the-stack-and-the-web-app","content":"Address"},{"heading":"start-the-stack-and-the-web-app","content":"Postgres"},{"heading":"start-the-stack-and-the-web-app","content":"`postgresql://nextbase:nextbase@127.0.0.1:35577/nextbase`"},{"heading":"start-the-stack-and-the-web-app","content":"Mailpit inbox"},{"heading":"start-the-stack-and-the-web-app","content":"http\\://127.0.0.1:8025 (SMTP on port 1025)"},{"heading":"start-the-stack-and-the-web-app","content":"MinIO console"},{"heading":"start-the-stack-and-the-web-app","content":"http\\://127.0.0.1:9001 (S3 API on port 9000)"},{"heading":"start-the-stack-and-the-web-app","content":"Centrifugo"},{"heading":"start-the-stack-and-the-web-app","content":"`http://127.0.0.1:8000`"},{"heading":"start-the-stack-and-the-web-app","content":"Valkey"},{"heading":"start-the-stack-and-the-web-app","content":"`redis://127.0.0.1:6379`"},{"heading":"start-the-stack-and-the-web-app","content":"If an image can't be pulled, see troubleshooting. If you prefer to run the steps separately, use `pnpm db:start`, `pnpm db:migrate`, `pnpm db:seed` and then `pnpm web#dev`."},{"heading":"start-the-admin-app","content":"`pnpm local:start` doesn't start the admin app. In a second terminal, run:"},{"heading":"start-the-admin-app","content":"This starts `apps/admin` with `next dev --port 3001`. Open http\\://localhost:3001; it redirects to `/app-admin`, and to `/login` on the same host when you aren't signed in as an administrator. The web app's sidebar shows an **Open admin panel** link for administrators once `NEXT_PUBLIC_ADMIN_APP_URL` is set, which the root `.env.example` already does."},{"heading":"start-the-admin-app","content":"`pnpm dev` from the root runs the `dev` script of every package through Turborepo, which includes both Next.js apps, Drizzle Studio and the email preview server. The email preview also defaults to port 3001, so when you want both the admin app and the email preview, start the admin app on its own as above and give the preview another port."},{"heading":"sign-in","content":"The seed creates four accounts. All of them use the password `password123`:"},{"heading":"sign-in","content":"Email"},{"heading":"sign-in","content":"Role"},{"heading":"sign-in","content":"`admin@example.com`"},{"heading":"sign-in","content":"Application administrator"},{"heading":"sign-in","content":"`user@example.com`"},{"heading":"sign-in","content":"Workspace owner"},{"heading":"sign-in","content":"`user2@example.com`"},{"heading":"sign-in","content":"Second member and invitation fixture"},{"heading":"sign-in","content":"`invited@example.com`"},{"heading":"sign-in","content":"Pending invitation fixture"},{"heading":"sign-in","content":"Sign in at `http://localhost:3000/login` with `user@example.com` to see a workspace with a seeded project. Sign in at `http://localhost:3001/login` with `admin@example.com` to open the admin panel at `/app-admin`; the admin sign-in form takes email and password only."},{"heading":"sign-in","content":"To try the real flow, sign up at `/sign-up` on the web app with a new address. The verification email lands in Mailpit at `http://127.0.0.1:8025`. After verifying, complete onboarding and create a workspace, then add a project through the website so you have data for an agent later."},{"heading":"verify-the-setup","content":"Unit tests and static checks run without Docker:"},{"heading":"verify-the-setup","content":"Integration tests need the stack from `pnpm db:start`:"},{"heading":"verify-the-setup","content":"`pnpm verify:local` runs everything CI runs: provider purity, migration lint, a clean-database replay of migrations and seeds in a throwaway Postgres container, structural parity, lint, typecheck, unit tests, integration tests and a production build of every package."},{"heading":"verify-the-setup","content":"`pnpm test:e2e` builds both apps and starts the web app on port 3100 with `apps/web/.env.test` and the admin app on port 3101 (override with `ADMIN_BASE_URL`), then runs the Playwright suites. The `admin` specs run against the admin origin. Run `pnpm --filter web exec playwright install --with-deps chromium` once first."},{"heading":"stop-restart-or-reset","content":"Stop the containers and keep their data:"},{"heading":"stop-restart-or-reset","content":"`pnpm db:status` shows the Compose services. Starting again with `pnpm local:start` reapplies migrations and reruns the idempotent seeds, which is safe."},{"heading":"stop-restart-or-reset","content":"Data lives in the `postgres-data` and `minio-data` Docker volumes. The repository has no reset script; to start from an empty database, remove the volumes with Docker Compose from `apps/database`:"},{"heading":"useful-commands","content":"Task"},{"heading":"useful-commands","content":"Command"},{"heading":"useful-commands","content":"Web app only"},{"heading":"useful-commands","content":"`pnpm web#dev`"},{"heading":"useful-commands","content":"Admin app only (port 3001)"},{"heading":"useful-commands","content":"`pnpm --filter admin dev`"},{"heading":"useful-commands","content":"Build one app"},{"heading":"useful-commands","content":"`pnpm --filter web build` or `pnpm --filter admin build`"},{"heading":"useful-commands","content":"Drizzle Studio"},{"heading":"useful-commands","content":"`pnpm db:studio`"},{"heading":"useful-commands","content":"Generate a migration after editing the schema"},{"heading":"useful-commands","content":"`pnpm db:generate`"},{"heading":"useful-commands","content":"Regenerate `database/types`"},{"heading":"useful-commands","content":"`pnpm gen-types`"},{"heading":"useful-commands","content":"Email template preview (also defaults to port 3001)"},{"heading":"useful-commands","content":"`pnpm --filter email dev`"},{"heading":"useful-commands","content":"Run the outbox dispatcher once"},{"heading":"useful-commands","content":"`pnpm --filter web realtime:dispatch:once`"},{"heading":"useful-commands","content":"Remove expired unattached uploads"},{"heading":"useful-commands","content":"`pnpm --filter web storage:cleanup`"},{"heading":"useful-commands","content":"`pnpm db:push:force` pushes the schema directly without a migration file. Use it only for disposable local prototyping."},{"heading":"useful-commands","content":"When you can sign in and see a workspace, continue with authentication or connect an agent through agent connections."}],"headings":[{"id":"clone-the-repository","content":"Clone the repository"},{"id":"install-dependencies","content":"Install dependencies"},{"id":"create-the-env-files","content":"Create the env files"},{"id":"start-the-stack-and-the-web-app","content":"Start the stack and the web app"},{"id":"start-the-admin-app","content":"Start the admin app"},{"id":"sign-in","content":"Sign in"},{"id":"verify-the-setup","content":"Verify the setup"},{"id":"stop-restart-or-reset","content":"Stop, restart or reset"},{"id":"useful-commands","content":"Useful commands"}]},"url":"/docs/v5/starters/nextjs-neon/installation","title":"Installation","description":"Clone the Next.js + Neon starter, start the local Postgres stack with Docker, run the web and admin apps, seed and sign in."},{"_id":"/docs/v5/starters/nextjs-neon/project-structure","structured":{"contents":[{"content":"The kit is a pnpm workspace with Turborepo. Four apps and one shared package live under `apps/` and `packages/`. Everything is TypeScript, formatted with Prettier and linted with oxlint."},{"heading":"the-tree","content":"<File name=\"proxy.ts\" />"},{"heading":"the-tree","content":"<File name=\"typed-cache-tags.ts\" />"},{"heading":"the-tree","content":"<File name=\".env.test\" />"},{"heading":"the-tree","content":"<File name=\"next.config.ts\" />"},{"heading":"the-tree","content":"<File name=\"package.json\" />"},{"heading":"the-tree","content":"<File name=\".env.local.example\" />"},{"heading":"the-tree","content":"<File name=\"next.config.ts\" />"},{"heading":"the-tree","content":"<File name=\"package.json\" />"},{"heading":"the-tree","content":"<File name=\"tsconfig.json\" />"},{"heading":"the-tree","content":"<File name=\"schema.ts\" />"},{"heading":"the-tree","content":"<File name=\"client.ts\" />"},{"heading":"the-tree","content":"<File name=\"migrate.ts\" />"},{"heading":"the-tree","content":"<File name=\"seed.ts\" />"},{"heading":"the-tree","content":"<File name=\"database.types.ts\" />"},{"heading":"the-tree","content":"<File name=\"centrifugo.json\" />"},{"heading":"the-tree","content":"<File name=\"docker-compose.yml\" />"},{"heading":"the-tree","content":"<File name=\"drizzle.config.ts\" />"},{"heading":"the-tree","content":"<File name=\"index.tsx\" />"},{"heading":"the-tree","content":"<File name=\"verify-local.sh\" />"},{"heading":"the-tree","content":"<File name=\"verify-clean-migrations.sh\" />"},{"heading":"the-tree","content":"<File name=\"check-postgres-migrations.mjs\" />"},{"heading":"the-tree","content":"<File name=\"check-provider-purity.mjs\" />"},{"heading":"the-tree","content":"<File name=\"admin-app.md\" />"},{"heading":"the-tree","content":"<File name=\"mcp-connections.md\" />"},{"heading":"the-tree","content":"<File name=\"config.json\" />"},{"heading":"the-tree","content":"<File name=\".env.example\" />"},{"heading":"the-tree","content":"<File name=\"AGENTS.md\" />"},{"heading":"the-tree","content":"<File name=\"package.json\" />"},{"heading":"the-tree","content":"<File name=\"pnpm-workspace.yaml\" />"},{"heading":"the-tree","content":"<File name=\"turbo.json\" />"},{"heading":"the-tree","content":"Package"},{"heading":"the-tree","content":"Name"},{"heading":"the-tree","content":"What it is"},{"heading":"the-tree","content":"`apps/web`"},{"heading":"the-tree","content":"`web`"},{"heading":"the-tree","content":"The customer Next.js 16 app on port 3000: marketing pages, auth, workspaces and projects, server actions, domain logic, the User MCP, CLIs and Playwright tests"},{"heading":"the-tree","content":"`apps/admin`"},{"heading":"the-tree","content":"`admin`"},{"heading":"the-tree","content":"The administrator Next.js 16 app on port 3001: admin sign-in, the `/app-admin` panel, feedback and roadmap moderation, the Admin MCP and the impersonation handoff"},{"heading":"the-tree","content":"`apps/database`"},{"heading":"the-tree","content":"`database`"},{"heading":"the-tree","content":"Drizzle schema, Postgres migrations, migrate and seed scripts, generated types, the Docker Compose stack"},{"heading":"the-tree","content":"`apps/email`"},{"heading":"the-tree","content":"`email`"},{"heading":"the-tree","content":"React Email templates and the render helpers the web app imports"},{"heading":"the-tree","content":"`packages/typescript-config`"},{"heading":"the-tree","content":"Shared `tsconfig.base.json`"},{"heading":"the-tree","content":"`apps/database/drizzle/` holds the historical MySQL migrations from the PlanetScale version. They are never applied; leave them alone."},{"heading":"how-the-admin-app-shares-code","content":"`apps/admin/tsconfig.json` maps `@/*` to `./src/*` first and `../web/src/*` second, so an import such as `@/lib/auth/session` or `@/components/ui/button` resolves to the web app's file unless the admin app has its own copy. The admin app therefore has no `lib/`, no `rsc-data/` and only a handful of components; everything it owns is routes under `src/app/`, administrative queries and actions under `src/data/admin/`, and the admin sidebar and breadcrumbs under `src/components/`. Its `next.config.ts` sets the Turbopack root to the repository so those cross-app imports compile. Keep customer-facing code in `apps/web`; put administrative mutations only in `apps/admin`."},{"heading":"routes-and-pages","content":"Public marketing pages: `apps/web/src/app/(external-pages)/`. Nav links are in `components/navigation-menu/external-navbar/constants.ts`."},{"heading":"routes-and-pages","content":"Signed-in customer area: `apps/web/src/app/(dynamic-pages)/(authenticated-pages)/`. Workspace pages sit under `(application-pages)/workspace/[workspaceSlug]/`, project pages under `(application-pages)/project/[projectSlug]/`, and account settings under `user/`. Workspace pages have parallel `@navbar` and `@sidebar` slots with `default.tsx` fallbacks."},{"heading":"routes-and-pages","content":"Auth pages: `(login-pages)` and `(misc-login-pages)`. Public feedback and roadmap: `(updates-pages)`. The impersonation landing page is `apps/web/src/app/auth/impersonation/page.tsx`."},{"heading":"routes-and-pages","content":"Admin panel: `apps/admin/src/app/(dynamic-pages)/(authenticated-pages)/app-admin/(admin-pages)/` with `users`, `workspaces`, `settings`, `payment-gateway` and `marketing/{authors,blog,tags,changelog}`, plus `@navbar` and `@sidebar` slots. Feedback moderation is `apps/admin/src/app/feedback/`, roadmap moderation `apps/admin/src/app/roadmap/`, and the admin sign-in page `apps/admin/src/app/login/page.tsx`."},{"heading":"routes-and-pages","content":"Route handlers: `apps/web/src/app/api/`. Better Auth is at `api/auth/[...all]` (and again in the admin app), the Stripe webhook at `api/stripe/webhooks`, uploads under `api/storage/uploads`, the realtime token at `api/realtime/token`, the impersonation redemption at `api/impersonation`, and the cache refresh the admin app calls at `api/internal/admin-revalidate`. The admin app adds `api/admin/marketing/media` for CMS uploads."},{"heading":"routes-and-pages","content":"Sidebar links: `apps/web/src/components/sidebar-workspace-nav.tsx` and `sidebar-platform-nav.tsx` in the web app; `apps/web/src/components/sidebar-admin-panel-nav.tsx` renders the **Open admin panel** link; `apps/admin/src/components/sidebar-admin-panel-nav.tsx` is the admin app's menu."},{"heading":"routes-and-pages","content":"A page exports `metadata`, awaits `params`, parses them with zod from `utils/zod-schemas/params.ts`, and wraps dynamic content in `<Suspense>`. `cacheComponents` is on in both apps, so cached reads use `\"use cache\"` with tags from `typed-cache-tags.ts`. Admin data must stay live and permission-scoped; never prerender it into public caches."},{"heading":"server-and-data-access","content":"`apps/web/src/data/{anon,user}/` and `apps/admin/src/data/admin/` are `\"use server\"` modules with queries and next-cool-action actions. `authActionClient` and `adminActionClient` come from `apps/web/src/lib/safe-action.ts`."},{"heading":"server-and-data-access","content":"`apps/web/src/rsc-data/` wraps those queries with React `cache()` for server components, for example `getCachedWorkspaceBySlug`."},{"heading":"server-and-data-access","content":"Reusable rules live in `lib/domain/` (policies with tests, `events.ts` for audit and outbox rows) and `lib/services/`."},{"heading":"server-and-data-access","content":"Admin mutations that change public content call `refreshWebAfterAdminMutation` from `apps/web/src/lib/admin-revalidation.ts` so the web app revalidates."},{"heading":"server-and-data-access","content":"Import `db`, `pool`, `schema` and `newId` from the `database` package. Don't create new pools."},{"heading":"schema-and-migrations","content":"Edit `apps/database/src/schema.ts`, then `pnpm db:generate`, review the SQL in `apps/database/drizzle-postgres/`, `pnpm db:check-migrations`, `pnpm db:migrate`, `pnpm gen-types`, update the seeds, then `pnpm db:verify-clean`."},{"heading":"schema-and-migrations","content":"Migrations are additive and must replay on an empty database. Never edit an applied migration or its snapshot."},{"heading":"schema-and-migrations","content":"Seeds: `apps/database/src/seed.ts` (minimal) and `apps/web/src/cli/seed.ts` (full fixtures). Both must stay idempotent."},{"heading":"schema-and-migrations","content":"The `db-migration` skill in `.agents/skills/db-migration/SKILL.md` describes the whole loop."},{"heading":"authentication","content":"`apps/web/src/lib/auth/` holds the Better Auth server and client, session helpers, actors, workspace permissions and the impersonation handoff helpers. The admin app reuses all of it. See authentication."},{"heading":"emails","content":"Templates are in `apps/email/emails/*.tsx`. Add a render helper in `apps/email/src/index.tsx` and a render test, then send through `apps/web/src/lib/email/send-email.ts`. Preview templates with `pnpm --filter email dev`; the preview server defaults to port 3001, the same port as the admin app, so pass another port when both are running."},{"heading":"background-jobs","content":"There are no queues. Mutations write to `outbox_events`, and `apps/web/src/cli/dispatch-outbox.ts` claims batches of 50 and publishes them to Centrifugo (`pnpm realtime:dispatch`, or `realtime:dispatch:once` for a single pass). `apps/web/src/cli/cleanup-storage.ts` removes expired unattached uploads. Realtime helpers are in `lib/realtime/`, storage in `lib/storage/uploads.ts` and `lib/storage/provider.ts`."},{"heading":"mcp","content":"`apps/web/src/mcp/` holds the implementation. The web app mounts it for the user audience in `app/mcp`, `app/oauth` and `app/.well-known`; the admin app mounts the same handler for the admin audience in its own `app/mcp`, `app/oauth` and `app/.well-known`. See agent connections."},{"heading":"tests","content":"Unit tests sit next to the code as `*.test.ts(x)` and run with `pnpm test` (Vitest). Tests for the shared auth code and the handoff routes live in `apps/web`, including `apps/web/src/app/api/impersonation/route.test.ts`."},{"heading":"tests","content":"Integration tests are in `apps/web/src/integration/*.integration.test.ts` and need the Docker stack: `pnpm test:integration`."},{"heading":"tests","content":"Playwright specs are in `apps/web/e2e/` (`anon`, `user`, `admin`, `invitations`, `lifecycle`, `password-updation`, `visual`, with `_helpers` and `_setups`), configured in `apps/web/playwright/playwright.config.mts`. The config builds both apps and starts the web app on port 3100 with `apps/web/.env.test` and the admin app on port 3101; the `admin` project and the admin setup run against the admin origin."},{"heading":"tests","content":"Repository gates: `scripts/check-provider-purity.mjs`, `scripts/check-postgres-migrations.mjs`, `scripts/verify-clean-migrations.sh` and `scripts/check-ultimate-parity.mjs`, all run by `pnpm verify:local` and in `.github/workflows/ci.yml`."},{"heading":"ai-coding-tool-configuration","content":"Edit only `AGENTS.md` (root and per app, including `apps/admin/AGENTS.md`), `.agents/config.json` and `.agents/skills/`, then run `pnpm agents:sync`. `CLAUDE.md`, `GEMINI.md`, `.mcp.json`, `.cursor/`, `.vscode/mcp.json` and the other tool files are generated, and CI runs `pnpm agents:check` to catch drift."}],"headings":[{"id":"the-tree","content":"The tree"},{"id":"how-the-admin-app-shares-code","content":"How the admin app shares code"},{"id":"where-to-make-changes","content":"Where to make changes"},{"id":"routes-and-pages","content":"Routes and pages"},{"id":"server-and-data-access","content":"Server and data access"},{"id":"schema-and-migrations","content":"Schema and migrations"},{"id":"authentication","content":"Authentication"},{"id":"emails","content":"Emails"},{"id":"background-jobs","content":"Background jobs"},{"id":"mcp","content":"MCP"},{"id":"tests","content":"Tests"},{"id":"ai-coding-tool-configuration","content":"AI coding tool configuration"}]},"url":"/docs/v5/starters/nextjs-neon/project-structure","title":"Project structure","description":"The Next.js + Neon monorepo at a glance and where to make changes for routes, the admin app, data, schema, auth, emails, jobs, MCP and tests."},{"_id":"/docs/v5/starters/nextjs-supabase/agent-connections","structured":{"contents":[{"content":"This kit exposes two remote MCP servers, one per origin: the User MCP on the web app and the Admin MCP on the admin app. They use the Supabase Auth session you already have; nothing else is installed on the user's side. For the cross-kit overview read user and admin MCPs, and for the client walkthrough read connect an agent."},{"heading":"where-users-manage-connections","content":"Connection"},{"heading":"where-users-manage-connections","content":"App and settings page"},{"heading":"where-users-manage-connections","content":"Live demo server"},{"heading":"where-users-manage-connections","content":"User"},{"heading":"where-users-manage-connections","content":"Web app, `/mcp/connections/user`"},{"heading":"where-users-manage-connections","content":"`https://nextjs-supabase.usenextbase.com/mcp/user`"},{"heading":"where-users-manage-connections","content":"Admin"},{"heading":"where-users-manage-connections","content":"Admin app, `/mcp/connections/admin`"},{"heading":"where-users-manage-connections","content":"`https://nextjs-supabase-admin.usenextbase.com/mcp/admin`"},{"heading":"where-users-manage-connections","content":"The web app's sidebar links to **Agent connections** (`sidebar-platform-nav.tsx`); the admin app's sidebar links to **Admin agent connections** (`sidebar-admin-panel-nav.tsx`). Each page shows the server address to copy, the account's active connections for that server and a **Disconnect** button. The admin page requires an application administrator session on the admin origin."},{"heading":"where-users-manage-connections","content":"Each app serves only its own audience. `apps/web/src/mcp/adapter.ts` passes `audiences: [\"user\"]` and `apps/admin/src/mcp/adapter.ts` passes `audiences: [\"admin\"]`, so `/mcp/admin` on the web origin and `/mcp/user` on the admin origin return 404. For your own deployment the addresses are the web app's `NEXT_PUBLIC_SITE_URL` followed by `/mcp/user` and the admin app's `NEXT_PUBLIC_SITE_URL` followed by `/mcp/admin`. The settings pages print the exact values."},{"heading":"tools-and-permissions","content":"Both servers are read-only in v5. Every tool carries `readOnlyHint: true` and `destructiveHint: false`."},{"heading":"tools-and-permissions","content":"Server"},{"heading":"tools-and-permissions","content":"Tool"},{"heading":"tools-and-permissions","content":"Scope"},{"heading":"tools-and-permissions","content":"What it returns"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`account_context`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"The signed-in user's id and email, and the workspace approved for this connection"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`projects_list`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"Up to `limit` projects (1 to 50, default 20) in the approved workspace, newest first"},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_users_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"Up to `limit` user profiles (`id`, `full_name`, `created_at`)"},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_workspaces_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"Up to `limit` workspaces (`id`, `name`, `slug`, `created_at`)"},{"heading":"tools-and-permissions","content":"A user connection is bound to one workspace chosen during consent. Only workspaces where the account is `owner` or `admin`, or has the `view_projects` permission, are offered. `projects_list` runs the SQL function `mcp_projects`, which rechecks the live session and the membership in the same statement as the read."},{"heading":"tools-and-permissions","content":"Admin tools call `mcp_live_actor` on every request and fail with \"Administrator access required\" if the `user_roles` row is gone. A user token is never accepted at `/mcp/admin`, even for an administrator."},{"heading":"oauth-and-consent","content":"Each origin is its own authorization server. The issuer is `{NEXT_PUBLIC_SITE_URL}/oauth` of the app that serves the resource. Discovery documents on each origin:"},{"heading":"oauth-and-consent","content":"`/.well-known/oauth-protected-resource/mcp/user` (web) or `/.well-known/oauth-protected-resource/mcp/admin` (admin)"},{"heading":"oauth-and-consent","content":"`/.well-known/oauth-authorization-server/oauth`"},{"heading":"oauth-and-consent","content":"Endpoints are `/oauth/register`, `/oauth/authorize`, `/oauth/token` and `/oauth/revoke`. Public clients only, with dynamic registration, authorization code and mandatory PKCE S256. Callback URLs must be HTTPS, except loopback HTTP on `localhost`, `127.0.0.1` or `::1`."},{"heading":"oauth-and-consent","content":"The consent page is `/mcp/authorize`. If you are not signed in, the login page receives `next=/mcp/authorize?...` and the app sends you back through `/auth/callback` to the same pending request. The page shows the client name (marked unverified), callback address, server and scope, and for user connections a workspace selector. If the account has no eligible workspace, approval is disabled until you create or join one."},{"heading":"oauth-and-consent","content":"Lifetimes from `src/mcp/core.ts`: pending consent 10 minutes, authorization codes 5 minutes, access tokens 15 minutes, refresh for at most 30 days and never past the original Supabase session. Refresh tokens rotate; reuse revokes the whole connection. The UI limits each account to 20 active connections."},{"heading":"oauth-and-consent","content":"Browser clients are accepted from the configured app origin, `https://claude.ai` and `https://chatgpt.com`. Desktop and server clients without an `Origin` header are accepted too."},{"heading":"where-the-code-lives","content":"The MCP module is duplicated in both apps; keep the copies in sync when you change the protocol core."},{"heading":"where-the-code-lives","content":"File"},{"heading":"where-the-code-lives","content":"Role"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.ts` and `apps/admin/src/mcp/core.ts`"},{"heading":"where-the-code-lives","content":"Protocol, OAuth endpoints, consent and connection pages, tool registration. Shared with the other Postgres kits"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/supabase-adapter.ts` and `apps/admin/src/mcp/supabase-adapter.ts`"},{"heading":"where-the-code-lives","content":"Supabase-specific actor checks, workspace lookup and the tool implementations"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/supabase-store.ts` and `apps/admin/src/mcp/supabase-store.ts`"},{"heading":"where-the-code-lives","content":"Storage of registrations, pending consents and credentials in `public.mcp_records`"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/adapter.ts` (`audiences: [\"user\"]`) and `apps/admin/src/mcp/adapter.ts` (`audiences: [\"admin\"]`)"},{"heading":"where-the-code-lives","content":"Build the handler from each app's `NEXT_PUBLIC_SITE_URL`, `NEXT_PUBLIC_SUPABASE_URL` and `SUPABASE_SECRET_KEY`"},{"heading":"where-the-code-lives","content":"`src/app/mcp/[...path]/route.ts`, `src/app/oauth/[...path]/route.ts`, `src/app/.well-known/...` in each app"},{"heading":"where-the-code-lives","content":"Thin Next.js route handlers that forward to `handleMcp`"},{"heading":"where-the-code-lives","content":"`apps/database/supabase/migrations/20261014114351_user_admin_mcp.sql`"},{"heading":"where-the-code-lives","content":"`mcp_records` table and the `mcp_live_actor` and `mcp_projects` functions, shared by both apps"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.test.ts`"},{"heading":"where-the-code-lives","content":"Vitest coverage for protocol, PKCE, CSRF, replay, expiry, demotion and revocation. The admin copy has no tests of its own"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/supabase-store.integration.test.ts`"},{"heading":"where-the-code-lives","content":"Storage tests against a local database, enabled by the `MCP_TEST_SUPABASE_*` variables"},{"heading":"where-the-code-lives","content":"The migration grants `mcp_records` and both functions to `service_role` only. `anon` and `authenticated` cannot read credentials or call the bridge functions. The repository's own write-up is `docs/mcp-connections.md`."},{"heading":"adding-a-tool-safely","content":"Add the tool in the `audience === \"user\"` branch of `createMcpHandler` in the web app's `core.ts`, or in the admin branch of the admin app's `core.ts`, with the same `annotations` object and a bounded `limit` input."},{"heading":"adding-a-tool-safely","content":"Implement the data access in that app's `supabase-adapter.ts`. Call `liveActor(actor)` first, and for workspace data pass the actor's user id and session id into a SQL function that checks membership inside the query, like `mcp_projects` does."},{"heading":"adding-a-tool-safely","content":"Never trust `userId` or `workspaceId` arguments from the client. The grant's workspace and the live session are the only source of permission."},{"heading":"adding-a-tool-safely","content":"If the tool writes data, add a new scope. `requiredScope` currently only knows `user:read` and `admin:read`, and the consent page labels everything \"Read access only\". Changing that needs new consent copy and tests."},{"heading":"adding-a-tool-safely","content":"Extend `apps/web/src/mcp/core.test.ts`, run `pnpm test`, and run the integration test against a local database. Mirror protocol changes into the admin copy."},{"heading":"adding-a-tool-safely","content":"See tools and permissions for the shared rules, and troubleshooting for 401 and 403 cases."}],"headings":[{"id":"where-users-manage-connections","content":"Where users manage connections"},{"id":"tools-and-permissions","content":"Tools and permissions"},{"id":"oauth-and-consent","content":"OAuth and consent"},{"id":"where-the-code-lives","content":"Where the code lives"},{"id":"adding-a-tool-safely","content":"Adding a tool safely"}]},"url":"/docs/v5/starters/nextjs-supabase/agent-connections","title":"Agent connections","description":"The User MCP and Admin MCP of the Next.js + Supabase starter: endpoints on the web and admin origins, tools, consent flow and where the code lives."},{"_id":"/docs/v5/starters/nextjs-supabase/authentication","structured":{"contents":[{"content":"This kit uses Supabase Auth for every account, including the sessions that approve agent connections and the sessions on the admin app. Nothing on this page is a second login system. For the cross-kit concepts, read the shared authentication page."},{"heading":"where-the-configuration-lives","content":"Concern"},{"heading":"where-the-configuration-lives","content":"Location"},{"heading":"where-the-configuration-lives","content":"Local Auth settings (site URL, redirect URLs, confirmations, providers)"},{"heading":"where-the-configuration-lives","content":"`apps/database/supabase/config.toml`, `[auth]` section"},{"heading":"where-the-configuration-lives","content":"Hosted Auth settings"},{"heading":"where-the-configuration-lives","content":"The Supabase dashboard for your project"},{"heading":"where-the-configuration-lives","content":"Supabase URL and keys for the apps"},{"heading":"where-the-configuration-lives","content":"`apps/web/.env.local` and `apps/admin/.env.local`, see environment variables"},{"heading":"where-the-configuration-lives","content":"Sign-in, sign-up, magic link and provider server actions"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/data/auth/auth.ts`"},{"heading":"where-the-configuration-lives","content":"Login and sign-up pages"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/app/(dynamic-pages)/(login-pages)/login` and `.../sign-up`"},{"heading":"where-the-configuration-lives","content":"Password reset and account pages"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/app/(dynamic-pages)/(misc-login-pages)/` (`forgot-password`, `update-password`, `confirm-delete-user`, `auth/*`)"},{"heading":"where-the-configuration-lives","content":"Supabase clients per context"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/supabase-clients/{user,admin,anon,unkey}/` and `apps/admin/src/supabase-clients/{user,admin,anon}/`"},{"heading":"where-the-configuration-lives","content":"Web middleware chain"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/proxy.ts` and `apps/web/src/middlewares/`"},{"heading":"where-the-configuration-lives","content":"Admin app login and gate"},{"heading":"where-the-configuration-lives","content":"`apps/admin/src/app/login/`, `apps/admin/src/app/auth/{callback,confirm}/` and `apps/admin/src/proxy.ts`"},{"heading":"email-and-password","content":"Sign-up calls `supabase.auth.signUp` with `emailRedirectTo` pointing at `/auth/callback`. Local `config.toml` sets `enable_confirmations = true`, so a new user must open the verification email before signing in. The app shows a \"check your email\" card until then."},{"heading":"email-and-password","content":"Locally, verification emails arrive in Mailpit at the `MAILPIT_URL` printed by `supabase status --output-format json`. In production, configure SMTP for the Supabase project. The web app's `RESEND_API_KEY` does not affect Supabase Auth emails. See emails."},{"heading":"email-and-password","content":"Two routes complete email flows on the web app:"},{"heading":"email-and-password","content":"`/auth/callback` exchanges a `code` for a session and then redirects to `next` or `/dashboard`."},{"heading":"email-and-password","content":"`/auth/confirm` verifies a `token_hash` for email confirmation or password recovery and redirects to `/dashboard` or `/update-password`."},{"heading":"email-and-password","content":"Password reset starts at `/forgot-password` and finishes at `/update-password`. `config.toml` lists exactly four local redirect URLs: `/auth/callback` and `/auth/confirm` on `http://localhost:3000` (web) and on `http://localhost:3002` (admin). The `site_url` stays on the web origin."},{"heading":"magic-links-and-social-providers","content":"`signInWithMagicLinkAction` uses `signInWithOtp` with the same callback. The login page offers it alongside the password form."},{"heading":"magic-links-and-social-providers","content":"Social login is implemented server-side in `signInWithProviderAction`. The app's provider list is `google`, `github` and `twitter` (`apps/web/src/utils/zod-schemas/social-providers.ts`), and `config.toml` enables all three for local Auth with credentials read from `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `TWITTER_API_KEY` and `TWITTER_API_SECRET`."},{"heading":"magic-links-and-social-providers","content":"The login and sign-up pages do not render provider buttons. The components exist (`apps/web/src/components/authentication/render-providers.tsx` and `apps/web/src/app/(dynamic-pages)/(login-pages)/sign-up/provider-signup-form.tsx`). To turn a provider on, render `ProviderSignupForm`, add the provider's credentials to your env, and register the callback URL in the provider's console and in Supabase Auth."},{"heading":"sessions-and-protected-routes","content":"`apps/web/src/proxy.ts` runs the list in `middlewares/middleware-list.ts` in order for matching paths:"},{"heading":"sessions-and-protected-routes","content":"`authMiddleware` on the protected paths from `middlewares/paths.ts` (`/dashboard`, `/settings`, `/profile`, `/workspace`, `/project`, `/home`, `/user`, `/logout` and `/onboarding`). It calls `supabase.auth.getClaims()`, refreshes cookies and redirects to `/login` when there is no session. Claims are parsed with `userClaimsSchema` so malformed tokens fail loudly."},{"heading":"sessions-and-protected-routes","content":"`dashboardOnboardingMiddleware` and `onboardingRedirectMiddleware` send users to `/onboarding` until `onboardingHasAcceptedTerms`, `onboardingHasCompletedProfile` and `onboardingHasCreatedWorkspace` are all set in their user metadata, and back to `/dashboard` afterwards."},{"heading":"sessions-and-protected-routes","content":"The session is resolved once and passed along the chain. Server components, server actions and route handlers each use a matching client from `src/supabase-clients/user/`. The service-role client in `src/supabase-clients/admin/` is server-only and is never imported into client code. There is no `/app-admin` route in the web app any more; `app-admin` is only a reserved slug in `apps/web/src/constants.ts`."},{"heading":"becoming-an-application-admin","content":"The admin role is stored in two places that must agree:"},{"heading":"becoming-an-application-admin","content":"A row in `public.user_roles` with `role = 'admin'`. RLS policies and the Admin MCP check this table through `is_application_admin()` and `mcp_live_actor()`."},{"heading":"becoming-an-application-admin","content":"`app_metadata.user_role = 'admin'` on the Auth user. The admin app's `proxy.ts` and `/app-admin` layout read this claim through `isSupabaseUserClaimAppAdmin` (`utils/is-supabase-user-app-admin.ts`, present in both apps)."},{"heading":"becoming-an-application-admin","content":"The trusted way to set both is the SQL function `make_user_app_admin(user_id uuid)` from `apps/database/supabase/migrations/20240901054606_application_admin.sql`. It can only run as `postgres`, `supabase_admin` or `dashboard_user`, so call it from the Supabase SQL editor or `psql`, never from the app:"},{"heading":"becoming-an-application-admin","content":"The user then signs out and back in so the new claim is in their token. `remove_app_admin_privilege_for_user(user_id)` reverses it. A normal account cannot write `app_metadata` or `user_roles`; both are revoked from `anon` and `authenticated`."},{"heading":"becoming-an-application-admin","content":"Locally, `seed.sql` already calls `make_user_app_admin` for the seeded admin user `testadmin@usenextbase.com`. The Playwright admin setup does the same through a direct Postgres connection."},{"heading":"becoming-an-application-admin","content":"A `custom_access_token_hook` function also ships in the migrations, but the `[auth.hook.custom_access_token]` block in `config.toml` is commented out. The admin claim works without it because `make_user_app_admin` writes `raw_app_meta_data` directly."},{"heading":"the-admin-app","content":"Administration is a separate Next.js app, `apps/admin`, that deploys on its own origin (port 3002 locally) and shares the Supabase project with the web app. See `docs/admin-app.md` in the repository."},{"heading":"the-admin-app","content":"`/login` is an email and password form (`signInWithPassword`). The admin app has its own `/auth/callback` and `/auth/confirm` handlers and a `/logout` route; `/` redirects to `/app-admin`."},{"heading":"the-admin-app","content":"`apps/admin/src/proxy.ts` runs on `/app-admin`, `/feedback` and `/roadmap`. Without a session it redirects to `/login?next=...`; with a session whose `app_metadata.user_role` is not `admin` it returns 403."},{"heading":"the-admin-app","content":"The `/app-admin` layout rechecks the claim and calls `unauthorized()` otherwise. Mutations go through `adminActionClient` in `apps/admin/src/lib/safe-action.ts`, which verifies the admin role again before the service-role client is used."},{"heading":"the-admin-app","content":"`/app-admin` holds users, workspaces, marketing content (blog, changelog, authors, tags), payment gateway data and settings; `/feedback` and `/roadmap` on the admin origin are the moderation views."},{"heading":"the-admin-app","content":"Support sign-in and impersonation (`apps/admin/src/data/admin/user.tsx`) call `auth.admin.generateLink` and build a link to the web app's `/auth/confirm` using `NEXT_PUBLIC_WEB_APP_URL`. The web app verifies the token and sets its own host-scoped cookies; the admin session never leaves the admin origin."},{"heading":"the-admin-app","content":"Owning a workspace does not make someone an application admin. See workspaces and the admin area."},{"heading":"what-rls-protects","content":"Row Level Security is enabled on every user-facing table defined in `apps/database/supabase/migrations/`. Policies key off `auth.uid()`, workspace membership in `workspace_members`, the `workspace_member_role` enum (`owner`, `admin`, `member`, `readonly`) and per-member `permissions`. Workspace settings are split into a member-visible table and an admin-only table."},{"heading":"what-rls-protects","content":"`apps/database/supabase/tests/` holds pgTap suites for user, workspace, project and marketing tables, function behaviour, security isolation, RLS enforcement and Data API grant safety. Run them with `pnpm --filter database test-db` while Supabase is running, and add a test whenever you add a table or policy."},{"heading":"what-rls-protects","content":"The MCP credential table `mcp_records` is also behind RLS with all browser grants revoked; only `service_role` can read it. See agent connections."}],"headings":[{"id":"where-the-configuration-lives","content":"Where the configuration lives"},{"id":"email-and-password","content":"Email and password"},{"id":"magic-links-and-social-providers","content":"Magic links and social providers"},{"id":"sessions-and-protected-routes","content":"Sessions and protected routes"},{"id":"becoming-an-application-admin","content":"Becoming an application admin"},{"id":"the-admin-app","content":"The admin app"},{"id":"what-rls-protects","content":"What RLS protects"}]},"url":"/docs/v5/starters/nextjs-supabase/authentication","title":"Authentication","description":"How Supabase Auth, email verification, social providers, the admin role, the separate admin app and RLS work in the Next.js + Supabase starter."},{"_id":"/docs/v5/starters/nextjs-supabase/deployment","structured":{"contents":[{"content":"The live demo runs on Vercel as two projects, the web app at `nextjs-supabase.usenextbase.com` and the admin app at `nextjs-supabase-admin.usenextbase.com`, sharing one hosted Supabase project. This page covers what is specific to this kit. For the platform-generic steps read deploy on Vercel. The repository also ships `apps/web/netlify.toml` (`command = \"pnpm run build\"`) if you prefer Netlify for the web app; the env work is the same. The repository's own notes are in `docs/admin-app.md`."},{"heading":"create-and-link-the-supabase-project","content":"Create a project in the Supabase dashboard. From the database package, link the CLI to it:"},{"heading":"create-and-link-the-supabase-project","content":"The example env files have a commented `SUPABASE_PROJECT_REF` line you can use to remember the ref; neither app reads it."},{"heading":"apply-migrations","content":"This applies everything in `apps/database/supabase/migrations/`, including `20261014114351_user_admin_mcp.sql`, which creates `mcp_records` and the `mcp_live_actor` and `mcp_projects` functions. The MCP routes on both apps return errors until that migration is applied, so push before the first deploy."},{"heading":"apply-migrations","content":"`seed.sql` is for local development only; `db push` does not run it. Then regenerate types from the linked project if you want them to match production:"},{"heading":"configure-supabase-auth-for-both-domains","content":"In the project's Auth settings:"},{"heading":"configure-supabase-auth-for-both-domains","content":"Set the site URL to the web app's production origin, for example `https://app.example.com`. Keep it on the web origin, not the admin origin."},{"heading":"configure-supabase-auth-for-both-domains","content":"Add redirect URLs for `/auth/callback` and `/auth/confirm` on both origins: `https://app.example.com/auth/callback`, `https://app.example.com/auth/confirm`, `https://admin.example.com/auth/callback` and `https://admin.example.com/auth/confirm`. This mirrors the four entries in the local `config.toml`."},{"heading":"configure-supabase-auth-for-both-domains","content":"Keep email confirmations on if you want the same behaviour as local."},{"heading":"configure-supabase-auth-for-both-domains","content":"Configure SMTP (the demo uses Resend) so verification, magic link and recovery emails are delivered. See emails."},{"heading":"configure-supabase-auth-for-both-domains","content":"Add social provider credentials only for the providers you render in the web app. See authentication."},{"heading":"configure-supabase-auth-for-both-domains","content":"Preview deployments need their own redirect URLs if you test login there."},{"heading":"create-two-vercel-projects","content":"Import the repository twice. Set the root directory to `apps/web` for the first project and `apps/admin` for the second, and keep access to the whole workspace enabled so each build can resolve `database` and `@nextbase/typescript-config`. Vercel detects Next.js and pnpm from the lockfile; each project builds with its package's `build` script. Map your product domain to the web project and the admin domain to the admin project (the demo uses the `<subdomain>-admin` pattern)."},{"heading":"create-two-vercel-projects","content":"The web `build` script first runs `scripts/generate-env-local.ts`. When `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY` are present in the build environment, the script exits immediately. If any is missing, it tries to run `supabase status` and the build fails. Set all three before the first build. The admin `build` script is plain `next build`."},{"heading":"set-production-environment-variables","content":"Use the exact names from environment variables. The minimum for the web project:"},{"heading":"set-production-environment-variables","content":"Variable"},{"heading":"set-production-environment-variables","content":"Value"},{"heading":"set-production-environment-variables","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"set-production-environment-variables","content":"The web origin, no trailing path, for example `https://app.example.com`"},{"heading":"set-production-environment-variables","content":"`NEXT_PUBLIC_SUPABASE_URL`"},{"heading":"set-production-environment-variables","content":"The hosted project's API URL"},{"heading":"set-production-environment-variables","content":"`NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`"},{"heading":"set-production-environment-variables","content":"The hosted project's publishable key"},{"heading":"set-production-environment-variables","content":"`SUPABASE_SECRET_KEY`"},{"heading":"set-production-environment-variables","content":"The hosted project's secret key, server-only"},{"heading":"set-production-environment-variables","content":"`ADMIN_EMAIL`"},{"heading":"set-production-environment-variables","content":"A sender address verified in Resend"},{"heading":"set-production-environment-variables","content":"`RESEND_API_KEY`"},{"heading":"set-production-environment-variables","content":"Your Resend key"},{"heading":"set-production-environment-variables","content":"`REVALIDATE_SECRET`"},{"heading":"set-production-environment-variables","content":"A random string, identical on both projects"},{"heading":"set-production-environment-variables","content":"The minimum for the admin project:"},{"heading":"set-production-environment-variables","content":"Variable"},{"heading":"set-production-environment-variables","content":"Value"},{"heading":"set-production-environment-variables","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"set-production-environment-variables","content":"The admin origin, for example `https://admin.example.com`"},{"heading":"set-production-environment-variables","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"set-production-environment-variables","content":"The web origin, for example `https://app.example.com`. Support sign-in links and cache invalidation calls go here"},{"heading":"set-production-environment-variables","content":"`NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SECRET_KEY`"},{"heading":"set-production-environment-variables","content":"The same hosted project"},{"heading":"set-production-environment-variables","content":"`REVALIDATE_SECRET`"},{"heading":"set-production-environment-variables","content":"The same value as the web project"},{"heading":"set-production-environment-variables","content":"Leave `USE_LOCAL_EMAIL` unset on both. Add `STRIPE_SECRET_KEY`, `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` and `STRIPE_WEBHOOK_SECRET` to the web project if you keep billing, then create a Stripe webhook endpoint pointing at `https://app.example.com/api/stripe/webhooks`; add them to the admin project too if you use the payment gateway pages. Add `ADMIN_EMAIL` and `RESEND_API_KEY` to the admin project if admin actions send email. Add Sentry, PostHog, Unkey, OpenAI and OTEL variables only for the features you use. Never give a server secret a `NEXT_PUBLIC_` prefix."},{"heading":"set-production-environment-variables","content":"`NEXT_PUBLIC_SITE_URL` matters more here than in most kits: on each app it is the OAuth issuer for that app's MCP server (`{NEXT_PUBLIC_SITE_URL}/oauth`) and the resource prefix for `/mcp/user` or `/mcp/admin`. Set it to the exact origin users will visit; a mismatch breaks agent connections even when the site itself works."},{"heading":"verify-locally-then-deploy","content":"Deploy previews of both projects first, then production. If you use the repository's signoff flow, `pnpm signoff` runs the same checks and posts the commit status."},{"heading":"make-yourself-an-application-admin","content":"Find your user id in the Supabase dashboard after you sign up on the web app, then in the SQL editor:"},{"heading":"make-yourself-an-application-admin","content":"Sign out and back in. Sign in at the admin origin's `/login` with the same email and password; `/app-admin` and `/mcp/connections/admin` are now available there. The function refuses to run from the app's own database roles, so this is the only supported path."},{"heading":"post-deploy-smoke-checks","content":"`GET /api/health` on the web origin returns `{\"ok\":true}`."},{"heading":"post-deploy-smoke-checks","content":"Open `/sign-up` on the web app, create an account, receive the verification email and finish onboarding with a new workspace."},{"heading":"post-deploy-smoke-checks","content":"Open `/login`, sign in with the password, then try `/forgot-password` and finish at `/update-password`."},{"heading":"post-deploy-smoke-checks","content":"Create a project, invite a second user and accept the invitation from the email."},{"heading":"post-deploy-smoke-checks","content":"Open `/mcp/connections/user` on the web app and confirm the displayed server address uses the web origin. Open `/.well-known/oauth-protected-resource/mcp/user` and `/.well-known/oauth-authorization-server/oauth` there and check the URLs inside."},{"heading":"post-deploy-smoke-checks","content":"Follow connect an agent, approve a workspace and ask the agent to run `projects_list`."},{"heading":"post-deploy-smoke-checks","content":"As an admin, open the admin origin: `/login`, then `/app-admin` and `/mcp/connections/admin`. Open `/.well-known/oauth-protected-resource/mcp/admin` there, then connect and call `admin_workspaces_list`. The web origin's `/mcp/admin` should return 404."},{"heading":"post-deploy-smoke-checks","content":"From `/app-admin/users`, open a user and generate a support sign-in link; it must point at the web origin's `/auth/confirm` and sign you in there without touching the admin session."},{"heading":"post-deploy-smoke-checks","content":"Publish a blog post or changelog entry from the admin app and confirm it appears on the web app; a failure here usually means `REVALIDATE_SECRET` or `NEXT_PUBLIC_WEB_APP_URL` differs between the projects."},{"heading":"post-deploy-smoke-checks","content":"If billing is on, trigger a Stripe test checkout and confirm the webhook is received."},{"heading":"updating-a-running-deployment","content":"Apply new migrations with `supabase db push` from `apps/database` before deploying the web or admin build that depends on them. Keep the Supabase key names unchanged; the apps do not read the legacy anon or service-role names. If you change `NEXT_PUBLIC_SITE_URL` on either app, that app's existing agent connections are bound to the old issuer and users need to reconnect. If you change `NEXT_PUBLIC_WEB_APP_URL` on the admin app, update the Supabase redirect URLs to match."},{"heading":"updating-a-running-deployment","content":"For login redirect, email and 401/403 problems after deploying, see troubleshooting."}],"headings":[{"id":"create-and-link-the-supabase-project","content":"Create and link the Supabase project"},{"id":"apply-migrations","content":"Apply migrations"},{"id":"configure-supabase-auth-for-both-domains","content":"Configure Supabase Auth for both domains"},{"id":"create-two-vercel-projects","content":"Create two Vercel projects"},{"id":"set-production-environment-variables","content":"Set production environment variables"},{"id":"verify-locally-then-deploy","content":"Verify locally, then deploy"},{"id":"make-yourself-an-application-admin","content":"Make yourself an application admin"},{"id":"post-deploy-smoke-checks","content":"Post-deploy smoke checks"},{"id":"updating-a-running-deployment","content":"Updating a running deployment"}]},"url":"/docs/v5/starters/nextjs-supabase/deployment","title":"Deployment","description":"Provision a Supabase project, apply migrations and deploy the Next.js + Supabase starter's web and admin apps to Vercel."},{"_id":"/docs/v5/starters/nextjs-supabase/environment-variables","structured":{"contents":[{"content":"The kit has one env file per Next.js app: `apps/web/.env.local` (from `apps/web/.env.local.example`) and `apps/admin/.env.local` (from `apps/admin/.env.local.example`). Each app also ships a `.env.test` with the same placeholders; the web one is the template the Playwright global setup turns into `apps/web/.env.test.local`. The database and email apps have no env files of their own."},{"content":"`pnpm supabase:sync-env` writes the local Supabase URL and keys into both `.env.local` files. Variables prefixed `NEXT_PUBLIC_` are bundled into the browser. Everything else is server-only. Never commit `.env.local`; it is in `.gitignore`."},{"content":"This kit reads `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY`. The older `NEXT_PUBLIC_SUPABASE_ANON_KEY` and `SUPABASE_SERVICE_ROLE_KEY` names are not read anywhere in either app."},{"heading":"supabase","content":"Variable"},{"heading":"supabase","content":"Required"},{"heading":"supabase","content":"Exposure"},{"heading":"supabase","content":"Purpose"},{"heading":"supabase","content":"Where to get it"},{"heading":"supabase","content":"`NEXT_PUBLIC_SUPABASE_URL`"},{"heading":"supabase","content":"Yes"},{"heading":"supabase","content":"Public"},{"heading":"supabase","content":"Supabase API URL used by every client, middleware, the MCP adapter and the Supabase Storage `remotePatterns` entry in `next.config.ts`"},{"heading":"supabase","content":"`pnpm supabase:sync-env`, or `env.API_URL` from `supabase status --output-format json`. Production: project settings in the Supabase dashboard"},{"heading":"supabase","content":"`NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`"},{"heading":"supabase","content":"Yes"},{"heading":"supabase","content":"Public"},{"heading":"supabase","content":"Browser and user-session clients"},{"heading":"supabase","content":"`pnpm supabase:sync-env`, or `env.PUBLISHABLE_KEY`. Production: API keys in the dashboard"},{"heading":"supabase","content":"`SUPABASE_SECRET_KEY`"},{"heading":"supabase","content":"Yes"},{"heading":"supabase","content":"Server"},{"heading":"supabase","content":"Service-role client in `src/supabase-clients/admin/` and `src/mcp/`"},{"heading":"supabase","content":"`pnpm supabase:sync-env`, or `env.SECRET_KEY`. Production: API keys in the dashboard"},{"heading":"supabase","content":"`SUPABASE_DATABASE_PASSWORD`"},{"heading":"supabase","content":"Optional"},{"heading":"supabase","content":"Server"},{"heading":"supabase","content":"Present in the example with the local default. Not read by the web app's source"},{"heading":"supabase","content":"Local default is `postgres`"},{"heading":"supabase","content":"`SUPABASE_POSTGRES_DB_URL`"},{"heading":"supabase","content":"Tests"},{"heading":"supabase","content":"Server"},{"heading":"supabase","content":"Direct Postgres connection for the Playwright admin helpers. The global setup overwrites it in `.env.test.local` from `env.DB_URL`"},{"heading":"supabase","content":"`env.DB_URL` from `supabase status`"},{"heading":"supabase","content":"`SUPABASE_JWT_SIGNING_KEY`"},{"heading":"supabase","content":"Optional"},{"heading":"supabase","content":"Server"},{"heading":"supabase","content":"Signs tokens for the Unkey-backed Supabase client in `src/supabase-clients/unkey/`"},{"heading":"supabase","content":"Only needed when you issue API keys through Unkey"},{"heading":"supabase","content":"`SUPABASE_PROJECT_REF`"},{"heading":"supabase","content":"Optional, commented out"},{"heading":"supabase","content":"Server"},{"heading":"supabase","content":"Reference for linking the CLI to a hosted project. Not read by the web app's source"},{"heading":"supabase","content":"Supabase project settings"},{"heading":"stripe","content":"Variable"},{"heading":"stripe","content":"Required"},{"heading":"stripe","content":"Exposure"},{"heading":"stripe","content":"Purpose"},{"heading":"stripe","content":"Where to get it"},{"heading":"stripe","content":"`STRIPE_SECRET_KEY`"},{"heading":"stripe","content":"For billing"},{"heading":"stripe","content":"Server"},{"heading":"stripe","content":"Payment gateway in `src/payments/stripe-payment-gateway.ts`"},{"heading":"stripe","content":"Stripe dashboard, API keys"},{"heading":"stripe","content":"`NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`"},{"heading":"stripe","content":"For billing"},{"heading":"stripe","content":"Public"},{"heading":"stripe","content":"Stripe.js in the browser"},{"heading":"stripe","content":"Stripe dashboard, API keys"},{"heading":"stripe","content":"`STRIPE_WEBHOOK_SECRET`"},{"heading":"stripe","content":"For billing"},{"heading":"stripe","content":"Server"},{"heading":"stripe","content":"Verifies `/api/stripe/webhooks` signatures"},{"heading":"stripe","content":"Stripe webhook endpoint, or the secret printed by `stripe listen --forward-to localhost:3000/api/stripe/webhooks`"},{"heading":"host","content":"Variable"},{"heading":"host","content":"Required"},{"heading":"host","content":"Exposure"},{"heading":"host","content":"Purpose"},{"heading":"host","content":"Where to get it"},{"heading":"host","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"host","content":"Yes"},{"heading":"host","content":"Public"},{"heading":"host","content":"Canonical web origin for redirects, auth callbacks and the User MCP issuer. Falls back to `NEXT_PUBLIC_VERCEL_URL` when unset"},{"heading":"host","content":"`http://localhost:3000` locally; your production domain in deployment"},{"heading":"email","content":"Variable"},{"heading":"email","content":"Required"},{"heading":"email","content":"Exposure"},{"heading":"email","content":"Purpose"},{"heading":"email","content":"Where to get it"},{"heading":"email","content":"`ADMIN_EMAIL`"},{"heading":"email","content":"Yes"},{"heading":"email","content":"Server"},{"heading":"email","content":"Sender and admin notification address for app emails"},{"heading":"email","content":"Your own address; use a verified Resend sender in production"},{"heading":"email","content":"`RESEND_API_KEY`"},{"heading":"email","content":"Production"},{"heading":"email","content":"Server"},{"heading":"email","content":"Resend client for app emails when `USE_LOCAL_EMAIL` is not `true`"},{"heading":"email","content":"Resend dashboard"},{"heading":"email","content":"`USE_LOCAL_EMAIL`"},{"heading":"email","content":"Local"},{"heading":"email","content":"Server"},{"heading":"email","content":"When `true`, app emails go to the Mailpit API instead of Resend"},{"heading":"email","content":"Set `true` locally, remove or set `false` in production"},{"heading":"email","content":"`MAILPIT_URL`"},{"heading":"email","content":"Local, commented out"},{"heading":"email","content":"Server"},{"heading":"email","content":"Mailpit base URL for `src/utils/send-email-mailpit.ts` and the Playwright mail helper. Defaults to `http://localhost:54324` when unset"},{"heading":"email","content":"`env.MAILPIT_URL` from `supabase status --output-format json`"},{"heading":"email","content":"Supabase Auth emails (verification, magic link, recovery) are not sent by the web app. Configure SMTP in the Supabase project; see emails."},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Variable"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Required"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Exposure"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Purpose"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Where to get it"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"`NEXT_PUBLIC_POSTHOG_API_KEY`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Optional"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Public"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"PostHog browser and server clients"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"PostHog project settings"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"`NEXT_PUBLIC_POSTHOG_APP_ID`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Optional"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Public"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Listed in the example and typed in `environment.d.ts`; not read by the app source"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"PostHog"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"`NEXT_PUBLIC_POSTHOG_HOST`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Optional"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Public"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"PostHog ingestion host"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"PostHog project settings"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"`NEXT_PUBLIC_GA_ID`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Optional"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Public"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Google Analytics measurement ID"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Google Analytics"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"`UNKEY_ROOT_KEY`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Optional"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Server"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Issue and verify API keys from `/user/settings/developer`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Unkey dashboard"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"`UNKEY_API_ID`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Optional"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Server"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"The Unkey API that keys belong to"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Unkey dashboard"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"`OPENAI_API_KEY`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Optional"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Server"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"AI chat route (`/api/chat`) and DALL-E image generation"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"OpenAI dashboard"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"`NEXT_PUBLIC_SENTRY_DSN`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Optional"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Public"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Read by `sentry.client.config.ts`, `sentry.server.config.ts` and `sentry.edge.config.ts`. Sentry stays off until you uncomment `withSentryConfig` in `next.config.ts`"},{"heading":"analytics-api-keys-ai-and-error-tracking-optional","content":"Sentry project settings"},{"heading":"supabase-social-providers-optional","content":"Variable"},{"heading":"supabase-social-providers-optional","content":"Required"},{"heading":"supabase-social-providers-optional","content":"Exposure"},{"heading":"supabase-social-providers-optional","content":"Purpose"},{"heading":"supabase-social-providers-optional","content":"Where to get it"},{"heading":"supabase-social-providers-optional","content":"`TWITTER_API_KEY`, `TWITTER_API_SECRET`"},{"heading":"supabase-social-providers-optional","content":"Optional"},{"heading":"supabase-social-providers-optional","content":"Server"},{"heading":"supabase-social-providers-optional","content":"Read by `config.toml` via `env(...)` for `[auth.external.twitter]`"},{"heading":"supabase-social-providers-optional","content":"Twitter developer portal"},{"heading":"supabase-social-providers-optional","content":"`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`"},{"heading":"supabase-social-providers-optional","content":"Optional"},{"heading":"supabase-social-providers-optional","content":"Server"},{"heading":"supabase-social-providers-optional","content":"`[auth.external.google]` in `config.toml`"},{"heading":"supabase-social-providers-optional","content":"Google Cloud console"},{"heading":"supabase-social-providers-optional","content":"`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`"},{"heading":"supabase-social-providers-optional","content":"Optional"},{"heading":"supabase-social-providers-optional","content":"Server"},{"heading":"supabase-social-providers-optional","content":"`[auth.external.github]` in `config.toml`"},{"heading":"supabase-social-providers-optional","content":"GitHub OAuth apps"},{"heading":"supabase-social-providers-optional","content":"These are consumed by the Supabase CLI when it starts the local project, not by Next.js. For a hosted project, enter them in the Supabase Auth providers settings instead. See authentication."},{"heading":"observability-and-revalidation-optional","content":"Variable"},{"heading":"observability-and-revalidation-optional","content":"Required"},{"heading":"observability-and-revalidation-optional","content":"Exposure"},{"heading":"observability-and-revalidation-optional","content":"Purpose"},{"heading":"observability-and-revalidation-optional","content":"Where to get it"},{"heading":"observability-and-revalidation-optional","content":"`OTEL_ENABLED`"},{"heading":"observability-and-revalidation-optional","content":"Optional"},{"heading":"observability-and-revalidation-optional","content":"Server"},{"heading":"observability-and-revalidation-optional","content":"Turns on `@vercel/otel` tracing in `instrumentation.ts`"},{"heading":"observability-and-revalidation-optional","content":"`true` or unset"},{"heading":"observability-and-revalidation-optional","content":"`OTEL_SERVICE_NAME`"},{"heading":"observability-and-revalidation-optional","content":"Optional"},{"heading":"observability-and-revalidation-optional","content":"Server"},{"heading":"observability-and-revalidation-optional","content":"Service name reported in traces"},{"heading":"observability-and-revalidation-optional","content":"Any name; example uses `nextbase-ultimate-web`"},{"heading":"observability-and-revalidation-optional","content":"`OTEL_EXPORTER_OTLP_ENDPOINT`"},{"heading":"observability-and-revalidation-optional","content":"Optional"},{"heading":"observability-and-revalidation-optional","content":"Server"},{"heading":"observability-and-revalidation-optional","content":"OTLP collector endpoint. Standard OpenTelemetry variable, not read directly by app code"},{"heading":"observability-and-revalidation-optional","content":"Your collector"},{"heading":"observability-and-revalidation-optional","content":"`OTEL_EXPORTER_OTLP_HEADERS`"},{"heading":"observability-and-revalidation-optional","content":"Optional"},{"heading":"observability-and-revalidation-optional","content":"Server"},{"heading":"observability-and-revalidation-optional","content":"OTLP exporter headers. Standard OpenTelemetry variable"},{"heading":"observability-and-revalidation-optional","content":"Your collector"},{"heading":"observability-and-revalidation-optional","content":"`LOG_LEVEL`"},{"heading":"observability-and-revalidation-optional","content":"Optional"},{"heading":"observability-and-revalidation-optional","content":"Server"},{"heading":"observability-and-revalidation-optional","content":"pino log level for `src/utils/logger.ts` and `src/lib/logger.ts`"},{"heading":"observability-and-revalidation-optional","content":"`info`, `debug`, and so on"},{"heading":"observability-and-revalidation-optional","content":"`REVALIDATE_SECRET`"},{"heading":"observability-and-revalidation-optional","content":"With the admin app"},{"heading":"observability-and-revalidation-optional","content":"Server"},{"heading":"observability-and-revalidation-optional","content":"Expected `x-revalidate-secret` header for `POST /api/revalidate`, and the Bearer token the admin app sends to `POST /api/internal/admin-revalidate` after publishing content. Must match the admin app's value"},{"heading":"observability-and-revalidation-optional","content":"Generate a random string and set it on both apps"},{"heading":"appsadminenvlocal","content":"The admin example repeats the Supabase, Stripe, email, analytics, provider, OTEL, `LOG_LEVEL` and `REVALIDATE_SECRET` lines above (without `SUPABASE_POSTGRES_DB_URL`, `SUPABASE_JWT_SIGNING_KEY`, `MAILPIT_URL`, `OPENAI_API_KEY` and `NEXT_PUBLIC_SENTRY_DSN`). Supabase values must be the same project as the web app. The admin app's source reads these:"},{"heading":"appsadminenvlocal","content":"Variable"},{"heading":"appsadminenvlocal","content":"Required"},{"heading":"appsadminenvlocal","content":"Exposure"},{"heading":"appsadminenvlocal","content":"Purpose"},{"heading":"appsadminenvlocal","content":"Where to get it"},{"heading":"appsadminenvlocal","content":"`NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SECRET_KEY`"},{"heading":"appsadminenvlocal","content":"Yes"},{"heading":"appsadminenvlocal","content":"Public, Public, Server"},{"heading":"appsadminenvlocal","content":"Same meaning as in the web app; the secret key powers the service-role queries in `src/data/admin/` and the Admin MCP"},{"heading":"appsadminenvlocal","content":"`pnpm supabase:sync-env` locally; the dashboard in production"},{"heading":"appsadminenvlocal","content":"`NEXT_PUBLIC_SITE_URL`"},{"heading":"appsadminenvlocal","content":"Yes"},{"heading":"appsadminenvlocal","content":"Public"},{"heading":"appsadminenvlocal","content":"The admin app's own origin: auth callbacks and the Admin MCP issuer. Falls back to `NEXT_PUBLIC_VERCEL_URL`"},{"heading":"appsadminenvlocal","content":"`http://localhost:3002` locally; the admin domain in production"},{"heading":"appsadminenvlocal","content":"`NEXT_PUBLIC_WEB_APP_URL`"},{"heading":"appsadminenvlocal","content":"Yes"},{"heading":"appsadminenvlocal","content":"Public"},{"heading":"appsadminenvlocal","content":"The web app's origin. Used for the \"Open web app\" link, support sign-in and impersonation links (which land on the web app's `/auth/confirm`) and the cache invalidation call. Must be a bare HTTP(S) origin"},{"heading":"appsadminenvlocal","content":"`http://localhost:3000` locally; the product domain in production"},{"heading":"appsadminenvlocal","content":"`REVALIDATE_SECRET`"},{"heading":"appsadminenvlocal","content":"Yes for publishing"},{"heading":"appsadminenvlocal","content":"Server"},{"heading":"appsadminenvlocal","content":"Sent as a Bearer token to the web app's `/api/internal/admin-revalidate` so published blog, changelog and feedback changes appear on the web app"},{"heading":"appsadminenvlocal","content":"Same value as the web app"},{"heading":"appsadminenvlocal","content":"`ADMIN_EMAIL`, `RESEND_API_KEY`, `USE_LOCAL_EMAIL`"},{"heading":"appsadminenvlocal","content":"For admin emails"},{"heading":"appsadminenvlocal","content":"Server"},{"heading":"appsadminenvlocal","content":"Emails sent from admin actions through `src/utils/api-routes/utils.ts`. The admin copy of `send-email-mailpit.ts` posts to `http://localhost:54324` and does not read `MAILPIT_URL`"},{"heading":"appsadminenvlocal","content":"As for the web app"},{"heading":"appsadminenvlocal","content":"`STRIPE_SECRET_KEY`, `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`, `STRIPE_WEBHOOK_SECRET`"},{"heading":"appsadminenvlocal","content":"For the payment gateway pages"},{"heading":"appsadminenvlocal","content":"Server, Public, Server"},{"heading":"appsadminenvlocal","content":"Payment gateway controls under `/app-admin/payment-gateway`"},{"heading":"appsadminenvlocal","content":"Stripe dashboard"},{"heading":"appsadminenvlocal","content":"`SUPABASE_DATABASE_PASSWORD`, the PostHog, GA and Unkey variables, the social provider credentials, the OTEL variables and `LOG_LEVEL` are present in the admin example but not read by the admin app's source."},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Variable"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Exposure"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Purpose"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Notes"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"`NEXT_PUBLIC_APP_NAME`"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Public"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Fallback service name in `apps/web/instrumentation.ts`"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Optional"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"`NEXT_PUBLIC_VERCEL_URL`"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Public"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Fallback origin when `NEXT_PUBLIC_SITE_URL` is unset, in both apps"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Set by Vercel automatically"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"`PORT`"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Server"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Port for the web app's `pnpm start` and the Playwright `baseURL`"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Defaults to 3000"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"`WEB_BASE_URL`, `ADMIN_BASE_URL`"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Test only"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Point Playwright at already running web and admin servers instead of starting them"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Default to `http://localhost:3000` and `http://localhost:3002`"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"`MCP_TEST_SUPABASE_URL`, `MCP_TEST_SUPABASE_SERVICE_KEY`, `MCP_TEST_SUPABASE_ANON_KEY`"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Test only"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Enable `apps/web/src/mcp/supabase-store.integration.test.ts` against a local database"},{"heading":"referenced-in-code-but-not-in-the-example-files","content":"Loopback hosts only"},{"heading":"production-checklist","content":"Web app: `NEXT_PUBLIC_SITE_URL`, `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SECRET_KEY`, `ADMIN_EMAIL`, `RESEND_API_KEY` and `REVALIDATE_SECRET`. Add the three Stripe variables if you keep billing. Do not set `USE_LOCAL_EMAIL=true` in production."},{"heading":"production-checklist","content":"Admin app: the three Supabase variables, `NEXT_PUBLIC_SITE_URL` (admin origin), `NEXT_PUBLIC_WEB_APP_URL` (web origin) and the same `REVALIDATE_SECRET`. Add Stripe and email variables if you use those controls. The deployment page has the full flow."}],"headings":[{"id":"appswebenvlocal","content":"apps/web/.env.local"},{"id":"supabase","content":"Supabase"},{"id":"stripe","content":"Stripe"},{"id":"host","content":"Host"},{"id":"email","content":"Email"},{"id":"analytics-api-keys-ai-and-error-tracking-optional","content":"Analytics, API keys, AI and error tracking (optional)"},{"id":"supabase-social-providers-optional","content":"Supabase social providers (optional)"},{"id":"observability-and-revalidation-optional","content":"Observability and revalidation (optional)"},{"id":"appsadminenvlocal","content":"apps/admin/.env.local"},{"id":"referenced-in-code-but-not-in-the-example-files","content":"Referenced in code but not in the example files"},{"id":"production-checklist","content":"Production checklist"}]},"url":"/docs/v5/starters/nextjs-supabase/environment-variables","title":"Environment variables","description":"Every variable in the Next.js + Supabase starter's env files for the web and admin apps, what it does and where the value comes from."},{"_id":"/docs/v5/starters/nextjs-supabase","structured":{"contents":[{"content":"This kit uses Next.js App Router, Supabase Postgres and Supabase Auth. You get the account, workspace and project foundation, a separately deployed admin app, and separate agent connections for users and application administrators."},{"content":"Open the live demo · Repository"},{"heading":"the-stack","content":"Layer"},{"heading":"the-stack","content":"What the kit uses"},{"heading":"the-stack","content":"Frontend"},{"heading":"the-stack","content":"Next.js 16 App Router on React 19, Tailwind CSS v4, shadcn/ui"},{"heading":"the-stack","content":"Database"},{"heading":"the-stack","content":"Supabase Postgres with Row Level Security on every user-facing table"},{"heading":"the-stack","content":"Auth"},{"heading":"the-stack","content":"Supabase Auth (email + password, magic link, optional social providers)"},{"heading":"the-stack","content":"Email"},{"heading":"the-stack","content":"React Email templates, Resend in production, Mailpit locally"},{"heading":"the-stack","content":"Payments"},{"heading":"the-stack","content":"Stripe behind a payment gateway abstraction"},{"heading":"the-stack","content":"Hosting"},{"heading":"the-stack","content":"The demo runs on Vercel as two projects (web and admin). A `netlify.toml` is also included for the web app."},{"heading":"the-stack","content":"Tooling"},{"heading":"the-stack","content":"Turborepo, pnpm, Oxlint, Oxfmt, Vitest, Playwright, pgTap"},{"heading":"the-stack","content":"The repository is a Turborepo workspace with four apps: `apps/web` (the Next.js product), `apps/admin` (the administration app, deployed on its own origin), `apps/database` (Supabase config, migrations, seed and pgTap tests) and `apps/email` (React Email preview server). See project structure."},{"heading":"what-you-get-out-of-the-box","content":"Accounts with sign-up, login, email verification, password reset and magic links."},{"heading":"what-you-get-out-of-the-box","content":"Onboarding that completes a profile, creates a workspace and resolves pending invitations."},{"heading":"what-you-get-out-of-the-box","content":"Multi-tenant workspaces at `/workspace/[workspaceSlug]` with `owner`, `admin`, `member` and `readonly` roles enforced in RLS."},{"heading":"what-you-get-out-of-the-box","content":"Projects scoped to a workspace, with approval status, comments and Realtime updates."},{"heading":"what-you-get-out-of-the-box","content":"Workspace invitations with email delivery, expiry and accept/decline flows."},{"heading":"what-you-get-out-of-the-box","content":"A separate admin app (`apps/admin`, port 3002 locally) that serves `/app-admin` for users, workspaces, marketing content, feedback moderation, the payment gateway and settings, plus support sign-in links into the web app."},{"heading":"what-you-get-out-of-the-box","content":"User MCP on the web app at `/mcp/user` and Admin MCP on the admin app at `/mcp/admin`, both with OAuth."},{"heading":"what-you-get-out-of-the-box","content":"Stripe billing: subscriptions, one-time payments, invoices, customer portal and a webhook handler at `/api/stripe/webhooks`."},{"heading":"what-you-get-out-of-the-box","content":"41 React Email templates and a local preview server."},{"heading":"what-you-get-out-of-the-box","content":"In-app Realtime notifications."},{"heading":"what-you-get-out-of-the-box","content":"Marketing site, MDX blog, Fumadocs documentation at `/docs`, public roadmap, changelog and feedback boards."},{"heading":"what-you-get-out-of-the-box","content":"Optional integrations wired but off until configured: Sentry, PostHog, Google Analytics, Unkey API keys, OpenAI chat and image generation, OpenTelemetry."},{"heading":"prerequisites","content":"Node.js 24. `.nvmrc` pins `v24` and `apps/web` and `apps/admin` declare `engines.node >= 24.0.0`."},{"heading":"prerequisites","content":"pnpm 11. The root `package.json` sets `packageManager` to `pnpm@11.1.2`; `corepack enable` picks it up."},{"heading":"prerequisites","content":"Docker or Podman for the local Supabase stack, or a sandbox that supports Supabase's native runtime. The kit pins the Supabase CLI to `2.121.0` and enables the stack engine, so every git worktree gets its own local project. See local Supabase per agent."},{"heading":"prerequisites","content":"For production: a Supabase project and a Resend account. A Stripe account if you keep billing. Sentry, PostHog, Unkey and OpenAI accounts only if you turn those features on."},{"heading":"naming-inside-the-repository","content":"The repository's README, root `package.json` (`nextbase-ultimate`) and Supabase `project_id` (`nextbase-ultimate`) still use the product's older name, Nextbase Ultimate. It is the same kit. The web package is `web`, the admin package is `admin`, the database package is `database` and the email package is `@nextbase/email`; those are the names you pass to `pnpm --filter`."},{"heading":"next-steps","content":"Installation: clone, start a local Supabase project and run the web and admin apps."},{"heading":"next-steps","content":"Environment variables: every variable in `apps/web/.env.local.example` and `apps/admin/.env.local.example`."},{"heading":"next-steps","content":"Authentication: Supabase Auth, email verification, the admin role, the admin app and RLS."},{"heading":"next-steps","content":"Agent connections: the User MCP and Admin MCP for this kit."},{"heading":"next-steps","content":"Project structure: where things live and where to make changes."},{"heading":"next-steps","content":"Deployment: Supabase project, migrations, two Vercel projects and the first smoke checks."},{"heading":"next-steps","content":"For the shared concepts, read getting started, workspaces and the admin area and user and admin MCPs."}],"headings":[{"id":"the-stack","content":"The stack"},{"id":"what-you-get-out-of-the-box","content":"What you get out of the box"},{"id":"prerequisites","content":"Prerequisites"},{"id":"naming-inside-the-repository","content":"Naming inside the repository"},{"id":"next-steps","content":"Next steps"}]},"url":"/docs/v5/starters/nextjs-supabase","title":"Next.js + Supabase","description":"Overview of the Next.js + Supabase starter: what it ships with, what you need, and where to go next."},{"_id":"/docs/v5/starters/nextjs-supabase/installation","structured":{"contents":[{"content":"This page gets the kit running on your computer. It follows the local Supabase per agent flow, so each git worktree gets its own Supabase project with ports assigned by the CLI. Read that page once before you start; this page only repeats the commands you need."},{"content":"`apps/database/package.json` pins the Supabase CLI to `2.121.0` and `apps/database/supabase/config.toml` sets `[experimental] stack = true` with no fixed ports. The kit's own scripts (`setup.sh`, `pnpm database#start`, `pnpm supabase:sync-env`, `apps/web/scripts/generate-env-local.ts` and the Playwright global setup) all read the project's URLs from `supabase status --output-format json`, so you do not need to edit the config or work around the scripts."},{"heading":"clone-the-repository","content":"If GitHub says the repository doesn't exist, sign in with the account that has access."},{"heading":"use-nodejs-24-and-pnpm-11","content":"`.nvmrc` pins `v24`. Corepack reads `packageManager: pnpm@11.1.2` from the root `package.json`. `pnpm install` also runs `fumadocs-mdx` in `apps/web` as a postinstall step."},{"heading":"create-the-env-files","content":"Only when they are missing:"},{"heading":"create-the-env-files","content":"Never overwrite an existing `.env.local`. The web example sets `NEXT_PUBLIC_SITE_URL=http://localhost:3000` and `USE_LOCAL_EMAIL=true`; the admin example sets `NEXT_PUBLIC_SITE_URL=http://localhost:3002` and `NEXT_PUBLIC_WEB_APP_URL=http://localhost:3000`. Both carry placeholder Supabase values that the next steps replace. See environment variables."},{"heading":"start-the-local-supabase-project","content":"This runs `supabase start` in `apps/database`. Use `pnpm --filter database exec supabase start --runtime native` in a supported sandbox without Docker. Starting applies every migration in `apps/database/supabase/migrations/` and runs `apps/database/supabase/seed.sql`."},{"heading":"start-the-local-supabase-project","content":"Then write the project's URL and keys into both apps:"},{"heading":"start-the-local-supabase-project","content":"`scripts/sync-supabase-env.ts` runs `supabase status --output-format json` and rewrites `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY` in `apps/web/.env.local` and `apps/admin/.env.local`, leaving every other line alone. To look at the values yourself:"},{"heading":"start-the-local-supabase-project","content":"The JSON `env` object also contains `STUDIO_URL`, `DB_URL` and `MAILPIT_URL`. Copy `MAILPIT_URL` into `apps/web/.env.local` as `MAILPIT_URL` so the web app's local emails reach this project's Mailpit (the example has the line commented out)."},{"heading":"start-the-local-supabase-project","content":"`setup.sh` at the repository root performs this step and the previous two for you: copy missing env files, `pnpm i`, start Supabase, `pnpm supabase:sync-env`, `pnpm gen-types-local`."},{"heading":"generate-database-types","content":"This writes `apps/database/lib/database.types.ts`, which `apps/web` and `apps/admin` import as `database/types`. Run it again after you add a migration."},{"heading":"start-the-dev-servers","content":"`pnpm dev` runs `turbo run dev --parallel`, which starts the Next.js web app on http\\://localhost:3000, the React Email preview on http\\://localhost:3001 and the admin app on http\\://localhost:3002. Use `pnpm web#dev` for the web app alone and `pnpm admin#dev` for the admin app alone."},{"heading":"start-the-dev-servers","content":"Restart the dev servers after you refresh connection details or switch branches."},{"heading":"sign-up-and-create-a-workspace","content":"Open `http://localhost:3000/sign-up`. Local Supabase Auth has `enable_confirmations = true`, so the app waits until you open the verification link. Find it in Mailpit at the `MAILPIT_URL` from `supabase status`."},{"heading":"sign-up-and-create-a-workspace","content":"After verifying, onboarding asks for your profile and creates your first workspace. Add a project from the workspace page so you have data to test with later."},{"heading":"sign-up-and-create-a-workspace","content":"The seed also creates four test users, including the application admin `testadmin@usenextbase.com`. Their shared password is written at the top of the users section in `apps/database/supabase/seed.sql`."},{"heading":"sign-in-to-the-admin-app","content":"Open `http://localhost:3002/login` and sign in with the seeded admin account (email and password). The admin app redirects `/` to `/app-admin` and returns 403 for accounts without the admin role. See authentication for how to grant the role to your own account."},{"heading":"verify-the-setup","content":"This copies any missing `.env.local` files, then runs lint, typecheck and Vitest unit tests across the workspace (web and admin included) and a compile-mode production build of the web app. It does not need the Supabase stack. The build step calls `apps/web/scripts/generate-env-local.ts`, which skips env generation when the three Supabase variables are already set and otherwise reads them from `supabase status --output-format json`."},{"heading":"verify-the-setup","content":"For database tests and browser tests, with Supabase running:"},{"heading":"verify-the-setup","content":"`test-db` runs the pgTap suites in `apps/database/supabase/tests/`. `test-integration` runs Playwright from `apps/web`. Its global setup reads `supabase status --output-format json` and writes the Supabase URL, keys, `DB_URL` and `MAILPIT_URL` into `apps/web/.env.test.local`. The config starts the web app with `pnpm start` on the port in `PORT` (default 3000) and the admin app with `pnpm --filter admin start` on 3002 unless `ADMIN_BASE_URL` points at a running instance; the `e2e/admin/` specs run against the admin origin."},{"heading":"local-email","content":"Two different senders exist locally:"},{"heading":"local-email","content":"Supabase Auth sends verification, magic link and recovery emails through the local mail server. Open them at `MAILPIT_URL`."},{"heading":"local-email","content":"The web app's own emails (invitations, notifications) go through `apps/web/src/utils/api-routes/utils.ts`. With `USE_LOCAL_EMAIL=true` they are posted to the Mailpit API at `MAILPIT_URL` (`apps/web/src/utils/send-email-mailpit.ts`), falling back to `http://localhost:54324` when the variable is unset. The admin app's copy of `send-email-mailpit.ts` still posts to `http://localhost:54324`; edit that URL if your project's Mailpit runs on a different port and you want admin emails locally."},{"heading":"stop-or-reset","content":"Stopping keeps the project's data. To rebuild the database from migrations and seed:"},{"heading":"stop-or-reset","content":"See stop or switch projects before using `supabase stack destroy`. If something fails, check troubleshooting."}],"headings":[{"id":"clone-the-repository","content":"Clone the repository"},{"id":"use-nodejs-24-and-pnpm-11","content":"Use Node.js 24 and pnpm 11"},{"id":"create-the-env-files","content":"Create the env files"},{"id":"start-the-local-supabase-project","content":"Start the local Supabase project"},{"id":"generate-database-types","content":"Generate database types"},{"id":"start-the-dev-servers","content":"Start the dev servers"},{"id":"sign-up-and-create-a-workspace","content":"Sign up and create a workspace"},{"id":"sign-in-to-the-admin-app","content":"Sign in to the admin app"},{"id":"verify-the-setup","content":"Verify the setup"},{"id":"local-email","content":"Local email"},{"id":"stop-or-reset","content":"Stop or reset"}]},"url":"/docs/v5/starters/nextjs-supabase/installation","title":"Installation","description":"Clone the Next.js + Supabase starter, start a local Supabase project, run the web and admin apps and sign in for the first time."},{"_id":"/docs/v5/starters/nextjs-supabase/project-structure","structured":{"contents":[{"content":"The kit is a Turborepo workspace managed with pnpm. `pnpm-workspace.yaml` includes `apps/*` and `packages/*`. Root scripts in `package.json` fan out through `turbo.json`."},{"heading":"repository-tree","content":"<File name=\"proxy.ts\" />"},{"heading":"repository-tree","content":"<File name=\"environment.d.ts\" />"},{"heading":"repository-tree","content":"<File name=\".env.local.example\" />"},{"heading":"repository-tree","content":"<File name=\".env.test\" />"},{"heading":"repository-tree","content":"<File name=\"next.config.ts\" />"},{"heading":"repository-tree","content":"<File name=\"netlify.toml\" />"},{"heading":"repository-tree","content":"<File name=\"instrumentation.ts\" />"},{"heading":"repository-tree","content":"<File name=\"vitest.config.mts\" />"},{"heading":"repository-tree","content":"<File name=\"package.json\" />"},{"heading":"repository-tree","content":"<File name=\"proxy.ts\" />"},{"heading":"repository-tree","content":"<File name=\"environment.d.ts\" />"},{"heading":"repository-tree","content":"<File name=\".env.local.example\" />"},{"heading":"repository-tree","content":"<File name=\".env.test\" />"},{"heading":"repository-tree","content":"<File name=\"next.config.ts\" />"},{"heading":"repository-tree","content":"<File name=\"vitest.config.mts\" />"},{"heading":"repository-tree","content":"<File name=\"package.json\" />"},{"heading":"repository-tree","content":"<File name=\"database.types.ts\" />"},{"heading":"repository-tree","content":"<File name=\"generate-signing-keys.sh\" />"},{"heading":"repository-tree","content":"<File name=\"config.toml\" />"},{"heading":"repository-tree","content":"<File name=\"seed.sql\" />"},{"heading":"repository-tree","content":"<File name=\"package.json\" />"},{"heading":"repository-tree","content":"<File name=\"package.json\" />"},{"heading":"repository-tree","content":"<File name=\"admin-app.md\" />"},{"heading":"repository-tree","content":"<File name=\"mcp-connections.md\" />"},{"heading":"repository-tree","content":"<File name=\"verify-local.sh\" />"},{"heading":"repository-tree","content":"<File name=\"signoff.sh\" />"},{"heading":"repository-tree","content":"<File name=\"sync-supabase-env.ts\" />"},{"heading":"repository-tree","content":"<File name=\"setup.sh\" />"},{"heading":"repository-tree","content":"<File name=\"turbo.json\" />"},{"heading":"repository-tree","content":"<File name=\"pnpm-workspace.yaml\" />"},{"heading":"repository-tree","content":"<File name=\"package.json\" />"},{"heading":"repository-tree","content":"<File name=\".nvmrc\" />"},{"heading":"repository-tree","content":"<File name=\"AGENTS.md\" />"},{"heading":"repository-tree","content":"<File name=\"CHANGELOG.md\" />"},{"heading":"the-four-apps","content":"**`apps/web`** is the product: a Next.js 16 App Router app with Cache Components enabled in `next.config.ts`. It depends on `database` for generated types and on `@nextbase/typescript-config` for the shared tsconfig. Its `next.config.ts` derives the Supabase Storage image pattern from `NEXT_PUBLIC_SUPABASE_URL`."},{"heading":"the-four-apps","content":"**`apps/admin`** is the administration app (package name `admin`, `next dev --port 3002`). It serves `/app-admin`, the feedback and roadmap moderation views, the Admin MCP and the support sign-in flow, and it deploys independently from the web app against the same Supabase project. Root scripts `pnpm admin#dev` and `pnpm admin#build` target it. `docs/admin-app.md` describes the split."},{"heading":"the-four-apps","content":"**`apps/database`** owns Supabase. `supabase/config.toml` configures the local project with the stack engine enabled, `supabase/migrations/` holds the SQL migrations, `supabase/seed.sql` seeds test users and content, and `supabase/tests/` holds pgTap suites. `lib/database.types.ts` is generated and exported as `database/types`. The package pins the `supabase` CLI to `2.121.0`."},{"heading":"the-four-apps","content":"**`apps/email`** is the React Email preview server (`pnpm --filter @nextbase/email dev`, port 3001). The templates in `emails/` are the design source; the web app renders its own copies with `@react-email/*` components."},{"heading":"routes-and-pages","content":"Web routes are under `apps/web/src/app`, grouped by access level:"},{"heading":"routes-and-pages","content":"`(external-pages)`: landing page, `/blog`, `/changelog`, `/terms`, `/docs` entry."},{"heading":"routes-and-pages","content":"`(dynamic-pages)/(login-pages)`: `/login`, `/sign-up`."},{"heading":"routes-and-pages","content":"`(dynamic-pages)/(misc-login-pages)`: `/forgot-password`, `/update-password`, `/confirm-delete-user`, `/auth/callback`, `/auth/confirm`."},{"heading":"routes-and-pages","content":"`(dynamic-pages)/(updates-pages)`: public `/feedback` and `/roadmap`."},{"heading":"routes-and-pages","content":"`(dynamic-pages)/(authenticated-pages)`: `/dashboard`, `/onboarding`, `/user/*`, `(application-pages)/workspace/[workspaceSlug]/*` (home, projects, settings with billing and members) and `(application-pages)/project/[projectSlug]`."},{"heading":"routes-and-pages","content":"`api/`: `stripe/webhooks`, `invitations`, `chat`, `search`, `revalidate`, `internal/admin-revalidate` (called by the admin app), `token/refresh`, `health`, `v1/me` and the OG image routes."},{"heading":"routes-and-pages","content":"`mcp/[...path]`, `oauth/[...path]` and `.well-known/*`: the User MCP route handlers."},{"heading":"routes-and-pages","content":"Admin routes are under `apps/admin/src/app`: `app-admin/(admin-pages)/*` (dashboard, `users`, `workspaces`, `marketing/{blog,changelog,authors,tags}`, `payment-gateway/{customers,invoices,subscriptions}`, `settings`), `(dynamic-pages)/(updates-pages)` for feedback and roadmap moderation, `api/admin/marketing/media`, `login`, `logout`, `auth/{callback,confirm}` and the Admin MCP handlers under `mcp`, `oauth` and `.well-known`."},{"heading":"routes-and-pages","content":"When you add a protected web route, add its matcher to `apps/web/src/middlewares/paths.ts`, otherwise `proxy.ts` will not run the auth chain for it. The admin app's `proxy.ts` uses a fixed matcher for `/app-admin`, `/feedback` and `/roadmap`."},{"heading":"server-and-data-access","content":"Web database access is in `apps/web/src/data/`, split into `user/` (RLS-scoped, user session), `anon/` (public marketing data) and `auth/` (sign-in actions). Admin queries that need the service-role client live in `apps/admin/src/data/admin/`, gated by `adminActionClient` from `apps/admin/src/lib/safe-action.ts`. Files there start with `\"use server\"`. Server actions are built with `next-cool-action` wrappers (`apps/web/src/lib/safe-action.ts` and the admin equivalent)."},{"heading":"server-and-data-access","content":"Pick the Supabase client that matches the context from `src/supabase-clients/` in each app: server component, server action, route handler, middleware, browser, admin or (web only) Unkey."},{"heading":"schema-and-migrations","content":"Add a timestamped SQL file to `apps/database/supabase/migrations/`. Enable RLS on every new table and write its policies in the same file. Then:"},{"heading":"schema-and-migrations","content":"Add a pgTap test under `apps/database/supabase/tests/` for the new policies. Use `pnpm --filter database exec supabase migration list --local` to check what is applied."},{"heading":"authentication","content":"Server actions in `apps/web/src/data/auth/auth.ts`, UI in `apps/web/src/components/authentication/`, middleware in `apps/web/src/middlewares/`, claims schema in `apps/web/src/utils/zod-schemas/user-claims-schema.ts`, admin check in `utils/is-supabase-user-app-admin.ts` (both apps). Admin login and gate in `apps/admin/src/app/login/` and `apps/admin/src/proxy.ts`. Local Auth settings are in `apps/database/supabase/config.toml`. See authentication."},{"heading":"emails","content":"Templates in `apps/email/emails/`. Sending goes through `src/utils/api-routes/utils.ts` in each app, which chooses Mailpit (`USE_LOCAL_EMAIL=true`) or Resend. The web copy of `send-email-mailpit.ts` reads `MAILPIT_URL`. Invitation and notification emails are triggered from `apps/web/src/data/user/invitation.tsx` and `apps/web/src/data/user/user.tsx`."},{"heading":"billing","content":"`apps/web/src/payments/abstract-payment-gateway.ts` defines the interface; `stripe-payment-gateway.ts` implements it (the admin app has its own copy under `apps/admin/src/payments/`). The webhook route is `apps/web/src/app/api/stripe/webhooks/route.ts`. Self-service billing UI is under `workspace/[workspaceSlug]/settings/billing` in the web app; the payment gateway admin pages are under `apps/admin/src/app/app-admin/(admin-pages)/payment-gateway`."},{"heading":"content-publishing-across-apps","content":"Blog, changelog and feedback are edited in the admin app and read by the web app. After a publish, `apps/admin/src/lib/revalidate-web-app.ts` posts to the web app's `/api/internal/admin-revalidate` with the shared `REVALIDATE_SECRET` so the web app's cached pages refresh."},{"heading":"background-work","content":"There is no job queue or worker in this kit. Stripe webhooks, Supabase Realtime channels and the on-demand `POST /api/revalidate` endpoint are the asynchronous entry points."},{"heading":"mcp","content":"`src/mcp/` in each app holds the protocol core, the Supabase adapter and the record store; the route files under `app/mcp`, `app/oauth` and `app/.well-known` are one-liners. The web copy serves the user audience, the admin copy the admin audience. See agent connections."},{"heading":"tests","content":"Unit tests sit next to the code as `*.test.ts` and run with `pnpm test` (Vitest with `--root src` in both `web` and `admin`)."},{"heading":"tests","content":"Browser tests are in `apps/web/e2e/` (`anon`, `user`, `admin`, `invitations`, `password-updation`, with `_setups` and `_helpers`) and run with `pnpm test-integration` using `apps/web/playwright/playwright.config.mts`. The config starts both the web and admin production servers; the `admin` specs run against the admin origin."},{"heading":"tests","content":"Database tests are in `apps/database/supabase/tests/` and run with `pnpm --filter database test-db`."},{"heading":"tests","content":"`pnpm verify:local` runs lint, typecheck, unit tests and a compile-mode build of the web app; `pnpm signoff` runs it and posts a `signoff` status with the `gh-signoff` extension."},{"heading":"tests","content":"The nightly `integration-tests.yml` workflow starts Supabase, resets the database, runs pgTap, builds and runs Playwright. `release.yml` opens a Changesets version PR on pushes to `main`."}],"headings":[{"id":"repository-tree","content":"Repository tree"},{"id":"the-four-apps","content":"The four apps"},{"id":"where-to-make-changes","content":"Where to make changes"},{"id":"routes-and-pages","content":"Routes and pages"},{"id":"server-and-data-access","content":"Server and data access"},{"id":"schema-and-migrations","content":"Schema and migrations"},{"id":"authentication","content":"Authentication"},{"id":"emails","content":"Emails"},{"id":"billing","content":"Billing"},{"id":"content-publishing-across-apps","content":"Content publishing across apps"},{"id":"background-work","content":"Background work"},{"id":"mcp","content":"MCP"},{"id":"tests","content":"Tests"}]},"url":"/docs/v5/starters/nextjs-supabase/project-structure","title":"Project structure","description":"The monorepo layout of the Next.js + Supabase starter, including the separate admin app, and where to make each kind of change."},{"_id":"/docs/v5/starters/tanstack-convex/agent-connections","structured":{"contents":[{"content":"The kit exposes two MCP servers from the Convex backend. Both use the existing Convex Auth login and delegated OAuth tokens. Agents never receive the user's password, a Convex JWT or an API key. Read the shared User and admin MCPs page for the model, and connect an agent for the client-side steps."},{"content":"The MCP code ships in `apps/database/convex/mcp/`, and the repository's `docs/mcp-connections.md` describes it in detail. This page follows that document."},{"heading":"where-users-manage-connections","content":"Connection"},{"heading":"where-users-manage-connections","content":"Settings page"},{"heading":"where-users-manage-connections","content":"Live demo server"},{"heading":"where-users-manage-connections","content":"User"},{"heading":"where-users-manage-connections","content":"Web app, `/user/settings/connections` (sidebar: **Agent connections**)"},{"heading":"where-users-manage-connections","content":"`https://ceaseless-lark-846.convex.site/mcp/user`"},{"heading":"where-users-manage-connections","content":"Admin"},{"heading":"where-users-manage-connections","content":"Admin app, `/app-admin/connections` (admin menu: **Admin agent connections**), on the admin origin `https://tanstack-convex-admin.usenextbase.com` in the demo"},{"heading":"where-users-manage-connections","content":"`https://ceaseless-lark-846.convex.site/mcp/admin`"},{"heading":"where-users-manage-connections","content":"Each settings page shows the server URL with a **Copy URL** button, the list of connections with their scopes and last use, a revoke action, and recent activity. The admin page requires a current platform administrator and lives in the separate admin app (`apps/admin`, port 3002 locally)."},{"heading":"where-users-manage-connections","content":"Your own deployment has its own addresses. The MCP endpoints live on the backend's `CONVEX_SITE_URL`, not on either frontend origin: `http://127.0.0.1:3211/mcp/user` locally, `https://<name>.convex.site/mcp/user` in production. Copy the URL from the settings page rather than typing it."},{"heading":"endpoints","content":"All of these are mounted on the Convex site URL by `mountMcp` in `apps/database/convex/mcp/http.ts`:"},{"heading":"endpoints","content":"Endpoint"},{"heading":"endpoints","content":"Purpose"},{"heading":"endpoints","content":"`/mcp/user`"},{"heading":"endpoints","content":"User server: `account_context`, `projects_list`, `projects_create`"},{"heading":"endpoints","content":"`/mcp/admin`"},{"heading":"endpoints","content":"Admin server: `admin_users_list`, `admin_workspaces_list`"},{"heading":"endpoints","content":"`/.well-known/oauth-protected-resource/mcp/user`"},{"heading":"endpoints","content":"User resource discovery"},{"heading":"endpoints","content":"`/.well-known/oauth-protected-resource/mcp/admin`"},{"heading":"endpoints","content":"Admin resource discovery"},{"heading":"endpoints","content":"`/.well-known/oauth-authorization-server/oauth`"},{"heading":"endpoints","content":"OAuth issuer discovery (issuer is `<CONVEX_SITE_URL>/oauth`)"},{"heading":"endpoints","content":"`/oauth/register`, `/oauth/authorize`, `/oauth/token`, `/oauth/revoke`"},{"heading":"endpoints","content":"OAuth lifecycle"},{"heading":"endpoints","content":"The consent page is `/mcp/authorize?request_id=...` in both frontends (`apps/web/src/routes/mcp.authorize.tsx` and `apps/admin/src/routes/mcp.authorize.tsx`). For user connections the backend redirects to `MCP_APP_URL`, falling back to `APP_URL` and then `SITE_URL`. For admin connections it redirects to `ADMIN_SITE_URL` first, then the same fallbacks. If an admin request lands on the web app anyway, the web consent page shows an **Open admin app** link built from `VITE_ADMIN_APP_URL`."},{"heading":"tools-and-permissions","content":"Server"},{"heading":"tools-and-permissions","content":"Tool"},{"heading":"tools-and-permissions","content":"Scope"},{"heading":"tools-and-permissions","content":"Access"},{"heading":"tools-and-permissions","content":"What it does"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`account_context`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"Read"},{"heading":"tools-and-permissions","content":"Signed-in account plus the one authorized workspace and granted scopes"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`projects_list`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"Read"},{"heading":"tools-and-permissions","content":"Projects in the authorized workspace, newest first, with `continueCursor`; `limit` 1 to 50, default 20"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`projects_create`"},{"heading":"tools-and-permissions","content":"`user:write`"},{"heading":"tools-and-permissions","content":"Write"},{"heading":"tools-and-permissions","content":"Creates a project through `createProjectForActor`, the same operation the website uses"},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_users_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"Read"},{"heading":"tools-and-permissions","content":"Profiles (id, email, name, platform role); no credentials or sessions"},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_workspaces_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"Read"},{"heading":"tools-and-permissions","content":"Workspaces (id, name, slug)"},{"heading":"tools-and-permissions","content":"The user server offers `user:read` and `user:write`; the admin server offers only `admin:read`. A token for one resource is rejected by the other, even for a platform administrator."},{"heading":"tools-and-permissions","content":"Every tool call runs `verifiedCaller` in `apps/database/convex/mcp/connections.ts`. It checks that the connection exists, is not revoked or expired, includes the scope, that the approving Convex Auth session is still valid, that the account is not being deleted, and then re-runs the real permission check: platform admin role for admin tools, workspace membership for user tools, and a non-`readonly` membership for `projects_create`. The gateway's `caller` argument is hidden from tool schemas; clients cannot supply it."},{"heading":"consent-flow-in-this-kit","content":"The agent registers as a public client (`/oauth/register`), then opens `/oauth/authorize` with PKCE S256 and the exact resource URL."},{"heading":"consent-flow-in-this-kit","content":"The backend stores a pending request (10 minutes) and redirects to `/mcp/authorize` on the web app (user server) or the admin app (admin server). If you are not signed in on that origin, the page links to `/login?returnTo=...` and brings you back."},{"heading":"consent-flow-in-this-kit","content":"The consent card shows the client name (marked as supplied by the client), the exact callback URL and the requested permissions. For the user server you choose one workspace; the write permission (create projects) is off by default and resets when you change workspace. For the admin server there is no workspace, and only a platform administrator can approve."},{"heading":"consent-flow-in-this-kit","content":"Approving creates an `mcpConnections` row tied to your current auth session and returns a one-time code (5 minutes). The agent exchanges it for a 15 minute access token and a refresh token."},{"heading":"consent-flow-in-this-kit","content":"Connections and refresh tokens last 30 days, or until you revoke them, sign out of the approving session, lose the workspace membership, or lose the admin role. Refresh tokens rotate once; reusing one revokes the connection. Each account can hold 20 active connections."},{"heading":"consent-flow-in-this-kit","content":"Browser clients are allowed from the app origin (`MCP_APP_URL`, falling back to `APP_URL` then `SITE_URL`), `https://claude.ai` and `https://chatgpt.com`. Desktop and server clients without an Origin header can connect. Confidential clients and client ID metadata documents are not supported."},{"heading":"consent-flow-in-this-kit","content":"An hourly cron (`apps/database/convex/crons.ts`) prunes expired clients, requests, codes and tokens, keeping used refresh tokens seven days past expiry for replay detection, and removes activity older than 90 days."},{"heading":"where-the-code-lives","content":"<File name=\"convex.config.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"crons.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"http.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"mcp.test.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"actor.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"cleanup.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"connections.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"http.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"oauth.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"protocol.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"schema.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"tools.ts\" />"},{"heading":"where-the-code-lives","content":"<File name=\"components/mcp-connections.tsx\" />"},{"heading":"where-the-code-lives","content":"<File name=\"components/mcp-consent.tsx\" />"},{"heading":"where-the-code-lives","content":"<File name=\"routes/mcp.authorize.tsx\" />"},{"heading":"where-the-code-lives","content":"<File name=\"routes/_authenticated.user.settings.connections.tsx\" />"},{"heading":"where-the-code-lives","content":"<File name=\"components/mcp-connections.tsx\" />"},{"heading":"where-the-code-lives","content":"<File name=\"components/mcp-consent.tsx\" />"},{"heading":"where-the-code-lives","content":"<File name=\"routes/mcp.authorize.tsx\" />"},{"heading":"where-the-code-lives","content":"<File name=\"routes/_authenticated.app-admin.connections.tsx\" />"},{"heading":"where-the-code-lives","content":"`convex.config.ts` installs the pinned `convex-mcp-gateway@2.1.1` component twice, as `mcpUser` and `mcpAdmin`. Keep them separate; do not share a registry between the two servers. The MCP tables (`mcpClients`, `mcpRequests`, `mcpConnections`, `mcpCodes`, `mcpTokens`, `mcpActivity`) are defined in `mcp/schema.ts` and spread into the main schema."},{"heading":"adding-a-tool-safely","content":"Write an `internalQuery` or `internalMutation` in `mcp/tools.ts` whose first argument is `caller: mcpCallerValidator`. Start the handler with `await verifiedCaller(ctx, caller, \"user\" | \"admin\", \"<scope>\", writable)`."},{"heading":"adding-a-tool-safely","content":"Call an existing application function or shared helper with the verified `grant.userId` and `grant.workspaceId`. Do not trust a `userId` or `workspaceId` passed by the client."},{"heading":"adding-a-tool-safely","content":"Register it in `userTools` or `adminTools` with `defineMcpQuery` or `defineMcpMutation`, `identityArg: \"caller\"` and `metadata: { scope }`. The gateway's `authorize` callback only allows tools whose scope the connection granted."},{"heading":"adding-a-tool-safely","content":"For writes, call `record(ctx, grant, \"<tool_name>\")` so the activity appears in settings, and keep paging bounded with `bounded(limit)`."},{"heading":"adding-a-tool-safely","content":"Add cases to `apps/database/convex/mcp.test.ts`, then test the real HTTP flow against a local Convex backend. The in-memory tests do not prove that bearer tokens reach HTTP actions."},{"heading":"adding-a-tool-safely","content":"Never publish local fixture functions or synthetic identity helpers to production. The repository also ships an `mcp-tool` skill in `.agents/skills/` that walks AI coding tools through these steps."}],"headings":[{"id":"where-users-manage-connections","content":"Where users manage connections"},{"id":"endpoints","content":"Endpoints"},{"id":"tools-and-permissions","content":"Tools and permissions"},{"id":"consent-flow-in-this-kit","content":"Consent flow in this kit"},{"id":"where-the-code-lives","content":"Where the code lives"},{"id":"adding-a-tool-safely","content":"Adding a tool safely"}]},"url":"/docs/v5/starters/tanstack-convex/agent-connections","title":"Agent connections","description":"User MCP and Admin MCP in the TanStack + Convex starter: endpoints, tools, consent and how to extend them."},{"_id":"/docs/v5/starters/tanstack-convex/authentication","structured":{"contents":[{"content":"This kit uses Convex Auth. The same backend login serves the web app, the admin app, the Convex functions and the agent connections, but the web and admin apps hold separate sessions on their own origins. For the concepts shared across all kits, read the v5 authentication guide."},{"heading":"where-the-configuration-lives","content":"Piece"},{"heading":"where-the-configuration-lives","content":"Path"},{"heading":"where-the-configuration-lives","content":"Providers"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/auth.ts`"},{"heading":"where-the-configuration-lives","content":"Redirect allowlist"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/lib/auth_redirect.ts`, called from the `redirect` callback in `auth.ts` with `SITE_URL` and `ADMIN_SITE_URL`"},{"heading":"where-the-configuration-lives","content":"JWT domain for Convex"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/auth.config.ts` (uses `CONVEX_SITE_URL`)"},{"heading":"where-the-configuration-lives","content":"Auth HTTP routes"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/http.ts` via `auth.addHttpRoutes(http)`"},{"heading":"where-the-configuration-lives","content":"Client provider"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/router.tsx` and `apps/admin/src/router.tsx` wrap each router in `ConvexAuthProvider`"},{"heading":"where-the-configuration-lives","content":"Route guards"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/routes/_authenticated.tsx` and `apps/admin/src/routes/_authenticated.tsx`"},{"heading":"where-the-configuration-lives","content":"Web login, sign-up, reset pages"},{"heading":"where-the-configuration-lives","content":"`apps/web/src/routes/login.tsx`, `sign-up.tsx`, `forgot-password.tsx`, `update-password.tsx`, `logout.tsx`"},{"heading":"where-the-configuration-lives","content":"Admin login and reset pages"},{"heading":"where-the-configuration-lives","content":"`apps/admin/src/routes/login.tsx`, `forgot-password.tsx`, `update-password.tsx` (no sign-up page)"},{"heading":"where-the-configuration-lives","content":"Social buttons and magic link form"},{"heading":"where-the-configuration-lives","content":"`components/auth/oauth-buttons.tsx` and `components/auth/magic-link-form.tsx` in both apps"},{"heading":"where-the-configuration-lives","content":"Impersonation"},{"heading":"where-the-configuration-lives","content":"`apps/database/convex/impersonation.ts`, `apps/admin/src/lib/impersonation.ts`, `apps/web/src/routes/auth.impersonate.tsx`, `apps/web/src/components/impersonation-banner.tsx`"},{"heading":"where-the-configuration-lives","content":"Auth tables come from `authTables` in `@convex-dev/auth/server` and are spread into `apps/database/convex/schema.ts`. The kit adds indexes on `authVerifiers` so account deletion stays bounded."},{"heading":"where-the-configuration-lives","content":"Signing keys (`JWT_PRIVATE_KEY`, `JWKS`) and `SITE_URL` are written to the selected deployment by `pnpm auth:setup`. Run it once per deployment. `ADMIN_SITE_URL` is set separately with `pnpm convex env set`."},{"heading":"providers","content":"`auth.ts` registers six providers:"},{"heading":"providers","content":"**Password** with `verify` set to the Resend provider, so sign-up sends a verification code, and `reset` set to a dedicated `password-reset` Email provider that sends a 15 minute link to `/update-password?mode=reset` built from `SITE_URL`."},{"heading":"providers","content":"**Resend** (magic link). The login and sign-up pages include a magic link form that calls `signIn(\"resend\", ...)`."},{"heading":"providers","content":"**Google**, **GitHub** and **Twitter** from `@auth/core/providers`."},{"heading":"providers","content":"**admin-impersonation**, a `ConvexCredentials` provider that exchanges a one-use impersonation token for a session (see below). It is not a login method users see."},{"heading":"providers","content":"All of them read their credentials from the Convex deployment environment: `AUTH_RESEND_KEY`, `AUTH_EMAIL_FROM`, `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET`, `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET`, `AUTH_TWITTER_ID` and `AUTH_TWITTER_SECRET`. The environment variables page lists them."},{"heading":"providers","content":"Social buttons are hidden unless the provider id appears in the public `VITE_OAUTH_PROVIDERS` list of the app you are looking at (the web app and the admin app each have their own env file). Listing a provider does not configure it; set its `AUTH_*` credentials and register the callback with the provider too. The repository does not document the exact callback path; take it from the Convex Auth documentation for your provider."},{"heading":"email-verification-and-the-local-inbox","content":"Sign-up posts `flow=signUp` to the password provider. If the result is not already signed in, the page navigates to `/update-password?mode=verify&email=...` where you enter the code from the email."},{"heading":"email-verification-and-the-local-inbox","content":"There is no local inbox in this kit. Verification, reset and magic link emails are sent by Resend from the Convex deployment, even locally. To test these flows, set `AUTH_RESEND_KEY` and `AUTH_EMAIL_FROM` in the local deployment:"},{"heading":"email-verification-and-the-local-inbox","content":"The `apps/email` dev server on port 3001 previews the React Email templates for app emails (invitation, email change, account deletion). It is not an inbox and does not render the Convex Auth emails. The shared emails guide covers Resend for the Convex kits."},{"heading":"email-verification-and-the-local-inbox","content":"The README says password sign-in works right after `pnpm auth:setup`, and it does for an existing account. Creating a new account at `/sign-up` still needs Resend, because the sign-up UI routes to the verification step when the provider does not return a signed-in session. Configure Resend before testing a complete sign-up locally."},{"heading":"sessions-and-route-protection","content":"Each browser origin holds its own Convex Auth session; Convex functions read it with `getAuthUserId`. The web app's `_authenticated` layout route is client-only (`ssr: false`). It shows a loader while auth resolves, redirects unauthenticated visitors to `/login?returnTo=...`, and sends users who have not finished onboarding to `/onboarding`."},{"heading":"sessions-and-route-protection","content":"The admin app's `_authenticated` layout does the same on the admin origin, then checks `admin.accessStatus`: administrators see the admin pages, the account matching `NEXTBASE_ADMIN_EMAIL` is sent to `/app-admin` to claim access, and everyone else sees \"Administrator access required\" with links to open the web app or sign out."},{"heading":"sessions-and-route-protection","content":"The `redirect` callback in `auth.ts` only allows redirects to `SITE_URL` (the web origin) and `ADMIN_SITE_URL` (the admin origin). OAuth and magic link callbacks started from the admin login return to the admin origin, so `ADMIN_SITE_URL` must be set in every deployment where the admin app signs in."},{"heading":"sessions-and-route-protection","content":"Router guards are navigation UX only. Every public Convex function performs its own checks through the wrappers in `apps/database/convex/lib/functions.ts`:"},{"heading":"sessions-and-route-protection","content":"Wrapper"},{"heading":"sessions-and-route-protection","content":"Check"},{"heading":"sessions-and-route-protection","content":"`authenticatedQuery` / `authenticatedMutation`"},{"heading":"sessions-and-route-protection","content":"A valid user; mutations also refuse accounts with a running or failed deletion job"},{"heading":"sessions-and-route-protection","content":"`workspaceMemberQuery` / `workspaceMemberMutation`"},{"heading":"sessions-and-route-protection","content":"Membership in `workspaceId`; mutations also refuse workspaces being deleted"},{"heading":"sessions-and-route-protection","content":"`workspaceManagerMutation`"},{"heading":"sessions-and-route-protection","content":"Membership with role `owner` or `admin`"},{"heading":"sessions-and-route-protection","content":"`platformAdminQuery` / `platformAdminMutation`"},{"heading":"sessions-and-route-protection","content":"A `profiles` row with `platformRole: \"admin\"`"},{"heading":"sessions-and-route-protection","content":"Workspace roles are `owner`, `admin`, `member` and `readonly` (`apps/database/convex/lib/validators.ts`). Mutations run through `convex-helpers` triggers that keep the user, workspace, project and invitation aggregates in sync. Elevated work is internal-only, and external effects (emails, Stripe, deletions) are recorded as durable `externalJobs` before a workflow runs them."},{"heading":"sessions-and-route-protection","content":"Sign out of the web app at `/logout`. Signing out ends any agent connection that was approved in that session."},{"heading":"becoming-an-application-admin","content":"Admin status is a `platformRole` on the user's profile, separate from workspace roles. The admin area lives in the separate admin app (`apps/admin`, `http://localhost:3002` locally), not in the web app. The first admin claims it:"},{"heading":"becoming-an-application-admin","content":"Set the trusted email and the admin origin in the Convex deployment (local or production):"},{"heading":"becoming-an-application-admin","content":"Create the account with exactly that address in the web app (the admin app has no sign-up page), then open the admin app and sign in with it. The admin app redirects `/` to `/app-admin`."},{"heading":"becoming-an-application-admin","content":"Click **Claim admin access**. `admin.claim` in `apps/database/convex/admin.ts` compares the signed-in email with `NEXTBASE_ADMIN_EMAIL`, sets `platformRole: \"admin\"` and writes a `platform_admin.claimed` entry to `adminAuditLogs`."},{"heading":"becoming-an-application-admin","content":"The button only appears when the emails match (`admin.accessStatus.canClaim`). Other accounts see \"Administrator access required\". The repository does not include a UI for promoting further admins; the `adminUsers.ts` functions are the place to look if you need one."},{"heading":"becoming-an-application-admin","content":"The admin app serves `/app-admin` with users (list and detail), workspaces, payment gateway (customers, subscriptions, invoices), feedback, marketing, audit, settings and `/app-admin/connections` for the Admin MCP. The repository's `docs/separate-admin-app.md` describes the split."},{"heading":"impersonation","content":"From a user's detail page in the admin app, **Impersonate in web app** asks for a support reason. `impersonation.create` requires an active platform administrator session, creates a random one-use token, stores only its SHA-256 hash and writes the reason to the audit log. The admin app builds `https://<web origin>/auth/impersonate#token=...` from `VITE_WEB_APP_URL` (HTTPS is required outside localhost) and opens it."},{"heading":"impersonation","content":"The web app's `/auth/impersonate` route removes the fragment, signs in with the `admin-impersonation` provider, which runs `impersonation.consume`, and navigates to the customer app. Handoffs expire after 60 seconds; the resulting support session expires after 15 minutes. Expired or replayed links, ended admin sessions, demoted administrators, deleted or ineligible users and administrator targets are rejected. An impersonation banner in the web app offers **End support session**, which signs out the temporary session and returns to the admin app's user list through `VITE_ADMIN_APP_URL`. The administrator's own session on the admin origin is untouched."},{"heading":"http-api-keys","content":"Separately from sessions, users can create API keys at `/user/settings/developer` in the web app. Keys are shown once and stored as a SHA-256 hash; they carry scopes and authorize `GET /api/v1/me`, `GET /api/v1/workspaces` and `POST /api/ai/stream` on the Convex site URL with a `Bearer nb_live_...` header. These keys are not MCP credentials."}],"headings":[{"id":"where-the-configuration-lives","content":"Where the configuration lives"},{"id":"providers","content":"Providers"},{"id":"email-verification-and-the-local-inbox","content":"Email verification and the local inbox"},{"id":"sessions-and-route-protection","content":"Sessions and route protection"},{"id":"becoming-an-application-admin","content":"Becoming an application admin"},{"id":"impersonation","content":"Impersonation"},{"id":"http-api-keys","content":"HTTP API keys"}]},"url":"/docs/v5/starters/tanstack-convex/authentication","title":"Authentication","description":"How Convex Auth works in the TanStack + Convex starter, including verification emails, social login, sessions, the separate admin app, the admin claim flow and impersonation."},{"_id":"/docs/v5/starters/tanstack-convex/deployment","structured":{"contents":[{"content":"Production is three deployments against one backend: the Convex deployment (schema, functions, HTTP actions, MCP), the web app and the admin app, each built by Vite and Nitro. The committed `vercel.json` builds the web frontend only; it does not run `convex deploy`, so you publish the backend separately and set the public `VITE_` variables in each Vercel project by hand. This page follows the repository's `deployment.md` and `docs/separate-admin-app.md`; the shared Vercel guide covers the platform-generic steps."},{"heading":"before-you-deploy","content":"You need a Convex account, the Convex CLI (installed with the repo), two Vercel projects, and a Resend sender, because sign-up cannot finish without `AUTH_RESEND_KEY` and `AUTH_EMAIL_FROM`."},{"heading":"where-each-value-lives","content":"Place"},{"heading":"where-each-value-lives","content":"Variables"},{"heading":"where-each-value-lives","content":"Convex deployment (`pnpm convex env set`)"},{"heading":"where-each-value-lives","content":"`APP_URL`, `NEXTBASE_ADMIN_EMAIL`, `EMAIL_CHANGE_SECRET`, `SITE_URL`, `ADMIN_SITE_URL`, `JWT_PRIVATE_KEY` and `JWKS` (from `auth:setup`), `MCP_APP_URL`, and every `AUTH_*`, `RESEND_*`, `EMAIL_FROM`, `STRIPE_*` and `OPENAI_*` secret"},{"heading":"where-each-value-lives","content":"Web Vercel project"},{"heading":"where-each-value-lives","content":"`VITE_CONVEX_URL`, `VITE_ADMIN_APP_URL`, optional `VITE_OAUTH_PROVIDERS`, `VITE_POSTHOG_KEY`, `VITE_POSTHOG_HOST`"},{"heading":"where-each-value-lives","content":"Admin Vercel project"},{"heading":"where-each-value-lives","content":"`VITE_CONVEX_URL`, `VITE_WEB_APP_URL`, optional `VITE_OAUTH_PROVIDERS`"},{"heading":"where-each-value-lives","content":"Your local `.env.local` files are only read by the Convex CLI and Vite on your machine; nothing in production reads them."},{"heading":"provision-the-convex-deployment","content":"Use `pnpm convex deployment select` instead of `init` to pick an existing deployment. If you select the production deployment with the CLI, it writes `CONVEX_DEPLOYMENT=prod:<name>` and the URLs into your local `.env.local`; switch back with `pnpm db:local:select` before local development. The project's URLs are in the Convex dashboard under Settings > URL & Deploy Key:"},{"heading":"provision-the-convex-deployment","content":"`https://<name>.convex.cloud` is the functions URL (`VITE_CONVEX_URL` in both Vercel projects)"},{"heading":"provision-the-convex-deployment","content":"`https://<name>.convex.site` is the HTTP actions origin (`CONVEX_SITE_URL`), where `/mcp/*`, `/oauth/*`, `/api/*` and the auth routes live"},{"heading":"set-the-backend-environment","content":"All secrets live in the Convex deployment, never in a browser bundle. Set the required values:"},{"heading":"set-the-backend-environment","content":"`APP_URL` must be the public HTTPS origin of the web app without a path or credentials. `ADMIN_SITE_URL` is the admin origin; the auth redirect allowlist permits exactly these two origins, and admin MCP consent is routed to it. Then add the optional groups you use: `AUTH_GOOGLE_ID`/`AUTH_GOOGLE_SECRET` and the GitHub and Twitter pairs, `RESEND_API_KEY` and `EMAIL_FROM`, `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET`, `OPENAI_API_KEY` and `OPENAI_ALLOWED_MODELS`. The environment variables page describes each one."},{"heading":"set-the-backend-environment","content":"Run Convex Auth setup against the production deployment so it has signing keys and `SITE_URL`:"},{"heading":"set-the-backend-environment","content":"Set `SITE_URL` to your web origin when the CLI asks, or correct it afterwards with `pnpm convex env set SITE_URL https://app.example.com`. Keep existing `JWT_PRIVATE_KEY`, `JWKS` and Auth tables when updating an app that already has users."},{"heading":"set-the-backend-environment","content":"For the user MCP consent page, set the web origin explicitly:"},{"heading":"set-the-backend-environment","content":"Without it the backend falls back to `APP_URL`, then `SITE_URL`."},{"heading":"deploy-the-convex-backend","content":"The Vercel builds do not touch the backend, so publish it first from your machine or your own CI:"},{"heading":"deploy-the-convex-backend","content":"This publishes `apps/database/convex` (schema, functions, both MCP gateway components). Run it again whenever Convex code changes; frontend-only changes do not need it. If you prefer to build everything locally in one go, `pnpm convex deploy --cmd 'pnpm build' --cmd-url-env-var-name VITE_CONVEX_URL` also runs the Turbo build with `VITE_CONVEX_URL` set to the production URL; the flag is required because Vite lives in a workspace package and Convex cannot detect the `VITE_` prefix from the root `package.json`."},{"heading":"create-the-web-vercel-project","content":"Import the repository with the project root set to the repository root. `vercel.json` already sets:"},{"heading":"create-the-web-vercel-project","content":"Install command: `pnpm install`"},{"heading":"create-the-web-vercel-project","content":"Build command: `pnpm --filter @nextbase/web build && mkdir -p .vercel && cp -r apps/web/.vercel/output .vercel/output`"},{"heading":"create-the-web-vercel-project","content":"Output directory: `.vercel/output`"},{"heading":"create-the-web-vercel-project","content":"Framework: none"},{"heading":"create-the-web-vercel-project","content":"The copy step moves the Nitro Build Output API artifacts from `apps/web/.vercel/output` to the repository root where Vercel expects them. It also adds `X-Frame-Options`, `Content-Security-Policy: frame-ancestors 'none'` and `Referrer-Policy` headers on `/mcp/*`."},{"heading":"create-the-web-vercel-project","content":"Add these Vercel environment variables:"},{"heading":"create-the-web-vercel-project","content":"Variable"},{"heading":"create-the-web-vercel-project","content":"Value"},{"heading":"create-the-web-vercel-project","content":"`VITE_CONVEX_URL`"},{"heading":"create-the-web-vercel-project","content":"`https://<name>.convex.cloud` from the Convex dashboard"},{"heading":"create-the-web-vercel-project","content":"`VITE_ADMIN_APP_URL`"},{"heading":"create-the-web-vercel-project","content":"The admin origin, for example `https://admin.example.com`"},{"heading":"create-the-web-vercel-project","content":"`VITE_OAUTH_PROVIDERS`"},{"heading":"create-the-web-vercel-project","content":"Optional, for example `google,github,twitter`"},{"heading":"create-the-web-vercel-project","content":"`VITE_POSTHOG_KEY`, `VITE_POSTHOG_HOST`"},{"heading":"create-the-web-vercel-project","content":"Optional analytics"},{"heading":"create-the-web-vercel-project","content":"Preview deployments need their own Convex deployment and `VITE_CONVEX_URL` if you want them to work; the repository does not document a preview setup."},{"heading":"create-the-web-vercel-project","content":"If you would rather have one Vercel build publish both the backend and the web frontend, add `CONVEX_DEPLOY_KEY` (Convex dashboard, Settings > URL & Deploy Key > Generate Production Deploy Key, scoped to Production) to the web project and change the build command to:"},{"heading":"create-the-web-vercel-project","content":"`convex deploy` then sets `VITE_CONVEX_URL` itself, so you do not set it in Vercel. Do not prefix the build command with `bash`; `pnpm` is a Node executable."},{"heading":"create-the-admin-vercel-project","content":"Create a second Vercel project from the same repository with the project root set to `apps/admin`. `apps/admin/vercel.json` sets:"},{"heading":"create-the-admin-vercel-project","content":"Install command: `pnpm install --frozen-lockfile`"},{"heading":"create-the-admin-vercel-project","content":"Build command: `pnpm run build`"},{"heading":"create-the-admin-vercel-project","content":"Output directory: `.vercel/output`"},{"heading":"create-the-admin-vercel-project","content":"Framework: none"},{"heading":"create-the-admin-vercel-project","content":"It also sends `X-Robots-Tag: noindex, nofollow`, `Referrer-Policy: no-referrer` and `X-Frame-Options: DENY` on every response. If your Vercel project has to stay rooted at the repository, the root `vercel.admin.json` holds the equivalent configuration (`pnpm --filter @nextbase/admin build` plus the copy step); apply it to that project yourself."},{"heading":"create-the-admin-vercel-project","content":"Add these Vercel environment variables:"},{"heading":"create-the-admin-vercel-project","content":"Variable"},{"heading":"create-the-admin-vercel-project","content":"Value"},{"heading":"create-the-admin-vercel-project","content":"`VITE_CONVEX_URL`"},{"heading":"create-the-admin-vercel-project","content":"The same `https://<name>.convex.cloud` as the web project"},{"heading":"create-the-admin-vercel-project","content":"`VITE_WEB_APP_URL`"},{"heading":"create-the-admin-vercel-project","content":"The web origin, for example `https://app.example.com`. Impersonation refuses non-HTTPS origins"},{"heading":"create-the-admin-vercel-project","content":"`VITE_OAUTH_PROVIDERS`"},{"heading":"create-the-admin-vercel-project","content":"Optional, if the admin login should show social buttons"},{"heading":"create-the-admin-vercel-project","content":"Give the project its own domain and make sure it matches `ADMIN_SITE_URL` in the Convex deployment and `VITE_ADMIN_APP_URL` in the web project. The demo uses `tanstack-convex-admin.usenextbase.com`."},{"heading":"seed-and-backfill","content":"After the first deployment:"},{"heading":"seed-and-backfill","content":"When upgrading an existing deployment to the exact users aggregate, run once:"},{"heading":"seed-and-backfill","content":"Add `--prod` if your CLI is not already targeting the production deployment."},{"heading":"claim-admin-and-register-stripe","content":"Sign up in the web app with the `NEXTBASE_ADMIN_EMAIL` address, then open the admin app, sign in with the same account and click **Claim admin access** on `/app-admin`. Do this right after the first production deploy."},{"heading":"claim-admin-and-register-stripe","content":"If you use Stripe, add a webhook endpoint at `https://<name>.convex.site/api/stripe/webhook`, then set `STRIPE_WEBHOOK_SECRET` and `STRIPE_SECRET_KEY` with `pnpm convex env set`."},{"heading":"auth-callbacks-and-origins","content":"Convex Auth's HTTP routes are served from the `.convex.site` origin. `SITE_URL` must be the web origin and `ADMIN_SITE_URL` the admin origin so redirects, reset links and admin logins return to the right app; the redirect callback rejects any other origin. Register the provider callback URLs required by Convex Auth for Google, GitHub and Twitter against the same production deployment. Only providers listed in `VITE_OAUTH_PROVIDERS` appear in the UI of the app that lists them."},{"heading":"deploy-to-another-host","content":"Change the Nitro preset in `apps/web/vite.config.ts` (and `apps/admin/vite.config.ts`) from `vercel` to one your host supports (`node-server`, `netlify`, `cloudflare-pages`), run `pnpm build`, and serve the output. The web build only needs `VITE_CONVEX_URL` and `VITE_ADMIN_APP_URL` at build time, the admin build `VITE_CONVEX_URL` and `VITE_WEB_APP_URL`. The Cloudflare guide is written for the D1 kits; the Convex backend stays on Convex whichever host serves the frontends."},{"heading":"post-deploy-smoke-checks","content":"`GET https://<name>.convex.site/api/health` returns `{ \"ok\": true, \"service\": \"nextbase-convex\" }`."},{"heading":"post-deploy-smoke-checks","content":"Open `/login` and `/sign-up` on the web app, create a test account, receive the verification email and finish onboarding with a workspace."},{"heading":"post-deploy-smoke-checks","content":"Open the admin origin with the admin email, sign in and claim access on `/app-admin`."},{"heading":"post-deploy-smoke-checks","content":"From a user's detail page in the admin app, run **Impersonate in web app** and confirm the web app shows the support session banner and **End support session** returns you to the admin app."},{"heading":"post-deploy-smoke-checks","content":"Open `/user/settings/connections` in the web app and confirm the User MCP URL shows your `.convex.site` origin, not the demo's. Follow connect an agent to run `account_context` and `projects_list`."},{"heading":"post-deploy-smoke-checks","content":"Open `/app-admin/connections` in the admin app and check the Admin MCP URL in the same way; the consent page for an admin connection should open on the admin origin."},{"heading":"post-deploy-smoke-checks","content":"If you use Stripe, send a test event from the Stripe dashboard and confirm it appears in the admin payment gateway pages."},{"heading":"post-deploy-smoke-checks","content":"Check that `pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm build` still pass on the deployed commit."},{"heading":"post-deploy-smoke-checks","content":"See troubleshooting if login loops, emails do not arrive or an agent cannot reach consent."}],"headings":[{"id":"before-you-deploy","content":"Before you deploy"},{"id":"where-each-value-lives","content":"Where each value lives"},{"id":"provision-the-convex-deployment","content":"Provision the Convex deployment"},{"id":"set-the-backend-environment","content":"Set the backend environment"},{"id":"deploy-the-convex-backend","content":"Deploy the Convex backend"},{"id":"create-the-web-vercel-project","content":"Create the web Vercel project"},{"id":"create-the-admin-vercel-project","content":"Create the admin Vercel project"},{"id":"seed-and-backfill","content":"Seed and backfill"},{"id":"claim-admin-and-register-stripe","content":"Claim admin and register Stripe"},{"id":"auth-callbacks-and-origins","content":"Auth callbacks and origins"},{"id":"deploy-to-another-host","content":"Deploy to another host"},{"id":"post-deploy-smoke-checks","content":"Post-deploy smoke checks"}]},"url":"/docs/v5/starters/tanstack-convex/deployment","title":"Deployment","description":"Deploy the TanStack + Convex starter: a Convex production deployment for the backend and two Vercel projects for the web app and the admin app."},{"_id":"/docs/v5/starters/tanstack-convex/environment-variables","structured":{"contents":[{"content":"The kit has two example files: `.env.local.example` at the repository root (Convex CLI and the web app) and `apps/admin/.env.local.example` (the admin app). Three different places read these variables, and that is the main thing to get right."},{"content":"**Web browser bundle (public):** variables prefixed `VITE_` in the root `.env.local` are inlined into the web bundle by Vite at build time, because `apps/web/vite.config.ts` sets `envDir: \"../..\"`. Never put a secret in a `VITE_` variable."},{"content":"**Admin browser bundle (public):** the admin app reads its own `apps/admin/.env.local` (`envDir: \".\"`). The root file does not reach it."},{"content":"**Convex deployment (server-only):** everything without a `VITE_` prefix is read by Convex functions with `process.env`. A value in `.env.local` does not reach the backend. Set it in the selected deployment with `pnpm convex env set NAME value`, or in the Convex dashboard."},{"content":"Both `.env.local` files are gitignored. In the root file, `CONVEX_DEPLOYMENT`, `VITE_CONVEX_URL` and `CONVEX_SITE_URL` are maintained by `pnpm db:local:select` (or `pnpm convex deployment select`); everything else is yours. If the CLI creates the file from scratch it writes `CONVEX_URL` instead of `VITE_CONVEX_URL`, so copy the example first (see installation)."},{"content":"The values in the example files (`re_example`, `sk_test_example`, `whsec_example`, `admin@example.com`) are placeholders. Replace them with your own and never commit real keys."},{"heading":"deployment-selection-and-urls","content":"Variable"},{"heading":"deployment-selection-and-urls","content":"Where it is read"},{"heading":"deployment-selection-and-urls","content":"Required"},{"heading":"deployment-selection-and-urls","content":"Purpose"},{"heading":"deployment-selection-and-urls","content":"Where to get the value"},{"heading":"deployment-selection-and-urls","content":"`CONVEX_DEPLOYMENT`"},{"heading":"deployment-selection-and-urls","content":"Convex CLI, `dev:local` guard"},{"heading":"deployment-selection-and-urls","content":"Required"},{"heading":"deployment-selection-and-urls","content":"Selected deployment, for example `local:nextbase` or `prod:<name>`"},{"heading":"deployment-selection-and-urls","content":"Written by `pnpm db:local:select` or `pnpm convex deployment select`"},{"heading":"deployment-selection-and-urls","content":"`VITE_CONVEX_URL`"},{"heading":"deployment-selection-and-urls","content":"Web browser, `dev:local` guard"},{"heading":"deployment-selection-and-urls","content":"Required"},{"heading":"deployment-selection-and-urls","content":"Convex functions URL the web app connects to"},{"heading":"deployment-selection-and-urls","content":"`http://127.0.0.1:3210` locally; `https://<name>.convex.cloud` in production (Convex dashboard, Settings > URL & Deploy Key). Written by the CLI when the line already exists"},{"heading":"deployment-selection-and-urls","content":"`CONVEX_SITE_URL`"},{"heading":"deployment-selection-and-urls","content":"Convex deployment"},{"heading":"deployment-selection-and-urls","content":"Required"},{"heading":"deployment-selection-and-urls","content":"Origin of HTTP actions, including `/mcp/*`, `/oauth/*` and `/api/*`; also used by `auth.config.ts` as the JWT domain"},{"heading":"deployment-selection-and-urls","content":"`http://127.0.0.1:3211` locally; `https://<name>.convex.site` in production. Convex supplies it in cloud deployments"},{"heading":"deployment-selection-and-urls","content":"`APP_URL`"},{"heading":"deployment-selection-and-urls","content":"Convex deployment"},{"heading":"deployment-selection-and-urls","content":"Required"},{"heading":"deployment-selection-and-urls","content":"Public origin of the web app, used to build links in app emails and as a fallback consent-page origin for MCP"},{"heading":"deployment-selection-and-urls","content":"`http://localhost:3000` locally; your HTTPS origin in production. Must be an absolute URL without credentials, HTTPS outside localhost. Never generated"},{"heading":"authentication-and-email","content":"Variable"},{"heading":"authentication-and-email","content":"Where it is read"},{"heading":"authentication-and-email","content":"Required"},{"heading":"authentication-and-email","content":"Purpose"},{"heading":"authentication-and-email","content":"Where to get the value"},{"heading":"authentication-and-email","content":"`NEXTBASE_ADMIN_EMAIL`"},{"heading":"authentication-and-email","content":"Convex deployment"},{"heading":"authentication-and-email","content":"Required for admin"},{"heading":"authentication-and-email","content":"The one signed-in account allowed to claim platform admin on the admin app's `/app-admin`"},{"heading":"authentication-and-email","content":"Your own address"},{"heading":"authentication-and-email","content":"`AUTH_RESEND_KEY`"},{"heading":"authentication-and-email","content":"Convex deployment"},{"heading":"authentication-and-email","content":"Needed for sign-up"},{"heading":"authentication-and-email","content":"Resend API key used by Convex Auth for verification codes, password reset and magic links. Sign-up cannot complete without it"},{"heading":"authentication-and-email","content":"Resend dashboard"},{"heading":"authentication-and-email","content":"`AUTH_EMAIL_FROM`"},{"heading":"authentication-and-email","content":"Convex deployment"},{"heading":"authentication-and-email","content":"Needed for sign-up"},{"heading":"authentication-and-email","content":"Sender for auth emails, for example `Nextbase <auth@example.com>`"},{"heading":"authentication-and-email","content":"A verified Resend domain"},{"heading":"authentication-and-email","content":"`RESEND_API_KEY`"},{"heading":"authentication-and-email","content":"Convex deployment"},{"heading":"authentication-and-email","content":"Optional"},{"heading":"authentication-and-email","content":"Dedicated key for app emails (invitations, email change codes, deletion confirmation). Falls back to `AUTH_RESEND_KEY`"},{"heading":"authentication-and-email","content":"Resend dashboard"},{"heading":"authentication-and-email","content":"`EMAIL_FROM`"},{"heading":"authentication-and-email","content":"Convex deployment"},{"heading":"authentication-and-email","content":"Optional"},{"heading":"authentication-and-email","content":"Sender for app emails. Falls back to `AUTH_EMAIL_FROM`"},{"heading":"authentication-and-email","content":"A verified Resend domain"},{"heading":"authentication-and-email","content":"`EMAIL_CHANGE_SECRET`"},{"heading":"authentication-and-email","content":"Convex deployment"},{"heading":"authentication-and-email","content":"Required"},{"heading":"authentication-and-email","content":"Hashes email-change verification codes"},{"heading":"authentication-and-email","content":"Generate at least 32 random characters"},{"heading":"authentication-and-email","content":"`SITE_URL`"},{"heading":"authentication-and-email","content":"Convex deployment"},{"heading":"authentication-and-email","content":"Set by `auth:setup`"},{"heading":"authentication-and-email","content":"Convex Auth's site URL (the web origin): the first allowed redirect origin, the base for password reset links and a consent-page fallback"},{"heading":"authentication-and-email","content":"Written by `pnpm auth:setup`; set it to your production web origin when deploying"},{"heading":"authentication-and-email","content":"`ADMIN_SITE_URL`"},{"heading":"authentication-and-email","content":"Convex deployment"},{"heading":"authentication-and-email","content":"Required for the admin app"},{"heading":"authentication-and-email","content":"Origin of the admin app. Added to the auth redirect allowlist so admin login, OAuth and magic link callbacks return to the admin origin; admin MCP consent is routed here"},{"heading":"authentication-and-email","content":"`http://localhost:3002` locally; your admin HTTPS origin in production. Listed as a comment in the root example"},{"heading":"authentication-and-email","content":"`JWT_PRIVATE_KEY`, `JWKS`"},{"heading":"authentication-and-email","content":"Convex deployment"},{"heading":"authentication-and-email","content":"Set by `auth:setup`"},{"heading":"authentication-and-email","content":"Convex Auth signing keys"},{"heading":"authentication-and-email","content":"Generated by `pnpm auth:setup`. Not in the example file; never paste them into a frontend env"},{"heading":"social-login","content":"Variable"},{"heading":"social-login","content":"Where it is read"},{"heading":"social-login","content":"Required"},{"heading":"social-login","content":"Purpose"},{"heading":"social-login","content":"Where to get the value"},{"heading":"social-login","content":"`VITE_OAUTH_PROVIDERS`"},{"heading":"social-login","content":"Web browser, admin browser"},{"heading":"social-login","content":"Optional"},{"heading":"social-login","content":"Comma-separated list of buttons to show: `google,github,twitter`. Providers not listed are hidden. Set it in both env files if the admin app should show the buttons too"},{"heading":"social-login","content":"Your choice"},{"heading":"social-login","content":"`AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET`"},{"heading":"social-login","content":"Convex deployment"},{"heading":"social-login","content":"Optional"},{"heading":"social-login","content":"Google OAuth credentials"},{"heading":"social-login","content":"Google Cloud console"},{"heading":"social-login","content":"`AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET`"},{"heading":"social-login","content":"Convex deployment"},{"heading":"social-login","content":"Optional"},{"heading":"social-login","content":"GitHub OAuth credentials"},{"heading":"social-login","content":"GitHub developer settings"},{"heading":"social-login","content":"`AUTH_TWITTER_ID`, `AUTH_TWITTER_SECRET`"},{"heading":"social-login","content":"Convex deployment"},{"heading":"social-login","content":"Optional"},{"heading":"social-login","content":"Twitter OAuth credentials"},{"heading":"social-login","content":"Twitter developer portal"},{"heading":"social-login","content":"Showing a button with `VITE_OAUTH_PROVIDERS` does not configure the provider. Its `AUTH_*` credentials must be set in the deployment too."},{"heading":"billing-ai-and-analytics","content":"Variable"},{"heading":"billing-ai-and-analytics","content":"Where it is read"},{"heading":"billing-ai-and-analytics","content":"Required"},{"heading":"billing-ai-and-analytics","content":"Purpose"},{"heading":"billing-ai-and-analytics","content":"Where to get the value"},{"heading":"billing-ai-and-analytics","content":"`STRIPE_SECRET_KEY`"},{"heading":"billing-ai-and-analytics","content":"Convex deployment"},{"heading":"billing-ai-and-analytics","content":"Optional"},{"heading":"billing-ai-and-analytics","content":"Stripe API access for billing"},{"heading":"billing-ai-and-analytics","content":"Stripe dashboard"},{"heading":"billing-ai-and-analytics","content":"`STRIPE_WEBHOOK_SECRET`"},{"heading":"billing-ai-and-analytics","content":"Convex deployment"},{"heading":"billing-ai-and-analytics","content":"Optional"},{"heading":"billing-ai-and-analytics","content":"Verifies signatures on `POST /api/stripe/webhook`"},{"heading":"billing-ai-and-analytics","content":"Stripe webhook endpoint settings"},{"heading":"billing-ai-and-analytics","content":"`OPENAI_API_KEY`"},{"heading":"billing-ai-and-analytics","content":"Convex deployment"},{"heading":"billing-ai-and-analytics","content":"Optional"},{"heading":"billing-ai-and-analytics","content":"Upstream key for `POST /api/ai/stream`"},{"heading":"billing-ai-and-analytics","content":"OpenAI dashboard"},{"heading":"billing-ai-and-analytics","content":"`OPENAI_ALLOWED_MODELS`"},{"heading":"billing-ai-and-analytics","content":"Convex deployment"},{"heading":"billing-ai-and-analytics","content":"Optional"},{"heading":"billing-ai-and-analytics","content":"Comma-separated allowlist for the streaming endpoint; defaults to `gpt-5-mini`"},{"heading":"billing-ai-and-analytics","content":"Your choice"},{"heading":"billing-ai-and-analytics","content":"`VITE_POSTHOG_KEY`"},{"heading":"billing-ai-and-analytics","content":"Web browser"},{"heading":"billing-ai-and-analytics","content":"Optional"},{"heading":"billing-ai-and-analytics","content":"Enables PostHog capture; leave unset to disable"},{"heading":"billing-ai-and-analytics","content":"PostHog project settings"},{"heading":"billing-ai-and-analytics","content":"`VITE_POSTHOG_HOST`"},{"heading":"billing-ai-and-analytics","content":"Web browser"},{"heading":"billing-ai-and-analytics","content":"Optional"},{"heading":"billing-ai-and-analytics","content":"PostHog host, default `https://us.i.posthog.com`"},{"heading":"billing-ai-and-analytics","content":"PostHog project settings"},{"heading":"separate-admin-app-and-mcp","content":"Variable"},{"heading":"separate-admin-app-and-mcp","content":"Where it is read"},{"heading":"separate-admin-app-and-mcp","content":"Required"},{"heading":"separate-admin-app-and-mcp","content":"Purpose"},{"heading":"separate-admin-app-and-mcp","content":"Where to get the value"},{"heading":"separate-admin-app-and-mcp","content":"`VITE_ADMIN_APP_URL`"},{"heading":"separate-admin-app-and-mcp","content":"Web browser"},{"heading":"separate-admin-app-and-mcp","content":"Required"},{"heading":"separate-admin-app-and-mcp","content":"Origin of the admin app. The web app uses it to hand admin MCP consent requests to the admin app and to return from an impersonation session"},{"heading":"separate-admin-app-and-mcp","content":"`http://localhost:3002` locally; your admin origin in production"},{"heading":"separate-admin-app-and-mcp","content":"`MCP_APP_URL`"},{"heading":"separate-admin-app-and-mcp","content":"Convex deployment"},{"heading":"separate-admin-app-and-mcp","content":"Optional"},{"heading":"separate-admin-app-and-mcp","content":"Canonical web origin for the user `/mcp/authorize` consent page and the MCP browser origin allowlist. Falls back to `APP_URL`, then `SITE_URL`"},{"heading":"separate-admin-app-and-mcp","content":"Your web origin"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Variable"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Where it is read"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Required"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Purpose"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Where to get the value"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"`VITE_CONVEX_URL`"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Admin browser"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Required"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"The same Convex functions URL the web app uses"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"`http://127.0.0.1:3210` locally; `https://<name>.convex.cloud` in production. The example ships a cloud placeholder"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"`VITE_WEB_APP_URL`"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Admin browser"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Required"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Customer web origin. Target of impersonation handoffs (`/auth/impersonate`) and the \"Open web app\" link. The admin app throws if it is missing when you impersonate, and requires HTTPS outside localhost"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"`http://localhost:3000` locally; your web origin in production"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"`VITE_OAUTH_PROVIDERS`"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Admin browser"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Optional"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Social buttons on the admin login page, same format as the web app"},{"heading":"admin-app-file-appsadminenvlocalexample","content":"Your choice"},{"heading":"build-time-variables-on-vercel","content":"`turbo.json` lists `VITE_CONVEX_URL`, `VITE_OAUTH_PROVIDERS`, `VITE_POSTHOG_HOST`, `VITE_POSTHOG_KEY`, `VITE_WEB_APP_URL` and `VITE_ADMIN_APP_URL` as inputs to the `build` task. The committed `vercel.json` builds the frontend only and does not run `convex deploy`, so set `VITE_CONVEX_URL` and `VITE_ADMIN_APP_URL` (plus any optional `VITE_` values) by hand in the web Vercel project, and `VITE_CONVEX_URL` and `VITE_WEB_APP_URL` in the admin Vercel project. `CONVEX_DEPLOY_KEY` is only needed if you change the build command to run `convex deploy`. The deployment page has the details."}],"headings":[{"id":"deployment-selection-and-urls","content":"Deployment selection and URLs"},{"id":"authentication-and-email","content":"Authentication and email"},{"id":"social-login","content":"Social login"},{"id":"billing-ai-and-analytics","content":"Billing, AI and analytics"},{"id":"separate-admin-app-and-mcp","content":"Separate admin app and MCP"},{"id":"admin-app-file-appsadminenvlocalexample","content":"Admin app file (`apps/admin/.env.local.example`)"},{"id":"build-time-variables-on-vercel","content":"Build-time variables on Vercel"}]},"url":"/docs/v5/starters/tanstack-convex/environment-variables","title":"Environment variables","description":"Every variable in the starter's two example env files, which runtime reads it, and where its value comes from."},{"_id":"/docs/v5/starters/tanstack-convex","structured":{"contents":[{"content":"This kit uses TanStack Start, Convex reactive queries and Convex Auth. You get the account, workspace and project foundation, a separate admin application, and separate agent connections for users and application administrators."},{"content":"Open the live demo · Admin demo · Repository"},{"heading":"the-stack","content":"Layer"},{"heading":"the-stack","content":"What the kit uses"},{"heading":"the-stack","content":"Frontend"},{"heading":"the-stack","content":"Two TanStack Start apps (`apps/web` for customers, `apps/admin` for platform administration) with TanStack Router, Vite, React 19, Tailwind CSS 4 and Radix UI primitives"},{"heading":"the-stack","content":"Backend and database"},{"heading":"the-stack","content":"Convex (schema, queries, mutations, HTTP actions, scheduled work, file storage) in `apps/database/convex`, shared by both apps"},{"heading":"the-stack","content":"Authentication"},{"heading":"the-stack","content":"Convex Auth with password, magic link (Resend) and Google, GitHub and Twitter OAuth providers"},{"heading":"the-stack","content":"Emails"},{"heading":"the-stack","content":"React Email templates in `apps/email`, delivered through Resend"},{"heading":"the-stack","content":"Hosting used by the demo"},{"heading":"the-stack","content":"Two Vercel projects (web and admin, Nitro `vercel` preset) and one Convex cloud deployment for the backend"},{"heading":"the-stack","content":"Public routes may server-render. Protected application and admin routes render on the client after Convex Auth resolves. Convex is the application backend; TanStack server functions are reserved for HTML, headers and web-runtime concerns."},{"heading":"what-you-get-out-of-the-box","content":"Accounts with onboarding, profile settings, security settings, email change and account deletion"},{"heading":"what-you-get-out-of-the-box","content":"Workspaces with owner, admin, member and readonly roles, invitations, settings and workspace deletion"},{"heading":"what-you-get-out-of-the-box","content":"Projects with slugs, statuses, comments and project chats"},{"heading":"what-you-get-out-of-the-box","content":"A separate admin app (`apps/admin`, port 3002) at `/app-admin` with users, workspaces, payment gateway views, feedback, marketing content, an audit log, settings and admin agent connections"},{"heading":"what-you-get-out-of-the-box","content":"Audited impersonation: an administrator opens a customer's account in the web app through a one-use handoff token"},{"heading":"what-you-get-out-of-the-box","content":"A User MCP and an Admin MCP with OAuth consent, so agents can act through the app's own permissions"},{"heading":"what-you-get-out-of-the-box","content":"Stripe billing: customers, products, prices, subscriptions, invoices, usage, credits and a signed webhook"},{"heading":"what-you-get-out-of-the-box","content":"Public content: blog, changelog, roadmap, docs and feedback boards"},{"heading":"what-you-get-out-of-the-box","content":"API keys for the authenticated HTTP API (`/api/v1/me`, `/api/v1/workspaces`) and an OpenAI streaming endpoint"},{"heading":"what-you-get-out-of-the-box","content":"File uploads through Convex storage, notifications, rate limits, aggregates and durable background jobs"},{"heading":"what-you-get-out-of-the-box","content":"Optional PostHog browser analytics"},{"heading":"what-you-get-out-of-the-box","content":"Instructions and MCP configuration for AI coding tools generated from one `AGENTS.md` with `pnpm agents:sync`"},{"heading":"prerequisites","content":"Node.js 24. The repository's `.nvmrc` says `24` and `package.json` `engines` requires `>=24`."},{"heading":"prerequisites","content":"pnpm 10.33.0, declared in the `packageManager` field. Run `corepack enable` and let it pick the version."},{"heading":"prerequisites","content":"No Docker. Convex ships its own local backend, stored in `.convex/`."},{"heading":"prerequisites","content":"A Convex account is only needed when you deploy. Local development uses the CLI's anonymous agent mode."},{"heading":"prerequisites","content":"Optional accounts: Resend (auth and app emails), Google, GitHub or Twitter (social login), Stripe (billing), OpenAI (AI endpoint), PostHog (analytics), Vercel (hosting)."},{"heading":"next-steps","content":"Installation: clone, start the local Convex backend and run the web and admin apps."},{"heading":"next-steps","content":"Environment variables: every variable in the two example env files."},{"heading":"next-steps","content":"Authentication: Convex Auth, verification emails, the admin claim flow and impersonation."},{"heading":"next-steps","content":"Agent connections: User MCP and Admin MCP, tools and consent."},{"heading":"next-steps","content":"Project structure: the monorepo and where to make changes."},{"heading":"next-steps","content":"Deployment: Convex production deployment and two Vercel projects."},{"heading":"next-steps","content":"For the shared guides, start with getting started and troubleshooting."}],"headings":[{"id":"the-stack","content":"The stack"},{"id":"what-you-get-out-of-the-box","content":"What you get out of the box"},{"id":"prerequisites","content":"Prerequisites"},{"id":"next-steps","content":"Next steps"}]},"url":"/docs/v5/starters/tanstack-convex","title":"TanStack + Convex","description":"Overview of the TanStack Start + Convex starter: what it ships, what you need, and where to go next."},{"_id":"/docs/v5/starters/tanstack-convex/installation","structured":{"contents":[{"content":"This page takes you from a fresh clone to a signed-in account with a workspace, plus the admin app running on its own port. The whole flow runs on your computer; no Convex account or Docker is needed."},{"content":"The shared getting started page covers the parts that are the same for every kit."},{"heading":"clone-the-repository","content":"If GitHub says the repository doesn't exist, check that you are signed in with the account that has access."},{"heading":"install-dependencies","content":"Use Node.js 24 (the version in `.nvmrc`; `package.json` `engines` requires `>=24`) and let corepack select pnpm 10.33.0 from the `packageManager` field."},{"heading":"create-and-select-the-local-convex-backend","content":"Copy the example env file first, then create and select the local deployment:"},{"heading":"create-and-select-the-local-convex-backend","content":"`db:local:init` runs `convex init` with `CONVEX_AGENT_MODE=anonymous`, so no Convex login is needed. `db:local:select` runs `convex deployment select local` and fills in `CONVEX_DEPLOYMENT`, `VITE_CONVEX_URL` and `CONVEX_SITE_URL` in `.env.local` at the repository root. The backend state (rows, storage, schedules, function execution) lives in `.convex/`, which is gitignored."},{"heading":"create-and-select-the-local-convex-backend","content":"Copying the example file first matters. When `.env.local` already contains a `VITE_CONVEX_URL` line, the CLI updates it. When the file does not exist, the CLI creates it with `CONVEX_URL` instead, because Vite is not a root dependency, and you have to add `VITE_CONVEX_URL` yourself. The `dev:local` guard accepts either name for its loopback check, but stops with a message telling you to add `VITE_CONVEX_URL=http://127.0.0.1:3210` if only `CONVEX_URL` is present, because the web app reads the `VITE_` name."},{"heading":"create-and-select-the-local-convex-backend","content":"After the select step, the first lines of `.env.local` should look like this:"},{"heading":"create-and-select-the-local-convex-backend","content":"`APP_URL` is never generated; it comes from the example file. The example also sets `VITE_ADMIN_APP_URL=http://localhost:3002` for the admin app link and `MCP_APP_URL=http://localhost:3000` for the MCP consent page."},{"heading":"create-and-select-the-local-convex-backend","content":"`.env.local.example` documents every variable, but the Convex CLI maintains the deployment lines in `.env.local`. Copy the example only when you have no `.env.local` yet, and keep your own values if you already have one."},{"heading":"configure-the-admin-app","content":"The admin app has its own env file because `apps/admin/vite.config.ts` sets `envDir: \".\"`. Copy its example and point it at the local backend:"},{"heading":"configure-the-admin-app","content":"The example ships with a cloud placeholder for `VITE_CONVEX_URL`; replace it with the same local URL the web app uses. `VITE_WEB_APP_URL` is where impersonation handoffs and the \"Open web app\" link go."},{"heading":"set-up-convex-auth","content":"This runs the `@convex-dev/auth` CLI against the selected deployment. It configures the auth signing keys (`JWT_PRIVATE_KEY` and `JWKS`) and `SITE_URL` in that deployment's environment. Password sign-in works after this step. Run it again if you create a new local deployment."},{"heading":"set-up-convex-auth","content":"So the admin app can sign in on its own origin, also set the admin origin in the local deployment:"},{"heading":"start-the-dev-servers","content":"`dev:local` runs the guard, then starts four processes with `concurrently`: `convex dev` (local backend on `http://127.0.0.1:3210`, HTTP actions on `http://127.0.0.1:3211`), the web app, the admin app and the React Email preview server."},{"heading":"start-the-dev-servers","content":"App"},{"heading":"start-the-dev-servers","content":"URL"},{"heading":"start-the-dev-servers","content":"Web app"},{"heading":"start-the-dev-servers","content":"http\\://localhost:3000 (`apps/web/vite.config.ts`)"},{"heading":"start-the-dev-servers","content":"Email template preview"},{"heading":"start-the-dev-servers","content":"`http://localhost:3001`"},{"heading":"start-the-dev-servers","content":"Admin app"},{"heading":"start-the-dev-servers","content":"http\\://localhost:3002 (`apps/admin/vite.config.ts`); `/` redirects to `/app-admin`"},{"heading":"start-the-dev-servers","content":"To run only the admin app, use `pnpm dev:admin`. Keep the Convex process running while you use either app."},{"heading":"seed-reference-data-optional","content":"In a second terminal:"},{"heading":"seed-reference-data-optional","content":"This runs `dev/seed:seed`, which inserts the `product` row in `appSettings` (product name and support email) once. It is idempotent."},{"heading":"create-your-account-and-workspace","content":"Open http\\://localhost:3000/sign-up and create an account with a name, email and password. The kit then sends a verification code and takes you to `/update-password?mode=verify` to enter it."},{"heading":"create-your-account-and-workspace","content":"Verification, password reset and magic link emails go through Resend even in local development. Set `AUTH_RESEND_KEY` and `AUTH_EMAIL_FROM` in the local deployment with `pnpm convex env set NAME value` to receive them. Without them, use a social provider you have configured, or see the authentication page for what works without email."},{"heading":"create-your-account-and-workspace","content":"After signing in you land on `/onboarding`, complete your profile and create a workspace. Then add a project from the workspace's projects page so you have data to test with."},{"heading":"verify-the-setup","content":"Run the quality gates from the repository root:"},{"heading":"verify-the-setup","content":"`pnpm test` runs Vitest in each workspace (Convex functions use `convex-test`). `pnpm test:e2e` runs Playwright against `http://127.0.0.1:3000`; without `PLAYWRIGHT_BASE_URL` it starts its own local Convex backend and web server. Install the browser first with `pnpm --filter @nextbase/web exec playwright install --with-deps chromium`."},{"heading":"make-yourself-an-application-admin","content":"Set the admin email in the local deployment, sign in with that address on the admin app at http\\://localhost:3002, then use **Claim admin access** on `/app-admin`:"},{"heading":"make-yourself-an-application-admin","content":"The admin app has its own login page and session; signing in to the web app does not sign you in to the admin app. The authentication page explains the claim flow."},{"heading":"useful-commands","content":"Command"},{"heading":"useful-commands","content":"What it does"},{"heading":"useful-commands","content":"`pnpm dev`"},{"heading":"useful-commands","content":"Same as `dev:local` but without the local-deployment guard"},{"heading":"useful-commands","content":"`pnpm dev:admin`"},{"heading":"useful-commands","content":"Starts only the admin app on port 3002"},{"heading":"useful-commands","content":"`pnpm build:web`, `pnpm build:admin`"},{"heading":"useful-commands","content":"Build one frontend without Turbo"},{"heading":"useful-commands","content":"`pnpm db:codegen`"},{"heading":"useful-commands","content":"Regenerates `apps/database/convex/_generated`"},{"heading":"useful-commands","content":"`pnpm db:types`"},{"heading":"useful-commands","content":"Regenerates `apps/database/lib/database.types.ts` from the schema validators"},{"heading":"useful-commands","content":"`pnpm db:status`"},{"heading":"useful-commands","content":"Opens the Convex dashboard for the selected deployment"},{"heading":"useful-commands","content":"`pnpm agents:sync`"},{"heading":"useful-commands","content":"Regenerates the AI coding tool files from `AGENTS.md` and `.agents/`; CI runs `pnpm agents:check`"},{"heading":"useful-commands","content":"`pnpm convex run migrations:backfillUsersAggregate`"},{"heading":"useful-commands","content":"One-time backfill of the users aggregate when upgrading an existing deployment"},{"heading":"stop-and-reset","content":"Stop the dev servers with `Ctrl+C`; `concurrently` stops the other processes when one exits. The local data in `.convex/` is kept between runs."},{"heading":"stop-and-reset","content":"To reset the local backend, stop the servers and delete the `.convex/` directory, then run `pnpm db:local:init`, `pnpm db:local:select` and `pnpm auth:setup` again. Deleting `.convex/` removes every local row, upload and scheduled job, including the deployment environment variables you set with `pnpm convex env set`."},{"heading":"stop-and-reset","content":"If a request fails after a branch change, check that `convex dev` is still running and that `.env.local` points at the local deployment. See troubleshooting."}],"headings":[{"id":"clone-the-repository","content":"Clone the repository"},{"id":"install-dependencies","content":"Install dependencies"},{"id":"create-and-select-the-local-convex-backend","content":"Create and select the local Convex backend"},{"id":"configure-the-admin-app","content":"Configure the admin app"},{"id":"set-up-convex-auth","content":"Set up Convex Auth"},{"id":"start-the-dev-servers","content":"Start the dev servers"},{"id":"seed-reference-data-optional","content":"Seed reference data (optional)"},{"id":"create-your-account-and-workspace","content":"Create your account and workspace"},{"id":"verify-the-setup","content":"Verify the setup"},{"id":"make-yourself-an-application-admin","content":"Make yourself an application admin"},{"id":"useful-commands","content":"Useful commands"},{"id":"stop-and-reset","content":"Stop and reset"}]},"url":"/docs/v5/starters/tanstack-convex/installation","title":"Installation","description":"Run the TanStack + Convex starter locally with the Convex local backend, Convex Auth, the web app and the admin app."},{"_id":"/docs/v5/starters/tanstack-convex/project-structure","structured":{"contents":[{"content":"The repository is a pnpm workspace (`apps/*` and `packages/*`) driven by Turbo. Convex is configured at the root (`convex.json` points `functions` at `apps/database/convex`), so Convex CLI commands run from the repository root."},{"heading":"tree","content":"<File name=\"public.spec.ts\" />"},{"heading":"tree","content":"<File name=\"responsive.spec.ts\" />"},{"heading":"tree","content":"<File name=\"app-shell.tsx\" />"},{"heading":"tree","content":"<File name=\"impersonation-banner.tsx\" />"},{"heading":"tree","content":"<File name=\"impersonation-callback.tsx\" />"},{"heading":"tree","content":"<File name=\"mcp-connections.tsx\" />"},{"heading":"tree","content":"<File name=\"mcp-consent.tsx\" />"},{"heading":"tree","content":"<File name=\"workspace-switcher.tsx\" />"},{"heading":"tree","content":"<File name=\"notification-menu.tsx\" />"},{"heading":"tree","content":"<File name=\"observability-provider.tsx\" />"},{"heading":"tree","content":"<File name=\"content.ts\" />"},{"heading":"tree","content":"<File name=\"docs-content.ts\" />"},{"heading":"tree","content":"<File name=\"errors.ts\" />"},{"heading":"tree","content":"<File name=\"logger.ts\" />"},{"heading":"tree","content":"<File name=\"navigation.ts\" />"},{"heading":"tree","content":"<File name=\"__root.tsx\" />"},{"heading":"tree","content":"<File name=\"index.tsx\" />"},{"heading":"tree","content":"<File name=\"login.tsx\" />"},{"heading":"tree","content":"<File name=\"sign-up.tsx\" />"},{"heading":"tree","content":"<File name=\"auth.impersonate.tsx\" />"},{"heading":"tree","content":"<File name=\"mcp.authorize.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.tsx\" />"},{"heading":"tree","content":"<File name=\"app.tsx\" />"},{"heading":"tree","content":"<File name=\"workspace.$workspaceSlug.projects.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.workspace.$workspaceSlug.settings.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.user.settings.connections.tsx\" />"},{"heading":"tree","content":"<File name=\"blog.index.tsx\" />"},{"heading":"tree","content":"<File name=\"docs.$.tsx\" />"},{"heading":"tree","content":"<File name=\"router.tsx\" />"},{"heading":"tree","content":"<File name=\"routeTree.gen.ts\" />"},{"heading":"tree","content":"<File name=\"globals.css\" />"},{"heading":"tree","content":"<File name=\"vite.config.ts\" />"},{"heading":"tree","content":"<File name=\"vitest.config.ts\" />"},{"heading":"tree","content":"<File name=\"playwright.config.ts\" />"},{"heading":"tree","content":"<File name=\"package.json\" />"},{"heading":"tree","content":"<File name=\"app-shell.tsx\" />"},{"heading":"tree","content":"<File name=\"mcp-connections.tsx\" />"},{"heading":"tree","content":"<File name=\"mcp-consent.tsx\" />"},{"heading":"tree","content":"<File name=\"impersonation.ts\" />"},{"heading":"tree","content":"<File name=\"navigation.ts\" />"},{"heading":"tree","content":"<File name=\"__root.tsx\" />"},{"heading":"tree","content":"<File name=\"index.tsx\" />"},{"heading":"tree","content":"<File name=\"login.tsx\" />"},{"heading":"tree","content":"<File name=\"mcp.authorize.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.tsx\" />"},{"heading":"tree","content":"<File name=\"app-admin_.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.app-admin.users.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.app-admin.users_.$userId.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.app-admin.workspaces.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.app-admin.payment-gateway.customers.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.app-admin.feedback.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.app-admin.marketing.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.app-admin.audit.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.app-admin.settings.tsx\" />"},{"heading":"tree","content":"<File name=\"_authenticated.app-admin.connections.tsx\" />"},{"heading":"tree","content":"<File name=\"router.tsx\" />"},{"heading":"tree","content":"<File name=\"routeTree.gen.ts\" />"},{"heading":"tree","content":"<File name=\".env.local.example\" />"},{"heading":"tree","content":"<File name=\"vercel.json\" />"},{"heading":"tree","content":"<File name=\"vite.config.ts\" />"},{"heading":"tree","content":"<File name=\"package.json\" />"},{"heading":"tree","content":"<File name=\"seed.ts\" />"},{"heading":"tree","content":"<File name=\"functions.ts\" />"},{"heading":"tree","content":"<File name=\"authz.ts\" />"},{"heading":"tree","content":"<File name=\"auth_redirect.ts\" />"},{"heading":"tree","content":"<File name=\"validators.ts\" />"},{"heading":"tree","content":"<File name=\"triggers.ts\" />"},{"heading":"tree","content":"<File name=\"aggregates.ts\" />"},{"heading":"tree","content":"<File name=\"appOrigin.ts\" />"},{"heading":"tree","content":"<File name=\"errors.ts\" />"},{"heading":"tree","content":"<File name=\"core.ts\" />"},{"heading":"tree","content":"<File name=\"billing.ts\" />"},{"heading":"tree","content":"<File name=\"content.ts\" />"},{"heading":"tree","content":"<File name=\"ai.ts\" />"},{"heading":"tree","content":"<File name=\"preferences.ts\" />"},{"heading":"tree","content":"<File name=\"system.ts\" />"},{"heading":"tree","content":"<File name=\"schema.ts\" />"},{"heading":"tree","content":"<File name=\"convex.config.ts\" />"},{"heading":"tree","content":"<File name=\"auth.ts\" />"},{"heading":"tree","content":"<File name=\"auth.config.ts\" />"},{"heading":"tree","content":"<File name=\"http.ts\" />"},{"heading":"tree","content":"<File name=\"crons.ts\" />"},{"heading":"tree","content":"<File name=\"users.ts\" />"},{"heading":"tree","content":"<File name=\"workspaces.ts\" />"},{"heading":"tree","content":"<File name=\"members.ts\" />"},{"heading":"tree","content":"<File name=\"invitations.ts\" />"},{"heading":"tree","content":"<File name=\"projects.ts\" />"},{"heading":"tree","content":"<File name=\"admin.ts\" />"},{"heading":"tree","content":"<File name=\"impersonation.ts\" />"},{"heading":"tree","content":"<File name=\"billing.ts\" />"},{"heading":"tree","content":"<File name=\"stripe.ts\" />"},{"heading":"tree","content":"<File name=\"workflow.ts\" />"},{"heading":"tree","content":"<File name=\"emailDelivery.tsx\" />"},{"heading":"tree","content":"<File name=\"migrations.ts\" />"},{"heading":"tree","content":"<File name=\"database.types.ts\" />"},{"heading":"tree","content":"<File name=\"generate-types.mjs\" />"},{"heading":"tree","content":"<File name=\"package.json\" />"},{"heading":"tree","content":"<File name=\"workspace-invitation.tsx\" />"},{"heading":"tree","content":"<File name=\"email-change-verification.tsx\" />"},{"heading":"tree","content":"<File name=\"account-deletion-confirmation.tsx\" />"},{"heading":"tree","content":"<File name=\"package.json\" />"},{"heading":"tree","content":"<File name=\"base.json\" />"},{"heading":"tree","content":"<File name=\"mcp-connections.md\" />"},{"heading":"tree","content":"<File name=\"separate-admin-app.md\" />"},{"heading":"tree","content":"<File name=\"assert-local-deployment.mjs\" />"},{"heading":"tree","content":"<File name=\"config.json\" />"},{"heading":"tree","content":"<File name=\".env.local.example\" />"},{"heading":"tree","content":"<File name=\".nvmrc\" />"},{"heading":"tree","content":"<File name=\"AGENTS.md\" />"},{"heading":"tree","content":"<File name=\"convex.json\" />"},{"heading":"tree","content":"<File name=\"deployment.md\" />"},{"heading":"tree","content":"<File name=\"package.json\" />"},{"heading":"tree","content":"<File name=\"pnpm-workspace.yaml\" />"},{"heading":"tree","content":"<File name=\"turbo.json\" />"},{"heading":"tree","content":"<File name=\"vercel.json\" />"},{"heading":"tree","content":"<File name=\"vercel.admin.json\" />"},{"heading":"tree","content":"The four workspaces are `@nextbase/web`, `@nextbase/admin`, `@nextbase/database` and `@nextbase/email`. Both frontends import the generated Convex API as `@nextbase/database/api`, data model types as `@nextbase/database/data-model`, and schema-derived types as `@nextbase/database/types`. The `parity/` folder holds a manifest of Nextbase Ultimate features this kit reproduces; `.changeset/` drives versioning."},{"heading":"tree","content":"`AGENTS.md` (root and per package) is the single source of instructions for AI coding tools. `pnpm agents:sync` generates `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md`, `.cursor/mcp.json` and the other tool files from it, from `.agents/config.json` (MCP servers and hooks) and from `.agents/skills/` (`new-page`, `db-migration`, `billing-plan`, `mcp-tool`). Edit the sources, not the generated files; CI runs `pnpm agents:check`."},{"heading":"routes-and-pages","content":"Routes are files in `apps/web/src/routes/` and `apps/admin/src/routes/` using TanStack Router's file conventions. Each app's `routeTree.gen.ts` is generated; do not edit it. In the web app, public routes (`index.tsx`, `blog.*`, `changelog*`, `docs.*`, `feedback*`, `roadmap.tsx`, `terms.tsx`, `login.tsx`, `sign-up.tsx`) may server-render. Anything under `_authenticated.*` is client-only and goes through the auth and onboarding gate in `_authenticated.tsx`."},{"heading":"routes-and-pages","content":"The sidebar and layout are in each app's `src/components/app-shell.tsx`; add a navigation item there when you add an authenticated page. UI primitives are in `components/ui/` (shadcn-style, configured by `apps/web/components.json` for the web app). Global styles and Tailwind 4 live in `src/styles/globals.css`."},{"heading":"admin-app","content":"`apps/admin` is a second TanStack Start app on port 3002 with its own `.env.local` (`envDir: \".\"`), its own Convex Auth session and its own Vercel project (`apps/admin/vercel.json`). All `/app-admin/*` pages live here; the web app has no admin routes. The admin gate in `apps/admin/src/routes/_authenticated.tsx` checks `admin.accessStatus`, and `lib/impersonation.ts` builds the handoff URL to the web origin from `VITE_WEB_APP_URL`. The repository's `docs/separate-admin-app.md` covers configuration, deployment and impersonation."},{"heading":"server-and-data-access","content":"All application logic is in Convex functions under `apps/database/convex/`, one file per domain (`users.ts`, `workspaces.ts`, `members.ts`, `invitations.ts`, `projects.ts`, `notifications.ts`, `billing.ts`, `feedback.ts`, `marketing.ts`, and so on). Define new functions with the wrappers from `lib/functions.ts` so authentication, membership, admin and deletion-barrier checks are applied. Throw user-facing errors with `appError` from `lib/errors.ts`."},{"heading":"server-and-data-access","content":"Admin-only functions are in `admin.ts`, `adminUsers.ts`, `adminWorkspaces.ts`, `adminBilling.ts`, `adminAudit.ts` and `impersonation.ts`. Public HTTP endpoints (health, Stripe webhook, API key routes, AI stream) are in `http.ts`, which also mounts the MCP and OAuth routes from `mcp/http.ts`."},{"heading":"server-and-data-access","content":"Both apps read Convex through `@convex-dev/react-query` (see each `router.tsx`) and `convex/react` hooks. Keep TanStack server functions for HTML, headers and web-runtime concerns, not for a second data API."},{"heading":"schema-and-migrations","content":"Tables are split by area in `apps/database/convex/schema/` and combined in `schema.ts`, together with the MCP tables from `mcp/schema.ts`. Shared validators (roles, statuses) are in `lib/validators.ts`. After a schema change run `pnpm db:codegen` for `_generated/` and `pnpm db:types` for `lib/database.types.ts`; CI fails if `_generated/` is out of date."},{"heading":"schema-and-migrations","content":"Convex applies schema changes when you deploy. Data migrations use `@convex-dev/migrations` in `migrations.ts` (for example `backfillUsersAggregate`); run them with `pnpm convex run migrations:<name>`. Aggregates (`lib/aggregates.ts`) stay consistent through triggers in `lib/triggers.ts`, which is why mutations must go through the wrapped `mutation` functions."},{"heading":"authentication","content":"Providers in `auth.ts`, redirect allowlist in `lib/auth_redirect.ts`, JWT domain in `auth.config.ts`, admin claim in `admin.ts`, impersonation in `impersonation.ts`. Frontend pages are `login.tsx`, `sign-up.tsx` (web only), `forgot-password.tsx`, `update-password.tsx` and the components in `components/auth/` of each app. See the authentication page."},{"heading":"emails","content":"Templates are React Email components in `apps/email/emails/`, exported from `apps/email/package.json`. Rendering and Resend delivery happen in `apps/database/convex/emailDelivery.tsx` (a Node action). Add a template, export it, then add a delivery action and an `externalJobs` kind for it. Convex Auth emails (verification, reset, magic link) are configured in `auth.ts`, not in `apps/email`."},{"heading":"background-jobs-and-workers","content":"There is no separate worker process. `workflow.ts` wraps `@convex-dev/workflow`; `enqueueExternalJob` writes an `externalJobs` row with an idempotency key and starts a durable workflow for kinds such as `invitation.email`, `account.delete`, `workspace.delete`, `stripe.event` and `stripe.catalog_sync`. Rate limits are in `rateLimits.ts`. Scheduled work uses Convex crons in `crons.ts` (currently the hourly MCP cleanup)."},{"heading":"billing","content":"Stripe webhook verification is in `http.ts`; event handling in `stripe.ts`; products, prices, subscriptions, invoices, usage and credits in `billing.ts` and `schema/billing.ts`. The workspace billing page is `_authenticated.workspace.$workspaceSlug.billing.tsx` in the web app; the admin views are the `payment-gateway.*` routes in the admin app."},{"heading":"mcp","content":"Everything server-side is in `apps/database/convex/mcp/` plus `mcp.test.ts`. The user UI is in the web app (`components/mcp-connections.tsx`, `components/mcp-consent.tsx`, `routes/mcp.authorize.tsx`, `routes/_authenticated.user.settings.connections.tsx`); the admin UI is the same set of files in the admin app plus `routes/_authenticated.app-admin.connections.tsx`. The agent connections page explains how to add a tool."},{"heading":"tests","content":"Convex functions: Vitest with `convex-test`, files next to the code (`apps/database/convex/*.test.ts`) and in `apps/database/tests/`."},{"heading":"tests","content":"Web and admin unit tests: Vitest with jsdom in `src/**/*.test.ts(x)` of each app."},{"heading":"tests","content":"End to end: Playwright specs in `apps/web/e2e/`, configured by `apps/web/playwright.config.ts` (Chromium desktop and Pixel 7 projects). The admin app has no e2e suite."},{"heading":"tests","content":"Run `pnpm test` for unit tests and `pnpm test:e2e` for Playwright. `pnpm lint`, `pnpm typecheck`, `pnpm format:check` and `pnpm agents:check` complete the gates CI runs (`.github/workflows/ci.yml`)."}],"headings":[{"id":"tree","content":"Tree"},{"id":"where-to-make-changes","content":"Where to make changes"},{"id":"routes-and-pages","content":"Routes and pages"},{"id":"admin-app","content":"Admin app"},{"id":"server-and-data-access","content":"Server and data access"},{"id":"schema-and-migrations","content":"Schema and migrations"},{"id":"authentication","content":"Authentication"},{"id":"emails","content":"Emails"},{"id":"background-jobs-and-workers","content":"Background jobs and workers"},{"id":"billing","content":"Billing"},{"id":"mcp","content":"MCP"},{"id":"tests","content":"Tests"}]},"url":"/docs/v5/starters/tanstack-convex/project-structure","title":"Project structure","description":"The TanStack + Convex monorepo layout and where to make changes to routes, data, auth, the admin app, emails, jobs, MCP and tests."},{"_id":"/docs/v5/starters/tanstack-d1/agent-connections","structured":{"contents":[{"content":"This kit exposes two independent remote MCP servers, one per Worker: the web Worker serves the User MCP and the admin Worker serves the Admin MCP. Both use your existing Better Auth accounts and both are read-only in v5. The code ships on `main` in `apps/web/src/mcp/`, with the `mcp_records` table in migration `0025_user_admin_mcp.sql`. Read user and admin MCPs for the concepts and connect an agent for the client-side steps."},{"heading":"where-users-manage-connections","content":"Connection"},{"heading":"where-users-manage-connections","content":"Settings page"},{"heading":"where-users-manage-connections","content":"Live demo server"},{"heading":"where-users-manage-connections","content":"User"},{"heading":"where-users-manage-connections","content":"`/mcp/connections/user` on the web app"},{"heading":"where-users-manage-connections","content":"`https://tanstack-d1.usenextbase.com/mcp/user`"},{"heading":"where-users-manage-connections","content":"Admin"},{"heading":"where-users-manage-connections","content":"`/mcp/connections/admin` on the admin app"},{"heading":"where-users-manage-connections","content":"`https://tanstack-d1-admin.usenextbase.com/mcp/admin`"},{"heading":"where-users-manage-connections","content":"\"Agent connections\" is the first item in the signed-in navigation of the web app and is also linked from `/user/settings`. Administrators open the admin origin for the Admin MCP. Each page shows the server address to copy, lists that account's connections for that server, and offers **Disconnect**."},{"heading":"where-users-manage-connections","content":"Your own deployment uses its own origins: the User MCP is the web Worker's `SITE_URL` + `/mcp/user` and the Admin MCP is the admin Worker's `SITE_URL` + `/mcp/admin`. The settings pages print the exact value."},{"heading":"where-users-manage-connections","content":"`apps/web/src/mcp/adapter.ts` serves only the `user` audience when `ADMIN_APP` is unset and only the `admin` audience when `ADMIN_APP` is `true`, which is how the admin Worker is configured. Requesting the other audience returns 404: `https://tanstack-d1.usenextbase.com/mcp/admin` is a 404, while `https://tanstack-d1-admin.usenextbase.com/mcp/admin` answers with the expected 401 challenge."},{"heading":"tools-and-permissions","content":"Server"},{"heading":"tools-and-permissions","content":"Tool"},{"heading":"tools-and-permissions","content":"Scope"},{"heading":"tools-and-permissions","content":"What it returns"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`account_context`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"The signed-in user's ID and email and the workspace approved for this connection."},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`projects_list`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"Projects in the approved workspace only, newest first. Optional `limit` from 1 to 50, default 20."},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_users_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"A bounded list of users (id, name, email, role). Optional `limit`, same bounds."},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_workspaces_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"A bounded list of workspaces that are not being deleted. Optional `limit`, same bounds."},{"heading":"tools-and-permissions","content":"Every tool is annotated read-only, non-destructive and idempotent. There is no `projects_create` in this kit. A user connection selects exactly one workspace; being an application administrator does not widen it, and a user token is rejected at the admin endpoint."},{"heading":"tools-and-permissions","content":"Before answering, each tool call reloads the live session: the session must exist and not be expired, must not be an impersonation session, and the user must be verified and not banned. `projects_list` additionally resolves the workspace through `requireWorkspaceContext` and checks `workspace.read`. The admin tools re-check `role = 'admin'` and `ADMIN_APP === \"true\"` on every call, so removing the role ends access immediately."},{"heading":"oauth-behaviour-in-this-kit","content":"Both Workers handle `/mcp/*`, `/oauth/*` and `/.well-known/oauth-*` before React Router sees the request. Each origin is its own authorization server; a token issued by one is not valid at the other."},{"heading":"oauth-behaviour-in-this-kit","content":"Discovery: `/.well-known/oauth-protected-resource/mcp/user` on the web origin, `/.well-known/oauth-protected-resource/mcp/admin` on the admin origin, and `/.well-known/oauth-authorization-server/oauth` on both. The issuer is that Worker's `SITE_URL` + `/oauth`."},{"heading":"oauth-behaviour-in-this-kit","content":"Endpoints: `/oauth/register`, `/oauth/authorize`, `/oauth/token`, `/oauth/revoke`, and the consent page at `/mcp/authorize`."},{"heading":"oauth-behaviour-in-this-kit","content":"Public clients only, with dynamic client registration, authorization code and mandatory PKCE S256. Confidential clients and client ID metadata documents are not supported."},{"heading":"oauth-behaviour-in-this-kit","content":"Callback URLs must be HTTPS, except HTTP loopback on `localhost`, `127.0.0.1` or `::1`."},{"heading":"oauth-behaviour-in-this-kit","content":"If you are not signed in, consent redirects to `/login?next=/mcp/authorize?...` on the same origin. Login, sign-up and email verification preserve that return path and load the consent page as a full document. On the admin origin that is the admin login page."},{"heading":"oauth-behaviour-in-this-kit","content":"The consent screen shows the client name (marked unverified), callback address, server, permission and your email, and lets you pick a workspace for user access. If you have no workspace it asks you to create one first."},{"heading":"oauth-behaviour-in-this-kit","content":"Pending consent lasts 10 minutes, codes 5 minutes, access tokens 15 minutes. A connection can refresh for up to 30 days but never longer than the sign-in session that approved it. Refresh tokens rotate and reuse revokes the whole family."},{"heading":"oauth-behaviour-in-this-kit","content":"Browser origins allowed: that Worker's `SITE_URL`, `https://claude.ai` and `https://chatgpt.com`. Desktop and server clients without an `Origin` header are accepted."},{"heading":"oauth-behaviour-in-this-kit","content":"Each account can hold up to 20 active connections. Consent and disconnect need a same-origin form post with a session-bound CSRF value."},{"heading":"oauth-behaviour-in-this-kit","content":"Opening an MCP URL in a browser without a token returns a 401 with a `WWW-Authenticate` challenge pointing at the resource metadata. That is expected."},{"heading":"storage","content":"OAuth registrations, pending consents and delegated credentials live in one additive table, `mcp_records`, created by `apps/database/migrations/0025_user_admin_mcp.sql` and mirrored in `apps/database/src/schema/core.ts`. Both Workers read and write the same table through the shared `APP_DB`. Secrets are stored only as SHA-256 hashes. `pnpm db:migrate` applies the migration locally and `pnpm db:migrate:preview` or `pnpm db:migrate:production` remotely."},{"heading":"where-the-code-lives","content":"File"},{"heading":"where-the-code-lives","content":"Role"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.ts`"},{"heading":"where-the-code-lives","content":"Provider-neutral protocol: discovery, OAuth endpoints, consent HTML, connection pages, token lifecycle and the MCP server with its tools. `isMcpPath` decides which paths it owns and the adapter's `audiences` decide which server an origin serves."},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/adapter.ts`"},{"heading":"where-the-code-lives","content":"The D1 and Better Auth adapter: `liveActor`, `workspaces`, `projects`, `adminUsers`, `adminWorkspaces` and the native session lookup. Picks the audience from `ADMIN_APP`."},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/d1-store.ts`"},{"heading":"where-the-code-lives","content":"Compare-and-set storage over `mcp_records`."},{"heading":"where-the-code-lives","content":"`apps/web/workers/app.ts`"},{"heading":"where-the-code-lives","content":"Routes `/mcp/*`, `/oauth/*` and `/.well-known/oauth-*` to `handleMcp` on the web Worker."},{"heading":"where-the-code-lives","content":"`apps/admin/workers/app.ts`"},{"heading":"where-the-code-lives","content":"Imports the same `handleMcp` and `isMcpPath` so the admin Worker serves the Admin MCP."},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.test.ts`"},{"heading":"where-the-code-lives","content":"Protocol, PKCE, CSRF, replay, concurrency, expiry, membership and revocation tests."},{"heading":"where-the-code-lives","content":"`apps/web/test/mcp.integration.test.ts`"},{"heading":"where-the-code-lives","content":"Real D1 and Better Auth sign-up, login and delegated user and admin flows."},{"heading":"where-the-code-lives","content":"`docs/mcp-connections.md`"},{"heading":"where-the-code-lives","content":"The repository's own write-up of the design."},{"heading":"adding-a-tool-safely","content":"Register it in `core.ts` next to the existing `server.registerTool` calls for the right audience. User tokens never see admin tools and the reverse, so keep the audience split."},{"heading":"adding-a-tool-safely","content":"Put the data access in `adapter.ts` and go through the same checks the website uses: `requireWorkspaceContext` and `authorizeWorkspace` for workspace data, a fresh `liveActor` admin check plus the `ADMIN_APP` guard for admin data."},{"heading":"adding-a-tool-safely","content":"Treat tool arguments such as a workspace or user ID as untrusted. The authorized workspace comes from the grant, not from the agent."},{"heading":"adding-a-tool-safely","content":"Keep results bounded. Reuse the `limit` schema (1 to 50)."},{"heading":"adding-a-tool-safely","content":"A write tool needs a new scope, consent wording that says so, and tests in `core.test.ts` and `mcp.integration.test.ts` before you rely on it. The shared tools and permissions page explains the scope model."},{"heading":"adding-a-tool-safely","content":"If a connection stops working after you sign out, sign in again on the same origin and reconnect from the settings page. See troubleshooting for the other common cases."}],"headings":[{"id":"where-users-manage-connections","content":"Where users manage connections"},{"id":"tools-and-permissions","content":"Tools and permissions"},{"id":"oauth-behaviour-in-this-kit","content":"OAuth behaviour in this kit"},{"id":"storage","content":"Storage"},{"id":"where-the-code-lives","content":"Where the code lives"},{"id":"adding-a-tool-safely","content":"Adding a tool safely"}]},"url":"/docs/v5/starters/tanstack-d1/agent-connections","title":"Agent connections","description":"The User MCP and Admin MCP in the TanStack + Cloudflare D1 starter: endpoints, tools, OAuth behaviour and where the code lives."},{"_id":"/docs/v5/starters/tanstack-d1/authentication","structured":{"contents":[{"content":"This kit uses Better Auth with D1 as its store. There is no Supabase Auth and no JWT claims. Every loader and action re-reads the session, resolves roles on the server, and checks an explicit capability before touching tenant data."},{"content":"The shared authentication page covers the concepts common to all kits. This page is about what this repository actually does."},{"heading":"where-the-configuration-lives","content":"Concern"},{"heading":"where-the-configuration-lives","content":"File"},{"heading":"where-the-configuration-lives","content":"Better Auth instance, plugins, hooks"},{"heading":"where-the-configuration-lives","content":"`apps/web/app/server/auth.server.ts` (shared by the admin app)"},{"heading":"where-the-configuration-lives","content":"Better Auth HTTP handler and redirect handling"},{"heading":"where-the-configuration-lives","content":"`apps/web/app/server/auth-handler.server.ts`, `auth-route.server.ts`, route `apps/web/app/routes/api-auth.ts` (`/api/auth/*`); the admin app mounts the same handler at `apps/admin/app/routes/api-auth.ts`"},{"heading":"where-the-configuration-lives","content":"Session to principal"},{"heading":"where-the-configuration-lives","content":"`apps/web/app/server/session.server.ts`"},{"heading":"where-the-configuration-lives","content":"Workspace capabilities"},{"heading":"where-the-configuration-lives","content":"`apps/web/app/server/authorization.server.ts`"},{"heading":"where-the-configuration-lives","content":"Impersonation limits"},{"heading":"where-the-configuration-lives","content":"`apps/web/app/server/impersonation.server.ts`, `apps/web/app/routes/impersonate.ts`, `apps/web/app/routes/stop-impersonating.ts`"},{"heading":"where-the-configuration-lives","content":"Auth rate limiting Durable Object"},{"heading":"where-the-configuration-lives","content":"`apps/web/workers/auth-rate-limiter.ts`, `apps/web/app/server/auth-rate-limit.server.ts`"},{"heading":"where-the-configuration-lives","content":"Auth tables (`user`, `session`, `account`, `verification`)"},{"heading":"where-the-configuration-lives","content":"`apps/database/src/schema/auth.ts`"},{"heading":"where-the-configuration-lives","content":"Browser client"},{"heading":"where-the-configuration-lives","content":"`apps/web/app/lib/auth-client.ts`"},{"heading":"where-the-configuration-lives","content":"Admin app login and console"},{"heading":"where-the-configuration-lives","content":"`apps/admin/app/routes/login.tsx`, `apps/admin/app/routes/admin.tsx`"},{"heading":"where-the-configuration-lives","content":"Better Auth exclusively owns its four tables. Application data about a user goes into projection tables such as `user_profiles` and `user_settings`, which a database hook creates right after sign-up and a daily `auth.reconcile` job repairs if they are missing."},{"heading":"sign-in-methods","content":"**Email and password** is on with `requireEmailVerification: true`. Sign-up sends a verification email, and verifying signs the user in automatically. Password reset goes through `/forgot-password` and `/reset-password`."},{"heading":"sign-in-methods","content":"**Magic links** are on through the Better Auth `magicLink` plugin. Links expire after 15 minutes and are stored hashed. The login page offers \"a secure magic link\" as an alternative to the password."},{"heading":"sign-in-methods","content":"**Social providers** are optional. `auth.server.ts` registers Google, GitHub and Twitter only when both the client ID and client secret for that provider are present in the Worker environment. The login page reads the same variables, so a button appears only for a configured provider. Set the provider's callback to `SITE_URL` + `/api/auth/callback/<provider>` in that provider's console, and keep `SITE_URL` equal to the origin users actually visit. The admin app has its own `SITE_URL`, so register a second callback for it if administrators should use social login there."},{"heading":"email-verification-and-the-local-inbox","content":"All auth emails (verification, password reset, magic link, new sign-in notice) are not sent directly. `enqueueEmail` in `apps/web/app/server/email.server.ts` writes an `email.send` row to the `transactional_outbox` table and nudges the Queue. The jobs Worker renders the React Email template and either sends through Resend or, when `RESEND_API_KEY` is empty, inserts the message into the `dev_email_inbox` D1 table."},{"heading":"email-verification-and-the-local-inbox","content":"To finish a local sign-up, read the newest row from that table and open the link. The command is on the installation page. The jobs Worker must be running, which `pnpm dev` takes care of."},{"heading":"sessions","content":"Sessions live in the D1 `session` table. The cookie cache is disabled, so every request validates against the database."},{"heading":"sessions","content":"A session lasts 30 days and is refreshed once a day of activity."},{"heading":"sessions","content":"Cookies are marked secure when `SITE_URL` starts with `https://`."},{"heading":"sessions","content":"The client IP is read from `cf-connecting-ip`. Better Auth's own rate limit is 100 requests per 60 seconds, and auth entry points also pass through the `AUTH_RATE_LIMITER` Durable Object before any credential or email work."},{"heading":"sessions","content":"A new session triggers a \"New sign-in to your account\" email."},{"heading":"sessions","content":"Trusted origins are limited to `SITE_URL`, and `workers/app.ts` rejects cross-origin mutations before they reach a route."},{"heading":"sessions","content":"The admin app runs a second Better Auth instance with the same user table but its own `BETTER_AUTH_SECRET`, its own `SITE_URL` and the cookie prefix `nextbase-admin`, so a web session never works on the admin origin and the other way round."},{"heading":"platform-roles-and-the-admin-app","content":"The `user.role` column holds one of `user`, `support` or `admin`. Only `admin` grants application administration. `requirePlatformAdmin` in `session.server.ts` returns 403 for anyone else."},{"heading":"platform-roles-and-the-admin-app","content":"Application administration lives in the separate admin app, `apps/admin`, served on its own origin (`http://localhost:5174` locally, `https://tanstack-d1-admin.usenextbase.com` for the demo). Its routes are `/login`, `/forgot-password`, `/reset-password`, `/verify-email`, `/logout`, `/app-admin/*`, `/api/auth/*`, `/api/media/*`, `/api/health` and `/api/admin/insights`. There is no sign-up page: administrators create their account on the web app and are promoted there. The console has sections for overview, users, workspaces, payment gateway, marketing (blog and changelog CMS), feedback and settings. Administrators also manage \"Admin agent connections\" at `/mcp/connections/admin` on the admin origin."},{"heading":"platform-roles-and-the-admin-app","content":"The web app has no `/app-admin` or `/api/admin` routes and no admin link in its navigation. Every admin response carries `Cache-Control: no-store`, `X-Robots-Tag: noindex, nofollow` and `X-Frame-Options: DENY`."},{"heading":"the-trusted-admin-flow","content":"There is no self-service path to become an admin and the seed creates no admin password. The only route is a direct database update by someone who already has access to the D1 database:"},{"heading":"the-trusted-admin-flow","content":"Use `--remote --env production` instead of `--local` for a deployed database. The repository documents only the local command, so test the remote form against preview first. Then sign in on the admin origin's `/login`; a user without the role gets a 403 from `/app-admin`."},{"heading":"impersonation","content":"Administrators can impersonate a user from the users section of the admin app. Because the two apps are on different origins, the admin app asks Better Auth's `admin` plugin for a one-time token and redirects the browser to `WEB_APP_URL` + `/auth/impersonate?token=...`. The web route `apps/web/app/routes/impersonate.ts` redeems that single-use token, re-checks that the acting administrator still has the `admin` role, is verified and not banned, and that the target is not an administrator, then opens an impersonation session on the web origin. The administrator's own session stays on the admin origin, and `/stop-impersonating` sends the browser back to `ADMIN_APP_URL` + `/app-admin/users`."},{"heading":"impersonation","content":"Admins cannot impersonate other admins and an impersonation session lasts 30 minutes. While impersonating, every non-GET request is rejected by `assertRequestAllowedDuringImpersonation`, so the session is read-only. Admins can also issue a user a magic link from the console, either emailed or displayed once; the link points at `WEB_APP_URL`."},{"heading":"workspace-roles-and-capabilities","content":"Workspace membership has four roles. `authorization.server.ts` maps each role to a fixed set of actions:"},{"heading":"workspace-roles-and-capabilities","content":"Action"},{"heading":"workspace-roles-and-capabilities","content":"owner"},{"heading":"workspace-roles-and-capabilities","content":"admin"},{"heading":"workspace-roles-and-capabilities","content":"member"},{"heading":"workspace-roles-and-capabilities","content":"readonly"},{"heading":"workspace-roles-and-capabilities","content":"`workspace.read`"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"`workspace.update`, `member.invite`, `member.manage`, `billing.manage`, `project.delete`, `project.approve`"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"no"},{"heading":"workspace-roles-and-capabilities","content":"no"},{"heading":"workspace-roles-and-capabilities","content":"`workspace.delete`"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"no"},{"heading":"workspace-roles-and-capabilities","content":"no"},{"heading":"workspace-roles-and-capabilities","content":"no"},{"heading":"workspace-roles-and-capabilities","content":"`project.create`, `project.update`, `comment.create`, `ai.use`, `media.upload`"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"yes"},{"heading":"workspace-roles-and-capabilities","content":"no"},{"heading":"workspace-roles-and-capabilities","content":"A route never trusts a workspace ID from the browser. It calls `requireWorkspaceContext(env, principal, workspaceSlug)`, which looks up the caller's membership for the slug in the URL, then `authorizeWorkspace(context, action)`. Repositories receive that `WorkspaceContext`, so every query is tenant-scoped."},{"heading":"what-replaces-row-level-security","content":"D1 has no Postgres RLS. The repository's `docs/feature-parity.md` describes the layered replacement:"},{"heading":"what-replaces-row-level-security","content":"Authentication in every loader and action."},{"heading":"what-replaces-row-level-security","content":"Tenant identity derived from the session and route slug, never from form input."},{"heading":"what-replaces-row-level-security","content":"Capability checks before each mutation."},{"heading":"what-replaces-row-level-security","content":"Tenant predicates in every query and composite foreign keys."},{"heading":"what-replaces-row-level-security","content":"D1 triggers for invariants that must survive application bugs, such as the last-owner rule, the five-active-workspaces cap, the three-active-API-keys cap and atomic credit accounting."},{"heading":"what-replaces-row-level-security","content":"Integration tests in `apps/web/test` (for example `tenant-boundaries.integration.test.ts`) that exercise those boundaries against a real D1."},{"heading":"what-replaces-row-level-security","content":"Direct database access stays server-only. Keep new mutations behind the same `requireWorkspaceContext` and `authorizeWorkspace` calls."},{"heading":"agent-connections-use-the-same-accounts","content":"The User MCP and Admin MCP do not create a second identity system. A tool call reloads the canonical session row, checks that the user is verified and not banned, rejects impersonation sessions, and re-checks workspace membership or the admin role on every request. The User MCP is served by the web Worker and the Admin MCP by the admin Worker. See agent connections."}],"headings":[{"id":"where-the-configuration-lives","content":"Where the configuration lives"},{"id":"sign-in-methods","content":"Sign-in methods"},{"id":"email-verification-and-the-local-inbox","content":"Email verification and the local inbox"},{"id":"sessions","content":"Sessions"},{"id":"platform-roles-and-the-admin-app","content":"Platform roles and the admin app"},{"id":"the-trusted-admin-flow","content":"The trusted admin flow"},{"id":"impersonation","content":"Impersonation"},{"id":"workspace-roles-and-capabilities","content":"Workspace roles and capabilities"},{"id":"what-replaces-row-level-security","content":"What replaces Row Level Security"},{"id":"agent-connections-use-the-same-accounts","content":"Agent connections use the same accounts"}]},"url":"/docs/v5/starters/tanstack-d1/authentication","title":"Authentication","description":"How Better Auth, platform roles, the separate admin app and workspace capabilities protect the TanStack + Cloudflare D1 starter."},{"_id":"/docs/v5/starters/tanstack-d1/deployment","structured":{"contents":[{"content":"This kit deploys as three Cloudflare Workers that share one D1 database: the web Worker (`apps/web`), the admin Worker (`apps/admin`) and the jobs Worker (`apps/jobs`). The repository ships `preview` and `production` environments in all three Wrangler files. The platform-generic steps are in deploy on Cloudflare; this page covers what is specific to this repository."},{"content":"The repository's `docs/operations.md` is the runbook for migrations, outbox recovery, webhooks, rollback and secret rotation, and the \"Separate administrator app\" section of `README.md` covers the admin Worker. Read both before your first production release."},{"heading":"create-the-cloudflare-resources","content":"Create separate resources for preview and production. Never point preview bindings at production resources."},{"heading":"create-the-cloudflare-resources","content":"Repeat with the resource names declared in the `env.preview` and `env.production` blocks: `nextbase-preview-*` and `nextbase-production-*` for the R2 buckets, queues and dead-letter queues. Pick your own D1 database names; the shipped values are placeholders. The admin Worker creates nothing new: it binds the same database, buckets and queue."},{"heading":"create-the-cloudflare-resources","content":"Durable Objects need no provisioning. Wrangler creates the `RealtimeRoom` and `AuthRateLimiter` namespaces from the `migrations` block in `apps/web/wrangler.jsonc` on first deploy. Keep both migration tags (`v1`, `v2`) when you rename Workers or add environments."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Edit `apps/web/wrangler.jsonc`, `apps/admin/wrangler.jsonc` and `apps/jobs/wrangler.jsonc` together. For each environment:"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Setting"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Web Worker"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Admin Worker"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Jobs Worker"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Must match"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`name`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`nextbase-tanstack-d1-preview` / `-production`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`nextbase-tanstack-d1-admin-preview` / `-production`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`nextbase-jobs-preview` / `-production`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Every `durable_objects.bindings[].script_name` in the admin and jobs files must equal the web Worker's `name` for the same environment."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`d1_databases[].binding`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`APP_DB`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`APP_DB`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`APP_DB`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Yes, and keep the name `APP_DB`."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`d1_databases[].database_name` and `database_id`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"your database"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"your database"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"your database"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Yes. Replace the `11111111-...` placeholder and the demo's production ID with the real ID from `wrangler d1 create`."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`d1_databases[].migrations_dir`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`../database/migrations`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"same"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"same"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Yes."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`r2_buckets`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`PUBLIC_MEDIA`, `PRIVATE_MEDIA`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"same bindings"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"same bindings"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Same bucket names."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`queues.producers[].queue`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"the environment's queue"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"same queue"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"same queue"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Yes. Only the jobs file lists the consumers and the `-dlq` queue."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`vars.SITE_URL`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"your public web origin"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"your admin origin"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"the web origin"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Web and jobs identical; admin is its own origin."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`vars.ADMIN_APP_URL`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"the admin origin"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Must equal the admin `SITE_URL`."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`vars.WEB_APP_URL`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"the web origin"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Must equal the web `SITE_URL`. Never set it to the admin origin."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`vars.ADMIN_APP`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`true`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Keep it `true` on the admin Worker only."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`vars.ADMIN_EMAIL`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"a real mailbox"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"same"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"same"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Yes."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`vars.API_CORS_ORIGINS`, `vars.UNKEY_API_ID`, `vars.APP_NAME`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"web"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"admin"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Set on web; the admin file carries the same keys."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"`vars.MAIL_FROM`"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"optional verified sender"},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Jobs only."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Only the web Worker defines the `RealtimeRoom` and `AuthRateLimiter` classes. The admin Worker reaches both and the jobs Worker reaches `REALTIME` through `script_name`, so a mismatch there breaks realtime invalidations or admin rate limiting even though the deploy succeeds. Deploy the web Worker before the other two so the referenced script exists."},{"heading":"update-the-three-wrangler-files-and-keep-them-aligned","content":"If the `APP_DB` entries point at different databases, the web app will enqueue emails and jobs that the jobs Worker never sees, verification emails will never be delivered, and the admin console will show a different set of users. Check the `database_id` in all three files for the same environment before deploying."},{"heading":"set-the-secrets-on-the-worker-that-uses-them","content":"Repeat with `--env production`. `BETTER_AUTH_SECRET` is mandatory on the web and admin Workers: both throw at startup on a non-localhost `SITE_URL` without it, and the admin value must be a different secret from the web value. Add the social provider client IDs and secrets the same way if you use them. `RESEND_API_KEY` and the cache-purge credentials belong only on the jobs Worker. The full list is on the environment variables page."},{"heading":"apply-the-d1-migrations","content":"Apply the schema before the first application deploy, and again whenever a release adds a migration:"},{"heading":"apply-the-d1-migrations","content":"Both scripts run `wrangler d1 migrations apply APP_DB --remote` with the matching `--env` against `apps/web/wrangler.jsonc`. `pnpm db:migrate:remote` is an alias for the preview command. The run includes `0025_user_admin_mcp.sql` (the `mcp_records` table), which both the web and admin Workers need before they serve OAuth connections."},{"heading":"apply-the-d1-migrations","content":"Before a destructive migration, record a D1 Time Travel bookmark and the release SHA. Never reverse an applied migration; ship a forward repair migration instead."},{"heading":"build-and-deploy-the-workers","content":"`cf:deploy:<env>` sets `CLOUDFLARE_ENV` to the environment, runs the React Router build for `@nextbase/web`, deploys the web Worker with `wrangler deploy --env`, then deploys the jobs Worker with `wrangler deploy --env`. `cf:deploy:admin:<env>` does the same for `@nextbase/admin` alone. Run the web script first so the Durable Object classes exist for the admin and jobs bindings. The `CLOUDFLARE_ENV` value matters: the Vite build generates the redirected Wrangler configuration from it. Running `wrangler deploy --env production` after a build without that variable can keep the local placeholder database ID, so always use the root scripts."},{"heading":"build-and-deploy-the-workers","content":"The repository also has a manual GitHub Actions workflow, `.github/workflows/deploy.yml`. Trigger it with `workflow_dispatch`, choose the environment (`preview` or `production`) and the app (`web` or `admin`); it runs `pnpm verify` and `pnpm test:integration`, applies `db:migrate:<env>` only for the `web` app, then runs the matching `cf:deploy:*` script. It needs `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` as secrets on the GitHub deployment environment of the same name."},{"heading":"point-auth-email-and-mcp-at-the-public-origins","content":"`SITE_URL` in the web and jobs Wrangler files must be the address users visit, and `SITE_URL` in the admin file must be the admin address. Better Auth uses each as `baseURL` and trusted origin, so cookies, verification links and OAuth callbacks break if they differ."},{"heading":"point-auth-email-and-mcp-at-the-public-origins","content":"Set web `ADMIN_APP_URL` and admin `WEB_APP_URL` to each other's origin. Impersonation hands the browser from the admin origin to `WEB_APP_URL` + `/auth/impersonate` with a one-minute, single-use token and returns to `ADMIN_APP_URL` + `/app-admin/users`."},{"heading":"point-auth-email-and-mcp-at-the-public-origins","content":"Social providers: register `SITE_URL` + `/api/auth/callback/<provider>` with each provider you enabled, once per origin that should offer the button."},{"heading":"point-auth-email-and-mcp-at-the-public-origins","content":"Email sender: the jobs handler sends from `MAIL_FROM` when set, otherwise from `ADMIN_EMAIL`, through Resend. Verify that domain in Resend. See emails with Resend."},{"heading":"point-auth-email-and-mcp-at-the-public-origins","content":"Stripe: point the webhook endpoint at the web `SITE_URL` + `/api/stripe/webhooks` and subscribe to the event types listed in `docs/operations.md`."},{"heading":"point-auth-email-and-mcp-at-the-public-origins","content":"MCP: the User MCP is the web `SITE_URL` + `/mcp/user` and the Admin MCP is the admin `SITE_URL` + `/mcp/admin`. There is no separate `MCP_APP_URL` in this kit; each Worker's origin comes from its own `SITE_URL`."},{"heading":"point-auth-email-and-mcp-at-the-public-origins","content":"Public API: set `API_CORS_ORIGINS` to the exact origins allowed to call `/api/v1/me`."},{"heading":"point-auth-email-and-mcp-at-the-public-origins","content":"Custom domains: the demo attaches `tanstack-d1.usenextbase.com` and `tanstack-d1-admin.usenextbase.com` through two small Cloudflare Pages proxies (`infrastructure/demo-domain` and `infra/admin-domain`) with a service binding to the Worker. Deploy the admin proxy with `pnpm --filter @nextbase/admin cf:deploy:domain` and point the admin DNS CNAME at its `pages.dev` host, or attach a custom domain directly to each Worker instead."},{"heading":"smoke-check-the-live-app","content":"Open `/sign-up` on the web origin, create an account, and confirm the verification email arrives through Resend."},{"heading":"smoke-check-the-live-app","content":"Sign in, complete `/onboarding`, create a workspace and a project."},{"heading":"smoke-check-the-live-app","content":"Grant your account the `admin` role in the production database (see authentication), open `/login` on the admin origin and confirm you land on `/app-admin`."},{"heading":"smoke-check-the-live-app","content":"Open `/mcp/connections/user` on the web origin and `/mcp/connections/admin` on the admin origin, confirm the displayed addresses use the right origins, then connect an agent and call `projects_list`."},{"heading":"smoke-check-the-live-app","content":"From the admin users section, start an impersonation and confirm you land on the web app read-only and that **Stop impersonating** returns you to the admin origin."},{"heading":"smoke-check-the-live-app","content":"Request the jobs Worker's `/health` endpoint and check that `pending` and `dead` counts stay low."},{"heading":"smoke-check-the-live-app","content":"Watch Workers Logs for `jobs.retrying`, `jobs.dead` and `jobs.dead_lettered` events after the first minute cron."},{"heading":"smoke-check-the-live-app","content":"Send a signed Stripe test webhook and upload a public and a private media file if you use those features."},{"heading":"rollback","content":"Roll application code back by deploying the previous SHA with the same `cf:deploy:*` script, web first, then admin and jobs. Do not reverse an applied migration; use a forward repair migration or D1 Time Travel during an incident. The web Worker returns an `X-Request-Id` header on every response; start any investigation from that ID in `operational_exceptions`, as described in `docs/operations.md`."},{"heading":"rollback","content":"If the Worker deploys but login or the database fails, see troubleshooting."}],"headings":[{"id":"create-the-cloudflare-resources","content":"Create the Cloudflare resources"},{"id":"update-the-three-wrangler-files-and-keep-them-aligned","content":"Update the three Wrangler files and keep them aligned"},{"id":"set-the-secrets-on-the-worker-that-uses-them","content":"Set the secrets on the Worker that uses them"},{"id":"apply-the-d1-migrations","content":"Apply the D1 migrations"},{"id":"build-and-deploy-the-workers","content":"Build and deploy the Workers"},{"id":"point-auth-email-and-mcp-at-the-public-origins","content":"Point auth, email and MCP at the public origins"},{"id":"smoke-check-the-live-app","content":"Smoke-check the live app"},{"id":"rollback","content":"Rollback"}]},"url":"/docs/v5/starters/tanstack-d1/deployment","title":"Deployment","description":"Provision Cloudflare resources, align the web, admin and jobs Wrangler configs, apply D1 migrations and deploy the three Workers."},{"_id":"/docs/v5/starters/tanstack-d1/environment-variables","structured":{"contents":[{"content":"This kit has no browser-exposed environment variables. Everything below is read inside a Cloudflare Worker and never shipped to the client. Configuration is split in two places:"},{"content":"**Secrets** go in `.dev.vars` files locally and in Worker secrets (`wrangler secret put`) when deployed. The templates are `apps/web/.env.example`, `apps/admin/.env.example` and `apps/jobs/.env.example`."},{"content":"**Non-secret variables** live in the `vars` blocks of `apps/web/wrangler.jsonc`, `apps/admin/wrangler.jsonc` and `apps/jobs/wrangler.jsonc`, once per environment (top level for local, then `env.preview` and `env.production`)."},{"content":"Copy the templates only when the `.dev.vars` files are missing. See installation for the commands."},{"content":"The repository's runbook says Unkey, OpenAI, Stripe, Resend and Cloudflare credentials must never appear in `vars`, client loader data, logs or Queue payloads. Use Worker secrets for anything in the secrets tables below."},{"heading":"web-worker-secrets","content":"File: `apps/web/.dev.vars` (template `apps/web/.env.example`). Deployed: `pnpm --filter @nextbase/web exec wrangler secret put <NAME> --env <preview|production>`."},{"heading":"web-worker-secrets","content":"Variable"},{"heading":"web-worker-secrets","content":"Required"},{"heading":"web-worker-secrets","content":"What it is for"},{"heading":"web-worker-secrets","content":"Where to get the value"},{"heading":"web-worker-secrets","content":"`BETTER_AUTH_SECRET`"},{"heading":"web-worker-secrets","content":"Required outside localhost"},{"heading":"web-worker-secrets","content":"Signs Better Auth sessions and tokens. The Worker refuses to start on a non-local `SITE_URL` without it. Locally a development fallback is used."},{"heading":"web-worker-secrets","content":"Generate at least 32 random characters."},{"heading":"web-worker-secrets","content":"`ANALYTICS_PSEUDONYM_SECRET`"},{"heading":"web-worker-secrets","content":"Optional, recommended"},{"heading":"web-worker-secrets","content":"HMAC key that pseudonymizes user IDs in `product_analytics_events`. Falls back to the Better Auth secret. Rotating it breaks the link to earlier pseudonyms on purpose."},{"heading":"web-worker-secrets","content":"Generate an independent random secret."},{"heading":"web-worker-secrets","content":"`GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`"},{"heading":"web-worker-secrets","content":"Optional"},{"heading":"web-worker-secrets","content":"Enables the Google sign-in button. Both must be set."},{"heading":"web-worker-secrets","content":"Google Cloud OAuth client."},{"heading":"web-worker-secrets","content":"`GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`"},{"heading":"web-worker-secrets","content":"Optional"},{"heading":"web-worker-secrets","content":"Enables the GitHub sign-in button. Both must be set."},{"heading":"web-worker-secrets","content":"GitHub OAuth app."},{"heading":"web-worker-secrets","content":"`TWITTER_CLIENT_ID` / `TWITTER_CLIENT_SECRET`"},{"heading":"web-worker-secrets","content":"Optional"},{"heading":"web-worker-secrets","content":"Enables the Twitter sign-in button. Both must be set."},{"heading":"web-worker-secrets","content":"Twitter developer app."},{"heading":"web-worker-secrets","content":"`STRIPE_SECRET_KEY`"},{"heading":"web-worker-secrets","content":"Optional"},{"heading":"web-worker-secrets","content":"Stripe API access for Checkout, the billing portal and catalog reads."},{"heading":"web-worker-secrets","content":"Stripe dashboard, API keys."},{"heading":"web-worker-secrets","content":"`STRIPE_WEBHOOK_SECRET`"},{"heading":"web-worker-secrets","content":"Optional"},{"heading":"web-worker-secrets","content":"Verifies the raw body of `POST /api/stripe/webhooks`."},{"heading":"web-worker-secrets","content":"Stripe webhook endpoint signing secret."},{"heading":"web-worker-secrets","content":"`OPENAI_API_KEY`"},{"heading":"web-worker-secrets","content":"Optional"},{"heading":"web-worker-secrets","content":"AI SDK streaming chat at `/api/chat`."},{"heading":"web-worker-secrets","content":"OpenAI API keys."},{"heading":"web-worker-secrets","content":"`UNKEY_ROOT_KEY`"},{"heading":"web-worker-secrets","content":"Optional"},{"heading":"web-worker-secrets","content":"Creates and verifies developer API keys for `/api/v1/me` and `/user/api-keys`."},{"heading":"web-worker-secrets","content":"Unkey dashboard, root key."},{"heading":"web-worker-secrets","content":"`UNKEY_API_ID`"},{"heading":"web-worker-secrets","content":"Optional"},{"heading":"web-worker-secrets","content":"The Unkey API the keys belong to. Treated as public and also present in Wrangler `vars`."},{"heading":"web-worker-secrets","content":"Unkey dashboard, API ID."},{"heading":"web-worker-secrets","content":"`API_CORS_ORIGINS`"},{"heading":"web-worker-secrets","content":"Optional"},{"heading":"web-worker-secrets","content":"Comma-separated exact origins allowed to call the public API. Default `http://localhost:5173`. Also set in Wrangler `vars`."},{"heading":"web-worker-secrets","content":"Your deployed origin, for example `https://app.example.com`."},{"heading":"web-worker-secrets","content":"`RESEND_API_KEY`, `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ZONE_ID` are not in the web template and not in the web `Env` type. Email delivery and cache purges happen in the jobs Worker, so those secrets belong only there."},{"heading":"admin-worker-secrets-and-local-values","content":"File: `apps/admin/.dev.vars` (template `apps/admin/.env.example`). Deployed: `pnpm --filter @nextbase/admin exec wrangler secret put <NAME> --env <preview|production>`."},{"heading":"admin-worker-secrets-and-local-values","content":"Variable"},{"heading":"admin-worker-secrets-and-local-values","content":"Required"},{"heading":"admin-worker-secrets-and-local-values","content":"What it is for"},{"heading":"admin-worker-secrets-and-local-values","content":"Where to get the value"},{"heading":"admin-worker-secrets-and-local-values","content":"`SITE_URL`"},{"heading":"admin-worker-secrets-and-local-values","content":"In the template"},{"heading":"admin-worker-secrets-and-local-values","content":"The admin app's own origin, `http://localhost:5174` locally. Also set in the admin Wrangler `vars`."},{"heading":"admin-worker-secrets-and-local-values","content":"Your admin origin."},{"heading":"admin-worker-secrets-and-local-values","content":"`WEB_APP_URL`"},{"heading":"admin-worker-secrets-and-local-values","content":"In the template"},{"heading":"admin-worker-secrets-and-local-values","content":"The customer web app's origin, `http://localhost:5173` locally. Used to build the impersonation handoff and the links in magic links issued from the admin console. Also set in the admin Wrangler `vars`."},{"heading":"admin-worker-secrets-and-local-values","content":"Your web origin."},{"heading":"admin-worker-secrets-and-local-values","content":"`ADMIN_APP`"},{"heading":"admin-worker-secrets-and-local-values","content":"In the template, `true`"},{"heading":"admin-worker-secrets-and-local-values","content":"Turns the shared code into the administrator app: the Admin MCP audience, the `nextbase-admin` cookie prefix. Also set in the admin Wrangler `vars`."},{"heading":"admin-worker-secrets-and-local-values","content":"Keep `true`."},{"heading":"admin-worker-secrets-and-local-values","content":"`BETTER_AUTH_SECRET`"},{"heading":"admin-worker-secrets-and-local-values","content":"Required outside localhost"},{"heading":"admin-worker-secrets-and-local-values","content":"An independent secret for the admin app's own Better Auth instance. Never reuse the web Worker's value."},{"heading":"admin-worker-secrets-and-local-values","content":"Generate at least 32 random characters."},{"heading":"admin-worker-secrets-and-local-values","content":"The admin Worker shares the web app's code, so the optional web secrets (social provider keys, Stripe, OpenAI, Unkey) are read there too when you set them. Its login page shows a social button only for a provider configured on the admin Worker."},{"heading":"jobs-worker-secrets","content":"File: `apps/jobs/.dev.vars` (template `apps/jobs/.env.example`). Deployed: `pnpm --filter @nextbase/jobs exec wrangler secret put <NAME> --env <preview|production>`."},{"heading":"jobs-worker-secrets","content":"Variable"},{"heading":"jobs-worker-secrets","content":"Required"},{"heading":"jobs-worker-secrets","content":"What it is for"},{"heading":"jobs-worker-secrets","content":"Where to get the value"},{"heading":"jobs-worker-secrets","content":"`RESEND_API_KEY`"},{"heading":"jobs-worker-secrets","content":"Required in production"},{"heading":"jobs-worker-secrets","content":"Sends queued `email.send` jobs through Resend. When empty, emails are written to the `dev_email_inbox` D1 table instead."},{"heading":"jobs-worker-secrets","content":"Resend dashboard."},{"heading":"jobs-worker-secrets","content":"`STRIPE_SECRET_KEY`"},{"heading":"jobs-worker-secrets","content":"Optional"},{"heading":"jobs-worker-secrets","content":"Lets webhook processing re-fetch current Stripe objects before updating D1."},{"heading":"jobs-worker-secrets","content":"Stripe dashboard, API keys."},{"heading":"jobs-worker-secrets","content":"`UNKEY_ROOT_KEY`"},{"heading":"jobs-worker-secrets","content":"Optional"},{"heading":"jobs-worker-secrets","content":"Revokes provider API keys through the outbox after a local revocation."},{"heading":"jobs-worker-secrets","content":"Unkey dashboard, root key."},{"heading":"jobs-worker-secrets","content":"`CLOUDFLARE_API_TOKEN`"},{"heading":"jobs-worker-secrets","content":"Required in deployed environments"},{"heading":"jobs-worker-secrets","content":"Zone-scoped token used by `cache.purge` jobs. Missing credentials are tolerated only for localhost."},{"heading":"jobs-worker-secrets","content":"Cloudflare API token with cache purge for one zone."},{"heading":"jobs-worker-secrets","content":"`CLOUDFLARE_ZONE_ID`"},{"heading":"jobs-worker-secrets","content":"Required in deployed environments"},{"heading":"jobs-worker-secrets","content":"The zone whose cache is purged."},{"heading":"jobs-worker-secrets","content":"Cloudflare dashboard, zone overview."},{"heading":"web-worker-vars","content":"Set in `apps/web/wrangler.jsonc` for each environment."},{"heading":"web-worker-vars","content":"Variable"},{"heading":"web-worker-vars","content":"Local default"},{"heading":"web-worker-vars","content":"What it is for"},{"heading":"web-worker-vars","content":"`APP_NAME`"},{"heading":"web-worker-vars","content":"`Nextbase`"},{"heading":"web-worker-vars","content":"Shown in the UI and used as the Better Auth app name and in email subjects."},{"heading":"web-worker-vars","content":"`SITE_URL`"},{"heading":"web-worker-vars","content":"`http://localhost:5173`"},{"heading":"web-worker-vars","content":"The canonical origin. Better Auth `baseURL` and trusted origin, cross-origin mutation check, telemetry origin check, and the origin of the User MCP endpoint."},{"heading":"web-worker-vars","content":"`ADMIN_APP_URL`"},{"heading":"web-worker-vars","content":"`http://localhost:5174`"},{"heading":"web-worker-vars","content":"The admin app's origin. `/stop-impersonating` sends the administrator back to `ADMIN_APP_URL` + `/app-admin/users`."},{"heading":"web-worker-vars","content":"`ADMIN_EMAIL`"},{"heading":"web-worker-vars","content":"`admin@nextbase.local`"},{"heading":"web-worker-vars","content":"Support address shown in emails."},{"heading":"web-worker-vars","content":"`UNKEY_API_ID`"},{"heading":"web-worker-vars","content":"empty"},{"heading":"web-worker-vars","content":"Public Unkey API identifier for developer keys."},{"heading":"web-worker-vars","content":"`API_CORS_ORIGINS`"},{"heading":"web-worker-vars","content":"`http://localhost:5173`"},{"heading":"web-worker-vars","content":"Exact-origin CORS allowlist for `/api/v1/me`. Keep preview and production lists separate and never use `*`."},{"heading":"admin-worker-vars","content":"Set in `apps/admin/wrangler.jsonc` for each environment."},{"heading":"admin-worker-vars","content":"Variable"},{"heading":"admin-worker-vars","content":"Local default"},{"heading":"admin-worker-vars","content":"What it is for"},{"heading":"admin-worker-vars","content":"`APP_NAME`"},{"heading":"admin-worker-vars","content":"`Nextbase`"},{"heading":"admin-worker-vars","content":"Same role as on the web Worker."},{"heading":"admin-worker-vars","content":"`SITE_URL`"},{"heading":"admin-worker-vars","content":"`http://localhost:5174`"},{"heading":"admin-worker-vars","content":"The admin origin. Better Auth `baseURL`, same-origin check for mutations, and the origin of the Admin MCP endpoint and its OAuth issuer."},{"heading":"admin-worker-vars","content":"`ADMIN_APP`"},{"heading":"admin-worker-vars","content":"`true`"},{"heading":"admin-worker-vars","content":"Marks this Worker as the administrator app. The MCP handler serves only the `admin` audience when it is `true`."},{"heading":"admin-worker-vars","content":"`WEB_APP_URL`"},{"heading":"admin-worker-vars","content":"`http://localhost:5173`"},{"heading":"admin-worker-vars","content":"The web origin. Impersonation redirects to `WEB_APP_URL` + `/auth/impersonate`; cache purges and magic links issued from the console use it as the public site origin. Impersonation fails if it is missing."},{"heading":"admin-worker-vars","content":"`ADMIN_EMAIL`"},{"heading":"admin-worker-vars","content":"`admin@nextbase.local`"},{"heading":"admin-worker-vars","content":"Support address."},{"heading":"admin-worker-vars","content":"`UNKEY_API_ID`"},{"heading":"admin-worker-vars","content":"empty"},{"heading":"admin-worker-vars","content":"Present for the shared code; developer API keys are managed on the web app."},{"heading":"admin-worker-vars","content":"`API_CORS_ORIGINS`"},{"heading":"admin-worker-vars","content":"`http://localhost:5173`"},{"heading":"admin-worker-vars","content":"Present for the shared code; the public API route `/api/v1/me` exists only on the web app."},{"heading":"jobs-worker-vars","content":"Set in `apps/jobs/wrangler.jsonc` for each environment."},{"heading":"jobs-worker-vars","content":"Variable"},{"heading":"jobs-worker-vars","content":"Local default"},{"heading":"jobs-worker-vars","content":"What it is for"},{"heading":"jobs-worker-vars","content":"`SITE_URL`"},{"heading":"jobs-worker-vars","content":"`http://localhost:5173`"},{"heading":"jobs-worker-vars","content":"Used to build links inside emails and jobs. Keep it identical to the web Worker's value."},{"heading":"jobs-worker-vars","content":"`ADMIN_EMAIL`"},{"heading":"jobs-worker-vars","content":"`admin@nextbase.local`"},{"heading":"jobs-worker-vars","content":"The support address in templates, and the Resend `from` address when `MAIL_FROM` is not set."},{"heading":"jobs-worker-vars","content":"`MAIL_FROM`"},{"heading":"jobs-worker-vars","content":"not set"},{"heading":"jobs-worker-vars","content":"Optional. When present, `apps/jobs/src/handlers.ts` uses it as the Resend sender, so the sender can be a verified address such as `Nextbase <no-reply@example.com>` while `ADMIN_EMAIL` stays a real mailbox. The shipped `env.production` block sets it."},{"heading":"bindings-not-variables","content":"These are configured in the Wrangler files and are not env vars you type. Keep the binding names exactly as they are; the code depends on them."},{"heading":"bindings-not-variables","content":"Binding"},{"heading":"bindings-not-variables","content":"Type"},{"heading":"bindings-not-variables","content":"Used by"},{"heading":"bindings-not-variables","content":"`APP_DB`"},{"heading":"bindings-not-variables","content":"D1 database"},{"heading":"bindings-not-variables","content":"Web, admin and jobs. Must point at the same database in each environment."},{"heading":"bindings-not-variables","content":"`PUBLIC_MEDIA`, `PRIVATE_MEDIA`"},{"heading":"bindings-not-variables","content":"R2 buckets"},{"heading":"bindings-not-variables","content":"Web, admin and jobs"},{"heading":"bindings-not-variables","content":"`JOBS`"},{"heading":"bindings-not-variables","content":"Queue producer"},{"heading":"bindings-not-variables","content":"Web, admin and jobs; the jobs Worker is also the consumer with a `-dlq` dead-letter queue"},{"heading":"bindings-not-variables","content":"`REALTIME`"},{"heading":"bindings-not-variables","content":"Durable Object `RealtimeRoom`"},{"heading":"bindings-not-variables","content":"Defined in web; admin and jobs reference it through `script_name`"},{"heading":"bindings-not-variables","content":"`AUTH_RATE_LIMITER`"},{"heading":"bindings-not-variables","content":"Durable Object `AuthRateLimiter`"},{"heading":"bindings-not-variables","content":"Defined in web; admin references it through `script_name`"},{"heading":"bindings-not-variables","content":"`CF_VERSION_METADATA`"},{"heading":"bindings-not-variables","content":"Worker version metadata"},{"heading":"bindings-not-variables","content":"All three Workers, for release IDs in logs"},{"heading":"bindings-not-variables","content":"After changing a binding or a var, run `pnpm cf:typegen` to regenerate `worker-configuration.d.ts` in the web and jobs Workers, and `pnpm --filter @nextbase/admin cf:typegen` for the admin Worker, so the `Env` types match."},{"heading":"related","content":"Authentication for what `BETTER_AUTH_SECRET` and the provider keys turn on."},{"heading":"related","content":"Deployment for the full list of `wrangler secret put` commands."},{"heading":"related","content":"Emails with Resend for the shared email guidance."}],"headings":[{"id":"web-worker-secrets","content":"Web Worker secrets"},{"id":"admin-worker-secrets-and-local-values","content":"Admin Worker secrets and local values"},{"id":"jobs-worker-secrets","content":"Jobs Worker secrets"},{"id":"web-worker-vars","content":"Web Worker `vars`"},{"id":"admin-worker-vars","content":"Admin Worker `vars`"},{"id":"jobs-worker-vars","content":"Jobs Worker `vars`"},{"id":"bindings-not-variables","content":"Bindings, not variables"},{"id":"related","content":"Related"}]},"url":"/docs/v5/starters/tanstack-d1/environment-variables","title":"Environment variables","description":"Every secret and Wrangler variable the web Worker, the admin Worker and the jobs Worker read, where it lives and where to get the value."},{"_id":"/docs/v5/starters/tanstack-d1","structured":{"contents":[{"content":"This kit uses React Router on Cloudflare Workers with TanStack Query, D1, Drizzle and Better Auth. You get the account, workspace and project foundation, a separately deployed administrator app, and separate agent connections for users and application administrators."},{"content":"Open the live demo · Repository"},{"heading":"the-stack","content":"Layer"},{"heading":"the-stack","content":"What the kit uses"},{"heading":"the-stack","content":"Frontend"},{"heading":"the-stack","content":"React 19, React Router framework mode with streaming SSR, TanStack Query with a request-scoped query client, Tailwind CSS"},{"heading":"the-stack","content":"Server"},{"heading":"the-stack","content":"A Cloudflare Worker (`apps/web/workers/app.ts`) that runs the React Router loaders and actions, plus a second Worker for the administrator app (`apps/admin/workers/app.ts`)"},{"heading":"the-stack","content":"Database"},{"heading":"the-stack","content":"Cloudflare D1 with a Drizzle schema and forward-only SQL migrations in `apps/database`"},{"heading":"the-stack","content":"Authentication"},{"heading":"the-stack","content":"Better Auth with email and password, verified email, magic links and optional Google, GitHub and Twitter sign-in"},{"heading":"the-stack","content":"Background work"},{"heading":"the-stack","content":"A separate jobs Worker (`apps/jobs`) fed by a transactional outbox and a Cloudflare Queue, with a dead-letter queue and two cron schedules"},{"heading":"the-stack","content":"Storage and realtime"},{"heading":"the-stack","content":"Two R2 buckets (public and private media) and Durable Objects for WebSocket invalidations and auth rate limiting"},{"heading":"the-stack","content":"Email"},{"heading":"the-stack","content":"41 React Email templates, Resend in production and a D1 development inbox locally"},{"heading":"the-stack","content":"Hosting used by the demo"},{"heading":"the-stack","content":"Cloudflare Workers, with its D1 database and R2, Queue and Durable Object bindings"},{"heading":"the-stack","content":"There is no Postgres, no Docker and no separate database server. Local development uses Wrangler's local D1 database inside the Worker runtime."},{"heading":"what-you-get-out-of-the-box","content":"Accounts with sign-up, email verification, password reset, magic link login and optional social providers"},{"heading":"what-you-get-out-of-the-box","content":"Workspaces with owner, admin, member and readonly roles, invitations, onboarding and settings"},{"heading":"what-you-get-out-of-the-box","content":"Projects with comments, an approval state machine, audit events and realtime refreshes"},{"heading":"what-you-get-out-of-the-box","content":"A separate administrator app (`apps/admin`) on its own origin with users, workspaces, payments, CMS, feedback, settings and read-only impersonation"},{"heading":"what-you-get-out-of-the-box","content":"A User MCP on the web app and an Admin MCP on the admin app, so agents can read your app through OAuth connections"},{"heading":"what-you-get-out-of-the-box","content":"Stripe billing: catalog sync, webhooks, subscriptions, invoices and one-time Checkout"},{"heading":"what-you-get-out-of-the-box","content":"R2 media uploads with reservations, size and signature checks and cleanup jobs"},{"heading":"what-you-get-out-of-the-box","content":"AI SDK streaming chat with durable history and workspace credits"},{"heading":"what-you-get-out-of-the-box","content":"Marketing pages, compiled MDX docs with search, a blog and changelog CMS, roadmap and feedback boards"},{"heading":"what-you-get-out-of-the-box","content":"A public developer API (`/api/v1/me`) with Unkey API keys and an exact-origin CORS allowlist"},{"heading":"what-you-get-out-of-the-box","content":"Product analytics and scrubbed operational exception capture in D1"},{"heading":"what-you-get-out-of-the-box","content":"Vitest unit tests, Workers and Miniflare D1 integration tests, Playwright smoke tests, Oxlint and Oxfmt"},{"heading":"what-you-get-out-of-the-box","content":"The feature mapping from Nextbase Ultimate to this Cloudflare implementation is in `docs/feature-parity.md` in the repository. The administrator app is described in the \"Separate administrator app\" section of the repository `README.md`."},{"heading":"prerequisites","content":"Requirement"},{"heading":"prerequisites","content":"Detail"},{"heading":"prerequisites","content":"Node.js"},{"heading":"prerequisites","content":"24 or newer. The repository has a `.nvmrc` with `24` and `engines.node` is `>=24`."},{"heading":"prerequisites","content":"pnpm"},{"heading":"prerequisites","content":"`11.1.2`, pinned in the root `package.json` `packageManager` field. Run `corepack enable` and let Corepack pick it."},{"heading":"prerequisites","content":"Docker"},{"heading":"prerequisites","content":"Not needed. D1 runs locally through Wrangler."},{"heading":"prerequisites","content":"Cloudflare account"},{"heading":"prerequisites","content":"Only when you deploy. Local development needs no Cloudflare login."},{"heading":"prerequisites","content":"Resend account"},{"heading":"prerequisites","content":"Optional locally. Without `RESEND_API_KEY` on the jobs Worker, emails land in the local D1 inbox. Needed for production email."},{"heading":"prerequisites","content":"Stripe, OpenAI, Unkey, OAuth providers"},{"heading":"prerequisites","content":"Optional. Billing, AI chat, API keys and social login stay off until you add their keys."},{"heading":"prerequisites","content":"Everything that handles a request runs in Cloudflare Workers. The repository's `AGENTS.md` asks you not to add Node-only runtime APIs to `apps/web` or `apps/jobs`."},{"heading":"next-steps","content":"Installation: clone, migrate the local D1 database and start the dev servers, including the admin app."},{"heading":"next-steps","content":"Environment variables: every variable in the three `.env.example` files and the Wrangler `vars`."},{"heading":"next-steps","content":"Authentication: Better Auth, roles, the trusted admin flow and what protects your data."},{"heading":"next-steps","content":"Agent connections: the User MCP and Admin MCP endpoints and tools."},{"heading":"next-steps","content":"Project structure: where routes, services, schema, jobs and tests live."},{"heading":"next-steps","content":"Deployment: provisioning Cloudflare resources and deploying the three Workers."},{"heading":"next-steps","content":"For the shared concepts, read getting started and user and admin MCPs."}],"headings":[{"id":"the-stack","content":"The stack"},{"id":"what-you-get-out-of-the-box","content":"What you get out of the box"},{"id":"prerequisites","content":"Prerequisites"},{"id":"next-steps","content":"Next steps"}]},"url":"/docs/v5/starters/tanstack-d1","title":"TanStack + Cloudflare D1","description":"What the TanStack + Cloudflare D1 starter is, what it ships with, and what you need before you start."},{"_id":"/docs/v5/starters/tanstack-d1/installation","structured":{"contents":[{"content":"This page gets the kit running on your computer. Nothing here needs a Cloudflare account; Wrangler runs D1, R2, Queues and Durable Objects locally."},{"content":"Read getting started first if you are new to the v5 kits. Here is the kit-specific flow."},{"heading":"clone-the-repository","content":"If GitHub says the repository doesn't exist, check that you are signed in with the account that has access."},{"heading":"use-the-right-nodejs-and-pnpm","content":"The repository pins Node.js 24 (`.nvmrc`) and `pnpm@11.1.2` (`packageManager` in `package.json`). Let Corepack pick the pnpm version for you:"},{"heading":"use-the-right-nodejs-and-pnpm","content":"`pnpm install` also builds `esbuild`, `sharp` and `workerd`, which are the only packages allowed to run build scripts."},{"heading":"create-the-local-secrets-files","content":"The web Worker, the admin Worker and the jobs Worker each read a `.dev.vars` file next to their `wrangler.jsonc`. Copy the examples only when the files are missing, so you keep any keys you already added:"},{"heading":"create-the-local-secrets-files","content":"The defaults are enough for local work. Leave `RESEND_API_KEY` in `apps/jobs/.dev.vars` empty so emails stay in the local inbox. Every variable is described on the environment variables page."},{"heading":"apply-the-d1-migrations","content":"This runs `wrangler d1 migrations apply APP_DB --local` using `apps/web/wrangler.jsonc`, which points at the SQL files in `apps/database/migrations`. Migration `0003_seed.sql` is part of that run, so you also get the public blog and changelog content and a `Nextbase Team` seed user (`team@nextbase.local`) without a password. There is no separate seed step; `pnpm db:seed` only re-executes that same file."},{"heading":"apply-the-d1-migrations","content":"The local database is stored under `apps/web/.wrangler/state`. The jobs Worker starts with `--persist-to ../web/.wrangler/state` and the admin app's Vite config points `persistState` at the same folder, so all three Workers share one local D1."},{"heading":"start-the-dev-servers","content":"Turborepo runs the `dev` script of every package in parallel, which starts four things:"},{"heading":"start-the-dev-servers","content":"Process"},{"heading":"start-the-dev-servers","content":"Package"},{"heading":"start-the-dev-servers","content":"Address"},{"heading":"start-the-dev-servers","content":"Web app (React Router dev server)"},{"heading":"start-the-dev-servers","content":"`@nextbase/web`"},{"heading":"start-the-dev-servers","content":"`http://localhost:5173`"},{"heading":"start-the-dev-servers","content":"Admin app (React Router dev server)"},{"heading":"start-the-dev-servers","content":"`@nextbase/admin`"},{"heading":"start-the-dev-servers","content":"`http://localhost:5174`"},{"heading":"start-the-dev-servers","content":"Jobs Worker (`wrangler dev`)"},{"heading":"start-the-dev-servers","content":"`@nextbase/jobs`"},{"heading":"start-the-dev-servers","content":"`http://localhost:8787/health`"},{"heading":"start-the-dev-servers","content":"React Email template preview"},{"heading":"start-the-dev-servers","content":"`@nextbase/email`"},{"heading":"start-the-dev-servers","content":"port `3001`"},{"heading":"start-the-dev-servers","content":"Open `http://localhost:5173`. The admin app answers on `http://localhost:5174` and redirects to `/app-admin` after you sign in there; `pnpm admin:dev` starts it on its own. The jobs health endpoint returns the pending, leased and dead outbox counts as JSON. The email preview shows the 41 templates; it is not the inbox."},{"heading":"start-the-dev-servers","content":"The jobs Worker reaches the `RealtimeRoom` Durable Object through a `script_name` binding on the web Worker, which `wrangler dev` resolves through the local dev registry. Keep the web dev server running while the jobs Worker runs, otherwise `realtime.invalidate` jobs cannot reach the room."},{"heading":"create-your-first-account","content":"Go to `/sign-up` on the web app, create an account and you will land on `/verify-email?sent=1`. The verification email is not sent anywhere. Because `RESEND_API_KEY` is empty, the jobs Worker writes the rendered message into the `dev_email_inbox` table in D1."},{"heading":"create-your-first-account","content":"Read it with Wrangler from the repository root:"},{"heading":"create-your-first-account","content":"Copy the verification link from the `html` column into your browser. Verification signs you in and sends you to `/onboarding`, where you create your first workspace and accept the terms. You then land on `/workspace/<slug>/home`."},{"heading":"create-your-first-account","content":"Add a project through the website so you have data to look at before you connect an agent."},{"heading":"check-that-everything-works","content":"`pnpm verify` runs formatting, lint, type checks, unit tests and production builds for every package, including the admin app. `pnpm test:integration` runs the Workers and Miniflare D1 invariant tests in `apps/web/test`. `pnpm test:e2e` starts two servers, `pnpm db:migrate && pnpm dev` for the web app and `pnpm --dir ../admin dev` for the admin app, and runs the Playwright suite in `apps/web/e2e` against `http://localhost:5173` and `http://localhost:5174` with a single worker, because the tests share the persisted local database."},{"heading":"make-yourself-an-application-admin","content":"The seed does not create a reusable admin password. Sign up normally on the web app, verify, then grant the platform role to your own account:"},{"heading":"make-yourself-an-application-admin","content":"Then open `http://localhost:5174/login`, sign in with the same email and password, and you land on `/app-admin`. The admin app has its own session cookie, so signing in on the web app does not sign you in there. The web app has no `/app-admin` routes. The authentication page explains the roles."},{"heading":"stop-and-reset","content":"Press `Ctrl+C` in the terminal running `pnpm dev`. Local data stays in `apps/web/.wrangler/state`, which is ignored by git."},{"heading":"stop-and-reset","content":"To start from an empty database, stop the dev servers, delete `apps/web/.wrangler/state`, and run `pnpm db:migrate` again. Local R2 objects and Durable Object storage live in the same folder, so this resets them too."},{"heading":"stop-and-reset","content":"Never edit an applied migration. After changing the Drizzle schema in `apps/database/src/schema`, run `pnpm db:generate`, review the generated SQL, and add a new numbered file in `apps/database/migrations`. Then run `pnpm db:migrate` and `pnpm test:integration`."},{"heading":"stop-and-reset","content":"If the local app opens but requests fail, check the troubleshooting page. For D1 the usual cause is a dev server that is no longer running or a migration that has not been applied."}],"headings":[{"id":"clone-the-repository","content":"Clone the repository"},{"id":"use-the-right-nodejs-and-pnpm","content":"Use the right Node.js and pnpm"},{"id":"create-the-local-secrets-files","content":"Create the local secrets files"},{"id":"apply-the-d1-migrations","content":"Apply the D1 migrations"},{"id":"start-the-dev-servers","content":"Start the dev servers"},{"id":"create-your-first-account","content":"Create your first account"},{"id":"check-that-everything-works","content":"Check that everything works"},{"id":"make-yourself-an-application-admin","content":"Make yourself an application admin"},{"id":"stop-and-reset","content":"Stop and reset"}]},"url":"/docs/v5/starters/tanstack-d1/installation","title":"Installation","description":"Clone the TanStack + Cloudflare D1 starter, migrate the local D1 database, start the web, admin and jobs Workers and create your first account."},{"_id":"/docs/v5/starters/tanstack-d1/project-structure","structured":{"contents":[{"content":"The repository is a pnpm workspace orchestrated by Turborepo. Five apps and two shared packages live under `apps/*` and `packages/*`. Run every command from the repository root."},{"content":"<File name=\"routes.ts\" />"},{"content":"<File name=\"app.ts\" />"},{"content":"<File name=\"realtime-room.ts\" />"},{"content":"<File name=\"auth-rate-limiter.ts\" />"},{"content":"<File name=\"wrangler.jsonc\" />"},{"content":"<File name=\".env.example\" />"},{"content":"<File name=\"vite.config.ts\" />"},{"content":"<File name=\"react-router.config.ts\" />"},{"content":"<File name=\"playwright.config.ts\" />"},{"content":"<File name=\"vitest.config.ts\" />"},{"content":"<File name=\"vitest.integration.config.ts\" />"},{"content":"<File name=\"worker-configuration.d.ts\" />"},{"content":"<File name=\"routes.ts\" />"},{"content":"<File name=\"root.tsx\" />"},{"content":"<File name=\"app.ts\" />"},{"content":"<File name=\"wrangler.jsonc\" />"},{"content":"<File name=\".env.example\" />"},{"content":"<File name=\"vite.config.ts\" />"},{"content":"<File name=\"react-router.config.ts\" />"},{"content":"<File name=\"worker-configuration.d.ts\" />"},{"content":"<File name=\"index.ts\" />"},{"content":"<File name=\"handlers.ts\" />"},{"content":"<File name=\"outbox.ts\" />"},{"content":"<File name=\"stripe.ts\" />"},{"content":"<File name=\"media.ts\" />"},{"content":"<File name=\"unkey.ts\" />"},{"content":"<File name=\"account-deletion.ts\" />"},{"content":"<File name=\"observability.ts\" />"},{"content":"<File name=\"wrangler.jsonc\" />"},{"content":"<File name=\".env.example\" />"},{"content":"<File name=\"client.ts\" />"},{"content":"<File name=\"drizzle.config.ts\" />"},{"content":"<File name=\"feature-parity.md\" />"},{"content":"<File name=\"mcp-connections.md\" />"},{"content":"<File name=\"operations.md\" />"},{"content":"<File name=\"agent-config.yml\" />"},{"content":"<File name=\"ci.yml\" />"},{"content":"<File name=\"deploy.yml\" />"},{"content":"<File name=\"release.yml\" />"},{"content":"<File name=\"package.json\" />"},{"content":"<File name=\"pnpm-workspace.yaml\" />"},{"content":"<File name=\"turbo.json\" />"},{"content":"<File name=\"AGENTS.md\" />"},{"heading":"the-five-apps","content":"Package"},{"heading":"the-five-apps","content":"Path"},{"heading":"the-five-apps","content":"What it is"},{"heading":"the-five-apps","content":"`@nextbase/web`"},{"heading":"the-five-apps","content":"`apps/web`"},{"heading":"the-five-apps","content":"The customer-facing React Router app and the Worker that serves it, including the User MCP. Vite builds it with `@cloudflare/vite-plugin`; `wrangler.jsonc` is the Worker config."},{"heading":"the-five-apps","content":"`@nextbase/admin`"},{"heading":"the-five-apps","content":"`apps/admin`"},{"heading":"the-five-apps","content":"The administrator app, a second React Router app and Worker on its own origin (port `5174` locally). It has its own routes, login and Better Auth secret, reuses the web app's server modules through the `~` alias, and serves the Admin MCP."},{"heading":"the-five-apps","content":"`@nextbase/jobs`"},{"heading":"the-five-apps","content":"`apps/jobs`"},{"heading":"the-five-apps","content":"The background Worker: Queue consumer, dead-letter consumer, `/health` endpoint and two crons (`* * * * *` for outbox recovery and cleanup, `0 3 * * *` for daily reconciliation and retention)."},{"heading":"the-five-apps","content":"`@nextbase/database`"},{"heading":"the-five-apps","content":"`apps/database`"},{"heading":"the-five-apps","content":"Drizzle schema, generated D1 types, the hand-written SQL migrations and the seed."},{"heading":"the-five-apps","content":"`@nextbase/email`"},{"heading":"the-five-apps","content":"`apps/email`"},{"heading":"the-five-apps","content":"The 41 React Email templates and the renderer the jobs Worker calls. `pnpm dev` also runs its preview on port 3001."},{"heading":"the-five-apps","content":"`packages/contracts` holds the shared zod schemas and types: auth (`Principal`, `WorkspaceContext`, roles), job envelopes, problem details, observability helpers and workspace contracts. `packages/typescript-config` holds the shared `tsconfig` bases."},{"heading":"the-five-apps","content":"`infra/admin-domain` and `infrastructure/demo-domain` are two small Cloudflare Pages projects that proxy the demo's custom domains to the admin and web Workers through a service binding. You only need them if you attach a domain the same way."},{"heading":"routes-and-pages","content":"Web routes are declared in `apps/web/app/routes.ts` and implemented in `apps/web/app/routes/`. Public marketing, blog, changelog, docs, feedback and roadmap pages share `routes/public.tsx`. Signed-in pages sit under `routes/app-layout.tsx`: `dashboard.tsx`, `workspaces.tsx`, `workspace.tsx` (`/workspace/:workspaceSlug/*`), `project.tsx` and `user.tsx` (`/user/*`). API routes are the `api-*.ts` files. `impersonate.ts` and `stop-impersonating.ts` are the two halves of the cross-origin impersonation handoff. Add a route in `routes.ts` and a matching file next to the others."},{"heading":"routes-and-pages","content":"Admin routes are declared in `apps/admin/app/routes.ts`: `login.tsx`, the password and verification pages, `admin.tsx` (`/app-admin/*`, one module that switches on the section), `api-auth.ts`, `api-media.ts`, `api-health.ts` and `api-admin-insights.ts`. The web app has no `/app-admin` routes."},{"heading":"routes-and-pages","content":"Reusable UI lives in `apps/web/app/components/`, with `components/ui/` for primitives. Hooks are in `app/hooks/`, small utilities in `app/lib/`. The admin app imports these through the same `~` alias."},{"heading":"server-logic-and-data-access","content":"Everything that runs only on the Worker is in `apps/web/app/server/*.server.ts`. Start with `session.server.ts` for the principal, `authorization.server.ts` for workspace capabilities, `repositories.server.ts` for tenant-scoped reads and writes, `workspaces.server.ts` and `project-mutations.server.ts` for the core domain, `payments.server.ts` for Stripe and `queue.server.ts` for nudging the Queue. The admin app adds `apps/admin/app/server/admin-magic-link.server.ts` for issuing magic links to users from the console."},{"heading":"server-logic-and-data-access","content":"Keep the rule from `AGENTS.md`: resolve a `WorkspaceContext` from the session and route slug first, then pass it into repositories. Mutations that need atomicity use a single D1 `batch()` and write audit and outbox rows in that batch."},{"heading":"schema-and-migrations","content":"Edit the Drizzle tables in `apps/database/src/schema/` (`auth.ts`, `core.ts`, `billing.ts`, `content.ts`, `system.ts`). Run `pnpm db:generate` to produce SQL under `apps/database/migrations/generated/`, then add the reviewed statements as the next numbered file in `apps/database/migrations/` (the folder Wrangler applies, currently `0001` to `0025`; `0025_user_admin_mcp.sql` creates the `mcp_records` table for agent connections). Apply with `pnpm db:migrate` and run `pnpm test:integration`."},{"heading":"schema-and-migrations","content":"Migrations are forward-only. FTS5 search tables, the development email inbox and triggers live in the hand-written migrations rather than in Drizzle tables."},{"heading":"authentication","content":"`apps/web/app/server/auth.server.ts` for Better Auth options, plugins and hooks; `auth-handler.server.ts` and `auth-route.server.ts` for request handling; `impersonation.server.ts` for the read-only impersonation rule; `apps/web/workers/auth-rate-limiter.ts` for the limiter. The admin app reuses all of these with `ADMIN_APP=true`. Details are on the authentication page."},{"heading":"emails","content":"Templates are in `apps/email/emails/`, rendered by `apps/email/src/render.tsx`. Sending starts in `apps/web/app/server/email.server.ts` (`enqueueEmail`) and finishes in `apps/jobs/src/handlers.ts` (`sendEmail`), which chooses Resend or the local inbox. To add an email, add the template, then call `enqueueEmail` with its name and props."},{"heading":"background-jobs-and-workers","content":"Job types and the envelope schema are in `packages/contracts/src/jobs.ts`. The dispatcher is `dispatchJob` in `apps/jobs/src/handlers.ts`; leases, receipts and retries are in `outbox.ts`. A new job needs a type, a handler case, a stable idempotency key, and a producer that inserts the outbox row from the web or admin app. The Durable Objects (`RealtimeRoom`, `AuthRateLimiter`) are in `apps/web/workers/`; the admin and jobs Workers reach them through `script_name` bindings, as `apps/jobs/AGENTS.md` describes."},{"heading":"mcp","content":"`apps/web/src/mcp/` with `core.ts`, `adapter.ts` and `d1-store.ts`, mounted by both `apps/web/workers/app.ts` and `apps/admin/workers/app.ts`. See agent connections."},{"heading":"tests","content":"Kind"},{"heading":"tests","content":"Where"},{"heading":"tests","content":"Command"},{"heading":"tests","content":"Unit tests"},{"heading":"tests","content":"`*.test.ts` next to the code in every package"},{"heading":"tests","content":"`pnpm test`"},{"heading":"tests","content":"D1 integration tests (workerd and Miniflare)"},{"heading":"tests","content":"`apps/web/test/*.integration.test.ts`"},{"heading":"tests","content":"`pnpm test:integration`"},{"heading":"tests","content":"Browser smoke tests (web and admin)"},{"heading":"tests","content":"`apps/web/e2e/*.spec.ts`"},{"heading":"tests","content":"`pnpm test:e2e`"},{"heading":"tests","content":"Everything plus lint, format, types and build"},{"heading":"tests","content":"`pnpm verify`"},{"heading":"tests","content":"CI (`.github/workflows/ci.yml`) runs `pnpm verify`, `pnpm test:integration` and the Playwright suite on every pull request. `agent-config.yml` checks that the generated AI tool configuration is in sync (`pnpm agents:check`). Use `pnpm changeset` to describe a user-visible change; `release.yml` maintains the version PR."},{"heading":"configuration-files","content":"`apps/web/wrangler.jsonc`, `apps/admin/wrangler.jsonc` and `apps/jobs/wrangler.jsonc` define bindings and vars per environment. The root `package.json` holds the scripts you will use most (`db:*`, `cf:*`, `admin:dev`, `verify`). `turbo.json` wires `build`, `dev`, `test` and `test:integration` across packages. Lint and format use `.oxlintrc.json` and `.oxfmtrc.json`."}],"headings":[{"id":"the-five-apps","content":"The five apps"},{"id":"where-to-make-changes","content":"Where to make changes"},{"id":"routes-and-pages","content":"Routes and pages"},{"id":"server-logic-and-data-access","content":"Server logic and data access"},{"id":"schema-and-migrations","content":"Schema and migrations"},{"id":"authentication","content":"Authentication"},{"id":"emails","content":"Emails"},{"id":"background-jobs-and-workers","content":"Background jobs and workers"},{"id":"mcp","content":"MCP"},{"id":"tests","content":"Tests"},{"id":"configuration-files","content":"Configuration files"}]},"url":"/docs/v5/starters/tanstack-d1/project-structure","title":"Project structure","description":"The monorepo layout of the TanStack + Cloudflare D1 starter and where to make common changes."},{"_id":"/docs/v5/starters/tanstack-neon/agent-connections","structured":{"contents":[{"content":"The kit ships two remote MCP servers. The User MCP runs on the web origin and the Admin MCP runs on the admin origin, because the admin application deploys separately. Both use OAuth with your existing Better Auth account, and both are read-only in v5. The shared pages cover the general flow: User and admin MCPs, connect an agent and tools and permissions."},{"heading":"where-users-manage-connections","content":"Connection"},{"heading":"where-users-manage-connections","content":"Settings page"},{"heading":"where-users-manage-connections","content":"Who can open it"},{"heading":"where-users-manage-connections","content":"User"},{"heading":"where-users-manage-connections","content":"`/mcp/connections/user` on the web app"},{"heading":"where-users-manage-connections","content":"Any signed-in, verified account"},{"heading":"where-users-manage-connections","content":"Admin"},{"heading":"where-users-manage-connections","content":"`/mcp/connections/admin` on the admin app"},{"heading":"where-users-manage-connections","content":"Accounts with the global `admin` role"},{"heading":"where-users-manage-connections","content":"The web app shell lists **Agent connections** first in its navigation and the admin app shell lists **Admin agent connections**. These pages are rendered by Express, not React Router, so links to them use `reloadDocument`. Each page shows the server address to copy, lists the account's connections for that server with the client name, workspace (or \"Application administration\"), scope, connection date and expiry, and offers **Disconnect**. The UI caps active connections at 20 per account."},{"heading":"where-users-manage-connections","content":"Each server answers only its own audience. Asking the web app for `/mcp/admin` or `/mcp/connections/admin` returns `404`, and the admin app does the same for the user paths. The small cross-link at the bottom of each connections page points at the other audience's path on the same origin, so on a split deployment follow the table above instead."},{"heading":"endpoints","content":"Server"},{"heading":"endpoints","content":"Live demo"},{"heading":"endpoints","content":"Your deployment"},{"heading":"endpoints","content":"User MCP"},{"heading":"endpoints","content":"`https://tanstack-neon.usenextbase.com/mcp/user`"},{"heading":"endpoints","content":"`{web APP_BASE_URL}/mcp/user`"},{"heading":"endpoints","content":"Admin MCP"},{"heading":"endpoints","content":"`https://tanstack-neon-admin.usenextbase.com/mcp/admin`"},{"heading":"endpoints","content":"`{admin APP_BASE_URL}/mcp/admin`"},{"heading":"endpoints","content":"Each app uses its own `APP_BASE_URL` as the MCP origin. Opening either URL in a browser without a token returns `401` with a `WWW-Authenticate` challenge pointing at the resource metadata; that is expected."},{"heading":"endpoints","content":"OAuth discovery and endpoints, each on the origin that serves the MCP server:"},{"heading":"endpoints","content":"Protected resource metadata: `/.well-known/oauth-protected-resource/mcp/user` (web) and `/.well-known/oauth-protected-resource/mcp/admin` (admin)"},{"heading":"endpoints","content":"Authorization server metadata: `/.well-known/oauth-authorization-server/oauth` (issuer `{APP_BASE_URL}/oauth`). Each app advertises only its own scope in `scopes_supported`"},{"heading":"endpoints","content":"`/oauth/register`, `/oauth/authorize`, `/oauth/token`, `/oauth/revoke`"},{"heading":"endpoints","content":"Consent page: `/mcp/authorize`"},{"heading":"tools","content":"Server"},{"heading":"tools","content":"Tool"},{"heading":"tools","content":"Scope"},{"heading":"tools","content":"What it returns"},{"heading":"tools","content":"User"},{"heading":"tools","content":"`account_context`"},{"heading":"tools","content":"`user:read`"},{"heading":"tools","content":"The signed-in user's id and email, and the workspace approved for this connection"},{"heading":"tools","content":"User"},{"heading":"tools","content":"`projects_list`"},{"heading":"tools","content":"`user:read`"},{"heading":"tools","content":"Projects in the approved workspace, newest first. `limit` 1 to 50, default 20"},{"heading":"tools","content":"Admin"},{"heading":"tools","content":"`admin_users_list`"},{"heading":"tools","content":"`admin:read`"},{"heading":"tools","content":"Id, name, email and role of application users, newest first. `limit` 1 to 50, default 20"},{"heading":"tools","content":"Admin"},{"heading":"tools","content":"`admin_workspaces_list`"},{"heading":"tools","content":"`admin:read`"},{"heading":"tools","content":"Id, name and slug of all workspaces, newest first. `limit` 1 to 50, default 20"},{"heading":"tools","content":"All four are annotated `readOnlyHint: true`. There is no `projects_create` in this kit; the write tool exists only in the Convex kits. A user token never advertises admin tools and doesn't work at the admin endpoint, and an admin connecting to the User MCP gets only that one workspace's tools."},{"heading":"tools","content":"Each tool call reloads the grant and re-checks the native session in `apps/web/src/mcp/adapter.ts`: the session row must still exist and not be expired, the user must be verified and not banned, impersonated sessions are rejected, and admin tools re-check `role = 'admin'` on every request. Losing workspace membership or the admin role ends access even if the token hasn't expired."},{"heading":"oauth-and-consent-behaviour","content":"The protocol is Streamable HTTP with stateless JSON responses (official TypeScript MCP SDK `1.32.1`). The kit supports public clients with dynamic client registration, authorization code with mandatory PKCE S256, exact callback matching and exact resource binding. Confidential clients and client ID metadata documents are not supported. Callback URLs must be HTTPS, except HTTP loopback on `localhost`, `127.0.0.1` or `::1`. Request bodies are capped at 32 KB."},{"heading":"oauth-and-consent-behaviour","content":"When an agent starts a connection, the kit redirects to `/mcp/authorize?request_id=...`. If you aren't signed in, you go to `/login?returnTo=/mcp/authorize?...` and come back to the same request after a full document load. The consent page shows the client name (marked unverified, because the client supplies it), the callback address, the server, the scope with \"Read access only\" and the account you are signed in as. For a user connection you pick one workspace from the ones you belong to; if you have none, the page tells you to create or join a workspace first. The only choices are **Allow read access** and **Cancel**."},{"heading":"oauth-and-consent-behaviour","content":"Lifetimes: pending consent 10 minutes, authorization codes 5 minutes, access tokens 15 minutes, refresh up to 30 days and never longer than the native session that approved it. Refresh tokens rotate, and reusing an old one revokes the whole connection. Secrets are stored as SHA-256 hashes only."},{"heading":"oauth-and-consent-behaviour","content":"Browser origins allowed for MCP requests are the app's own `APP_BASE_URL`, `https://claude.ai` and `https://chatgpt.com`. Desktop and server clients without an `Origin` header are accepted."},{"heading":"where-the-code-lives","content":"Path"},{"heading":"where-the-code-lives","content":"Role"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.ts`"},{"heading":"where-the-code-lives","content":"Provider-independent protocol: discovery, registration, consent HTML, tokens, revocation, tool registry, rate limits"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/adapter.ts`"},{"heading":"where-the-code-lives","content":"This kit's adapter: `handleMcp(request, audience)` reads the live session from the Better Auth tables, lists workspaces and projects through `withActorTransaction`, checks `role = 'admin'` for admin tools"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/pg-store.ts`"},{"heading":"where-the-code-lives","content":"Storage in `public.mcp_records` using compare-and-set on a `revision` column"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/express.ts`"},{"heading":"where-the-code-lives","content":"Bridges Express to the fetch-style handler for `/mcp/*`, `/oauth/*` and `/.well-known/oauth-*`"},{"heading":"where-the-code-lives","content":"`apps/web/server.ts`"},{"heading":"where-the-code-lives","content":"Mounts `expressMcp(handleMcp, env.APP_BASE_URL)` with the default `user` audience"},{"heading":"where-the-code-lives","content":"`apps/admin/server.ts`"},{"heading":"where-the-code-lives","content":"Mounts the same bridge with `handleMcp(request, \"admin\")`, so only the admin audience exists on that origin"},{"heading":"where-the-code-lives","content":"`apps/database/migrations/0003_user_admin_mcp.sql`"},{"heading":"where-the-code-lives","content":"Creates `mcp_records` with RLS enabled, revokes it from `PUBLIC`, `app_runtime` and `worker_runtime`, grants it to `auth_runtime` only"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.test.ts`"},{"heading":"where-the-code-lives","content":"Unit tests for audience isolation, PKCE, callbacks, CSRF, replay, expiry, demotion, revocation and the split user/admin surfaces"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/pg-store.integration.test.ts`"},{"heading":"where-the-code-lives","content":"Storage test. Runs only when `MCP_TEST_PG_URL` points at a loopback database"},{"heading":"where-the-code-lives","content":"MCP credential storage runs on the `auth_runtime` connection, so application roles can't read tokens. Tool data for users still goes through `withActorTransaction` and RLS, the same as the website."},{"heading":"adding-a-tool-safely","content":"The repository has an `mcp-tool` skill in `.agents/skills/` that walks an AI coding tool through these steps."},{"heading":"adding-a-tool-safely","content":"Add the data access to `apps/web/src/mcp/adapter.ts`, inside `withActorTransaction` for anything tenant-scoped. Call the existing services or the same SQL shape they use rather than new unchecked queries."},{"heading":"adding-a-tool-safely","content":"Register the tool in `apps/web/src/mcp/core.ts` under the right audience (`user` or `admin`) with a bounded input schema and honest annotations. Keep `readOnlyHint: true` unless you are deliberately adding a write and a new scope."},{"heading":"adding-a-tool-safely","content":"Resolve the actor with `currentActor()` inside the handler. Never trust a `workspaceId` or `userId` argument as proof of permission; the grant already carries the approved workspace."},{"heading":"adding-a-tool-safely","content":"Add both the allowed and the denied case to `core.test.ts`, and an integration test if the query touches RLS."},{"heading":"adding-a-tool-safely","content":"Run `pnpm verify`, `pnpm test:integration` and `pnpm db:check` before deploying. Admin tools ship with the admin app, user tools with the web app; deploy whichever you changed."},{"heading":"adding-a-tool-safely","content":"If you add a write tool, also add a distinct scope, show it on the consent page, and keep the current read-only scopes unable to perform it. See the shared tools and permissions page for the cross-kit rules."}],"headings":[{"id":"where-users-manage-connections","content":"Where users manage connections"},{"id":"endpoints","content":"Endpoints"},{"id":"tools","content":"Tools"},{"id":"oauth-and-consent-behaviour","content":"OAuth and consent behaviour"},{"id":"where-the-code-lives","content":"Where the code lives"},{"id":"adding-a-tool-safely","content":"Adding a tool safely"}]},"url":"/docs/v5/starters/tanstack-neon/agent-connections","title":"Agent connections","description":"User MCP and Admin MCP in the TanStack + Neon starter: settings pages, endpoints on the web and admin origins, tools, OAuth behaviour and where the code lives."},{"_id":"/docs/v5/starters/tanstack-neon/authentication","structured":{"contents":[{"content":"This kit uses Better Auth with database sessions. The configuration is one file, `apps/web/src/server/auth.ts`, and it talks to PostgreSQL through the Drizzle adapter on a dedicated `auth_runtime` connection. The admin app imports the same module, so both deployments share one user table and one set of rules. The shared authentication page explains the concepts that are the same across kits; this page covers what this kit actually does."},{"heading":"sign-in-methods","content":"Method"},{"heading":"sign-in-methods","content":"Status"},{"heading":"sign-in-methods","content":"Notes"},{"heading":"sign-in-methods","content":"Email and password"},{"heading":"sign-in-methods","content":"On"},{"heading":"sign-in-methods","content":"Minimum 8 characters. Email verification is required before a session is issued, and the kit signs you in automatically after verification"},{"heading":"sign-in-methods","content":"Password reset"},{"heading":"sign-in-methods","content":"On"},{"heading":"sign-in-methods","content":"`/forgot-password` requests a link, `/reset-password` sets the new password"},{"heading":"sign-in-methods","content":"Magic link"},{"heading":"sign-in-methods","content":"Server only"},{"heading":"sign-in-methods","content":"The `magicLink` plugin is registered with 5-minute hashed tokens and the client plugin is loaded, but the shipped login page has no magic link form. Add one with `authClient.signIn.magicLink`"},{"heading":"sign-in-methods","content":"Google, GitHub, X"},{"heading":"sign-in-methods","content":"Optional"},{"heading":"sign-in-methods","content":"Each provider is registered only when both of its env variables are set. There are no provider buttons in the shipped UI yet. See environment variables"},{"heading":"sign-in-methods","content":"Rate limiting is enabled and stored in the database (`public.rate_limit`), so it works across several server instances. Cookies use the `nextbase` prefix and are marked secure when `NODE_ENV=production`. The client IP comes from the `x-nextbase-client-ip` header that Express sets from the trusted proxy."},{"heading":"sign-in-methods","content":"The web pages are `/sign-up`, `/login`, `/forgot-password` and `/reset-password`. Better Auth's own HTTP endpoints are mounted at `/api/auth/*` on both apps. After sign-in the kit honours a `returnTo` query parameter, but only for same-origin paths, and it does a full document load when the destination is an `/mcp/` page because those are served by Express rather than React Router."},{"heading":"email-verification-and-the-local-inbox","content":"Verification, password reset and magic link emails are written to `app.outbox_jobs` with an idempotency key (`enqueueEmail` in `apps/web/src/server/email.ts`). A separate process sends them over SMTP. Locally that means:"},{"heading":"email-verification-and-the-local-inbox","content":"Run `pnpm --filter @nextbase/web worker` in a second terminal, or nothing is sent."},{"heading":"email-verification-and-the-local-inbox","content":"Open Mailpit at `http://localhost:38025` and follow the link."},{"heading":"email-verification-and-the-local-inbox","content":"In production the same code runs from Vercel's `waitUntil` and the 15-minute cron described in deployment. A magic link that has already expired is never sent on a late retry. See emails with Resend for the provider side."},{"heading":"account-pages","content":"Signed-in users have `/app` (overview with their workspaces), `/app/workspaces` (create a workspace), `/app/projects` and `/app/settings`. The settings page is a static form in this kit; wire it to Better Auth's `updateUser` and `changeEmail` when you build account management."},{"heading":"global-roles-and-the-admin-app","content":"The Better Auth admin plugin defines three global roles in `auth.ts`:"},{"heading":"global-roles-and-the-admin-app","content":"Role"},{"heading":"global-roles-and-the-admin-app","content":"What it can do"},{"heading":"global-roles-and-the-admin-app","content":"`user`"},{"heading":"global-roles-and-the-admin-app","content":"Default for every new account"},{"heading":"global-roles-and-the-admin-app","content":"`support`"},{"heading":"global-roles-and-the-admin-app","content":"List and read users, ban, list and revoke sessions"},{"heading":"global-roles-and-the-admin-app","content":"`admin`"},{"heading":"global-roles-and-the-admin-app","content":"Everything `support` can do, plus set roles and impersonate. Only `admin` opens the admin app"},{"heading":"global-roles-and-the-admin-app","content":"The admin area is a separate application, `apps/admin`, on its own origin (`http://localhost:3001` locally, `https://tanstack-neon-admin.usenextbase.com` for the demo). Its routes are `/login`, `/app-admin`, `/app-admin/users` and placeholder sections for workspaces, payment, marketing, authors, tags, blog, changelog, feedback, roadmap and settings. The layout loader and every page loader call `requireAppAdmin` from `apps/web/src/server/authorization.ts`, which redirects anonymous visitors to `/login?returnTo=/app-admin` and returns `403` to anyone whose global role is not `admin` or whose session is an impersonation."},{"heading":"global-roles-and-the-admin-app","content":"The web app no longer serves `/app-admin`. It also returns `404` for `/api/auth/admin/*` and `/api/auth/one-time-token/*`, so the Better Auth admin endpoints are reachable only on the admin origin, and there only through the kit's own flows."},{"heading":"becoming-an-admin","content":"There is no sign-up path to admin, and the seeded `admin@nextbase.local` user has no credential at all, so you cannot sign in as it. The trusted flow is a direct database update run by someone with the `migration_owner` credentials:"},{"heading":"becoming-an-admin","content":"This runs `apps/database/src/grant-admin.ts`, which sets `role = 'admin'` on the matching Better Auth user and fails if the email doesn't exist. Sign out and in again so the new role is loaded, then sign in on the admin origin with the same email and password. Use the same command against production with `DATABASE_DIRECT_URL` pointing at Neon's direct endpoint."},{"heading":"impersonation","content":"Impersonation starts only from `/app-admin/users` in the admin app. The action requires a same-origin form post, a non-impersonated admin session and a support reason of 5 to 500 characters. It then:"},{"heading":"impersonation","content":"Calls Better Auth's `impersonateUser`, which creates a 15-minute session for the target (`impersonationSessionDuration: 900`)."},{"heading":"impersonation","content":"Writes an `admin.impersonation.started` audit event with the actor and the subject inside `withActorTransaction`, and registers a one-minute, single-use handoff record keyed to the issuing admin session. If either write fails, the new session is deleted."},{"heading":"impersonation","content":"Redirects to `{WEB_APP_URL}/auth/impersonation#token=...` with a hashed one-time token in the URL fragment and `Referrer-Policy: no-referrer`."},{"heading":"impersonation","content":"On the web origin, `/auth/impersonation` posts the token to `/api/impersonation`. The server verifies the token, rechecks in the database that the issuing session still belongs to a non-banned `admin`, consumes the handoff record and only then sets host-only cookies. The app layout shows a \"You are impersonating a user\" banner; **Return to admin** posts to `/api/impersonation/stop`, which signs the support session out and redirects to `{ADMIN_APP_URL}/app-admin/users`. The admin's own session is never touched. MCP connections reject impersonated sessions entirely."},{"heading":"impersonation","content":"`apps/admin/app/routes/admin-users.integration.test.ts` exercises the real flow against local PostgreSQL: restricted roles, mismatched audit subjects, handoff redemption and single use, host-only cookies and preservation of the administrator session."},{"heading":"workspace-roles","content":"Workspace access is separate from the global role. Each membership in `app.workspace_members` has one of `owner`, `admin`, `member` or `readonly` (the `app.workspace_role` enum). Creating a workspace makes you its `owner`, and a database trigger (`app.prevent_last_owner_removal`) stops the last owner from being removed."},{"heading":"workspace-roles","content":"Being an application admin doesn't make you a member of every workspace. The User MCP consent page, for example, only offers workspaces your account belongs to."},{"heading":"what-is-protected-and-how","content":"Authorization happens twice."},{"heading":"what-is-protected-and-how","content":"**In the domain services.** Route loaders, actions and `/api/v1` controllers call the same services in `apps/web/src/server/services/`. A loader starts with `requireSession(context)` or `requireAppAdmin(context)`; an API handler starts with `authenticated()`. Services take `(principal, requestId, input)` and the principal carries `actorUserId`, `subjectUserId`, `globalRole` and `impersonating`."},{"heading":"what-is-protected-and-how","content":"**In PostgreSQL.** Protected work runs inside `withActorTransaction` (`apps/web/src/server/actor-transaction.ts`). It opens a transaction on the `app_runtime` pool and sets `app.subject_id`, `app.actor_id` and `app.request_id` as transaction-local settings. RLS policies read those settings through `app.current_subject_id()`, `app.is_workspace_member()`, `app.has_workspace_role()` and `app.is_global_admin()` on every statement. Because the identity lives only inside the transaction, this works behind PgBouncer in transaction pooling mode."},{"heading":"what-is-protected-and-how","content":"Every table in the `app` schema except `outbox_jobs` has `ENABLE` and `FORCE ROW LEVEL SECURITY`: `user_settings`, `workspaces`, `workspace_members`, `workspace_invitations`, `projects`, `project_comments`, `notifications`, `audit_events`, `media_assets`, `feedback`, `feedback_reactions`, `user_api_keys`, `billing_customers`, `subscriptions`, `invoices`, `authors`, `tags`, `blog_posts`, `blog_post_tags` and `roadmap_items`. `pnpm db:check` verifies forced RLS on the 14 tenant tables it tracks, that no runtime role has `BYPASSRLS` or superuser or is a member of `neon_superuser` or `function_owner`, and that no `app` schema function is executable by `PUBLIC`."},{"heading":"the-database-roles","content":"Role"},{"heading":"the-database-roles","content":"Used by"},{"heading":"the-database-roles","content":"Trust level"},{"heading":"the-database-roles","content":"`migration_owner`"},{"heading":"the-database-roles","content":"Migrations, seed, `db:check`, `admin:grant`, drizzle-kit"},{"heading":"the-database-roles","content":"Owns the schema. Never used by a running process"},{"heading":"the-database-roles","content":"`auth_runtime`"},{"heading":"the-database-roles","content":"Better Auth adapter, support handoff records, MCP credential storage"},{"heading":"the-database-roles","content":"Trusted control plane for authentication records only"},{"heading":"the-database-roles","content":"`app_runtime`"},{"heading":"the-database-roles","content":"Tenant-facing application queries and outbox inserts"},{"heading":"the-database-roles","content":"Constrained by forced RLS through actor transactions"},{"heading":"the-database-roles","content":"`worker_runtime`"},{"heading":"the-database-roles","content":"Outbox worker"},{"heading":"the-database-roles","content":"Claims and completes jobs only"},{"heading":"the-database-roles","content":"`function_owner`"},{"heading":"the-database-roles","content":"Nobody logs in"},{"heading":"the-database-roles","content":"`NOLOGIN` with `BYPASSRLS`, owns the reviewed `SECURITY DEFINER` helpers in `0001_security.sql`"},{"heading":"the-database-roles","content":"Keep `auth_runtime` credentials out of browser code, general jobs and domain queries. Never weaken RLS or hand a runtime role `BYPASSRLS` to make a test pass. The integration suite (`pnpm test:integration`) covers both the allowed and denied side of these policies in `apps/database/src/rls.integration.test.ts` and `apps/web/src/server/services/workspaces.integration.test.ts`."}],"headings":[{"id":"sign-in-methods","content":"Sign-in methods"},{"id":"email-verification-and-the-local-inbox","content":"Email verification and the local inbox"},{"id":"account-pages","content":"Account pages"},{"id":"global-roles-and-the-admin-app","content":"Global roles and the admin app"},{"id":"becoming-an-admin","content":"Becoming an admin"},{"id":"impersonation","content":"Impersonation"},{"id":"workspace-roles","content":"Workspace roles"},{"id":"what-is-protected-and-how","content":"What is protected and how"},{"id":"the-database-roles","content":"The database roles"}]},"url":"/docs/v5/starters/tanstack-neon/authentication","title":"Authentication","description":"How Better Auth, global and workspace roles, the separate admin app and PostgreSQL row-level security work in the TanStack + Neon starter."},{"_id":"/docs/v5/starters/tanstack-neon/deployment","structured":{"contents":[{"content":"The live demo runs two Vercel projects, one rooted at `apps/web` and one at `apps/admin`, against a single Neon database. This page covers what is specific to this kit; the shared Vercel deployment guide has the platform-generic steps."},{"content":"`apps/web/vercel.json`, `apps/web/index.mjs`, `apps/admin/vercel.json` and `apps/admin/index.mjs` are the only deployment files. There is no Dockerfile or production Compose file. The README's \"Deployment\" section describes a generic three-process layout (migrate, worker, web) for hosts that run long-lived Node processes; the commands in it are the package scripts listed at the end of this page."},{"heading":"1-create-the-neon-database-and-roles","content":"Create a Neon project and a database named `nextbase` (the local scripts assume that name). Then create the same roles the local `apps/database/docker/postgres-init.sql` creates, with your own passwords:"},{"heading":"1-create-the-neon-database-and-roles","content":"`migration_owner`: login, owns the database and the `public` schema. Used only for migrations and the admin scripts."},{"heading":"1-create-the-neon-database-and-roles","content":"`auth_runtime`, `app_runtime`, `worker_runtime`: login, `NOSUPERUSER`, `NOBYPASSRLS`, `NOINHERIT`. They must not inherit `neon_superuser`."},{"heading":"1-create-the-neon-database-and-roles","content":"`function_owner`: `NOLOGIN` and `BYPASSRLS`; it only owns the reviewed `SECURITY DEFINER` helpers. Grant it to `migration_owner` with `SET TRUE`."},{"heading":"1-create-the-neon-database-and-roles","content":"Grant `CONNECT` on the database and `USAGE` on `public` to the three runtime roles. Neon's SQL editor or `psql` as the project owner can run the same statements as the init file, with the passwords replaced."},{"heading":"1-create-the-neon-database-and-roles","content":"Neon gives you two endpoints. Use the direct endpoint for `DATABASE_DIRECT_URL` and the pooled endpoint for `DATABASE_URL`, `AUTH_DATABASE_URL` and `WORKER_DATABASE_URL`. Both use port `5432` on Neon (locally the equivalents are `35432` and `36432`). Append `?sslmode=verify-full` to all four."},{"heading":"1-create-the-neon-database-and-roles","content":"The demo uses a Singapore Neon project; pick the region closest to your Vercel region."},{"heading":"2-apply-migrations-and-check-the-roles","content":"From your machine, with the production direct connection:"},{"heading":"2-apply-migrations-and-check-the-roles","content":"`db:migrate` is forward-only and checksum-protected. `db:check` fails if a runtime role has `BYPASSRLS` or superuser or is a member of `neon_superuser` or `function_owner`, if a tenant table lost forced RLS, or if an `app` function is executable by `PUBLIC`. Run both before the first deploy and after every release that adds a migration. Migrations never run during web or worker startup. The MCP migration (`0003_user_admin_mcp.sql`) is part of the baseline set, so a fresh database gets it on the first run."},{"heading":"2-apply-migrations-and-check-the-roles","content":"Don't run `pnpm db:seed` against production unless you want the demo workspace. To make yourself an admin, run `pnpm admin:grant -- you@example.com` with the same `DATABASE_DIRECT_URL` after you have signed up on the live site."},{"heading":"3-production-environment-variables","content":"Set every value from `.env.example` in each Vercel project's environment settings. The names are listed on environment variables. The ones that differ between the two projects or most often go wrong:"},{"heading":"3-production-environment-variables","content":"Variable"},{"heading":"3-production-environment-variables","content":"Web project"},{"heading":"3-production-environment-variables","content":"Admin project"},{"heading":"3-production-environment-variables","content":"`APP_BASE_URL`"},{"heading":"3-production-environment-variables","content":"The product origin, for example `https://app.example.com`. Better Auth base URL and User MCP issuer"},{"heading":"3-production-environment-variables","content":"The admin origin, for example `https://admin.example.com`. Admin MCP issuer"},{"heading":"3-production-environment-variables","content":"`TRUSTED_ORIGINS`"},{"heading":"3-production-environment-variables","content":"The product origin, plus preview origins if you use them"},{"heading":"3-production-environment-variables","content":"The admin origin"},{"heading":"3-production-environment-variables","content":"`WEB_APP_URL`"},{"heading":"3-production-environment-variables","content":"The product origin"},{"heading":"3-production-environment-variables","content":"The product origin (where impersonation handoffs land)"},{"heading":"3-production-environment-variables","content":"`ADMIN_APP_URL`"},{"heading":"3-production-environment-variables","content":"The admin origin (where support sessions return)"},{"heading":"3-production-environment-variables","content":"The admin origin"},{"heading":"3-production-environment-variables","content":"`BETTER_AUTH_SECRET`"},{"heading":"3-production-environment-variables","content":"At least 32 random characters. Changing it signs everyone out"},{"heading":"3-production-environment-variables","content":"The same value, or its own; cookies are host-only either way"},{"heading":"3-production-environment-variables","content":"`DATABASE_DIRECT_URL`, `DATABASE_URL`, `AUTH_DATABASE_URL`, `WORKER_DATABASE_URL`"},{"heading":"3-production-environment-variables","content":"The four Neon connection strings from step 1"},{"heading":"3-production-environment-variables","content":"The same four"},{"heading":"3-production-environment-variables","content":"`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `EMAIL_FROM`"},{"heading":"3-production-environment-variables","content":"Your provider. For Resend use `smtp.resend.com`, port `587`, and your API key as the password. `EMAIL_FROM` must be a verified sender"},{"heading":"3-production-environment-variables","content":"The same; the admin app also enqueues auth emails"},{"heading":"3-production-environment-variables","content":"`CRON_SECRET`"},{"heading":"3-production-environment-variables","content":"At least 32 characters. Vercel sends it to `/internal/jobs/run`"},{"heading":"3-production-environment-variables","content":"Not used; the admin app has no cron route"},{"heading":"3-production-environment-variables","content":"`NODE_ENV`"},{"heading":"3-production-environment-variables","content":"`production`"},{"heading":"3-production-environment-variables","content":"`production`"},{"heading":"3-production-environment-variables","content":"Configure OAuth provider callbacks under the web `APP_BASE_URL`. `/api/v1/webhooks/stripe` exists but answers `501` until you implement billing, so there is no webhook to register yet."},{"heading":"4-deploy-the-web-app-on-vercel","content":"Create a Vercel project from your repository with **Root Directory** set to `apps/web`. `apps/web/vercel.json` does the rest:"},{"heading":"4-deploy-the-web-app-on-vercel","content":"`framework: express`, with `index.mjs` as the function entry. It imports the bundled `build/runtime/server.js` and mounts it on an Express app."},{"heading":"4-deploy-the-web-app-on-vercel","content":"`buildCommand: pnpm build && cp -R build/client/. public/`. `pnpm build` runs `react-router build`, bundles `server.ts` and `worker.ts` with esbuild, and then runs `scripts/check-built-auth.mjs`, which boots the bundle with `VERCEL=1` and asserts that `/app` redirects anonymous visitors to `/login`."},{"heading":"4-deploy-the-web-app-on-vercel","content":"The function includes `build/**` and has `maxDuration: 300`."},{"heading":"4-deploy-the-web-app-on-vercel","content":"A cron calls `/internal/jobs/run` every 15 minutes with `Authorization: Bearer {CRON_SECRET}`."},{"heading":"4-deploy-the-web-app-on-vercel","content":"On Vercel there is no long-running worker. When `process.env.VERCEL` is set, the server dispatches a bounded outbox batch for each new email with `waitUntil`, and the cron retries whatever is still pending. This lets Neon suspend after its idle timeout. The `/mcp/*` and `/oauth/*` routes run inside the same function, so the User MCP endpoint is `{APP_BASE_URL}/mcp/user`."},{"heading":"4-deploy-the-web-app-on-vercel","content":"Deploy a preview first with its own `APP_BASE_URL` and `TRUSTED_ORIGINS`, then promote to production."},{"heading":"5-deploy-the-admin-app-on-vercel","content":"Create a second Vercel project from the same repository with **Root Directory** set to `apps/admin`. `apps/admin/vercel.json` is the same shape as the web one (`framework: express`, `index.mjs`, `build/**`, `maxDuration: 300`) without the cron. Its build runs `react-router build` and bundles `server.ts`; the admin server imports the web app's server modules by relative path, so the whole repository must be available to the build, which is the default for a monorepo root directory on Vercel."},{"heading":"5-deploy-the-admin-app-on-vercel","content":"Give it the admin values from the table above. The demo runs at `https://tanstack-neon-admin.usenextbase.com`. After deploy, `/` redirects to `/app-admin`, which sends anonymous visitors to `/login`. The Admin MCP endpoint is `{admin APP_BASE_URL}/mcp/admin`."},{"heading":"5-deploy-the-admin-app-on-vercel","content":"Deploy order for a release that touches both apps: migrations first, then the web app, then the admin app. The two apps share the database and the auth code, so keep them on the same commit."},{"heading":"6-smoke-checks-after-deploy","content":"`GET {APP_BASE_URL}/healthz` returns `{\"status\":\"ok\"}` on both origins; `GET /readyz` returns `{\"status\":\"ready\"}` once both database pools answer."},{"heading":"6-smoke-checks-after-deploy","content":"Open `/sign-up` on the web origin, create an account, receive the verification mail from your real sender, and sign in. If nothing arrives, check the SMTP values and, on Vercel, the function logs for \"Immediate email dispatch failed\" and the cron logs for `/internal/jobs/run`."},{"heading":"6-smoke-checks-after-deploy","content":"Create a workspace. This exercises `withActorTransaction` and RLS through the pooled connection."},{"heading":"6-smoke-checks-after-deploy","content":"Run `pnpm admin:grant -- you@example.com` against production, sign in again on the web origin, then sign in on the admin origin and open `/app-admin/users`. Start an impersonation with a reason; you should land on the web origin with the banner, and **Return to admin** should bring you back."},{"heading":"6-smoke-checks-after-deploy","content":"Open `/mcp/connections/user` on the web origin and `/mcp/connections/admin` on the admin origin; the server address shown must be the matching origin. `GET /mcp/user` and `GET /mcp/admin` without a token should return `401` with a `WWW-Authenticate` challenge. Then follow connect an agent and run `projects_list`."},{"heading":"6-smoke-checks-after-deploy","content":"Watch dead outbox jobs (`status = 'dead'` in `app.outbox_jobs`), readiness, error rate and Neon connection usage during the first rollout. Rollback is a redeploy of the previous build; migrations are forward-only, so write additive migrations and remove old columns in a later release."},{"heading":"other-node-hosts","content":"If you run the apps as long-lived processes instead of Vercel functions, the README describes three commands from one build:"},{"heading":"other-node-hosts","content":"Without `VERCEL` set, the servers listen on `PORT` and drain connections for up to 30 seconds on `SIGTERM`, and the daemon worker polls the outbox instead of relying on a cron. Deploy migrations first, then a worker compatible with the new job types, then the web and admin servers. You supply the container or process manager; the repository doesn't include one."}],"headings":[{"id":"1-create-the-neon-database-and-roles","content":"1\\. Create the Neon database and roles"},{"id":"2-apply-migrations-and-check-the-roles","content":"2\\. Apply migrations and check the roles"},{"id":"3-production-environment-variables","content":"3\\. Production environment variables"},{"id":"4-deploy-the-web-app-on-vercel","content":"4\\. Deploy the web app on Vercel"},{"id":"5-deploy-the-admin-app-on-vercel","content":"5\\. Deploy the admin app on Vercel"},{"id":"6-smoke-checks-after-deploy","content":"6\\. Smoke checks after deploy"},{"id":"other-node-hosts","content":"Other Node hosts"}]},"url":"/docs/v5/starters/tanstack-neon/deployment","title":"Deployment","description":"Provision Neon roles, apply migrations, and deploy the TanStack + Neon starter's web and admin apps as two Vercel projects."},{"_id":"/docs/v5/starters/tanstack-neon/environment-variables","structured":{"contents":[{"content":"The kit ships two committed templates: `.env.example` at the repository root for the web app, the worker, the database scripts and Docker Compose, and `apps/admin/.env.example` for the admin app. Your `.env` files are ignored by git."},{"content":"Validation lives in `apps/web/src/server/env.ts` (zod), which both the web and the admin server import. Every variable there has a local default that matches `.env.example`, so a missing value falls back to the Docker setup rather than failing. In production set each one explicitly. The database package has its own one-field schema in `apps/database/src/env.ts` that reads only `DATABASE_DIRECT_URL`."},{"content":"All variables are server-only. The React Router app is server-rendered by Express, nothing uses a `VITE_` prefix, and no value is handed to the browser."},{"content":"Docker Compose reads `.env` for the port variables. The `dev`, `worker`, `migrate` and `seed` scripts run plain `tsx` and read `process.env` only. Export the variables you change in the shell that runs the scripts, or run them through an env loader of your choice. On Vercel, set them in the project's environment settings."},{"content":"Create the file only when it is missing:"},{"heading":"application","content":"Variable"},{"heading":"application","content":"Required"},{"heading":"application","content":"What it is for"},{"heading":"application","content":"Where to get the value"},{"heading":"application","content":"`NODE_ENV`"},{"heading":"application","content":"Yes"},{"heading":"application","content":"`development`, `test` or `production`. Controls the CSP, secure cookies, Vite middleware and static serving"},{"heading":"application","content":"`development` locally, `production` when deployed"},{"heading":"application","content":"`PORT`"},{"heading":"application","content":"Yes"},{"heading":"application","content":"Port the Express server listens on. Default `3000`"},{"heading":"application","content":"Your choice. Match `APP_BASE_URL`. The admin `dev` script sets `3001`"},{"heading":"application","content":"`APP_BASE_URL`"},{"heading":"application","content":"Yes"},{"heading":"application","content":"Canonical origin of the app that reads it. Used by Better Auth as `baseURL`, as the origin check for impersonation requests, and as the MCP issuer and resource origin"},{"heading":"application","content":"`http://localhost:3000` for web, `http://localhost:3001` for admin, your HTTPS domains in production"},{"heading":"application","content":"`TRUSTED_ORIGINS`"},{"heading":"application","content":"Yes"},{"heading":"application","content":"Comma-separated browser origins allowed to make unsafe `/api/v1` requests and Better Auth calls"},{"heading":"application","content":"Usually the same as `APP_BASE_URL`. Add preview origins if you use them"},{"heading":"application","content":"`BETTER_AUTH_SECRET`"},{"heading":"application","content":"Yes"},{"heading":"application","content":"Signs sessions and cookies. Must be at least 32 characters"},{"heading":"application","content":"Generate at least 32 random bytes. Never reuse the example value"},{"heading":"application","content":"`WEB_APP_URL`"},{"heading":"application","content":"Yes"},{"heading":"application","content":"The product origin. The admin app builds the impersonation handoff link (`/auth/impersonation`) on it"},{"heading":"application","content":"`http://localhost:3000` locally, the web deployment's origin in production"},{"heading":"application","content":"`ADMIN_APP_URL`"},{"heading":"application","content":"Yes"},{"heading":"application","content":"The admin origin. The web app redirects to `{ADMIN_APP_URL}/app-admin/users` when a support session ends"},{"heading":"application","content":"`http://localhost:3001` locally, the admin deployment's origin in production"},{"heading":"database","content":"The kit uses four PostgreSQL credentials. Keep them separate; the whole RLS model depends on it. See authentication for what each role may do."},{"heading":"database","content":"Variable"},{"heading":"database","content":"Required"},{"heading":"database","content":"What it is for"},{"heading":"database","content":"Where to get the value"},{"heading":"database","content":"`DATABASE_DIRECT_URL`"},{"heading":"database","content":"Yes"},{"heading":"database","content":"Migrations, `db:seed`, `db:check`, `admin:grant` and drizzle-kit. Connects as `migration_owner` on the direct (non-pooled) endpoint"},{"heading":"database","content":"Locally `postgresql://migration_owner:...@localhost:35432/nextbase`. In Neon, the direct endpoint with the `migration_owner` password"},{"heading":"database","content":"`DATABASE_URL`"},{"heading":"database","content":"Yes"},{"heading":"database","content":"Tenant-aware application queries and outbox inserts as `app_runtime` through PgBouncer transaction pooling"},{"heading":"database","content":"Locally port `36432`. In Neon, the pooled endpoint with the `app_runtime` password"},{"heading":"database","content":"`AUTH_DATABASE_URL`"},{"heading":"database","content":"Yes"},{"heading":"database","content":"Better Auth tables, support handoff records and MCP credential storage as `auth_runtime`"},{"heading":"database","content":"Pooled endpoint, `auth_runtime` password"},{"heading":"database","content":"`WORKER_DATABASE_URL`"},{"heading":"database","content":"Yes"},{"heading":"database","content":"Outbox job claims and completions as `worker_runtime`"},{"heading":"database","content":"Pooled endpoint, `worker_runtime` password"},{"heading":"database","content":"`POSTGRES_PORT`"},{"heading":"database","content":"Local only"},{"heading":"database","content":"Host port Compose publishes for PostgreSQL. Default `35432`"},{"heading":"database","content":"Change when the port is taken, then update `DATABASE_DIRECT_URL`"},{"heading":"database","content":"`PGBOUNCER_PORT`"},{"heading":"database","content":"Local only"},{"heading":"database","content":"Host port Compose publishes for PgBouncer. Default `36432`"},{"heading":"database","content":"Change when the port is taken, then update the three runtime URLs"},{"heading":"database","content":"Production connection strings must end with `?sslmode=verify-full`. Both apps use the same four URLs."},{"heading":"email","content":"Variable"},{"heading":"email","content":"Required"},{"heading":"email","content":"What it is for"},{"heading":"email","content":"Where to get the value"},{"heading":"email","content":"`SMTP_HOST`"},{"heading":"email","content":"Yes"},{"heading":"email","content":"SMTP server used by the outbox worker (nodemailer). When it is `smtp.resend.com`, the kit adds a `Resend-Idempotency-Key` header per delivery"},{"heading":"email","content":"`localhost` for Mailpit. Your provider's SMTP host in production"},{"heading":"email","content":"`SMTP_PORT`"},{"heading":"email","content":"Yes"},{"heading":"email","content":"SMTP port. Default `31025` for Mailpit"},{"heading":"email","content":"Provider docs. `587` is the usual production value"},{"heading":"email","content":"`SMTP_USER`"},{"heading":"email","content":"Production"},{"heading":"email","content":"SMTP username. Authentication is enabled only when both user and password are set"},{"heading":"email","content":"Your provider. Leave empty for Mailpit"},{"heading":"email","content":"`SMTP_PASSWORD`"},{"heading":"email","content":"Production"},{"heading":"email","content":"SMTP password or API key"},{"heading":"email","content":"Your provider. Leave empty for Mailpit"},{"heading":"email","content":"`SMTP_SECURE`"},{"heading":"email","content":"Optional"},{"heading":"email","content":"`true` forces SMTPS. When omitted, the kit infers it from the port (465 is secure)"},{"heading":"email","content":"Only set it if your provider needs an override"},{"heading":"email","content":"`MAILPIT_UI_PORT`"},{"heading":"email","content":"Local only"},{"heading":"email","content":"Host port for the Mailpit web inbox. Default `38025`"},{"heading":"email","content":"Change when the port is taken"},{"heading":"email","content":"`EMAIL_FROM`"},{"heading":"email","content":"Yes"},{"heading":"email","content":"Sender shown on every email"},{"heading":"email","content":"A verified sender on your domain, for example `Nextbase <no-reply@example.com>`"},{"heading":"email","content":"Delivery is SMTP only; there is no `RESEND_API_KEY` in this kit. To use Resend, put its SMTP host in `SMTP_HOST` and the API key in `SMTP_PASSWORD`. See emails with Resend for the provider side."},{"heading":"object-storage","content":"These variables configure the local MinIO container and are listed in `.env.example` for the storage features you add. Nothing in `apps/web` or `apps/admin` reads them yet; `media_assets` exists in the schema without an upload flow."},{"heading":"object-storage","content":"Variable"},{"heading":"object-storage","content":"Required"},{"heading":"object-storage","content":"What it is for"},{"heading":"object-storage","content":"Where to get the value"},{"heading":"object-storage","content":"`MINIO_API_PORT`"},{"heading":"object-storage","content":"Local only"},{"heading":"object-storage","content":"Host port for the MinIO API. Default `39000`"},{"heading":"object-storage","content":"Change together with `S3_ENDPOINT`"},{"heading":"object-storage","content":"`MINIO_CONSOLE_PORT`"},{"heading":"object-storage","content":"Local only"},{"heading":"object-storage","content":"Host port for the MinIO console. Default `39001`"},{"heading":"object-storage","content":"Change when the port is taken"},{"heading":"object-storage","content":"`S3_ENDPOINT`"},{"heading":"object-storage","content":"Not read yet"},{"heading":"object-storage","content":"S3-compatible endpoint. Default `http://localhost:39000`"},{"heading":"object-storage","content":"Your bucket's endpoint when you wire uploads"},{"heading":"object-storage","content":"`S3_REGION`"},{"heading":"object-storage","content":"Not read yet"},{"heading":"object-storage","content":"Bucket region. Default `us-east-1`"},{"heading":"object-storage","content":"Your provider"},{"heading":"object-storage","content":"`S3_BUCKET`"},{"heading":"object-storage","content":"Not read yet"},{"heading":"object-storage","content":"Bucket name. Default `nextbase`. Compose does not create the bucket; make it in the MinIO console"},{"heading":"object-storage","content":"Your production bucket"},{"heading":"object-storage","content":"`S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`"},{"heading":"object-storage","content":"Not read yet"},{"heading":"object-storage","content":"Credentials. The defaults match the MinIO root user in `docker-compose.yml`"},{"heading":"object-storage","content":"Least-privilege credentials in production"},{"heading":"social-login","content":"All six are optional. A provider is registered with Better Auth only when both of its values are set. The shipped login page has no provider buttons; use `authClient.signIn.social` from `apps/web/app/lib/auth-client.ts` when you add them."},{"heading":"social-login","content":"Variable"},{"heading":"social-login","content":"Required"},{"heading":"social-login","content":"What it is for"},{"heading":"social-login","content":"Where to get the value"},{"heading":"social-login","content":"`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`"},{"heading":"social-login","content":"Optional"},{"heading":"social-login","content":"Google sign-in"},{"heading":"social-login","content":"Google Cloud console OAuth client with your `APP_BASE_URL` callback"},{"heading":"social-login","content":"`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`"},{"heading":"social-login","content":"Optional"},{"heading":"social-login","content":"GitHub sign-in"},{"heading":"social-login","content":"GitHub OAuth app"},{"heading":"social-login","content":"`TWITTER_CLIENT_ID`, `TWITTER_CLIENT_SECRET`"},{"heading":"social-login","content":"Optional"},{"heading":"social-login","content":"X sign-in"},{"heading":"social-login","content":"X developer portal"},{"heading":"placeholders-for-later-features","content":"These four are in `.env.example` so the file has a stable shape, but nothing reads them in this kit. `/api/v1/webhooks/stripe` exists and answers `501 STRIPE_NOT_CONFIGURED` until you implement billing."},{"heading":"placeholders-for-later-features","content":"Variable"},{"heading":"placeholders-for-later-features","content":"Status"},{"heading":"placeholders-for-later-features","content":"`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`"},{"heading":"placeholders-for-later-features","content":"Not read. Billing tables exist, no Stripe code"},{"heading":"placeholders-for-later-features","content":"`SENTRY_DSN`"},{"heading":"placeholders-for-later-features","content":"Not read. No error monitoring client is installed"},{"heading":"placeholders-for-later-features","content":"`POSTHOG_KEY`"},{"heading":"placeholders-for-later-features","content":"Not read. No analytics client is installed"},{"heading":"admin-app-template","content":"`apps/admin/.env.example` contains only the five values that differ from the web app:"},{"heading":"admin-app-template","content":"Variable"},{"heading":"admin-app-template","content":"Value in the template"},{"heading":"admin-app-template","content":"Notes"},{"heading":"admin-app-template","content":"`APP_BASE_URL`"},{"heading":"admin-app-template","content":"`http://localhost:3001`"},{"heading":"admin-app-template","content":"The admin app's own origin"},{"heading":"admin-app-template","content":"`WEB_APP_URL`"},{"heading":"admin-app-template","content":"`http://localhost:3000`"},{"heading":"admin-app-template","content":"Where impersonation handoffs are sent"},{"heading":"admin-app-template","content":"`ADMIN_APP_URL`"},{"heading":"admin-app-template","content":"`http://localhost:3001`"},{"heading":"admin-app-template","content":"Same as `APP_BASE_URL` for the admin app"},{"heading":"admin-app-template","content":"`TRUSTED_ORIGINS`"},{"heading":"admin-app-template","content":"`http://localhost:3001`"},{"heading":"admin-app-template","content":"The admin origin only"},{"heading":"admin-app-template","content":"`PORT`"},{"heading":"admin-app-template","content":"`3001`"},{"heading":"admin-app-template","content":"Matches the `dev` script"},{"heading":"admin-app-template","content":"The template's comment says the rest: use the same `AUTH_DATABASE_URL`, `DATABASE_URL`, `WORKER_DATABASE_URL` and `BETTER_AUTH_SECRET` as the web app. Cookies are host-only, so the two origins never share a session even with a shared secret; `docs/admin-app.md` describes giving each deployment its own secret if you prefer."},{"heading":"platform-variables-not-in-the-example-files","content":"Variable"},{"heading":"platform-variables-not-in-the-example-files","content":"Where"},{"heading":"platform-variables-not-in-the-example-files","content":"What it is for"},{"heading":"platform-variables-not-in-the-example-files","content":"`CRON_SECRET`"},{"heading":"platform-variables-not-in-the-example-files","content":"Vercel project for `apps/web`"},{"heading":"platform-variables-not-in-the-example-files","content":"Bearer token Vercel Cron sends to `/internal/jobs/run`. The kit requires at least 32 characters and rejects the request otherwise"},{"heading":"platform-variables-not-in-the-example-files","content":"`VERCEL`"},{"heading":"platform-variables-not-in-the-example-files","content":"Set by Vercel"},{"heading":"platform-variables-not-in-the-example-files","content":"When present, both servers export the Express app instead of listening, and emails are dispatched with `waitUntil` plus the cron"},{"heading":"platform-variables-not-in-the-example-files","content":"`NEXTBASE_WORKER_MODE`"},{"heading":"platform-variables-not-in-the-example-files","content":"Set by the scripts"},{"heading":"platform-variables-not-in-the-example-files","content":"`daemon` makes `worker.ts` poll forever. The `worker` and `worker:start` scripts set it for you"},{"heading":"platform-variables-not-in-the-example-files","content":"`MCP_TEST_PG_URL`"},{"heading":"platform-variables-not-in-the-example-files","content":"Local shell"},{"heading":"platform-variables-not-in-the-example-files","content":"Loopback pooled connection for `apps/web/src/mcp/pg-store.integration.test.ts`. The test skips without it and refuses non-loopback hosts"}],"headings":[{"id":"application","content":"Application"},{"id":"database","content":"Database"},{"id":"email","content":"Email"},{"id":"object-storage","content":"Object storage"},{"id":"social-login","content":"Social login"},{"id":"placeholders-for-later-features","content":"Placeholders for later features"},{"id":"admin-app-template","content":"Admin app template"},{"id":"platform-variables-not-in-the-example-files","content":"Platform variables not in the example files"}]},"url":"/docs/v5/starters/tanstack-neon/environment-variables","title":"Environment variables","description":"Every variable in the TanStack + Neon starter's env files, what it does and where the value comes from."},{"_id":"/docs/v5/starters/tanstack-neon","structured":{"contents":[{"content":"This kit uses React Router framework mode, Express and TanStack Query, with Neon Postgres, Drizzle and Better Auth. You get the account, workspace and project foundation, a separately deployed admin application, and OAuth-protected MCP servers for users and application administrators."},{"content":"Open the live demo · Admin demo · Repository"},{"content":"The GitHub repository is called `tanstack-planetscale-starter` and the local Compose project is named `nextbase-planetscale`. Both names are historical. There is no TanStack Router and no PlanetScale code in the kit: the router is React Router and the production database is Neon Postgres. You don't need a PlanetScale account."},{"heading":"the-stack","content":"Layer"},{"heading":"the-stack","content":"What the kit uses"},{"heading":"the-stack","content":"Frontend"},{"heading":"the-stack","content":"React Router 8 framework mode with server-side rendering, React 19, TanStack Query, Tailwind CSS 4"},{"heading":"the-stack","content":"Server"},{"heading":"the-stack","content":"Express 5 hosting the React Router build, Better Auth, a `/api/v1` HTTP API and the MCP routes. One server per app: `apps/web` for the product, `apps/admin` for administration"},{"heading":"the-stack","content":"Database"},{"heading":"the-stack","content":"Neon Postgres in production; PostgreSQL 18 plus PgBouncer in Docker locally. Drizzle schema with reviewed SQL migrations, forced row-level security and least-privilege runtime roles"},{"heading":"the-stack","content":"Authentication"},{"heading":"the-stack","content":"Better Auth with database sessions, email and password, optional Google, GitHub and X providers, and the admin plugin with `user`, `support` and `admin` roles"},{"heading":"the-stack","content":"Email"},{"heading":"the-stack","content":"Nodemailer over SMTP from a transactional outbox (`app.outbox_jobs`). A worker process sends locally; Vercel uses `waitUntil` plus a cron. Mailpit catches local mail. React Email templates live in `apps/email`"},{"heading":"the-stack","content":"Agents"},{"heading":"the-stack","content":"User MCP on the web origin and Admin MCP on the admin origin, both OAuth with PKCE and read-only tools"},{"heading":"the-stack","content":"Hosting used by the demo"},{"heading":"the-stack","content":"Two Vercel projects (web and admin) and one Neon project"},{"heading":"what-you-get-out-of-the-box","content":"Accounts: sign-up with required email verification, sign-in, password reset. Social providers appear when their keys are set. The magic link plugin is configured on the server; the shipped login page is password only."},{"heading":"what-you-get-out-of-the-box","content":"Workspaces: creation from the app or through `POST /api/v1/workspaces`, membership roles `owner`, `admin`, `member` and `readonly`, and a dashboard that lists the workspaces you belong to."},{"heading":"what-you-get-out-of-the-box","content":"A seeded `Acme Inc.` workspace with a `Launch` project, plus the schema for projects, comments, invitations, notifications, media, feedback, roadmap, blog, billing and API keys. Only the account and workspace flows have UI in this kit; the other tables are ready for your features."},{"heading":"what-you-get-out-of-the-box","content":"A separate admin application (`apps/admin`, port 3001) with a users list and audited impersonation: an administrator enters a support reason, the kit creates a 15-minute impersonated session, and the web app shows a banner with a way back to admin."},{"heading":"what-you-get-out-of-the-box","content":"User MCP and Admin MCP with OAuth, so an agent can read account context and projects, or list users and workspaces. See agent connections."},{"heading":"what-you-get-out-of-the-box","content":"An outbox worker with leases, heartbeats, exponential backoff and dead-lettering after 10 attempts."},{"heading":"what-you-get-out-of-the-box","content":"Structured pino logs, request IDs, a CSP nonce per request, `/healthz` and `/readyz` probes on both apps."},{"heading":"what-you-get-out-of-the-box","content":"Agent instruction files for Claude Code, Codex, Cursor, Gemini CLI, Copilot and others, generated from `AGENTS.md` by `pnpm agents:sync`."},{"heading":"what-you-get-out-of-the-box","content":"Every protected operation is authorized in a domain service and constrained again by PostgreSQL RLS. The kit uses four login roles: `migration_owner`, `app_runtime`, `auth_runtime` and `worker_runtime`. None of the runtime roles can bypass RLS. A fifth role, `function_owner`, has no login and owns the reviewed `SECURITY DEFINER` helpers."},{"heading":"prerequisites","content":"Requirement"},{"heading":"prerequisites","content":"Version or account"},{"heading":"prerequisites","content":"Node.js"},{"heading":"prerequisites","content":"24 (`.nvmrc` says `24`, `engines.node` is `>=24`)"},{"heading":"prerequisites","content":"pnpm"},{"heading":"prerequisites","content":"`11.1.2`, pinned by the `packageManager` field. Run `corepack enable` and let it pick the version"},{"heading":"prerequisites","content":"Docker with Compose"},{"heading":"prerequisites","content":"Needed locally for PostgreSQL, PgBouncer, Mailpit and MinIO"},{"heading":"prerequisites","content":"Neon account"},{"heading":"prerequisites","content":"For the production database"},{"heading":"prerequisites","content":"Vercel account"},{"heading":"prerequisites","content":"For the web and admin deployments. The demo uses two Vercel projects"},{"heading":"prerequisites","content":"Email provider"},{"heading":"prerequisites","content":"Any SMTP provider for production mail. The demo uses Resend's SMTP endpoint"},{"heading":"prerequisites","content":"Optional"},{"heading":"prerequisites","content":"Google, GitHub and X developer accounts for social sign-in"},{"heading":"next-steps","content":"Installation: clone, start Docker, migrate, seed and run the web and admin servers."},{"heading":"next-steps","content":"Environment variables: every variable in `.env.example` and `apps/admin/.env.example`."},{"heading":"next-steps","content":"Authentication: Better Auth, roles, the admin app and the RLS boundary."},{"heading":"next-steps","content":"Agent connections: User MCP, Admin MCP and the tools they expose."},{"heading":"next-steps","content":"Project structure: the monorepo and where to make changes."},{"heading":"next-steps","content":"Deployment: Neon roles, migrations, two Vercel projects and smoke checks."},{"heading":"next-steps","content":"If you are new to Nextbase v5, read getting started first for the shared flow."}],"headings":[{"id":"the-stack","content":"The stack"},{"id":"what-you-get-out-of-the-box","content":"What you get out of the box"},{"id":"prerequisites","content":"Prerequisites"},{"id":"next-steps","content":"Next steps"}]},"url":"/docs/v5/starters/tanstack-neon","title":"TanStack + Neon","description":"What the TanStack + Neon starter ships, what you need to run it, and where to go next."},{"_id":"/docs/v5/starters/tanstack-neon/installation","structured":{"contents":[{"content":"Local development doesn't need a Neon account. Docker Compose provides PostgreSQL 18, PgBouncer, Mailpit and MinIO, and the apps connect to those with the defaults in `.env.example`. You need Node.js 24, pnpm 11.1.2 and Docker with Compose. The shared getting started page has the general flow; this page has the exact commands for this kit."},{"heading":"clone-the-repository","content":"The repository keeps its older PlanetScale name. The database is Neon Postgres in production and plain PostgreSQL in Docker locally."},{"heading":"enable-pnpm-through-corepack","content":"The `packageManager` field pins pnpm `11.1.2`. Corepack uses that version automatically. `.npmrc` sets `engine-strict=true`, so an older Node.js fails at install time."},{"heading":"create-the-env-file","content":"Docker Compose reads `.env` for the published ports. The Node processes read `process.env` and fall back to defaults in `apps/web/src/server/env.ts` that match `.env.example`, so a first run works without changing anything. Nothing in the repository loads `.env` into the Node processes for you: when you change an application value such as `BETTER_AUTH_SECRET`, export it in the shell that runs `pnpm dev` or use an env loader of your choice. See environment variables for what each value does."},{"heading":"create-the-env-file","content":"The admin app has its own small template at `apps/admin/.env.example`. Locally you can leave it alone; the defaults already point the admin at `http://localhost:3001` and the web app at `http://localhost:3000`."},{"heading":"install-start-the-services-and-prepare-the-database","content":"`pnpm setup` runs four steps in order: `pnpm install`, `pnpm infra:start`, `pnpm db:migrate` and `pnpm db:seed`."},{"heading":"install-start-the-services-and-prepare-the-database","content":"`infra:start` runs `docker compose up -d --wait` for `postgres`, `pgbouncer`, `mailpit` and `minio`."},{"heading":"install-start-the-services-and-prepare-the-database","content":"The Postgres container runs `apps/database/docker/postgres-init.sql` on first start, which creates the `migration_owner`, `auth_runtime`, `app_runtime`, `worker_runtime` and `function_owner` roles."},{"heading":"install-start-the-services-and-prepare-the-database","content":"`db:migrate` applies the SQL files in `apps/database/migrations/` (`0000_baseline` through `0003_user_admin_mcp`) through the direct connection (`DATABASE_DIRECT_URL`) and records a SHA-256 checksum for each one in `public.nextbase_migrations`. Editing an already applied migration is a hard failure."},{"heading":"install-start-the-services-and-prepare-the-database","content":"`db:seed` creates an `admin@nextbase.local` user with the `admin` role, the `acme` workspace (`Acme Inc.`) owned by that user and a `Launch` project."},{"heading":"install-start-the-services-and-prepare-the-database","content":"The seed inserts the user row directly, without a password or any other credential. It exists so the seeded workspace has an owner. Sign up with your own account below and grant it the admin role with `pnpm admin:grant`."},{"heading":"install-start-the-services-and-prepare-the-database","content":"The Compose file pins `postgres:18-alpine`, `edoburu/pgbouncer:v1.24.1-p0`, `axllent/mailpit:v1.27` and `minio/minio:RELEASE.2025-09-07T16-13-09Z`. If one of them is unavailable in your registry, `infra:start` waits and fails. See troubleshooting for how to update the tag or start a subset of services."},{"heading":"start-the-web-and-admin-servers","content":"`pnpm dev` runs `turbo run dev --parallel`, which starts every workspace package that has a `dev` script: the web server (`tsx watch server.ts` in `apps/web`, port 3000), the admin server (`PORT=3001 tsx watch server.ts` in `apps/admin`) and the React Email preview server from `apps/email`."},{"heading":"start-the-web-and-admin-servers","content":"`apps/admin` listens on `3001` and `apps/email`'s `dev` script also passes `--port 3001`. Whichever starts second fails to bind. If you need both at once, start them individually: `pnpm --filter @nextbase/web dev` and `pnpm --filter @nextbase/admin dev` in two terminals, and run the preview server separately with `pnpm --filter @nextbase/email exec email dev --dir src/templates --port 3002`."},{"heading":"start-the-web-and-admin-servers","content":"Open these addresses:"},{"heading":"start-the-web-and-admin-servers","content":"Service"},{"heading":"start-the-web-and-admin-servers","content":"Address"},{"heading":"start-the-web-and-admin-servers","content":"Web application"},{"heading":"start-the-web-and-admin-servers","content":"`http://localhost:3000`"},{"heading":"start-the-web-and-admin-servers","content":"Admin application"},{"heading":"start-the-web-and-admin-servers","content":"`http://localhost:3001` (redirects to `/app-admin`, which asks you to sign in)"},{"heading":"start-the-web-and-admin-servers","content":"Mailpit inbox"},{"heading":"start-the-web-and-admin-servers","content":"`http://localhost:38025`"},{"heading":"start-the-web-and-admin-servers","content":"MinIO console"},{"heading":"start-the-web-and-admin-servers","content":"`http://localhost:39001` (user `nextbase`, password from `S3_SECRET_ACCESS_KEY` in `.env.example`)"},{"heading":"start-the-outbox-worker","content":"Verification and password reset emails are written to `app.outbox_jobs` and sent by a separate process. Without it, no email reaches Mailpit. In another terminal:"},{"heading":"start-the-outbox-worker","content":"This runs `tsx watch worker.ts` with `NEXTBASE_WORKER_MODE=daemon` and polls the outbox as the `worker_runtime` role. Keep it running while you develop."},{"heading":"create-your-account-and-workspace","content":"Open `http://localhost:3000/sign-up`, enter a name, email and a password of at least 8 characters. Email verification is required, so open Mailpit at `http://localhost:38025`, open the verification mail and follow the link. The kit signs you in after verification and sends you to `/app`."},{"heading":"create-your-account-and-workspace","content":"Open **Workspaces** and create one. The form posts to `/api/v1/workspaces`, which creates the workspace, makes you its owner and records a `workspace.created` audit event inside an actor transaction. The overview page then lists it."},{"heading":"grant-yourself-application-admin","content":"This runs `apps/database/src/grant-admin.ts`, which sets `role = 'admin'` on your Better Auth user through the direct connection and fails if the email doesn't exist. Sign out and back in, then open `http://localhost:3001/login`, sign in with the same email and password, and you land on `/app-admin`. See authentication for what the role controls."},{"heading":"check-that-everything-works","content":"`pnpm verify` is what CI runs. The integration suites in `apps/database`, `apps/web` and `apps/admin` need the Docker services running and the migrations applied; they refuse to run against a database that isn't on localhost. `TESTING.md` in the repository has the test policy."},{"heading":"stop-and-reset","content":"`infra:reset` deletes the local PostgreSQL and MinIO volumes. It never touches a remote database."},{"heading":"ports","content":"All local ports are set in `.env.example` and read by `docker-compose.yml`. Change `POSTGRES_PORT`, `PGBOUNCER_PORT`, `SMTP_PORT`, `MAILPIT_UI_PORT`, `MINIO_API_PORT` or `MINIO_CONSOLE_PORT` in `.env` when another kit already uses the defaults, and export the matching connection URLs so the apps follow. The web server port is `PORT` (default `3000`); if you change it, change `APP_BASE_URL`, `TRUSTED_ORIGINS` and the admin app's `WEB_APP_URL` as well. The admin app's `dev` script hard-codes `PORT=3001`; edit `apps/admin/package.json` and `ADMIN_APP_URL` together if you move it."}],"headings":[{"id":"clone-the-repository","content":"Clone the repository"},{"id":"enable-pnpm-through-corepack","content":"Enable pnpm through corepack"},{"id":"create-the-env-file","content":"Create the env file"},{"id":"install-start-the-services-and-prepare-the-database","content":"Install, start the services and prepare the database"},{"id":"start-the-web-and-admin-servers","content":"Start the web and admin servers"},{"id":"start-the-outbox-worker","content":"Start the outbox worker"},{"id":"create-your-account-and-workspace","content":"Create your account and workspace"},{"id":"grant-yourself-application-admin","content":"Grant yourself application admin"},{"id":"check-that-everything-works","content":"Check that everything works"},{"id":"stop-and-reset","content":"Stop and reset"},{"id":"ports","content":"Ports"}]},"url":"/docs/v5/starters/tanstack-neon/installation","title":"Installation","description":"Run the TanStack + Neon starter locally with Docker, apply migrations and seed data, start the web and admin apps, and create your first account."},{"_id":"/docs/v5/starters/tanstack-neon/project-structure","structured":{"contents":[{"content":"The repository is a pnpm and Turborepo monorepo with four workspace apps and one shared package. Route loaders, actions and HTTP controllers all call the same authorization-aware services, and server rendering never fetches from its own HTTP API. The admin app compiles its own route graph and server but imports its server modules from `apps/web/src/server`, so there is one copy of the auth, database and authorization code."},{"heading":"tree","content":"<File name=\"server.ts\" />"},{"heading":"tree","content":"<File name=\"worker.ts\" />"},{"heading":"tree","content":"<File name=\"index.mjs\" />"},{"heading":"tree","content":"<File name=\"vercel.json\" />"},{"heading":"tree","content":"<File name=\"react-router.config.ts\" />"},{"heading":"tree","content":"<File name=\"vite.config.ts\" />"},{"heading":"tree","content":"<File name=\"routes.ts\" />"},{"heading":"tree","content":"<File name=\"root.tsx\" />"},{"heading":"tree","content":"<File name=\"entry.server.tsx\" />"},{"heading":"tree","content":"<File name=\"auth.ts\" />"},{"heading":"tree","content":"<File name=\"env.ts\" />"},{"heading":"tree","content":"<File name=\"db.ts\" />"},{"heading":"tree","content":"<File name=\"email.ts\" />"},{"heading":"tree","content":"<File name=\"api.ts\" />"},{"heading":"tree","content":"<File name=\"actor-transaction.ts\" />"},{"heading":"tree","content":"<File name=\"authorization.ts\" />"},{"heading":"tree","content":"<File name=\"principal.ts\" />"},{"heading":"tree","content":"<File name=\"session.ts\" />"},{"heading":"tree","content":"<File name=\"request-context.ts\" />"},{"heading":"tree","content":"<File name=\"support-handoff.ts\" />"},{"heading":"tree","content":"<File name=\"cron-auth.ts\" />"},{"heading":"tree","content":"<File name=\"logger.ts\" />"},{"heading":"tree","content":"<File name=\"server.ts\" />"},{"heading":"tree","content":"<File name=\"index.mjs\" />"},{"heading":"tree","content":"<File name=\"vercel.json\" />"},{"heading":"tree","content":"<File name=\".env.example\" />"},{"heading":"tree","content":"<File name=\"routes.ts\" />"},{"heading":"tree","content":"<File name=\"root.tsx\" />"},{"heading":"tree","content":"<File name=\"shared-auth-client.ts\" />"},{"heading":"tree","content":"<File name=\"drizzle.config.ts\" />"},{"heading":"tree","content":"<File name=\"schema.ts\" />"},{"heading":"tree","content":"<File name=\"migrate.ts\" />"},{"heading":"tree","content":"<File name=\"seed.ts\" />"},{"heading":"tree","content":"<File name=\"check.ts\" />"},{"heading":"tree","content":"<File name=\"grant-admin.ts\" />"},{"heading":"tree","content":"<File name=\"env.ts\" />"},{"heading":"tree","content":"<File name=\"index.ts\" />"},{"heading":"tree","content":"<File name=\".env.example\" />"},{"heading":"tree","content":"<File name=\"docker-compose.yml\" />"},{"heading":"tree","content":"<File name=\"package.json\" />"},{"heading":"tree","content":"<File name=\"pnpm-workspace.yaml\" />"},{"heading":"tree","content":"<File name=\"turbo.json\" />"},{"heading":"tree","content":"<File name=\"AGENTS.md\" />"},{"heading":"tree","content":"<File name=\"TESTING.md\" />"},{"heading":"tree","content":"<File name=\"CHANGELOG.md\" />"},{"heading":"tree","content":"Path"},{"heading":"tree","content":"Responsibility"},{"heading":"tree","content":"`apps/web` (`@nextbase/web`)"},{"heading":"tree","content":"React Router UI, Express server, Better Auth, `/api/v1`, domain services, the User MCP server and the outbox worker. Deploys as the product on port 3000"},{"heading":"tree","content":"`apps/admin` (`@nextbase/admin`)"},{"heading":"tree","content":"The administration app: its own route graph, Express server, users list, audited impersonation and the Admin MCP server. Deploys separately on port 3001"},{"heading":"tree","content":"`apps/database` (`@nextbase/database`)"},{"heading":"tree","content":"Drizzle schema, reviewed SQL migrations, RLS policies, role bootstrap, seed, `db:check` and the admin grant script"},{"heading":"tree","content":"`apps/email` (`@nextbase/email`)"},{"heading":"tree","content":"React Email templates (`VerificationEmail`, `TeamInvitationEmail`) with a preview server. `apps/web` doesn't depend on it yet; the auth emails are inline HTML"},{"heading":"tree","content":"`packages/typescript-config`"},{"heading":"tree","content":"Shared `base.json` and `react.json` TypeScript configs"},{"heading":"tree","content":"`docs`"},{"heading":"tree","content":"`mcp-connections.md` (OAuth and MCP protocol details) and `admin-app.md` (origins, impersonation and admin deployment)"},{"heading":"tree","content":"`scripts/agents`"},{"heading":"tree","content":"`sync.mjs` generates every AI tool config from `AGENTS.md`, `.agents/config.json` and `.agents/skills/`"},{"heading":"tree","content":"`.agents`"},{"heading":"tree","content":"Hand-edited source for the generated agent files: MCP servers for coding tools and the `new-page`, `db-migration` and `mcp-tool` skills"},{"heading":"routes-and-pages","content":"Routes are declared in `apps/web/app/routes.ts` with React Router's `route()`, `layout()` and `index()` helpers, and each file lives in `apps/web/app/routes/`. Public pages are `/`, `/login`, `/sign-up`, `/forgot-password`, `/reset-password` and `/auth/impersonation`. `routes/app-layout.tsx` wraps `/app`, `/app/workspaces`, `/app/projects` and `/app/settings` and calls `requireSession`. Add a route entry, create the file, then run `pnpm typecheck` to regenerate the `+types` route types. The `new-page` skill in `.agents/skills/` describes the same steps for an AI coding tool."},{"heading":"routes-and-pages","content":"Admin pages live in `apps/admin/app/routes.ts` and `apps/admin/app/routes/`: `/` redirects to `/app-admin`, `/login` is the admin sign-in, `admin-layout.tsx` wraps `/app-admin` and `/app-admin/users`, and the remaining sections share `admin-section.tsx` as a placeholder. Every protected loader and action calls `requireAppAdmin` itself; the layout check alone is not enough."},{"heading":"routes-and-pages","content":"Shared UI lives in `apps/web/app/components/` (`app-shell.tsx`, `ui.tsx`, `logo.tsx`, `theme-toggle.tsx`). The admin app has its own copies under `apps/admin/app/components/` and `apps/admin/app/shared/`, and re-exports the Better Auth client from web through `shared-auth-client.ts`. Client-only helpers (`auth-client.ts`, `auth-return.ts`) are in `apps/web/app/lib/`."},{"heading":"routes-and-pages","content":"Path aliases in web: `~/*` is `app/*`, `@server/*` is `src/server/*`, `@contracts/*` is `src/contracts/*`. In the admin app `@server/*` resolves to `../web/src/server/*`."},{"heading":"server-and-data-access","content":"Loaders and actions get `requestId` and `getPrincipal()` from `apps/web/src/server/request-context.ts` and call services in `apps/web/src/server/services/` (`workspaces.ts` ships; add yours next to it). The same services back the HTTP API in `apps/web/src/server/api.ts`, mounted at `/api/v1` with `GET /bootstrap`, `GET /workspaces` and `POST /workspaces`. Mutating API requests must be JSON and same-origin; failures are `application/problem+json` with a `requestId`. Input shapes are zod schemas in `apps/web/src/contracts/`."},{"heading":"server-and-data-access","content":"Put new business rules in a service, wrap protected work in `withActorTransaction` from `apps/web/src/server/actor-transaction.ts`, and write an `audit_events` row for security-relevant mutations. Don't query `app.*` tables from a route file or outside an actor transaction."},{"heading":"schema-and-migrations","content":"The Drizzle schema is `apps/database/src/schema.ts`: Better Auth tables in `public` (`user`, `session`, `account`, `verification`, `rate_limit`, `mcp_records`) and domain tables in the `app` schema. Migrations are plain SQL files in `apps/database/migrations/`, applied in filename order by `apps/database/src/migrate.ts`, which stores a SHA-256 checksum per file in `public.nextbase_migrations`. The flow is:"},{"heading":"schema-and-migrations","content":"Edit `schema.ts`."},{"heading":"schema-and-migrations","content":"`pnpm --filter @nextbase/database exec drizzle-kit generate --name <change>` writes `migrations/NNNN_<change>.sql` plus the `meta/` snapshot and journal."},{"heading":"schema-and-migrations","content":"Append grants, `enable` and `force row level security`, and policies to that SQL file, following `0001_security.sql` and `0003_user_admin_mcp.sql`. New `app.*` tables need RLS and column-scoped `grant update` to `app_runtime`."},{"heading":"schema-and-migrations","content":"`pnpm db:migrate && pnpm db:check`, update `src/seed.ts` if needed, add RLS integration tests. Add the table to the list in `src/check.ts` so `db:check` keeps verifying it."},{"heading":"schema-and-migrations","content":"Never edit an applied migration; add a new one. Local role bootstrap and PgBouncer config live in `apps/database/docker/` (`postgres-init.sql`, `pgbouncer.ini`, `userlist.txt`)."},{"heading":"authentication","content":"Everything Better Auth is in `apps/web/src/server/auth.ts`, with helpers next to it: `principal.ts`, `session.ts`, `request-context.ts`, `authorization.ts` (`requireSession`, `requireAppAdmin`) and `support-handoff.ts` for the impersonation handoff records. Environment validation is `apps/web/src/server/env.ts`. See authentication."},{"heading":"emails","content":"Templates are React components in `apps/email/src/templates/` (`verification.tsx`, `team-invitation.tsx`, `layout.tsx`), exported from `apps/email/src/index.ts`. Preview them with `pnpm --filter @nextbase/email dev` (port 3001, which clashes with the admin app when both run). Sending goes through `enqueueEmail` in `apps/web/src/server/email.ts`, which writes an `email.send` outbox job; `sendEmailNow` is the nodemailer transport used by the worker. To use the templates from web, add `\"@nextbase/email\": \"workspace:*\"` to `apps/web/package.json` and render them before calling `enqueueEmail`."},{"heading":"background-jobs-and-the-worker","content":"`apps/web/worker.ts` claims rows from `app.outbox_jobs` with two-minute leases renewed by a heartbeat, retries with exponential backoff and marks jobs dead after 10 attempts. The only job type today is `email.send`. Add a new type by inserting a job with an idempotency key from a service and handling it in `executeJob`. `runOutboxBatch` is exported for the `/internal/jobs/run` cron and for request-time dispatch with `waitUntil` on Vercel."},{"heading":"mcp","content":"`apps/web/src/mcp/` holds `core.ts` (protocol and tools), `adapter.ts` (this kit's session, workspace and project access), `pg-store.ts` (storage) and `express.ts` (the Express bridge). `apps/web/server.ts` mounts the user audience and `apps/admin/server.ts` mounts the admin audience. See agent connections for how to add a tool."},{"heading":"tests","content":"Kind"},{"heading":"tests","content":"Where"},{"heading":"tests","content":"Command"},{"heading":"tests","content":"Unit and component"},{"heading":"tests","content":"`*.test.ts` and `*.test.tsx` next to the code (web uses jsdom and Testing Library, setup in `apps/web/src/test/setup.ts`)"},{"heading":"tests","content":"`pnpm test`"},{"heading":"tests","content":"Database and RLS"},{"heading":"tests","content":"`apps/database/src/rls.integration.test.ts`, `apps/web/src/server/services/workspaces.integration.test.ts`, `apps/web/src/mcp/pg-store.integration.test.ts`"},{"heading":"tests","content":"`pnpm test:integration` (needs `pnpm infra:start` and `pnpm db:migrate`)"},{"heading":"tests","content":"Admin impersonation"},{"heading":"tests","content":"`apps/admin/app/routes/admin-users.integration.test.ts`"},{"heading":"tests","content":"`pnpm --filter @nextbase/admin test:integration`"},{"heading":"tests","content":"Everything before a push"},{"heading":"tests","content":"`pnpm verify` (format check, lint, typecheck, unit tests, build)"},{"heading":"tests","content":"Single file: `pnpm --filter @nextbase/web exec vitest run <path>`. `TESTING.md` asks for a meaningful assertion for new behaviour and both the allowed and denied side of every authorization branch. CI (`.github/workflows/test.yml`) starts `postgres` and `pgbouncer`, runs migrations, seed and `db:check`, then typecheck, unit tests, integration tests and the build on every pull request, and `agent-config.yml` fails when the generated agent files are stale (`pnpm agents:check`)."}],"headings":[{"id":"tree","content":"Tree"},{"id":"where-to-make-changes","content":"Where to make changes"},{"id":"routes-and-pages","content":"Routes and pages"},{"id":"server-and-data-access","content":"Server and data access"},{"id":"schema-and-migrations","content":"Schema and migrations"},{"id":"authentication","content":"Authentication"},{"id":"emails","content":"Emails"},{"id":"background-jobs-and-the-worker","content":"Background jobs and the worker"},{"id":"mcp","content":"MCP"},{"id":"tests","content":"Tests"}]},"url":"/docs/v5/starters/tanstack-neon/project-structure","title":"Project structure","description":"The TanStack + Neon monorepo layout and where to change routes, services, schema, auth, the admin app, emails, jobs, MCP and tests."},{"_id":"/docs/v5/starters/tanstack-supabase/agent-connections","structured":{"contents":[{"content":"The kit serves two remote MCP servers, one per deployable app. The web app's Express process serves the User MCP next to the website; the admin app's Express process serves the Admin MCP next to the administration UI. Users connect agents to one workspace through the User MCP; application administrators connect through the Admin MCP. Both are read-only in v5. For the shared concepts and the client-side flow, see user and admin MCPs and connect an agent."},{"heading":"where-users-manage-connections","content":"Connection"},{"heading":"where-users-manage-connections","content":"App and settings page"},{"heading":"where-users-manage-connections","content":"Live demo server"},{"heading":"where-users-manage-connections","content":"User"},{"heading":"where-users-manage-connections","content":"Web app, `/mcp/connections/user`"},{"heading":"where-users-manage-connections","content":"`https://tanstack-supabase.usenextbase.com/mcp/user`"},{"heading":"where-users-manage-connections","content":"Admin"},{"heading":"where-users-manage-connections","content":"Admin app, `/mcp/connections/admin`"},{"heading":"where-users-manage-connections","content":"`https://tanstack-supabase-admin.usenextbase.com/mcp/admin`"},{"heading":"where-users-manage-connections","content":"The repository's `docs/admin-app.md` maps each kit's admin deployment to `<subdomain>-admin.usenextbase.com`, which gives the admin address above for the demo. The product navigation has an **Agent connections** entry and the account settings page links to it. Administrators see **Admin agent connections** in the admin app's navigation. Each page shows the server address to copy, the list of connected agents with their workspace and scope, and a **Disconnect** button per connection. The UI limits active connections to 20 per account."},{"heading":"where-users-manage-connections","content":"Your own deployments use each app's `APP_URL` as the origin, so the servers are `${APP_URL of web}/mcp/user` and `${APP_URL of admin}/mcp/admin`. The settings pages print the exact address. Each app advertises and authorizes only its own audience: `apps/web/src/mcp/adapter.ts` sets `audiences: [\"user\"]` and `apps/admin/src/mcp/adapter.ts` sets `audiences: [\"admin\"]`, so a request for `/mcp/admin` on the web origin (or `/mcp/user` on the admin origin) is rejected with `invalid_target`."},{"heading":"tools-and-permissions","content":"Server"},{"heading":"tools-and-permissions","content":"Tool"},{"heading":"tools-and-permissions","content":"Scope"},{"heading":"tools-and-permissions","content":"Input"},{"heading":"tools-and-permissions","content":"What it reads"},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`account_context`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"none"},{"heading":"tools-and-permissions","content":"The signed-in user's id and email plus the approved workspace."},{"heading":"tools-and-permissions","content":"User"},{"heading":"tools-and-permissions","content":"`projects_list`"},{"heading":"tools-and-permissions","content":"`user:read`"},{"heading":"tools-and-permissions","content":"`limit` (1 to 50, default 20)"},{"heading":"tools-and-permissions","content":"Projects in the approved workspace only: id, name, slug, status, timestamps."},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_users_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"`limit` (1 to 50, default 20)"},{"heading":"tools-and-permissions","content":"Newest `user_profiles` rows: id, full name, created date."},{"heading":"tools-and-permissions","content":"Admin"},{"heading":"tools-and-permissions","content":"`admin_workspaces_list`"},{"heading":"tools-and-permissions","content":"`admin:read`"},{"heading":"tools-and-permissions","content":"`limit` (1 to 50, default 20)"},{"heading":"tools-and-permissions","content":"Newest workspaces: id, name, slug, created date."},{"heading":"tools-and-permissions","content":"Every tool is registered with `readOnlyHint: true` and `destructiveHint: false`. There are no write tools, and the scopes do not grant writes. A user token never lists or calls admin tools, and an admin token is rejected at `/mcp/user`."},{"heading":"tools-and-permissions","content":"Each tool call re-authenticates the bearer token and reloads the live actor through `public.mcp_live_actor`, which requires an existing `auth.sessions` row, a confirmed email, no active ban and a session that has not expired. `projects_list` runs `public.mcp_projects`, which checks membership and the `view_projects` permission in the same SQL statement as the read. Admin tools additionally require a current `admin` row in `public.user_roles`."},{"heading":"consent-and-oauth-behaviour","content":"The agent needs to support Streamable HTTP, OAuth with public dynamic client registration and PKCE S256. The steps below apply to each app on its own origin."},{"heading":"consent-and-oauth-behaviour","content":"The client discovers the server through `/.well-known/oauth-protected-resource/mcp/user` (web) or `/.well-known/oauth-protected-resource/mcp/admin` (admin). The authorization server issuer is `${APP_URL}/oauth` with metadata at `/.well-known/oauth-authorization-server/oauth`."},{"heading":"consent-and-oauth-behaviour","content":"The client registers at `/oauth/register` and starts authorization at `/oauth/authorize`, which redirects to the consent page `/mcp/authorize?request_id=...`."},{"heading":"consent-and-oauth-behaviour","content":"If you are not signed in, the app sends you to `/login?next=/mcp/authorize?...` and returns you to the same request afterwards."},{"heading":"consent-and-oauth-behaviour","content":"The consent page shows the client's self-reported name (marked unverified), the callback address, the server, the scope with \"Read access only\" and your email. For a user connection you select one workspace; only workspaces where you are owner, admin or have `view_projects` are offered. Approve with **Allow read access** or **Cancel**."},{"heading":"consent-and-oauth-behaviour","content":"The client exchanges the code at `/oauth/token`. Revocation is `/oauth/revoke`."},{"heading":"consent-and-oauth-behaviour","content":"Lifetimes: pending consent 10 minutes, authorization codes 5 minutes, access tokens 15 minutes, refresh for at most 30 days and never beyond the original sign-in session. Refresh tokens rotate; reuse revokes the whole connection family. Secrets are stored only as SHA-256 hashes. Callback URLs must be HTTPS, except HTTP loopback addresses. Browser clients are accepted from the app's `APP_URL`, `https://claude.ai` and `https://chatgpt.com`; desktop and server clients without an `Origin` header are accepted too. Confidential clients and client ID metadata documents are not supported."},{"heading":"consent-and-oauth-behaviour","content":"Access ends when you disconnect, sign out of the session that approved the connection, lose workspace access or lose the administrator role. If an agent starts getting `401`, sign in again and reconnect."},{"heading":"where-the-code-lives","content":"The MCP module is the same code in both apps; only `adapter.ts` differs (the `audiences` setting). Paths below are for `apps/web`; `apps/admin/src/mcp/` has the same files minus the tests."},{"heading":"where-the-code-lives","content":"Path"},{"heading":"where-the-code-lives","content":"Purpose"},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.ts`"},{"heading":"where-the-code-lives","content":"OAuth server, consent and connection pages, the `Adapter` interface and the per-audience tool registry (`server.registerTool`)."},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/supabase-adapter.ts`"},{"heading":"where-the-code-lives","content":"Supabase implementation of `Adapter` using the service-role client and the `mcp_live_actor` and `mcp_projects` functions."},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/supabase-store.ts`"},{"heading":"where-the-code-lives","content":"Credential storage in `public.mcp_records` with compare-and-set updates."},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/adapter.ts`"},{"heading":"where-the-code-lives","content":"Wires env and request services into `handleMcp` and restricts the audience (`[\"user\"]` here, `[\"admin\"]` in the admin app)."},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/express.ts`"},{"heading":"where-the-code-lives","content":"Mounts the handler in `server/index.ts` for `/mcp/*`, `/oauth/*` and `/.well-known/oauth-*` before React Router."},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/core.test.ts`"},{"heading":"where-the-code-lives","content":"Protocol, audience, CSRF, PKCE, replay, expiry, membership and revocation tests with an in-memory store."},{"heading":"where-the-code-lives","content":"`apps/web/src/mcp/supabase-store.integration.test.ts`"},{"heading":"where-the-code-lives","content":"Storage test against a local Supabase project; skipped without the `MCP_TEST_SUPABASE_*` variables."},{"heading":"where-the-code-lives","content":"`apps/database/supabase/migrations/20261014114357_user_admin_mcp.sql`"},{"heading":"where-the-code-lives","content":"Creates `mcp_records` (service-role only, RLS enabled, browser grants revoked) and the two service-only functions."},{"heading":"where-the-code-lives","content":"`docs/mcp-connections.md`"},{"heading":"where-the-code-lives","content":"The repository's own contract for both servers. `docs/admin-app.md` records that the admin MCP moved to the admin origin."},{"heading":"where-the-code-lives","content":"`.agents/skills/mcp-tool/SKILL.md`"},{"heading":"where-the-code-lives","content":"Step-by-step skill for adding a tool, used by the AI coding tools configured in the repo."},{"heading":"where-the-code-lives","content":"Do not add React Router routes under `/mcp/`, `/oauth/` or `/.well-known/oauth-*` in either app; Express handles them first."},{"heading":"adding-a-tool-safely","content":"Follow the `mcp-tool` skill in the repository. In short:"},{"heading":"adding-a-tool-safely","content":"Extend the `Adapter` interface in `core.ts` and register the tool inside the matching audience branch (`audience === \"user\"` or the `else` branch). Reuse the shared `annotations` and `limitSchema`; validate any other input with Zod and bound strings with `.max(...)`."},{"heading":"adding-a-tool-safely","content":"Inside the handler always call `await currentActor()` and, for user tools, pass `auth.grant.workspace`. Never accept a workspace id from tool input and never reuse an actor captured earlier."},{"heading":"adding-a-tool-safely","content":"Implement the data access in `supabase-adapter.ts`. User tools should call a new service-only SQL function modelled on `public.mcp_projects` that checks `mcp_live_actor` and membership in the same statement. Admin tools re-check `(await liveActor(actor))?.admin` before a bounded select of explicit columns. Create the function in a new migration; never edit the shipped MCP migration. Then run `pnpm gen-types-local`."},{"heading":"adding-a-tool-safely","content":"Mirror the change into the other app's `src/mcp/` so the two copies stay identical (user tools only matter in the web app and admin tools only in the admin app, but the shared `core.ts` should not diverge)."},{"heading":"adding-a-tool-safely","content":"Add tests in `apps/web/src/mcp/core.test.ts`: update the exact tool-name lists, add a call test, a bounds test and a cross-audience test. Add a pgTAP test asserting `anon` and `authenticated` cannot execute the new function."},{"heading":"adding-a-tool-safely","content":"Update the tool table in `docs/mcp-connections.md`."},{"heading":"adding-a-tool-safely","content":"Adding a write tool needs a new scope and new consent copy first. The consent screen currently says \"Read access only\", and the repository's `AGENTS.md` lists adding write capability without that design as something to never do."}],"headings":[{"id":"where-users-manage-connections","content":"Where users manage connections"},{"id":"tools-and-permissions","content":"Tools and permissions"},{"id":"consent-and-oauth-behaviour","content":"Consent and OAuth behaviour"},{"id":"where-the-code-lives","content":"Where the code lives"},{"id":"adding-a-tool-safely","content":"Adding a tool safely"}]},"url":"/docs/v5/starters/tanstack-supabase/agent-connections","title":"Agent connections","description":"The User MCP and Admin MCP in the TanStack + Supabase starter: endpoints, tools, OAuth behaviour and how to extend them."},{"_id":"/docs/v5/starters/tanstack-supabase/authentication","structured":{"contents":[{"content":"This kit uses Supabase Auth for users, both apps and agent connections alike. The shared authentication page explains the concepts; this page shows where they live in this repository. Paths are given for `apps/web`; the admin app mirrors the same `src/.server/` modules."},{"heading":"how-a-request-is-authenticated","content":"The root middleware in `apps/web/src/root.tsx` creates one `RequestServices` object per request (`apps/web/src/.server/request-context.ts`) and places it in `servicesContext`. Loaders and actions call `context.get(servicesContext)` and get:"},{"heading":"how-a-request-is-authenticated","content":"`supabase()`: a request-scoped `@supabase/ssr` server client that reads the session from cookies. Row-level security applies to every query."},{"heading":"how-a-request-is-authenticated","content":"`serviceSupabase()`: a service-role client that bypasses RLS. Call it only after authenticating and authorizing the caller in the same request."},{"heading":"how-a-request-is-authenticated","content":"`user()`: the current Supabase user, or `null`."},{"heading":"how-a-request-is-authenticated","content":"Outgoing auth cookies accumulate on the response, so session refreshes reach the browser. Sessions are Supabase cookie sessions scoped to each app's own host; the local `config.toml` sets `jwt_expiry = 3600`."},{"heading":"how-a-request-is-authenticated","content":"Guards live in `src/.server/auth.ts` of each app:"},{"heading":"how-a-request-is-authenticated","content":"`requireUser(context)` redirects to `/login?next=<current path>` when there is no session."},{"heading":"how-a-request-is-authenticated","content":"`requireAdmin(context)` calls `requireUser` and then returns `403` unless `user.app_metadata.user_role === \"admin\"`."},{"heading":"how-a-request-is-authenticated","content":"In the web app, the `/app/*` routes nest under `routes/app-layout.tsx`, which requires a user, current terms acceptance and completed onboarding. In the admin app, the `/admin/*` routes nest under `routes/admin-layout.tsx`, which requires an admin."},{"heading":"sign-up-login-and-recovery","content":"Web app (`apps/web/src/routes.ts`):"},{"heading":"sign-up-login-and-recovery","content":"Flow"},{"heading":"sign-up-login-and-recovery","content":"Route"},{"heading":"sign-up-login-and-recovery","content":"What happens"},{"heading":"sign-up-login-and-recovery","content":"Sign up"},{"heading":"sign-up-login-and-recovery","content":"`/sign-up`"},{"heading":"sign-up-login-and-recovery","content":"`auth.signUp` with `emailRedirectTo` set to `${APP_URL}/auth/callback?next=...`. Shows \"Check your inbox to confirm your email.\""},{"heading":"sign-up-login-and-recovery","content":"Password login"},{"heading":"sign-up-login-and-recovery","content":"`/login`"},{"heading":"sign-up-login-and-recovery","content":"`auth.signInWithPassword`, then redirects to `next` (default `/app`)."},{"heading":"sign-up-login-and-recovery","content":"Magic link"},{"heading":"sign-up-login-and-recovery","content":"`/login`, \"Or email me a sign-in link\""},{"heading":"sign-up-login-and-recovery","content":"`auth.signInWithOtp` with the same callback."},{"heading":"sign-up-login-and-recovery","content":"Forgot password"},{"heading":"sign-up-login-and-recovery","content":"`/forgot-password`"},{"heading":"sign-up-login-and-recovery","content":"`auth.resetPasswordForEmail` redirecting to `/auth/callback?next=/reset-password`."},{"heading":"sign-up-login-and-recovery","content":"Reset password"},{"heading":"sign-up-login-and-recovery","content":"`/reset-password`"},{"heading":"sign-up-login-and-recovery","content":"Sets the new password for the recovered session."},{"heading":"sign-up-login-and-recovery","content":"Code exchange"},{"heading":"sign-up-login-and-recovery","content":"`/auth/callback`"},{"heading":"sign-up-login-and-recovery","content":"`auth.exchangeCodeForSession(code)`, then redirects to `next`."},{"heading":"sign-up-login-and-recovery","content":"Token hash"},{"heading":"sign-up-login-and-recovery","content":"`/auth/confirm`"},{"heading":"sign-up-login-and-recovery","content":"`auth.verifyOtp({ token_hash, type })` for email or recovery links."},{"heading":"sign-up-login-and-recovery","content":"Logout"},{"heading":"sign-up-login-and-recovery","content":"`/logout`"},{"heading":"sign-up-login-and-recovery","content":"Ends the session."},{"heading":"sign-up-login-and-recovery","content":"The admin app (`apps/admin/src/routes.ts`) has the same `/login`, `/forgot-password`, `/reset-password`, `/auth/callback`, `/auth/confirm` and `/logout` routes on its own origin, with `next` defaulting to `/admin`. It has no `/sign-up`: administrators are regular accounts created on the web app (or from `/admin/users`) and granted the admin role as described below."},{"heading":"sign-up-login-and-recovery","content":"Every `next` value passes through `safeRedirectPath`, so only same-site paths are accepted. If `next` starts with `/mcp/`, the app uses a full-document redirect so an agent consent request survives the login."},{"heading":"email-confirmation-and-the-local-inbox","content":"Local Supabase Auth requires confirmation before sign-in (`enable_confirmations = true` in `apps/database/supabase/config.toml`). Confirmation, magic link and recovery emails are sent by Supabase Auth, not by the apps. Locally, `[local_smtp]` is enabled and mail lands in Mailpit; open the `MAILPIT_URL` printed by `pnpm --filter database exec supabase status --output-format json`. In production, configure SMTP in the Supabase project's Auth settings as described in emails with Resend."},{"heading":"email-confirmation-and-the-local-inbox","content":"The seeded accounts (`testadmin@usenextbase.com`, `alice@example.com`, `bob@example.com`, `carol@example.com`, password `password123`) are pre-confirmed so you can skip the inbox during development."},{"heading":"redirect-urls","content":"`config.toml` allows these redirect targets for local development:"},{"heading":"redirect-urls","content":"`/auth/callback` and `/auth/confirm` are the only auth routes registered in `apps/web/src/routes.ts` and `apps/admin/src/routes.ts`, and they are the only entries in the allowlist. In production, add the same two paths under both the web and the admin origin in the Supabase dashboard (Auth, URL Configuration) and keep Site URL on the web app's `APP_URL`."},{"heading":"social-providers","content":"`config.toml` enables Google, GitHub and Twitter for the local Auth service, reading their credentials from `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `TWITTER_API_KEY` and `TWITTER_API_SECRET` in the shell that runs the CLI."},{"heading":"social-providers","content":"Neither app shows provider buttons. The login and sign-up pages offer password and magic link only, and there is no `signInWithOAuth` call in `apps/web/src` or `apps/admin/src`. To add social login, add the provider button and `auth.signInWithOAuth` call to `routes/login.tsx` and `routes/sign-up.tsx`, then enable the provider and its callback URL in your Supabase project."},{"heading":"becoming-an-application-admin","content":"Application administration is separate from workspace roles. Being a workspace owner does not open `/admin`."},{"heading":"becoming-an-application-admin","content":"The admin role is a row in `public.user_roles` with `role = 'admin'`, mirrored into the user's `app_metadata.user_role` claim. Assign it with the SQL function shipped in `apps/database/supabase/migrations/20240901054606_application_admin.sql`:"},{"heading":"becoming-an-application-admin","content":"The function refuses to run unless the current role is `postgres`, `supabase_admin` or `dashboard_user`, so run it from the Supabase SQL editor or a direct `psql` connection. It inserts the role row and writes `user_role = \"admin\"` into `auth.users.raw_app_meta_data`. The user must sign out and in again to receive the new claim. `remove_app_admin_privilege_for_user(uuid)` reverses it. The seed grants the role to `testadmin@usenextbase.com` the same way."},{"heading":"becoming-an-application-admin","content":"The migrations also define `public.custom_access_token_hook`, which would copy `user_roles` into the JWT on every token refresh, but the `[auth.hook.custom_access_token]` section in `config.toml` is commented out. Admin access works through `make_user_app_admin` without it. If you enable the hook in your Supabase project, keep `public.user_roles` as the source of truth."},{"heading":"where-the-admin-area-lives","content":"Administration is a separate deployment, `apps/admin`, running on `http://localhost:3002` locally and on its own origin in production (`APP_URL` of the admin app). It serves `/admin`, `/admin/users`, `/admin/workspaces`, `/admin/billing`, `/admin/marketing` and `/admin/feedback`, and `/` redirects to `/admin`. Admins also get the Admin MCP connection page at `/mcp/connections/admin` on that origin. The web app no longer registers any `/admin` route; both apps share the same Supabase project and RLS rules, and every privileged request re-checks the admin claim with `requireAdmin` before touching `serviceSupabase()`."},{"heading":"where-the-admin-area-lives","content":"Support sign-in from `/admin/users` uses the service-role client to call `auth.admin.generateLink({ type: \"magiclink\" })` for the chosen user and builds a link to the web app's `/auth/confirm` (`WEB_APP_URL`) with the hashed token and `next=/app`. The web app verifies the token and sets its own host-scoped session cookies; the admin session stays on the admin origin. The link can be copied or emailed through Resend when that integration is configured. See `docs/admin-app.md` in the repository."},{"heading":"what-is-protected-and-how","content":"**Row-level security** is the real tenant boundary. Every table has RLS enabled with explicit grants for `anon`, `authenticated` and `service_role`. Helper predicates include `public.is_workspace_member`, `public.can_write_workspace_resources`, `public.is_workspace_admin` and `public.is_application_admin`."},{"heading":"what-is-protected-and-how","content":"**Workspace roles** are `owner`, `admin`, `member` and `readonly` in `workspace_members.workspace_member_role`, plus a `permissions` JSONB with flags such as `view_projects` and `manage_billing`. Loaders still check membership (for example `requireWorkspaceAccess` in `routes/workspace.tsx`) to return clean `403` and `404` responses."},{"heading":"what-is-protected-and-how","content":"**Atomic writes** go through `security definer` SQL functions with `set search_path = ''`, such as `create_workspace_with_owner`, instead of chains of Data API calls."},{"heading":"what-is-protected-and-how","content":"**Service role** usage is confined to `src/.server/` and `*.server.ts` modules, after `requireUser` or `requireAdmin`."},{"heading":"what-is-protected-and-how","content":"**Form actions** are only accepted from the app's own origin (`isCanonicalOriginRequest` in `src/.server/url.ts`, derived from `APP_URL`). Each build's smoke test posts to a sign-in route from a foreign origin and expects `400`."},{"heading":"what-is-protected-and-how","content":"**Agent connections** re-check the live Supabase session, confirmed email, ban state and current role or membership on every tool call through `public.mcp_live_actor`. See agent connections."},{"heading":"what-is-protected-and-how","content":"The pgTAP suite in `apps/database/supabase/tests/` covers the allowed path, cross-tenant denial and restricted-role denial for these boundaries. Run it with `pnpm test-db`."}],"headings":[{"id":"how-a-request-is-authenticated","content":"How a request is authenticated"},{"id":"sign-up-login-and-recovery","content":"Sign-up, login and recovery"},{"id":"email-confirmation-and-the-local-inbox","content":"Email confirmation and the local inbox"},{"id":"redirect-urls","content":"Redirect URLs"},{"id":"social-providers","content":"Social providers"},{"id":"becoming-an-application-admin","content":"Becoming an application admin"},{"id":"where-the-admin-area-lives","content":"Where the admin area lives"},{"id":"what-is-protected-and-how","content":"What is protected, and how"}]},"url":"/docs/v5/starters/tanstack-supabase/authentication","title":"Authentication","description":"How Supabase Auth, cookie sessions, application roles and row-level security protect the TanStack + Supabase starter."},{"_id":"/docs/v5/starters/tanstack-supabase/deployment","structured":{"contents":[{"content":"The kit deploys as two Node 24 Express servers: the product (`apps/web`) and the administration app (`apps/admin`). Both share one Supabase project. The live demo runs each on its own Vercel project, and the repository ships the packaging for that, but any platform that can run a Node 24 process behind a TLS proxy works. Follow the Vercel deployment guide for the platform-generic steps; this page lists what is specific to this kit. The repository's `deployment.md` and `docs/admin-app.md` have the same material in their own words."},{"heading":"1-provision-supabase","content":"Create a project in the Supabase dashboard and note the project ref, API URL, anonymous key and service-role key."},{"heading":"1-provision-supabase","content":"Link the CLI and push the migrations from `apps/database`:"},{"heading":"1-provision-supabase","content":"This includes `20261014114357_user_admin_mcp.sql`, which creates `mcp_records` and the two service-only functions the MCP servers need. Apply it before the apps go live, or agent connections will fail. The seed file is not applied by `db push`; never load the local test accounts into production."},{"heading":"1-provision-supabase","content":"Then in the dashboard, under Auth and URL Configuration:"},{"heading":"1-provision-supabase","content":"Set **Site URL** to the web app's `APP_URL` (the product origin)."},{"heading":"1-provision-supabase","content":"Add **Redirect URLs** for `/auth/callback` and `/auth/confirm` on the web origin and again on the admin origin. These are the only auth routes registered in `apps/web/src/routes.ts` and `apps/admin/src/routes.ts`."},{"heading":"1-provision-supabase","content":"Configure SMTP for confirmation, magic-link and recovery emails. See emails with Resend. The apps' `RESEND_API_KEY` does not cover these."},{"heading":"1-provision-supabase","content":"Enable social providers only if you add provider buttons to the apps; the current login and sign-up pages offer password and magic link only."},{"heading":"1-provision-supabase","content":"If you want the admin JWT claim refreshed by the access-token hook, enable `custom_access_token_hook` in the project's Auth hooks. The function already exists after `db push`. It is optional: `make_user_app_admin` writes the claim directly."},{"heading":"2-production-environment-variables","content":"Set these in each deployment's secret manager. Names must match `apps/web/.env.example` and `apps/admin/.env.example`; see environment variables for the full list."},{"heading":"2-production-environment-variables","content":"Variable"},{"heading":"2-production-environment-variables","content":"Web app"},{"heading":"2-production-environment-variables","content":"Admin app"},{"heading":"2-production-environment-variables","content":"`NODE_ENV`"},{"heading":"2-production-environment-variables","content":"`production`"},{"heading":"2-production-environment-variables","content":"`production`"},{"heading":"2-production-environment-variables","content":"`APP_URL`"},{"heading":"2-production-environment-variables","content":"The product origin, for example `https://app.example.com`. No path, query, hash or credentials."},{"heading":"2-production-environment-variables","content":"The admin origin, for example `https://admin.example.com`."},{"heading":"2-production-environment-variables","content":"`WEB_APP_URL`"},{"heading":"2-production-environment-variables","content":"Not used"},{"heading":"2-production-environment-variables","content":"The product origin (the web app's `APP_URL`). Required for `/health/ready`; used for support sign-in links."},{"heading":"2-production-environment-variables","content":"`SUPABASE_URL`"},{"heading":"2-production-environment-variables","content":"The project's API URL."},{"heading":"2-production-environment-variables","content":"Same value."},{"heading":"2-production-environment-variables","content":"`SUPABASE_ANON_KEY`"},{"heading":"2-production-environment-variables","content":"The project's anonymous key."},{"heading":"2-production-environment-variables","content":"Same value."},{"heading":"2-production-environment-variables","content":"`SUPABASE_SERVICE_ROLE_KEY`"},{"heading":"2-production-environment-variables","content":"The project's service-role key. Server-only."},{"heading":"2-production-environment-variables","content":"Same value."},{"heading":"2-production-environment-variables","content":"`HOST`, `PORT`"},{"heading":"2-production-environment-variables","content":"Where Express binds on a Node host, for example `0.0.0.0` and `3000`. Not used on Vercel."},{"heading":"2-production-environment-variables","content":"Same, for example `0.0.0.0` and `3002`."},{"heading":"2-production-environment-variables","content":"`APP_URL` matters more than it looks: it is the only origin accepted for form actions behind the proxy (`isCanonicalOriginRequest`), the origin for auth email links, and the issuer and resource for that app's MCP server (`${APP_URL}/oauth` plus `${APP_URL}/mcp/user` on web or `${APP_URL}/mcp/admin` on admin). A preview deployment with a different origin needs its own `APP_URL` and its own Supabase redirect URL entries. The demo follows the repository's `<subdomain>-admin.usenextbase.com` pattern for the admin origin."},{"heading":"2-production-environment-variables","content":"Add optional integration variables as you turn features on: `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` (set them on admin too if you use its billing pages); `RESEND_API_KEY` and `EMAIL_FROM` (plus `EMAIL_REPLY_TO`); `API_KEY_PEPPER` with at least 32 random characters and `API_CORS_ORIGIN`; `OPENAI_API_KEY` with `OPENAI_CHAT_MODEL` and `OPENAI_IMAGE_MODEL`; `ANALYTICS_ENABLED=true` with `POSTHOG_KEY`. Server secrets must never get a public-variable prefix."},{"heading":"3-build","content":"From the repository root:"},{"heading":"3-build","content":"`pnpm build` builds every app through Turborepo. Each app build runs `react-router build`, bundles the Express entry with tsup to `dist/index.js`, boots that server and posts to a sign-in route from a foreign origin as a CSRF smoke test (`scripts/check-built-auth.mjs`, expecting `400`), then packages a Vercel Build Output in `.vercel/output` (`scripts/package-vercel.mjs`). The packaging step traces the runtime files with `@vercel/nft`, refuses to include any `.env*` file, writes a `nodejs24.x` function with the app's `index.mjs` as the handler, and runs the smoke test again against the packaged bundle. Use `pnpm web#build` or `pnpm admin#build` to build one app."},{"heading":"3-build","content":"Outputs per app (`apps/web` and `apps/admin`):"},{"heading":"3-build","content":"`build/client`: static assets"},{"heading":"3-build","content":"`build/server`: React Router server bundle"},{"heading":"3-build","content":"`dist/index.js`: Express entry point"},{"heading":"3-build","content":"`.vercel/output`: Vercel Build Output API v3 bundle"},{"heading":"4-deploy","content":"Both apps ship a `vercel.json` with `\"framework\": null` and `\"buildCommand\": \"pnpm build\"`, and an `index.mjs` that wraps the Express app for the Vercel function. When `VERCEL` is set, `server/index.ts` exports the app instead of calling `listen`, and the server trusts one proxy hop."},{"heading":"4-deploy","content":"Create two Vercel projects from the same repository. Set the root directory of one to `apps/web` and of the other to `apps/admin` so each picks up its own `vercel.json`, and keep access to the workspace source enabled so `pnpm build` can resolve the shared packages. Map the product domain to the web project and the admin domain to the admin project. Add the environment variables from step 2 to each project (`HOST` and `PORT` are not needed), deploy, and confirm the logs show the packaging step completing. The function runtime is `nodejs24.x`, which matches `.nvmrc`."},{"heading":"4-deploy","content":"Any platform that runs Node 24 containers and exposes TCP ports works (Fly.io, Railway, Render, AWS ECS and similar). Run one service per app:"},{"heading":"4-deploy","content":"Use Node 24."},{"heading":"4-deploy","content":"Run `pnpm install` and `pnpm build` in the build phase (or `pnpm web#build` / `pnpm admin#build`)."},{"heading":"4-deploy","content":"Start the web app with `pnpm --filter web start` and the admin app with `pnpm --filter admin start`; each runs `NODE_ENV=production node dist/index.js` in its own directory."},{"heading":"4-deploy","content":"Map each service's public HTTPS origin to its `APP_URL`, set the admin service's `WEB_APP_URL` to the web origin, and terminate TLS at the proxy."},{"heading":"4-deploy","content":"Point health checks at `/health/live` and `/health/ready` on both services."},{"heading":"5-make-yourself-an-administrator","content":"Sign up on the deployed web app and confirm the email. Then, from the Supabase SQL editor:"},{"heading":"5-make-yourself-an-administrator","content":"Sign out and in again, then open the admin origin's `/login` and `/admin`. Details are on the authentication page."},{"heading":"6-stripe-webhook-optional","content":"Add a webhook endpoint in Stripe pointing to `${APP_URL of web}/api/stripe/webhook` and copy its signing secret to `STRIPE_WEBHOOK_SECRET`. Billing pages show a disabled state until `STRIPE_SECRET_KEY` is set."},{"heading":"7-smoke-checks","content":"`GET /health/live` on both origins returns `200` with `{ \"status\": \"ok\" }`."},{"heading":"7-smoke-checks","content":"`GET /health/ready` on both origins returns `200` with `{ \"status\": \"ready\" }`. A `503` means one of `APP_URL`, `SUPABASE_URL`, `SUPABASE_ANON_KEY` or `SUPABASE_SERVICE_ROLE_KEY` is missing, or `WEB_APP_URL` on the admin app."},{"heading":"7-smoke-checks","content":"Open `/login` and `/sign-up` on the web app, create a test account, confirm its email, accept the terms and create a workspace. Add a project."},{"heading":"7-smoke-checks","content":"Test a password reset. Check that links return to your origin, not to localhost."},{"heading":"7-smoke-checks","content":"Open `/mcp/connections/user` on the web app and confirm the server address shows your web `APP_URL`. `GET ${APP_URL}/mcp/user` without a token should return a `401` challenge; `GET ${APP_URL}/.well-known/oauth-protected-resource/mcp/user` should return JSON. Connect an agent following connect an agent and call `projects_list`."},{"heading":"7-smoke-checks","content":"As an administrator, sign in on the admin origin, open `/admin` and `/mcp/connections/admin`, and generate a support sign-in link from `/admin/users` to confirm it lands on the web app's `/auth/confirm`."},{"heading":"7-smoke-checks","content":"Run `pnpm check` on the commit you deployed."},{"heading":"7-smoke-checks","content":"See troubleshooting if login returns to the wrong page or the MCP endpoint rejects an approved agent."}],"headings":[{"id":"1-provision-supabase","content":"1\\. Provision Supabase"},{"id":"2-production-environment-variables","content":"2\\. Production environment variables"},{"id":"3-build","content":"3\\. Build"},{"id":"4-deploy","content":"4\\. Deploy"},{"id":"5-make-yourself-an-administrator","content":"5\\. Make yourself an administrator"},{"id":"6-stripe-webhook-optional","content":"6\\. Stripe webhook (optional)"},{"id":"7-smoke-checks","content":"7\\. Smoke checks"}]},"url":"/docs/v5/starters/tanstack-supabase/deployment","title":"Deployment","description":"Provision Supabase, apply the migrations and publish the TanStack + Supabase starter's web and admin apps to Vercel or any Node 24 host."},{"_id":"/docs/v5/starters/tanstack-supabase/environment-variables","structured":{"contents":[{"content":"This kit has two env files, one per deployable app: `apps/web/.env` (from `apps/web/.env.example`) and `apps/admin/.env` (from `apps/admin/.env.example`). Each Express server loads its own file with `dotenv` at startup, and `src/.server/env.ts` in each app validates the core values with Zod. Optional integrations are read lazily by `src/.server/integrations.ts` and stay disabled when their variables are empty."},{"content":"The two example files are identical except for the admin app's `PORT`, `APP_URL` and the extra `WEB_APP_URL`. `pnpm supabase:sync-env` writes the three Supabase values into both files."},{"content":"Never commit either `.env` file. The Vercel packaging scripts refuse to bundle any `.env*` file, and `.gitignore` excludes everything except the examples."},{"heading":"public-versus-server-only","content":"Nothing in these files is inlined into the browser bundle by Vite. The servers pass `SUPABASE_URL` and `SUPABASE_ANON_KEY` to the browser through loader data for realtime subscriptions, so treat those two as public. Everything else is server-only. `SUPABASE_SERVICE_ROLE_KEY`, Stripe secrets and `API_KEY_PEPPER` must never appear in loader data, client code or logs."},{"heading":"server-and-supabase","content":"Variable"},{"heading":"server-and-supabase","content":"Required"},{"heading":"server-and-supabase","content":"Purpose"},{"heading":"server-and-supabase","content":"Where to get it"},{"heading":"server-and-supabase","content":"`NODE_ENV`"},{"heading":"server-and-supabase","content":"Optional"},{"heading":"server-and-supabase","content":"`development`, `test` or `production`. Defaults to `development`. In production the server serves `build/client` instead of starting Vite."},{"heading":"server-and-supabase","content":"Set `production` on your host."},{"heading":"server-and-supabase","content":"`HOST`"},{"heading":"server-and-supabase","content":"Optional"},{"heading":"server-and-supabase","content":"Interface Express binds to. Defaults to `0.0.0.0`."},{"heading":"server-and-supabase","content":"Your platform. Ignored on Vercel, which does not call `listen`."},{"heading":"server-and-supabase","content":"`PORT`"},{"heading":"server-and-supabase","content":"Optional"},{"heading":"server-and-supabase","content":"Port Express listens on. Defaults to `3000` in code; the admin example sets `3002`. Playwright reads the web value too."},{"heading":"server-and-supabase","content":"Your platform. Pick a different port per worktree when running several servers."},{"heading":"server-and-supabase","content":"`APP_URL`"},{"heading":"server-and-supabase","content":"Required in production"},{"heading":"server-and-supabase","content":"Public HTTP(S) origin of this app. Must have no path, query, hash or credentials. Drives the trusted form-action origin, auth email links and the MCP issuer and resource URLs."},{"heading":"server-and-supabase","content":"Web: `http://localhost:3000` locally, your product domain in deployment. Admin: `http://localhost:3002` locally, your admin domain in deployment."},{"heading":"server-and-supabase","content":"`WEB_APP_URL`"},{"heading":"server-and-supabase","content":"Admin only, required in production"},{"heading":"server-and-supabase","content":"Origin of the product web app. The admin app uses it to build support sign-in links that land on the web app's `/auth/confirm`. Same origin rules as `APP_URL`."},{"heading":"server-and-supabase","content":"`http://localhost:3000` locally; the web deployment's `APP_URL` in production. Not present in `apps/web/.env.example`."},{"heading":"server-and-supabase","content":"`SUPABASE_URL`"},{"heading":"server-and-supabase","content":"Required"},{"heading":"server-and-supabase","content":"Supabase project API URL. Must be set together with `SUPABASE_ANON_KEY`. Public."},{"heading":"server-and-supabase","content":"`env.API_URL` from `supabase status --output-format json` (the example file's placeholder says so), or the project's API settings."},{"heading":"server-and-supabase","content":"`SUPABASE_ANON_KEY`"},{"heading":"server-and-supabase","content":"Required"},{"heading":"server-and-supabase","content":"Anonymous key for the request-scoped session client. RLS applies. Public."},{"heading":"server-and-supabase","content":"`env.ANON_KEY` from the status output, or the project's API settings."},{"heading":"server-and-supabase","content":"`SUPABASE_SERVICE_ROLE_KEY`"},{"heading":"server-and-supabase","content":"Required in production"},{"heading":"server-and-supabase","content":"Service-role key for the server-only client used after authorization checks, billing webhooks, admin pages and the MCP store. Bypasses RLS."},{"heading":"server-and-supabase","content":"`env.SERVICE_ROLE_KEY` from the status output, or the project's API settings."},{"heading":"server-and-supabase","content":"`SUPABASE_SERVICE_ROLE_KEY` requires `SUPABASE_URL`. In production, the web app's `/health/ready` returns `503` until `APP_URL`, `SUPABASE_URL`, `SUPABASE_ANON_KEY` and `SUPABASE_SERVICE_ROLE_KEY` are all present; the admin app's `/health/ready` additionally requires `WEB_APP_URL`."},{"heading":"optional-billing-stripe","content":"Variable"},{"heading":"optional-billing-stripe","content":"Required"},{"heading":"optional-billing-stripe","content":"Purpose"},{"heading":"optional-billing-stripe","content":"Where to get it"},{"heading":"optional-billing-stripe","content":"`STRIPE_SECRET_KEY`"},{"heading":"optional-billing-stripe","content":"Optional"},{"heading":"optional-billing-stripe","content":"Enables checkout, customer portal, subscriptions and invoices. Billing shows a disabled state when empty."},{"heading":"optional-billing-stripe","content":"Stripe dashboard, API keys."},{"heading":"optional-billing-stripe","content":"`STRIPE_WEBHOOK_SECRET`"},{"heading":"optional-billing-stripe","content":"Optional"},{"heading":"optional-billing-stripe","content":"Verifies events posted to `/api/stripe/webhook` on the web app."},{"heading":"optional-billing-stripe","content":"Stripe dashboard, the webhook endpoint's signing secret."},{"heading":"optional-billing-stripe","content":"`STRIPE_API_URL`"},{"heading":"optional-billing-stripe","content":"Optional"},{"heading":"optional-billing-stripe","content":"Overrides the Stripe API base. Defaults to `https://api.stripe.com`."},{"heading":"optional-billing-stripe","content":"Only change it for a proxy or test double."},{"heading":"optional-billing-stripe","content":"Set the Stripe values on the admin app too if you use its billing administration pages."},{"heading":"optional-transactional-email-resend","content":"Variable"},{"heading":"optional-transactional-email-resend","content":"Required"},{"heading":"optional-transactional-email-resend","content":"Purpose"},{"heading":"optional-transactional-email-resend","content":"Where to get it"},{"heading":"optional-transactional-email-resend","content":"`RESEND_API_KEY`"},{"heading":"optional-transactional-email-resend","content":"Optional"},{"heading":"optional-transactional-email-resend","content":"Lets `sendTransactionalEmail` in `src/.server/mail.ts` deliver app emails. Both this and `EMAIL_FROM` must be set."},{"heading":"optional-transactional-email-resend","content":"Resend dashboard."},{"heading":"optional-transactional-email-resend","content":"`RESEND_API_URL`"},{"heading":"optional-transactional-email-resend","content":"Optional"},{"heading":"optional-transactional-email-resend","content":"Overrides the Resend API base. Defaults to `https://api.resend.com`."},{"heading":"optional-transactional-email-resend","content":"Only change it for a proxy or test double."},{"heading":"optional-transactional-email-resend","content":"`EMAIL_FROM`"},{"heading":"optional-transactional-email-resend","content":"Optional"},{"heading":"optional-transactional-email-resend","content":"Sender address for app emails."},{"heading":"optional-transactional-email-resend","content":"An address on a domain you verified in Resend."},{"heading":"optional-transactional-email-resend","content":"`EMAIL_REPLY_TO`"},{"heading":"optional-transactional-email-resend","content":"Optional"},{"heading":"optional-transactional-email-resend","content":"Reply-to address added to app emails."},{"heading":"optional-transactional-email-resend","content":"Your support address."},{"heading":"optional-transactional-email-resend","content":"These variables do not configure Supabase Auth emails (confirmation, magic link, recovery). Those are sent by Supabase using the project's Auth SMTP settings; locally they land in Mailpit. See emails with Resend."},{"heading":"optional-developer-api","content":"Variable"},{"heading":"optional-developer-api","content":"Required"},{"heading":"optional-developer-api","content":"Purpose"},{"heading":"optional-developer-api","content":"Where to get it"},{"heading":"optional-developer-api","content":"`API_KEY_PEPPER`"},{"heading":"optional-developer-api","content":"Optional"},{"heading":"optional-developer-api","content":"Enables first-party API keys and `/api/v1/me`. Ignored unless it has at least 32 characters."},{"heading":"optional-developer-api","content":"Generate a random string of 32 or more characters."},{"heading":"optional-developer-api","content":"`API_CORS_ORIGIN`"},{"heading":"optional-developer-api","content":"Optional"},{"heading":"optional-developer-api","content":"Allowed browser origin for `/api/v1/me`."},{"heading":"optional-developer-api","content":"The origin of the site that will call the API."},{"heading":"optional-ai-openai-compatible","content":"Variable"},{"heading":"optional-ai-openai-compatible","content":"Required"},{"heading":"optional-ai-openai-compatible","content":"Purpose"},{"heading":"optional-ai-openai-compatible","content":"Where to get it"},{"heading":"optional-ai-openai-compatible","content":"`OPENAI_API_KEY`"},{"heading":"optional-ai-openai-compatible","content":"Optional"},{"heading":"optional-ai-openai-compatible","content":"Enables `/api/ai/chat` and `/api/ai/image`."},{"heading":"optional-ai-openai-compatible","content":"Your provider's dashboard."},{"heading":"optional-ai-openai-compatible","content":"`OPENAI_API_URL`"},{"heading":"optional-ai-openai-compatible","content":"Optional"},{"heading":"optional-ai-openai-compatible","content":"API base. Defaults to `https://api.openai.com`."},{"heading":"optional-ai-openai-compatible","content":"Change it for another OpenAI-compatible provider."},{"heading":"optional-ai-openai-compatible","content":"`OPENAI_CHAT_MODEL`"},{"heading":"optional-ai-openai-compatible","content":"Optional"},{"heading":"optional-ai-openai-compatible","content":"Model used for chat. Chat stays disabled without it."},{"heading":"optional-ai-openai-compatible","content":"Your provider's model list."},{"heading":"optional-ai-openai-compatible","content":"`OPENAI_IMAGE_MODEL`"},{"heading":"optional-ai-openai-compatible","content":"Optional"},{"heading":"optional-ai-openai-compatible","content":"Model used for image generation. Image generation stays disabled without it."},{"heading":"optional-ai-openai-compatible","content":"Your provider's model list."},{"heading":"optional-product-analytics-posthog","content":"Variable"},{"heading":"optional-product-analytics-posthog","content":"Required"},{"heading":"optional-product-analytics-posthog","content":"Purpose"},{"heading":"optional-product-analytics-posthog","content":"Where to get it"},{"heading":"optional-product-analytics-posthog","content":"`ANALYTICS_ENABLED`"},{"heading":"optional-product-analytics-posthog","content":"Optional"},{"heading":"optional-product-analytics-posthog","content":"Collection runs only when this is exactly `true`. Defaults to `false`."},{"heading":"optional-product-analytics-posthog","content":"Set it yourself. The tracker also respects Do Not Track."},{"heading":"optional-product-analytics-posthog","content":"`POSTHOG_KEY`"},{"heading":"optional-product-analytics-posthog","content":"Optional"},{"heading":"optional-product-analytics-posthog","content":"PostHog project key, required when analytics is enabled."},{"heading":"optional-product-analytics-posthog","content":"PostHog project settings."},{"heading":"optional-product-analytics-posthog","content":"`POSTHOG_HOST`"},{"heading":"optional-product-analytics-posthog","content":"Optional"},{"heading":"optional-product-analytics-posthog","content":"PostHog host. Defaults to `https://us.i.posthog.com`."},{"heading":"optional-product-analytics-posthog","content":"PostHog project settings."},{"heading":"variables-that-are-not-in-the-example-files","content":"Variable"},{"heading":"variables-that-are-not-in-the-example-files","content":"Read by"},{"heading":"variables-that-are-not-in-the-example-files","content":"Purpose"},{"heading":"variables-that-are-not-in-the-example-files","content":"`LOG_LEVEL`"},{"heading":"variables-that-are-not-in-the-example-files","content":"`apps/web/server/index.ts`, `apps/admin/server/index.ts`"},{"heading":"variables-that-are-not-in-the-example-files","content":"pino log level. Defaults to `info`."},{"heading":"variables-that-are-not-in-the-example-files","content":"`VERCEL`"},{"heading":"variables-that-are-not-in-the-example-files","content":"both `server/index.ts` files"},{"heading":"variables-that-are-not-in-the-example-files","content":"Set by Vercel. When present the server exports the Express app instead of calling `listen`."},{"heading":"variables-that-are-not-in-the-example-files","content":"`ADMIN_BASE_URL`"},{"heading":"variables-that-are-not-in-the-example-files","content":"`apps/web/e2e/seeded-accounts.spec.ts`"},{"heading":"variables-that-are-not-in-the-example-files","content":"Where the Playwright suite finds the admin app. Defaults to `http://127.0.0.1:3002`."},{"heading":"variables-that-are-not-in-the-example-files","content":"`MCP_TEST_SUPABASE_URL`, `MCP_TEST_SUPABASE_SERVICE_KEY`, `MCP_TEST_SUPABASE_ANON_KEY`"},{"heading":"variables-that-are-not-in-the-example-files","content":"`apps/web/src/mcp/supabase-store.integration.test.ts`"},{"heading":"variables-that-are-not-in-the-example-files","content":"Point the MCP storage integration test at an isolated local Supabase project. The test is skipped when they are missing."},{"heading":"variables-that-are-not-in-the-example-files","content":"`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `TWITTER_API_KEY`, `TWITTER_API_SECRET`"},{"heading":"variables-that-are-not-in-the-example-files","content":"`apps/database/supabase/config.toml`"},{"heading":"variables-that-are-not-in-the-example-files","content":"Referenced as `env(...)` by the local Supabase Auth provider sections. They configure the local Auth service, not the apps. See authentication."},{"heading":"variables-that-are-not-in-the-example-files","content":"The repository does not ship an env example for `apps/database` or `apps/email`. Local Supabase reads `config.toml`, and the email preview server needs no configuration."}],"headings":[{"id":"public-versus-server-only","content":"Public versus server-only"},{"id":"server-and-supabase","content":"Server and Supabase"},{"id":"optional-billing-stripe","content":"Optional billing (Stripe)"},{"id":"optional-transactional-email-resend","content":"Optional transactional email (Resend)"},{"id":"optional-developer-api","content":"Optional developer API"},{"id":"optional-ai-openai-compatible","content":"Optional AI (OpenAI-compatible)"},{"id":"optional-product-analytics-posthog","content":"Optional product analytics (PostHog)"},{"id":"variables-that-are-not-in-the-example-files","content":"Variables that are not in the example files"}]},"url":"/docs/v5/starters/tanstack-supabase/environment-variables","title":"Environment variables","description":"Every variable in apps/web/.env.example and apps/admin/.env.example, what it does and where its value comes from."},{"_id":"/docs/v5/starters/tanstack-supabase","structured":{"contents":[{"content":"This kit uses React Router 8 framework mode and Express 5, with Supabase Postgres and Supabase Auth. You get the account, workspace and project foundation, a separately deployed administration app, and separate agent connections for users and application administrators."},{"content":"Open the live demo · Repository"},{"content":"Despite the repository name, the apps are React Router 8 in framework mode (SSR) served by explicit Express 5 servers. There is no TanStack Router or TanStack Query dependency in `apps/web/package.json` or `apps/admin/package.json`."},{"heading":"the-stack","content":"Layer"},{"heading":"the-stack","content":"What the kit uses"},{"heading":"the-stack","content":"Frontend"},{"heading":"the-stack","content":"React 19, React Router 8 framework mode (SSR), Vite 8, Tailwind 4, shadcn/ui"},{"heading":"the-stack","content":"Server"},{"heading":"the-stack","content":"Express 5 (`apps/web/server/index.ts` and `apps/admin/server/index.ts`), request-scoped Supabase clients"},{"heading":"the-stack","content":"Database"},{"heading":"the-stack","content":"Supabase Postgres with row-level security, SQL migrations and pgTAP tests; local projects run on the Supabase CLI stack engine (CLI pinned to `2.121.0`)"},{"heading":"the-stack","content":"Auth"},{"heading":"the-stack","content":"Supabase Auth (password and magic link) through `@supabase/ssr` cookies"},{"heading":"the-stack","content":"Agent access"},{"heading":"the-stack","content":"User MCP served by the web app under `/mcp/user`; Admin MCP served by the admin app under `/mcp/admin`"},{"heading":"the-stack","content":"Hosting (demo)"},{"heading":"the-stack","content":"Vercel, one project per app, using the Build Output packaging in `apps/*/scripts/package-vercel.mjs`"},{"heading":"what-you-get-out-of-the-box","content":"Accounts: sign up, login, magic link, password recovery, onboarding, terms acceptance, account deletion"},{"heading":"what-you-get-out-of-the-box","content":"Workspaces: multi-workspace membership, roles (owner, admin, member, readonly), invitations, notifications"},{"heading":"what-you-get-out-of-the-box","content":"Projects inside a workspace, with comments and realtime updates"},{"heading":"what-you-get-out-of-the-box","content":"A separate administration app (`apps/admin`, port 3002 locally) serving `/admin` for users, workspaces, billing, marketing content and feedback moderation, plus support sign-in into the product app"},{"heading":"what-you-get-out-of-the-box","content":"User MCP (web app) and Admin MCP (admin app) with OAuth consent, both read-only in v5"},{"heading":"what-you-get-out-of-the-box","content":"Marketing: blog, authors, tags, changelog, feedback boards, voting, roadmap and in-app docs"},{"heading":"what-you-get-out-of-the-box","content":"Optional integrations that fail closed when unset: Stripe billing, Resend transactional email, developer API keys with `/api/v1/me`, OpenAI-compatible AI chat and image endpoints, PostHog analytics"},{"heading":"what-you-get-out-of-the-box","content":"Health checks at `/health/live` and `/health/ready` on both apps, structured logging, Playwright browser tests, CI workflows, Renovate and Changesets"},{"heading":"what-you-get-out-of-the-box","content":"The `apps/email` package is a React Email template catalogue with a preview server. Neither app imports it; the emails the apps send are built in `apps/web/src/.server/mail.ts` and `apps/admin/src/.server/mail.ts`."},{"heading":"application-surfaces","content":"Web app (`apps/web`, `http://localhost:3000` locally):"},{"heading":"application-surfaces","content":"Area"},{"heading":"application-surfaces","content":"Routes"},{"heading":"application-surfaces","content":"Public"},{"heading":"application-surfaces","content":"`/`, `/blog`, `/changelog`, `/feedback`, `/roadmap`, `/docs`, `/terms`"},{"heading":"application-surfaces","content":"Authentication"},{"heading":"application-surfaces","content":"`/login`, `/sign-up`, `/forgot-password`, `/reset-password`, `/auth/callback`, `/auth/confirm`, `/logout`"},{"heading":"application-surfaces","content":"Product"},{"heading":"application-surfaces","content":"`/app`, `/app/onboarding`, `/app/workspaces`, `/app/invitations`, `/app/notifications`, `/app/billing`, `/app/settings`, `/app/settings/developer`"},{"heading":"application-surfaces","content":"Agent connections"},{"heading":"application-surfaces","content":"`/mcp/connections/user`, server at `/mcp/user`"},{"heading":"application-surfaces","content":"Operations and API"},{"heading":"application-surfaces","content":"`/health/live`, `/health/ready`, `/api/stripe/webhook`, `/api/v1/me`, `/api/ai/chat`, `/api/ai/image`, `/api/analytics`"},{"heading":"application-surfaces","content":"Admin app (`apps/admin`, `http://localhost:3002` locally):"},{"heading":"application-surfaces","content":"Area"},{"heading":"application-surfaces","content":"Routes"},{"heading":"application-surfaces","content":"Authentication"},{"heading":"application-surfaces","content":"`/login`, `/forgot-password`, `/reset-password`, `/auth/callback`, `/auth/confirm`, `/logout` (no sign-up; `/` redirects to `/admin`)"},{"heading":"application-surfaces","content":"Administration"},{"heading":"application-surfaces","content":"`/admin`, `/admin/users`, `/admin/users/:userId`, `/admin/workspaces`, `/admin/workspaces/:workspaceId`, `/admin/billing`, `/admin/marketing` (blog, authors, tags, changelog), `/admin/feedback`"},{"heading":"application-surfaces","content":"Agent connections"},{"heading":"application-surfaces","content":"`/mcp/connections/admin`, server at `/mcp/admin`"},{"heading":"application-surfaces","content":"Operations and API"},{"heading":"application-surfaces","content":"`/health/live`, `/health/ready`, `/api/admin/marketing/media`, `/app/notifications`, `/terms`"},{"heading":"prerequisites","content":"Node.js 24. `.nvmrc` pins `24` and `package.json` requires `>=24.0.0`."},{"heading":"prerequisites","content":"pnpm 11.1.2, selected by the `packageManager` field. Run `corepack enable` and let it pick the version."},{"heading":"prerequisites","content":"Docker (or Podman) for the local Supabase stack, or the native Supabase runtime in a supported sandbox. See local Supabase per agent."},{"heading":"prerequisites","content":"The Supabase CLI is installed as a dependency of `apps/database` (pinned to `2.121.0`), so you do not need a global install."},{"heading":"prerequisites","content":"A Supabase project for production. Stripe, Resend, OpenAI and PostHog accounts are optional."},{"heading":"next-steps","content":"Installation: clone, start a local Supabase project and run the dev servers."},{"heading":"next-steps","content":"Environment variables: every value in `apps/web/.env.example` and `apps/admin/.env.example`."},{"heading":"next-steps","content":"Authentication: how Supabase Auth, roles and RLS protect both apps."},{"heading":"next-steps","content":"Agent connections: the User MCP and Admin MCP."},{"heading":"next-steps","content":"Project structure: where things live and where to make changes."},{"heading":"next-steps","content":"Deployment: take both apps to production."},{"heading":"next-steps","content":"For the shared v5 concepts, start with getting started."}],"headings":[{"id":"the-stack","content":"The stack"},{"id":"what-you-get-out-of-the-box","content":"What you get out of the box"},{"id":"application-surfaces","content":"Application surfaces"},{"id":"prerequisites","content":"Prerequisites"},{"id":"next-steps","content":"Next steps"}]},"url":"/docs/v5/starters/tanstack-supabase","title":"TanStack + Supabase","description":"What the TanStack + Supabase starter ships, what you need to run it, and where to go next."},{"_id":"/docs/v5/starters/tanstack-supabase/installation","structured":{"contents":[{"content":"This page walks through a local setup from an empty directory to a signed-in workspace. It follows the local Supabase per agent workflow, so each git worktree gets its own Supabase project. Read that page once; this page only shows the commands that are specific to this kit."},{"heading":"clone-the-repository","content":"If GitHub says the repository does not exist, sign in with the account that has access."},{"heading":"install-dependencies","content":"Use Node.js 24 (`.nvmrc`) and let Corepack pick pnpm 11.1.2 from the `packageManager` field."},{"heading":"install-dependencies","content":"`pnpm install` also installs the Supabase CLI (pinned to `2.121.0`) into `apps/database`, so every database command below runs through `pnpm --filter database exec supabase`."},{"heading":"choose-one-command-or-step-by-step","content":"The repository ships a `pnpm setup` script (`setup.sh`) that does the rest of this page in one go: it runs `pnpm install`, copies `apps/web/.env.example` to `apps/web/.env` if that file is missing, starts the local Supabase project, writes its URL and keys into `apps/web/.env` and `apps/admin/.env`, and regenerates the database types."},{"heading":"choose-one-command-or-step-by-step","content":"If that worked, skip to \"Start the dev servers\" below. The remaining steps show what the script does, which you need when you want to use `--runtime native`, set a different web port, or debug a failed start."},{"heading":"create-the-env-files","content":"Copy the examples only if you have not created your own files yet."},{"heading":"create-the-env-files","content":"The web app defaults to `APP_URL=http://localhost:3000` with `HOST=0.0.0.0` and `PORT=3000`; the admin app defaults to `APP_URL=http://localhost:3002`, `PORT=3002` and `WEB_APP_URL=http://localhost:3000`, which it uses for support sign-in links into the product. Every variable is described on environment variables."},{"heading":"start-the-local-supabase-project","content":"This runs `supabase start` in `apps/database`. `apps/database/supabase/config.toml` enables the CLI stack engine (`[experimental] stack = true`), so there are no fixed service ports: the CLI picks free ports for this worktree's project. Pass the runtime explicitly when you need to, for example `pnpm --filter database exec supabase start --runtime native` in a supported sandbox without a container engine. The first start builds the database from `apps/database/supabase/migrations/` and loads `apps/database/supabase/seed.sql`."},{"heading":"connect-both-apps-to-the-project","content":"`scripts/sync-supabase-env.ts` runs `supabase status --output-format json`, reads the nested `env` object and writes the values into `apps/web/.env` and `apps/admin/.env` (creating a file from the example when one is missing, and leaving your other settings untouched):"},{"heading":"connect-both-apps-to-the-project","content":"CLI `env` key"},{"heading":"connect-both-apps-to-the-project","content":"App variable"},{"heading":"connect-both-apps-to-the-project","content":"`API_URL`"},{"heading":"connect-both-apps-to-the-project","content":"`SUPABASE_URL`"},{"heading":"connect-both-apps-to-the-project","content":"`ANON_KEY`"},{"heading":"connect-both-apps-to-the-project","content":"`SUPABASE_ANON_KEY`"},{"heading":"connect-both-apps-to-the-project","content":"`SERVICE_ROLE_KEY`"},{"heading":"connect-both-apps-to-the-project","content":"`SUPABASE_SERVICE_ROLE_KEY`"},{"heading":"connect-both-apps-to-the-project","content":"To see the same output yourself, including `STUDIO_URL`, `MAILPIT_URL` and `MCP_URL`:"},{"heading":"connect-both-apps-to-the-project","content":"You will need the Mailpit address for verification emails. If you use the repository's `supabase-local` MCP server with an AI coding tool, point its URL in `.agents/config.json` at `MCP_URL` and run `pnpm agents:sync`."},{"heading":"regenerate-database-types-optional","content":"The generated types are committed at `apps/database/lib/database.types.ts`. Regenerate them after any schema change:"},{"heading":"start-the-dev-servers","content":"This runs every app's `dev` script in parallel through Turborepo: the web app on `http://localhost:3000`, the admin app on `http://localhost:3002` (both from `HOST` and `PORT` in their `.env` files) and the React Email preview on `http://localhost:3001`. Use `pnpm web#dev` or `pnpm admin#dev` to start a single app."},{"heading":"start-the-dev-servers","content":"Restart the dev servers after changing env values or switching branches."},{"heading":"sign-in-and-create-a-workspace","content":"The seed creates four local accounts. Every one signs in with the password `password123`:"},{"heading":"sign-in-and-create-a-workspace","content":"Email"},{"heading":"sign-in-and-create-a-workspace","content":"Role"},{"heading":"sign-in-and-create-a-workspace","content":"`testadmin@usenextbase.com`"},{"heading":"sign-in-and-create-a-workspace","content":"Application admin, can open `/admin` on the admin app"},{"heading":"sign-in-and-create-a-workspace","content":"`alice@example.com`"},{"heading":"sign-in-and-create-a-workspace","content":"Regular user"},{"heading":"sign-in-and-create-a-workspace","content":"`bob@example.com`"},{"heading":"sign-in-and-create-a-workspace","content":"Regular user"},{"heading":"sign-in-and-create-a-workspace","content":"`carol@example.com`"},{"heading":"sign-in-and-create-a-workspace","content":"Regular user"},{"heading":"sign-in-and-create-a-workspace","content":"These accounts exist only in local and CI databases. Never seed them into production."},{"heading":"sign-in-and-create-a-workspace","content":"To test the full flow instead, open `http://localhost:3000/sign-up`, create an account and confirm it from the local inbox. Local Supabase Auth requires email confirmation (`enable_confirmations = true`) and delivers mail to Mailpit (`[local_smtp]` in `config.toml`); open the `MAILPIT_URL` from the status output. The confirmation link returns to `/auth/callback`, then `/app/onboarding` asks you to accept the terms and create your first workspace. Add a project inside it so you have data for an agent later."},{"heading":"sign-in-and-create-a-workspace","content":"To try administration, sign in as `testadmin@usenextbase.com` at `http://localhost:3002/login`. The admin app has no sign-up page; it redirects `/` to `/admin`."},{"heading":"verify-the-setup","content":"`pnpm check` includes both app builds, each of which boots the built server and posts to a sign-in route as a smoke test (`scripts/check-built-auth.mjs`) before packaging a Vercel Build Output. The Playwright suite in `apps/web/e2e/` signs in with the seeded admin account against the admin app (`ADMIN_BASE_URL`, default `http://127.0.0.1:3002`), so run it against a freshly seeded database."},{"heading":"stop-reset-and-switch","content":"`db:stop` keeps the project's data. `db reset` replays every migration and the seed file; run it after changing SQL, then `pnpm test-db` and `pnpm gen-types-local`. When you switch branches, start that worktree's project, run `pnpm supabase:sync-env` again so both `.env` files point at it, and restart `pnpm dev`. The troubleshooting page covers the common failures."}],"headings":[{"id":"clone-the-repository","content":"Clone the repository"},{"id":"install-dependencies","content":"Install dependencies"},{"id":"choose-one-command-or-step-by-step","content":"Choose: one command or step by step"},{"id":"create-the-env-files","content":"Create the env files"},{"id":"start-the-local-supabase-project","content":"Start the local Supabase project"},{"id":"connect-both-apps-to-the-project","content":"Connect both apps to the project"},{"id":"regenerate-database-types-optional","content":"Regenerate database types (optional)"},{"id":"start-the-dev-servers","content":"Start the dev servers"},{"id":"sign-in-and-create-a-workspace","content":"Sign in and create a workspace"},{"id":"verify-the-setup","content":"Verify the setup"},{"id":"stop-reset-and-switch","content":"Stop, reset and switch"}]},"url":"/docs/v5/starters/tanstack-supabase/installation","title":"Installation","description":"Run the TanStack + Supabase starter locally with its own Supabase project, then sign in and create a workspace."},{"_id":"/docs/v5/starters/tanstack-supabase/project-structure","structured":{"contents":[{"content":"The repository is a pnpm workspace (`apps/*` and `packages/*`) orchestrated by Turborepo. Four apps and one shared package."},{"content":"<File name=\"index.ts\" />"},{"content":"<File name=\"root.tsx\" />"},{"content":"<File name=\"routes.ts\" />"},{"content":"<File name=\"auth.ts\" />"},{"content":"<File name=\"env.ts\" />"},{"content":"<File name=\"integrations.ts\" />"},{"content":"<File name=\"mail.ts\" />"},{"content":"<File name=\"request-context.ts\" />"},{"content":"<File name=\"url.ts\" />"},{"content":"<File name=\"adapter.ts\" />"},{"content":"<File name=\"core.ts\" />"},{"content":"<File name=\"supabase-adapter.ts\" />"},{"content":"<File name=\"supabase-store.ts\" />"},{"content":"<File name=\"express.ts\" />"},{"content":"<File name=\"app-shell.tsx\" />"},{"content":"<File name=\"site-header.tsx\" />"},{"content":"<File name=\"check-built-auth.mjs\" />"},{"content":"<File name=\"package-vercel.mjs\" />"},{"content":"<File name=\".env.example\" />"},{"content":"<File name=\"react-router.config.ts\" />"},{"content":"<File name=\"vite.config.ts\" />"},{"content":"<File name=\"playwright.config.ts\" />"},{"content":"<File name=\"vercel.json\" />"},{"content":"<File name=\"index.mjs\" />"},{"content":"<File name=\"index.ts\" />"},{"content":"<File name=\"root.tsx\" />"},{"content":"<File name=\"routes.ts\" />"},{"content":"<File name=\"auth.ts\" />"},{"content":"<File name=\"env.ts\" />"},{"content":"<File name=\"web-app-url.ts\" />"},{"content":"<File name=\"adapter.ts\" />"},{"content":"<File name=\"core.ts\" />"},{"content":"<File name=\"app-shell.tsx\" />"},{"content":"<File name=\"check-built-auth.mjs\" />"},{"content":"<File name=\"package-vercel.mjs\" />"},{"content":"<File name=\".env.example\" />"},{"content":"<File name=\"vercel.json\" />"},{"content":"<File name=\"index.mjs\" />"},{"content":"<File name=\"config.toml\" />"},{"content":"<File name=\"seed.sql\" />"},{"content":"<File name=\"database.types.ts\" />"},{"content":"<File name=\"generate-signing-keys.sh\" />"},{"content":"<File name=\"tsconfig.base.json\" />"},{"content":"<File name=\"sync-supabase-env.ts\" />"},{"content":"<File name=\"config.json\" />"},{"content":"<File name=\"admin-app.md\" />"},{"content":"<File name=\"mcp-connections.md\" />"},{"content":"<File name=\"AGENTS.md\" />"},{"content":"<File name=\"README.md\" />"},{"content":"<File name=\"deployment.md\" />"},{"content":"<File name=\"setup.sh\" />"},{"content":"<File name=\"package.json\" />"},{"content":"<File name=\"pnpm-workspace.yaml\" />"},{"content":"<File name=\"turbo.json\" />"},{"heading":"the-apps","content":"Path"},{"heading":"the-apps","content":"What it is"},{"heading":"the-apps","content":"`apps/web`"},{"heading":"the-apps","content":"The product: React Router 8 framework-mode app (`appDirectory: \"src\"`) served by Express 5 on port 3000. Public marketing pages, auth, `/app/*`, the public API routes and the User MCP. In development Express mounts Vite in middleware mode; in production it serves `build/client` and imports `build/server/index.js`, bundled to `dist/index.js` by tsup."},{"heading":"the-apps","content":"`apps/admin`"},{"heading":"the-apps","content":"The administration app, same stack, port 3002 locally and its own deployment. Serves `/admin/*`, admin auth pages, the marketing media upload route and the Admin MCP. Shares the Supabase project with `apps/web`; `WEB_APP_URL` points it at the product for support sign-in links. Described in `docs/admin-app.md`."},{"heading":"the-apps","content":"`apps/database`"},{"heading":"the-apps","content":"The Supabase project: `config.toml` (stack engine enabled), hand-written migrations, pgTAP tests, local seed and the generated types exported as `database/types`. The Supabase CLI `2.121.0` is a dependency here."},{"heading":"the-apps","content":"`apps/email`"},{"heading":"the-apps","content":"React Email template catalogue (`emails/*.tsx`) with a preview server on port 3001. Not imported by the apps."},{"heading":"the-apps","content":"`packages/typescript-config`"},{"heading":"the-apps","content":"Shared strict `tsconfig.base.json`."},{"heading":"the-apps","content":"`apps/admin` is a trimmed copy of `apps/web`: the same `server/index.ts` shape, the same `src/.server/` modules (`auth.ts`, `env.ts`, `integrations.ts`, `mail.ts`, `request-context.ts`, plus `web-app-url.ts`), the same `src/mcp/` code with a different audience, and only the marketing feature module it needs for content administration. Conventions documented for `apps/web` below apply to both unless noted."},{"heading":"routes-and-pages","content":"Routes are registered explicitly in `apps/web/src/routes.ts` and `apps/admin/src/routes.ts` with `index`, `route` and `layout`. A file in `src/routes/` does nothing until it is listed there. Route modules export `loader`, `action`, `meta` and a default component. API resource routes are `.ts` files named `api-*.ts`."},{"heading":"routes-and-pages","content":"In the web app, `/app/*` pages nest under `routes/app-layout.tsx`; navigation entries live in `src/components/app-shell.tsx` (`productItems`) and `src/components/site-header.tsx` for public pages. `/` and `/terms` are prerendered (`react-router.config.ts`), so keep their loaders free of per-user data. In the admin app, `/admin/*` pages nest under `routes/admin-layout.tsx` and the navigation is in its own `src/components/app-shell.tsx`. The repository's `new-page` skill in `.agents/skills/` walks through adding a page."},{"heading":"server-and-data-access","content":"Loaders and actions get `RequestServices` from `context.get(servicesContext)` (`src/.server/request-context.ts`): `supabase()` for user-scoped reads and writes under RLS, `serviceSupabase()` only after `requireUser` or `requireAdmin` plus an explicit authorization check. Server-only code belongs in `src/.server/` or `*.server.ts` files, which React Router strips from client bundles. Mutations are React Router actions with a hidden `intent` field, validated with Zod (shared schemas in `src/features/core/core-utils.ts`), returning `{ error }` or `{ success }`."},{"heading":"server-and-data-access","content":"Feature data access for marketing content sits in `src/features/marketing/*.server.ts` (the public reads in the web app, the CMS writes in the admin app). Optional integrations are read through `src/.server/integrations.ts`; core env validation is in `src/.server/env.ts`."},{"heading":"schema-and-migrations","content":"Migrations in `apps/database/supabase/migrations/` are the only source of truth; `supabase/schemas/init.sql` is a stale snapshot, so do not diff against it. Never edit a shipped migration. Every new table needs RLS, named policies, explicit grants and indexes. Add pgTAP coverage in `supabase/tests/NN_*.sql` and commit the regenerated `apps/database/lib/database.types.ts`. The `db-migration` skill covers this flow and reads the local database address from `supabase status --output-format json`."},{"heading":"authentication","content":"Guards are in `src/.server/auth.ts` of each app; auth pages are `routes/login.tsx`, `routes/sign-up.tsx` (web only), `routes/forgot-password.tsx`, `routes/reset-password.tsx`, `routes/auth-callback.tsx` and `routes/auth-confirm.ts`. Local Auth settings (confirmations, redirect URLs for both ports, providers) are in `apps/database/supabase/config.toml`. See authentication."},{"heading":"emails","content":"App emails are built as HTML and text in `src/.server/mail.ts` and sent through Resend when `RESEND_API_KEY` and `EMAIL_FROM` are set. The templates in `apps/email/emails/` are a design catalogue; to send one from an app, add `@nextbase/email` as a workspace dependency, render it with `@react-email/render` and pass the HTML to `sendTransactionalEmail`. Supabase Auth emails are configured in the Supabase project, not here."},{"heading":"billing-and-background-work","content":"Stripe lives in `apps/web/src/.server/billing/` (`stripe.ts`, `payment-gateway.ts`) with the webhook route at `routes/api-stripe-webhook.ts`. There is no separate worker or queue in this kit; the Express servers handle everything, and the Stripe webhook on the web app is the only inbound background trigger. The admin app's `/admin/billing` reads the same tables."},{"heading":"mcp","content":"Each app's MCP and OAuth server is its `src/mcp/`, mounted by its `server/index.ts` before React Router. The web app serves the User MCP, the admin app the Admin MCP. Follow the `mcp-tool` skill and agent connections."},{"heading":"local-environment","content":"`setup.sh` (`pnpm setup`) and `scripts/sync-supabase-env.ts` (`pnpm supabase:sync-env`) are the only scripts that touch `.env` files: the sync script reads `env.API_URL`, `env.ANON_KEY` and `env.SERVICE_ROLE_KEY` from `supabase status --output-format json` and writes them into `apps/web/.env` and `apps/admin/.env`. Because `config.toml` enables the CLI stack engine, there are no fixed local ports to hard-code anywhere."},{"heading":"tests","content":"Vitest (jsdom) unit tests sit next to code as `*.test.ts(x)` in `apps/web/src` and `apps/admin/src`. Run `pnpm test`, or one file with `pnpm --filter web exec vitest run <file>` (or `--filter admin`)."},{"heading":"tests","content":"`*.integration.test.ts` files skip unless the `MCP_TEST_SUPABASE_*` variables point at a local project."},{"heading":"tests","content":"Playwright specs are in `apps/web/e2e/`; `pnpm test:e2e` starts the web and admin dev servers unless `CI` is set, and the seeded-account spec signs in on the admin app."},{"heading":"tests","content":"pgTAP tests are in `apps/database/supabase/tests/`; `pnpm test-db` needs a running local project."},{"heading":"tests","content":"`pnpm check` runs `format:check`, `lint`, `typecheck`, `test` and `build` across all apps. Each app build also runs its `scripts/check-built-auth.mjs` and `scripts/package-vercel.mjs`."},{"heading":"ai-coding-tool-configuration","content":"`AGENTS.md` (root and per app, including `apps/admin/AGENTS.md`), `.agents/config.json` and `.agents/skills/` are the hand-edited sources. `CLAUDE.md`, `GEMINI.md`, `.mcp.json`, `.cursor/`, `.vscode/mcp.json` and the other tool files are generated by `pnpm agents:sync`; CI runs `pnpm agents:check`. Edit the sources, not the generated files. The `supabase-local` MCP server URL in `.agents/config.json` should be the `MCP_URL` of your running local project."}],"headings":[{"id":"the-apps","content":"The apps"},{"id":"where-to-make-changes","content":"Where to make changes"},{"id":"routes-and-pages","content":"Routes and pages"},{"id":"server-and-data-access","content":"Server and data access"},{"id":"schema-and-migrations","content":"Schema and migrations"},{"id":"authentication","content":"Authentication"},{"id":"emails","content":"Emails"},{"id":"billing-and-background-work","content":"Billing and background work"},{"id":"mcp","content":"MCP"},{"id":"local-environment","content":"Local environment"},{"id":"tests","content":"Tests"},{"id":"ai-coding-tool-configuration","content":"AI coding tool configuration"}]},"url":"/docs/v5/starters/tanstack-supabase/project-structure","title":"Project structure","description":"The TanStack + Supabase monorepo layout and where to make common changes."},{"_id":"/docs/v5/troubleshooting","structured":{"contents":[{"content":"Most setup problems come down to a missing provider setting, the wrong public URL, or a backend that isn't running. Here are the checks I would start with."},{"heading":"the-repository-cant-be-cloned","content":"Check the repository name on your kit's installation page and the GitHub account you are using. Private repositories need access before cloning works."},{"heading":"the-local-app-opens-but-requests-fail","content":"Check that the database/backend is still running. Supabase and Postgres need their local services; Convex needs its selected local backend; D1 uses the Worker bindings."},{"heading":"the-local-app-opens-but-requests-fail","content":"For Supabase, follow local Supabase per agent. Remove fixed service ports and use the current project's `env.API_URL` and keys from `supabase status --output-format json`. After changing branches, start the selected project, refresh the web env values and restart the dev server. The web servers still need distinct ports."},{"heading":"the-local-app-opens-but-requests-fail","content":"For other database kits, check the ports in their local configuration when running more than one kit."},{"heading":"supabase-rejects-a-flag-or-env-sync-fails","content":"Run `pnpm --filter database exec supabase --version` and confirm the pinned CLI version and `[experimental] stack = true`. Stack commands use `--output-format json`; connection values are nested under `env`. Older sync scripts using `--output json` need updating, including copying the assigned API URL. The new engine doesn't support the old `stop --no-backup` cleanup flow; use `stop` to preserve data."},{"heading":"supabase-rejects-a-flag-or-env-sync-fails","content":"If native startup fails, check the supported platforms and download requirements. Use Docker on Windows or Intel Macs. A project keeps its first runtime; starting an existing Docker project with `--runtime native` does not convert it."},{"heading":"a-docker-image-cant-be-pulled","content":"Check the image tag in that kit's Compose file. An unavailable optional storage image can prevent an all-services startup command from finishing. Update the tag to one you can pull, or start the database and email services you need while you configure storage separately."},{"heading":"verification-emails-dont-arrive","content":"Check the sender, domain verification and email provider in the place your auth backend reads from. Supabase Auth uses project SMTP settings. Convex Auth reads deployment variables. Better Auth uses the application's server or Worker configuration."},{"heading":"verification-emails-dont-arrive","content":"For local testing, check the local inbox first. See emails with Resend."},{"heading":"login-returns-to-the-wrong-page","content":"Check the canonical app URL and auth callback allowlist. An MCP login should return to `/mcp/authorize` with the original pending request preserved."},{"heading":"login-returns-to-the-wrong-page","content":"If the request expired while you were signing in, restart the connection from your agent. Pending consent requests are short-lived."},{"heading":"the-mcp-endpoint-returns-401","content":"Opening an MCP URL without an agent token should return an authentication challenge. That is expected."},{"heading":"the-mcp-endpoint-returns-401","content":"If an approved agent gets 401, check that it is using the correct user/admin endpoint. The original sign-in session may have ended, the connection may be revoked, or the account may have lost access. Sign in and reconnect."},{"heading":"there-is-no-workspace-to-select","content":"Create or join a workspace in the website first. Your membership also needs permission to view projects. The consent page only offers workspaces the account can use."},{"heading":"the-admin-connection-returns-403","content":"Check the application's admin role. Being a workspace owner doesn't grant application-wide administration. Use the trusted admin setup for that kit."},{"heading":"the-agent-cant-create-a-project","content":"Only the Convex user MCPs have `projects_create` in v5. They need explicit `user:write` consent and current project creation permission. All six Supabase, Neon and D1 MCPs are read-only."},{"heading":"the-client-never-gets-to-consent","content":"Check that it supports public dynamic client registration and PKCE S256. Confidential clients and client ID metadata documents aren't supported yet."},{"heading":"the-client-never-gets-to-consent","content":"On Convex, copy the `.convex.site` MCP address from connection settings. The frontend website address is not its backend MCP address."},{"heading":"the-d1-app-fails-intermittently","content":"Check Worker logs for CPU limits and confirm the production database binding. The Next.js demo needed Workers Paid for its auth workload. A build that contains a placeholder D1 ID needs to be rebuilt with the production environment."}],"headings":[{"id":"the-repository-cant-be-cloned","content":"The repository can't be cloned"},{"id":"the-local-app-opens-but-requests-fail","content":"The local app opens, but requests fail"},{"id":"supabase-rejects-a-flag-or-env-sync-fails","content":"Supabase rejects a flag or env sync fails"},{"id":"a-docker-image-cant-be-pulled","content":"A Docker image can't be pulled"},{"id":"verification-emails-dont-arrive","content":"Verification emails don't arrive"},{"id":"login-returns-to-the-wrong-page","content":"Login returns to the wrong page"},{"id":"the-mcp-endpoint-returns-401","content":"The MCP endpoint returns 401"},{"id":"there-is-no-workspace-to-select","content":"There is no workspace to select"},{"id":"the-admin-connection-returns-403","content":"The admin connection returns 403"},{"id":"the-agent-cant-create-a-project","content":"The agent can't create a project"},{"id":"the-client-never-gets-to-consent","content":"The client never gets to consent"},{"id":"the-d1-app-fails-intermittently","content":"The D1 app fails intermittently"}]},"url":"/docs/v5/troubleshooting","title":"Troubleshooting","description":"A few things to check when setup, login or an agent connection gets stuck."},{"_id":"/docs/v5/workspaces-and-admin","structured":{"contents":[{"content":"A workspace is where your users work together. Projects belong to a workspace, and the app checks a user's membership before returning its data."},{"content":"Let's say you have a workspace for your design team and another for your customers. Connecting an agent to the design workspace should only give it access to that workspace's projects."},{"heading":"user-connections","content":"A User MCP connection selects one workspace during consent. The app checks the account, session and current workspace access on every tool call."},{"heading":"user-connections","content":"To connect to another workspace, create another connection and select that workspace. Changing a workspace ID in a tool request doesn't give the agent extra access."},{"heading":"workspace-owners-and-application-administrators","content":"A workspace owner manages a workspace. An application administrator manages the application across users and workspaces. Those are different roles."},{"heading":"workspace-owners-and-application-administrators","content":"Owning a workspace doesn't automatically allow an account to connect to the Admin MCP. Admin connections require the application's current administrator role."},{"heading":"granting-admin-access","content":"Use the admin setup documented in your kit's repository. The exact mechanism differs: Supabase uses trusted application metadata, Convex uses its application admin checks, and Better Auth kits keep admin roles in their existing auth data."},{"heading":"granting-admin-access","content":"Assign the role through trusted server-side administration. A normal account should not be able to make itself an administrator by editing its profile."},{"heading":"admin-mcp","content":"The Admin MCP can list users and workspaces across the application. All eight admin MCPs are read-only in v5."},{"heading":"admin-mcp","content":"An admin token is accepted only at the admin resource. Use a separate User MCP connection when the same administrator wants to work with one workspace's projects."},{"heading":"admin-mcp","content":"See tools and permissions for the exact tool names and scopes."}],"headings":[{"id":"user-connections","content":"User connections"},{"id":"workspace-owners-and-application-administrators","content":"Workspace owners and application administrators"},{"id":"granting-admin-access","content":"Granting admin access"},{"id":"admin-mcp","content":"Admin MCP"}]},"url":"/docs/v5/workspaces-and-admin","title":"Workspaces and the admin area","description":"Understand workspace access before you connect a user or administrator agent."}]