feat: whyrating - initial project from turbostarter boilerplate

This commit is contained in:
Alejandro Gutiérrez
2026-02-04 01:54:52 +01:00
commit 5cdc07cd39
1618 changed files with 338230 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
---
title: Overview
description: Get started with the admin dashboard in TurboStarter.
url: /docs/web/admin/overview
---
# Overview
TurboStarter ships with a fully functional admin dashboard - it's a comprehensive tool for managing your application and users from one central place.
The panel is designed to be intuitive and easy to use, while being customizable and scalable at the same time. You can access it at [/admin](http://localhost:3000/admin).
![Admin Dashboard](/images/docs/web/admin/home.png)
## Roles and permissions
With the initial configuration, your app has two roles available to users: `user` and `admin`. By default, all users are created with the `user` role.
To access the admin dashboard, a user must have the `admin` permission.
```ts
const UserRole = {
USER: "user",
ADMIN: "admin",
} as const;
```
You can, of course, define more roles and assign granular permissions, but we recommend keeping the number of roles to a minimum.
## Making a user an admin
To promote a user to the admin role, use your database provider's UI or leverage our built-in [Studio](/docs/web/database/overview#studio). After you find the user you want to promote, change their role from `user` to `admin`.
**Ensure the user you are promoting truly requires admin privileges, as they will gain access to all resources and permissions.**
<Callout title="Recommendations">
To determine whether a user is eligible for the `admin` role, review the following recommendations before promoting the user:
* The user's email is verified
* Two-factor authentication (2FA) is enabled
* The user is **not** banned or reported
</Callout>
<Callout title="Testing locally">
By default, when you [run services](/docs/web/installation/commands#setting-up-services) for the first time, your database is [seeded](/docs/web/installation/commands#seeding-database) with example data. This includes an admin user with test credentials that you can use to test admin functionality locally.
```json
{
"email": "me+admin@turbostarter.dev",
"password": "Pa$$w0rd"
}
```
You can modify these by setting the `SEED_EMAIL` and `SEED_PASSWORD` environment variables in the `.env.local` file and running the seed process again.
**This flow is for local testing purposes only. Do not use it in production.**
</Callout>
## Dashboard
The admin dashboard is your **central place** to manage your application. It includes management tools for each resource you have defined.
Users with the `admin` permission will see an additional dropdown item in the navigation menu, allowing them to access the admin dashboard.
![Admin access through the navigation menu](/images/docs/web/admin/user-navigation.png)
Explore each section of the page below to familiarize yourself with the available tools and options.

View File

@@ -0,0 +1,115 @@
---
title: Super Admin UI
description: Get familiar with the Super Admin dashboard and start managing your application.
url: /docs/web/admin/ui
---
# Super Admin UI
When you open [/admin](http://localhost:3000/admin), you will see the homepage of the admin dashboard. It includes some quick actions and a summary of the resources you have in your application. Feel free to customize it to your needs.
To simplify navigation, we also shipped a sidebar that you can use to navigate between different sections and access all admin capabilities.
![Super Admin UI](/images/docs/web/admin/home.png)
Check below for more details about each section.
## Users
Central place to manage your users. You can see the list of users, search and filter them e.g. by role, 2FA, banned state, and created date.
Use it to quickly find users that you need to manage or to see how your SaaS is performing.
![Users](/images/docs/web/admin/users.png)
When you click on a user, you will see the user details. You can edit the user's name and role, view the user's 2FA status, and see the user's created/updated timestamps.
You can also see and manage the resources related to this specific user like user's connected accounts/providers, subscriptions, memberships, etc.
![User](/images/docs/web/admin/user.png)
Beyond simply viewing user information, the admin dashboard enables you to perform a variety of essential user management actions, including:
* **Impersonate the user**: Temporarily log in as the selected user to troubleshoot their experience, verify permissions, or offer assistance directly from their perspective.
* **Ban or unban the user**: Restrict access to your application by banning users who violate terms of service, or lift restrictions when appropriate by unbanning them.
* **Delete the user**: Permanently remove a user and any associated data from your system when necessary, such as for GDPR compliance or at user request.
These administrative actions help you maintain a secure, compliant, and user-friendly environment for your SaaS platform.
## Organizations
See how your multi-tenancy is performing in an elegant way presented as a data table. You can search and filter organizations by name, slug, member count and many more.
![Organizations](/images/docs/web/admin/organizations.png)
In the single organization view, you can get an overview of the specified organization, e.g see its members or invitations that are associated with it.
![Organization](/images/docs/web/admin/organization.png)
Here are some example actions you can perform when managing an organization:
* **Edit organization details**: Change the organization name, slug, or other profile information.
* **Invite or remove members**: Add new users to an organization or revoke access from existing members.
* **Change member roles**: Promote a member to an admin or downgrade their access.
* **View and manage invitations**: See pending invites and revoke them if needed.
* **Delete organization**: Remove an organization and all its related data (action usually restricted to super admins).
* **Impersonate organization admin**: Temporarily assume the perspective of an organization's admin for troubleshooting.
* **Audit activity**: View a history of actions taken within the organization for security and compliance.
These actions help you maintain control over multi-tenant environments and ensure that your SaaS remains secure and organized.
## Customers
Manage your customers and their subscriptions. Use search, filters, and sorting to quickly find the right record and understand billing state at a glance.
![Customers](/images/docs/web/admin/customers.png)
A few example actions you can perform when managing a customer:
* **Open a customer** to view subscription details and billing history.
* **Change subscription plan** or move a customer to a different tier.
* **Start or extend a trial**, or **cancel a subscription** when needed.
* **Update billing details** like billing email and tax information.
* **Delete customer** to remove them and their billing profile (restricted action).
## Add your own resources
Its your admin panel at the end of the day - extend it with any domainspecific resources your product needs. The UI ships with reusable table, filter, form, and layout primitives so you can compose new sections quickly.
To make CRUD panels fast to build, we also provide dedicated hooks, UI components, and API helpers that handle the boring plumbing - data fetching, pagination, sorting, filters, and mutations — so you can focus on your domain logic instead of boilerplate.
<Steps>
<Step>
### Start from an example
Duplicate an existing resource (like `Users` or `Organizations`) as a baseline and adjust the schema/columns to your needs.
</Step>
<Step>
### Build the list view
Compose a data table with columns, sorting, fulltext search, and filters using the shipped primitives.
Leverage the dedicated hooks, UI components, and API helpers to handle fetching, pagination, sorting, filters, and mutations with minimal boilerplate.
</Step>
<Step>
### Add a details view
Create a singleresource page and, if helpful, add tabs for related entities (e.g., memberships, invoices) using the same building blocks.
</Step>
<Step>
### Wire up navigation
Register your route in the admin sidebar so the new resource appears alongside the builtins.
</Step>
<Step>
### Secure with permissions
Protect access using your RBAC rules and feature flags to control who can view or manage the resource.
</Step>
</Steps>
Et voilà! You now have a new resource in your admin panel 🥳

View File

@@ -0,0 +1,94 @@
---
title: Configuration
description: Configure AI integration in your TurboStarter project.
url: /docs/web/ai/configuration
---
# Configuration
To ensure scalability and avoid security vulnerabilities, AI requests are proxied by our [Hono backend](/docs/web/api/overview). This means you need to set up AI integration on both the client and server side.
<Callout title="Why proxy requests?">
We want to avoid exposing API keys directly to the browser, as this could lead to abuse of your key and generate unnecessary costs.
</Callout>
In this section, we'll explore the configuration for both sides to give you a smooth start.
## Server-side
On the backend, you need to set up two things: environment variables to configure the provider and the procedure to pass responses to the client. Let's go through it!
### Environment variables
You need to set the environment variables that correspond to the AI provider you want to use.
For example, for the OpenAI provider, you would need to set the following environment variables:
```dotenv
OPENAI_API_KEY=<your-openai-api-key>
```
However, if you want to use the Anthropic provider, you would need to set these environment variables:
```dotenv
ANTHROPIC_API_KEY=<your-anthropic-api-key>
```
You can find the list of all available providers in the [official documentation](https://sdk.vercel.ai/providers/ai-sdk-providers), along with the required variables that need to be set to ensure the integration works correctly.
### API endpoint
As we're proxying the requests, we need to register an [API endpoint](/docs/web/api/new-endpoint) that will be used to pass the responses to the client.
The steps will be the same as we described in the [API](/docs/web/api/new-endpoint) section. An example implementation could look like this:
```ts title="ai/router.ts"
export const aiRouter = new Hono().post("/chat", async (c) =>
streamText({
model: openai.responses("gpt-5"),
messages: convertToModelMessages((await c.req.json()).messages),
}).toUIMessageStreamResponse(),
);
```
As you can see, we're defining which provider and specific model we want to use here.
We're also using [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Concepts), which allows us to pass the result to the user as soon as the model starts generating it, without needing to wait for the full response to be completed. This gives the user a sense of immediacy and makes the conversation more interactive.
## Client-side
To consume the server response, we can leverage the ready-to-use hooks provided by the [Vercel AI SDK](https://sdk.vercel.ai/docs/ai-sdk-ui/chatbot), such as the `useChat` hook:
```tsx title="page.tsx"
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
const AI = () => {
const { messages } = useChat({
transport: new DefaultChatTransport({
api: "/api/ai/chat",
}),
});
return (
<div>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return <div key={`${message.id}-${i}`}>{part.text}</div>;
}
})}
</div>
))}
</div>
);
};
export default AI;
```
By leveraging this integration, we can easily manage the state of the AI request and update the UI as soon as the response is ready.
TurboStarter ships with a ready-to-use implementation of AI chat, allowing you to see this solution in action. Feel free to reuse or modify it according to your needs.

View File

@@ -0,0 +1,45 @@
---
title: Overview
description: Get started with AI integration in your TurboStarter project.
url: /docs/web/ai/overview
---
# Overview
For AI integration, TurboStarter leverages the [Vercel AI SDK](https://sdk.vercel.ai/docs/introduction), which provides a comprehensive set of tools and utilities to help you build AI applications more easily and efficiently.
<Callout title="Why Vercel AI SDK?">
It's a simple yet powerful library that exposes a unified API for all major AI providers.
This allows you to build your AI application without worrying about the intricacies of each underlying provider's API.
</Callout>
You can learn more about the `ai` package in the [official documentation](https://sdk.vercel.ai/docs/introduction).
## Features
The starter comes with the most common AI features built-in, such as:
* **Chat**: Build chat applications with ease.
* **Streaming responses**: Stream responses from your AI provider in real-time.
* **Image generation**: Generate images using AI technology.
* **Embeddings**: Generate embeddings for your data.
* **Vector stores**: Store and query your embeddings efficiently.
You can easily compose your application using these building blocks or extend them to suit your specific needs.
## Providers
**TurboStarter relies on the AI SDK to provide support for various AI providers.**
This means you can easily switch between different AI providers without changing your code, as long as they are supported by the `ai` package.
You can find the list of supported providers in the [official documentation](https://sdk.vercel.ai/providers/ai-sdk-providers).
<Callout title="Custom providers">
There is also the possibility to add your own custom provider. It just needs to implement the common interface and provide all the necessary methods.
Read more about this in the [official guide](https://sdk.vercel.ai/providers/community-providers/custom-providers).
</Callout>
The configuration for each provider is straightforward and simple. We'll explore this in more detail in the [Configuration](/docs/web/ai/configuration) section.

View File

@@ -0,0 +1,409 @@
---
title: Configuration
description: Learn how to configure web analytics in TurboStarter.
url: /docs/web/analytics/configuration
---
# Configuration
The `@turbostarter/analytics-web` package offers a streamlined and flexible approach to tracking events in your TurboStarter web app using various analytics providers. It abstracts the complexities of different analytics services and provides a consistent interface for event tracking.
In this section, we'll guide you through the configuration process for each supported provider.
Note that the configuration is validated against a schema, so you'll see error messages in the console if anything is misconfigured.
## Providers
TurboStarter supports multiple analytics providers, each with its own unique configuration. Below, you'll find detailed information on how to set up and use each supported provider. Choose the one that best suits your needs and follow the instructions in the respective accordion section.
<Accordions>
<Accordion title="Vercel Analytics" id="vercel">
To use Vercel Analytics as your provider, you need to [create a Vercel account](https://vercel.com/) and [set up a project](https://vercel.com/docs/projects/overview).
Next, enable analytics in your Vercel project settings:
1. Navigate to the [Vercel dashboard](https://vercel.com/dashboard).
2. Select your project.
3. Go to the *Analytics* section.
4. Click *Enable* in the dialog.
<Callout>
Enabling Web Analytics will add new routes (scoped at `/_vercel/insights/*`) after your next deployment.
</Callout>
Also, make sure to activate the Vercel provider as your analytics provider by updating the exports in:
<Tabs items={["index.tsx", "server.ts", "env.ts"]}>
<Tab value="index.tsx">
```ts
// [!code word:vercel]
export * from "./vercel";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:vercel]
export * from "./vercel/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:vercel]
export * from "./vercel/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/analytics/web/src/providers/vercel` directory.
For more information, please refer to the [Vercel Analytics documentation](https://vercel.com/docs/analytics/overview).
![Vercel Analytics dashboard](/images/docs/web/analytics/vercel.avif)
</Accordion>
<Accordion title="Google Analytics" id="google-analytics">
To use Google Analytics as your analytics provider, you need to [create a Google Analytics account](https://analytics.google.com/) and [set up a property](https://support.google.com/analytics/answer/9304153).
Next, add a data stream in your Google Analytics account settings:
1. Navigate to [Google Analytics](https://analytics.google.com/).
2. In the *Admin* section, under *Data collection and modification*, click on *Data Streams*.
3. Click *Add stream*.
4. Select *Web* as the platform.
5. Enter the required details for the stream (at minimum, provide a name and website URL).
6. Click *Create stream*.
After creating the stream, you'll need two pieces of information:
1. Your [Measurement ID](https://support.google.com/analytics/answer/12270356) (it should look like `G-XXXXXXXXXX`):
![Google Analytics Measurement ID](/images/docs/web/analytics/google/id.png)
2. Your [Measurement Protocol API secret](https://support.google.com/analytics/answer/9814495):
![Google Analytics Measurement Protocol API secret](/images/docs/web/analytics/google/api-secret.png)
Set these values in your `.env.local` file in the `apps/web` directory and in your deployment environment:
```dotenv
NEXT_PUBLIC_ANALYTICS_GOOGLE_MEASUREMENT_ID="your-measurement-id"
GOOGLE_ANALYTICS_SECRET="your-measurement-protocol-api-secret"
```
Also, make sure to activate the Google Analytics provider as your analytics provider by updating the exports in:
<Tabs items={["index.tsx", "server.ts", "env.ts"]}>
<Tab value="index.tsx">
```ts
// [!code word:google-analytics]
export * from "./google-analytics";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:google-analytics]
export * from "./google-analytics/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:google-analytics]
export * from "./google-analytics/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/analytics/web/src/providers/google-analytics` directory.
For more information, please refer to the [Google Analytics documentation](https://developers.google.com/analytics).
![Google Analytics dashboard](/images/docs/web/analytics/google/dashboard.jpg)
</Accordion>
<Accordion title="PostHog" id="posthog">
<Callout title="You can also use it for monitoring!">
PostHog is also one of pre-configured providers for [monitoring](/docs/web/monitoring/overview) in TurboStarter. You can learn more about it [here](/docs/web/monitoring/posthog).
</Callout>
To use PostHog as your analytics provider, you need to configure a PostHog instance. You can obtain the [Cloud](https://app.posthog.com/signup) instance by [creating an account](https://app.posthog.com/signup) or [self-host](https://posthog.com/docs/self-host) it.
Then, create a project and, based on your [project settings](https://app.posthog.com/project/settings), fill the following environment variables in your `.env.local` file in `apps/web` directory and your deployment environment:
```dotenv
NEXT_PUBLIC_POSTHOG_KEY="your-posthog-api-key"
NEXT_PUBLIC_POSTHOG_HOST="your-posthog-instance-host"
```
Also, make sure to activate the PostHog provider as your analytics provider by updating the exports in:
<Tabs items={["index.tsx", "server.ts", "env.ts"]}>
<Tab value="index.tsx">
```ts
// [!code word:posthog]
export * from "./posthog";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:posthog]
export * from "./posthog/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:posthog]
export * from "./posthog/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/analytics/web/src/providers/posthog` directory.
For more information, please refer to the [PostHog documentation](https://posthog.com/docs).
![PostHog dashboard](/images/docs/web/analytics/posthog.png)
</Accordion>
<Accordion title="Mixpanel" id="mixpanel">
To use Mixpanel as your analytics provider, you need to [create an account](https://mixpanel.com/) and [obtain your project token](https://help.mixpanel.com/hc/en-us/articles/115004502806-Find-Project-Token).
Then, set it as an environment variable in your `.env.local` file in the `apps/web` directory and your deployment environment:
```dotenv
NEXT_PUBLIC_MIXPANEL_TOKEN="your-project-token"
```
Also, make sure to activate the Mixpanel provider as your analytics provider by updating the exports in:
<Tabs items={["index.tsx", "server.ts", "env.ts"]}>
<Tab value="index.tsx">
```ts
// [!code word:mixpanel]
export * from "./mixpanel";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:mixpanel]
export * from "./mixpanel/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:mixpanel]
export * from "./mixpanel/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/analytics/web/src/providers/mixpanel` directory.
For more information, please refer to the [Mixpanel documentation](https://docs.mixpanel.com/).
![Mixpanel dashboard](/images/docs/web/analytics/mixpanel.png)
</Accordion>
<Accordion title="Plausible" id="plausible">
To use Plausible as your analytics provider, you need to [create an account](https://plausible.io/) and [set up a website](https://plausible.io/docs/add-website).
Then, set your domain and host in your `.env.local` file in the `apps/web` directory and your deployment environment:
```dotenv
NEXT_PUBLIC_PLAUSIBLE_DOMAIN="your-website-domain.com"
NEXT_PUBLIC_PLAUSIBLE_HOST="https://plausible.io"
```
Also, make sure to activate the Plausible provider as your analytics provider by updating the exports in:
<Tabs items={["index.tsx", "server.ts", "env.ts"]}>
<Tab value="index.tsx">
```ts
// [!code word:plausible]
export * from "./plausible";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:plausible]
export * from "./plausible/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:plausible]
export * from "./plausible/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/analytics/web/src/providers/plausible` directory.
For more information, please refer to the [Plausible documentation](https://plausible.io/docs).
![Plausible dashboard](/images/docs/web/analytics/plausible.png)
</Accordion>
<Accordion title="Umami" id="umami">
To use Umami as your analytics provider, you need to [set up Umami](https://umami.is/docs/getting-started) either by using their [cloud service](https://cloud.umami.is/) or [self-hosting](https://umami.is/docs/install).
Then, set your website ID and host in your `.env.local` file in the `apps/web` directory and your deployment environment:
```dotenv
NEXT_PUBLIC_UMAMI_WEBSITE_ID="your-website-id"
NEXT_PUBLIC_UMAMI_HOST="https://your-umami-instance.com"
UMAMI_API_HOST="https://your-umami-instance.com"
UMAMI_API_KEY="your-api-key"
```
Also, make sure to activate the Umami provider as your analytics provider by updating the exports in:
<Tabs items={["index.tsx", "server.ts", "env.ts"]}>
<Tab value="index.tsx">
```ts
// [!code word:umami]
export * from "./umami";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:umami]
export * from "./umami/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:umami]
export * from "./umami/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/analytics/web/src/providers/umami` directory.
For more information, please refer to the [Umami documentation](https://umami.is/docs).
![Umami dashboard](/images/docs/web/analytics/umami.jpg)
</Accordion>
<Accordion title="Open Panel" id="open-panel">
To use Open Panel as your analytics provider, you need to [create an account](https://openpanel.dev/) and [set up a client for your project](https://docs.openpanel.dev/docs).
Then, you would need to set your client ID and secret in your `.env.local` file in `apps/web` directory and your deployment environment:
```dotenv
NEXT_PUBLIC_OPEN_PANEL_CLIENT_ID="your-client-id"
OPEN_PANEL_CLIENT_SECRET="your-client-secret"
```
Also, make sure to activate the Open Panel provider as your analytics provider by updating the exports in:
<Tabs items={["index.tsx", "server.ts", "env.ts"]}>
<Tab value="index.tsx">
```ts
// [!code word:open-panel]
export * from "./open-panel";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:open-panel]
export * from "./open-panel/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:open-panel]
export * from "./open-panel/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/analytics/web/src/providers/open-panel` directory.
For more information, please refer to the [Open Panel documentation](https://docs.openpanel.dev/).
![Open Panel dashboard](/images/docs/web/analytics/open-panel.webp)
</Accordion>
<Accordion title="Vemetric" id="vemetric">
To use Vemetric as your analytics provider, you need to [create an account](https://vemetric.com/) and [obtain your project token](https://vemetric.com/docs/).
Then, set it as an environment variable in your `.env.local` file in the `apps/web` directory and your deployment environment:
```dotenv
NEXT_PUBLIC_VEMETRIC_PROJECT_TOKEN="your-project-token"
```
Also, make sure to activate the Vemetric provider as your analytics provider by updating the exports in:
<Tabs items={["index.tsx", "server.ts", "env.ts"]}>
<Tab value="index.tsx">
```ts
// [!code word:vemetric]
export * from "./vemetric";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:vemetric]
export * from "./vemetric/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:vemetric]
export * from "./vemetric/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/analytics/web/src/providers/vemetric` directory.
For more information, please refer to the [Vemetric documentation](https://vemetric.com/docs/).
![Vemetric dashboard](/images/docs/web/analytics/vemetric.webp)
</Accordion>
</Accordions>
## Client-side context
To enable tracking events, capturing page views and other analytics features **on the client-side**, you need to wrap your app with the `Provider` component that's implemented by every provider and available through the `@turbostarter/analytics-web` package:
```tsx title="providers.tsx"
// [!code word:AnalyticsProvider]
import { memo } from "react";
import { Provider as AnalyticsProvider } from "@turbostarter/analytics-web";
interface ProvidersProps {
readonly children: React.ReactNode;
}
export const Providers = memo<ProvidersProps>(({ children }) => {
return (
<OtherProviders>
<AnalyticsProvider>{children}</AnalyticsProvider>
</OtherProviders>
);
});
Providers.displayName = "Providers";
```
By implementing this setup, you ensure that all analytics events are properly tracked from your client-side code. This configuration allows you to safely utilize the [Analytics API](/docs/web/analytics/tracking) within your client components, enabling comprehensive event tracking and data collection.

View File

@@ -0,0 +1,35 @@
---
title: Overview
description: Get started with web analytics in TurboStarter.
url: /docs/web/analytics/overview
---
# Overview
TurboStarter comes with built-in analytics support for multiple providers as well as a unified API for tracking events. This API enables you to easily and consistently track user behavior and app usage across your SaaS application.
## Providers
The starter implements multiple providers for managing analytics. To learn more about each provider and how to configure them, see their respective sections:
<Cards>
<Card title="Vercel Analytics" href="/docs/web/analytics/configuration#vercel" />
<Card title="Google Analytics" href="/docs/web/analytics/configuration#google-analytics" />
<Card title="PostHog" href="/docs/web/analytics/configuration#posthog" />
<Card title="Mixpanel" href="/docs/web/analytics/configuration#mixpanel" />
<Card title="Plausible" href="/docs/web/analytics/configuration#plausible" />
<Card title="Umami" href="/docs/web/analytics/configuration#umami" />
<Card title="Open Panel" href="/docs/web/analytics/configuration#open-panel" />
<Card title="Vemetric" href="/docs/web/analytics/configuration#vemetric" />
</Cards>
All configuration and setup is built-in with a unified API, allowing you to switch between providers by simply changing the exports. You can even introduce your own provider without breaking any tracking-related logic.
In the following sections, we'll cover how to set up each provider and how to track events in your application.

View File

@@ -0,0 +1,139 @@
---
title: Tracking events
description: Learn how to track events in your TurboStarter web app.
url: /docs/web/analytics/tracking
---
# Tracking events
The implementation strategy for each analytics provider varies depending on whether it's designed for client-side or server-side use. We'll explore both approaches, as they are crucial for ensuring accurate and comprehensive analytics data in your web SaaS application.
## Client-side tracking
The client strategy for tracking events, which every provider must implement, is straightforward:
```ts
export type AllowedPropertyValues = string | number | boolean;
type TrackFunction = (
event: string,
data?: Record<string, AllowedPropertyValues>,
) => void;
export interface AnalyticsProviderClientStrategy {
Provider: ({ children }: { children: React.ReactNode }) => React.ReactNode;
track: TrackFunction;
}
```
<Callout>
You don't need to worry much about this implementation, as all the providers are already configured for you. However, it's useful to be aware of this structure if you plan to add your own custom provider.
</Callout>
As shown above, each provider must supply two key elements:
1. `Provider` - a component that [wraps your app](/docs/web/analytics/configuration#client-side-context).
2. `track` - a function responsible for sending event data to the provider.
To track an event, you simply need to invoke the `track` method, passing the event name and an optional data object:
```tsx
import { track } from "@turbostarter/analytics-web";
export const MyComponent = () => {
return (
<button onClick={() => track("button.click", { country: "US" })}>
Track event
</button>
);
};
```
## Identifying users
Linking events to specific users enables you to build a full picture of how they're using your product across different sessions, devices, and platforms.
For identification purposes, the client strategy can also expose `identify` and `reset` methods. They are optional and only needed if you want to identify users in your app and associate their actions with a specific user ID.
Not all analytics providers support user identification (for example, [Vercel Analytics](/docs/web/analytics/configuration#vercel) and [Plausible](/docs/web/analytics/configuration#plausible)), so make sure your chosen provider exposes these methods before using them.
```ts
type IdentifyFunction = (
userId: string,
traits?: Record<string, AllowedPropertyValues>,
) => void;
export interface AnalyticsProviderClientStrategy {
identify: IdentifyFunction;
reset: () => void;
}
```
To identify users on the client, call the `identify` function, passing the user's ID and an optional traits object:
```tsx
import { identify } from "@turbostarter/analytics-web";
identify("user-123", { name: "John Doe" });
```
This will associate all future events with the user's ID, allowing you to track user behavior and gain valuable insights into your application's usage patterns.
<Callout title="Configured by default!">
The `identify` method is configured out-of-the-box to react to changes in the user's authentication state.
When the user is authenticated, the `identify` method will be called with the user's ID and traits. When the user is logged out, the `reset` method will be called to clear the existing user identification.
</Callout>
## Server-side tracking
The server strategy for tracking events that every provider has to implement is even simpler:
```ts
export interface AnalyticsProviderServerStrategy {
track: TrackFunction;
}
```
<Callout>
You don't need to worry much about this implementation, as all the providers are already configured for you. However, it's useful to be aware of this structure if you plan to add your own custom provider.
</Callout>
This server-side strategy allows you to track events outside of the browser environment, which is particularly useful for scenarios involving server actions or React Server Components.
To track an event on the server side, simply call the `track` method, providing the event name and an optional data object:
```tsx
// [!code word:server]
import { track } from "@turbostarter/analytics-web/server";
track("button.click", {
country: "US",
region: "California",
});
```
<Callout type="error" title="Ensure correct import!">
Make sure to use the correct import for the `track` function. We're using the same name for both client and server tracking, but they are different functions. For server-side, just add `/server` to the import path (`@turbostarter/analytics-web/server`).
<Tabs items={["Client-side", "Server-side"]}>
<Tab value="Client-side">
```tsx
import { track } from "@turbostarter/analytics-web";
```
</Tab>
<Tab value="Server-side">
```tsx
// [!code word:server]
import { track } from "@turbostarter/analytics-web/server";
```
</Tab>
</Tabs>
</Callout>
<Callout title="Identifying users on the server" type="warn">
On the server, there are no dedicated identification helpers like `identify` or `reset`. Most providers that support user-level tracking expect you to pass an identifier or traits directly within the `track` call (for example, as a `userId` or similar property), so make sure to check your specific provider's documentation for the recommended way to include user information.
</Callout>
Congratulations! You've now mastered event tracking in your TurboStarter web app. With this knowledge, you're well-equipped to analyze user behaviors and gain valuable insights into your application's usage patterns. Happy analyzing! 📊

View File

@@ -0,0 +1,163 @@
---
title: Using API client
description: How to use API client to interact with the API.
url: /docs/web/api/client
---
# Using API client
In Next.js, you can access the API client in two ways:
* **server-side**: in server components and API routes
* **client-side**: in client components
When you create a new page and want to fetch data, you have flexibility in where to make the API calls. Server Components are great for initial data loading since the fetching happens during server-side rendering, eliminating an extra client-server round trip. The data is then efficiently streamed to the client.
By default in Next.js, every component is a Server Component. You can opt into client-side rendering by adding the `use client` directive at the top of a component file. Client Components are useful when you need interactive features or want to fetch data based on user interactions. While they're initially server-rendered, they're also hydrated and rendered on the client, allowing you to make API calls directly from the browser.
Let's explore both approaches to understand their differences and use cases.
## Server-side
We're creating a server-side API client inside `apps/web/src/lib/api/server.ts` file. The client automatically handles passing authentication headers from the user's session to secure API endpoints.
It's pre-configured with all the necessary setup, so you can start using it right away without any additional configuration.
Then, there is nothing simpler than calling the API from your server component:
```tsx title="page.tsx"
import { api } from "~/lib/api/server";
export default async function MyServerComponent() {
const response = await api.posts.$get();
const posts = await response.json();
/* do something with the data... */
return <div>{JSON.stringify(posts)}</div>;
}
```
<Card title="Next.js - Server components" description="nextjs.org" href="https://nextjs.org/docs/app/building-your-application/rendering/server-components" />
## Client-side
We're creating a separate client-side API client in `apps/web/src/lib/api/client.tsx` file. It's a simple wrapper around the [@tanstack/react-query](https://tanstack.com/query/latest/docs/framework/react/overview) that fetches or mutates data from the API.
It also requires wrapping your app in a `QueryClientProvider` component to provide the query client to the rest of the app:
```tsx title="layout.tsx"
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<QueryClientProvider>{children}</QueryClientProvider>
</body>
</html>
);
}
```
Of course, it's all already configured for you, so you just need to start using `api` in your client components:
```tsx title="page.tsx"
"use client";
import { api } from "~/lib/api/client";
export default function MyClientComponent() {
const { data: posts, isLoading } = useQuery({
queryKey: ["posts"],
queryFn: async () => {
const response = await api.posts.$get();
if (!response.ok) {
throw new Error("Failed to fetch posts!");
}
return response.json();
},
});
if (isLoading) {
return <div>Loading...</div>;
}
/* do something with the data... */
return <div>{JSON.stringify(posts)}</div>;
}
```
<Card title="Next.js - Client components" description="nextjs.org" href="https://nextjs.org/docs/app/building-your-application/rendering/client-components" />
<Callout type="warn" title="Ensure correct API url">
Inside the `apps/web/src/lib/api/utils.ts` we're calling a function to get base url of your api, so make sure it's set correctly (especially on production) and your API endpoint is corresponding with the name there.
```tsx title="utils.ts"
export const getBaseUrl = () => {
if (typeof window !== "undefined") return window.location.origin;
if (env.NEXT_PUBLIC_URL) return env.NEXT_PUBLIC_URL;
if (env.VERCEL_URL) return `https://${env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
};
```
As you can see we're mostly relying on the [environment variables](/docs/web/configuration/environment-variables) to get it, so there shouldn't be any issues with it, but in case, please be aware where to find it 😉
</Callout>
## Handling responses
As you can see in the examples above, the [Hono RPC](https://hono.dev/docs/guides/rpc) client returns a plain `Response` object, which you can use to get the data or handle errors. However, implementing this handling in every query or mutation can be tedious and will introduce unnecessary boilerplate in your codebase.
That's why we've developed the `handle` function that unwraps the response for you, handles errors, and returns the data in a consistent format. You can safely use it with any procedure from the API client:
<Tabs items={["Server-side", "Client-side"]}>
<Tab value="Server-side">
```tsx
// [!code word:handle]
import { handle } from "@turbostarter/api/utils";
import { api } from "~/lib/api/server";
export default async function MyServerComponent() {
const posts = await handle(api.posts.$get)();
/* do something with the data... */
return <div>{JSON.stringify(posts)}</div>;
}
```
</Tab>
<Tab value="Client-side">
```tsx
// [!code word:handle]
"use client";
import { handle } from "@turbostarter/api/utils";
import { api } from "~/lib/api/client";
export default function MyClientComponent() {
const { data: posts, isLoading } = useQuery({
queryKey: ["posts"],
queryFn: handle(api.posts.$get),
});
if (isLoading) {
return <div>Loading...</div>;
}
/* do something with the data... */
return <div>{JSON.stringify(posts)}</div>;
}
```
</Tab>
</Tabs>
With this approach, you can focus on the business logic instead of repeatedly writing code to handle API responses in your browser extension components, making your extension's codebase more readable and maintainable.
The same error handling and response unwrapping benefits apply whether you're building web, mobile, or extension interfaces - allowing you to keep your data fetching logic consistent across all platforms.

View File

@@ -0,0 +1,54 @@
---
title: Internationalization
description: Learn how to localize and translate your API.
url: /docs/web/api/internationalization
---
# Internationalization
Since TurboStarter provides fully featured [internationalization](/docs/web/internationalization/overview) out of the box, you can easily localize not only the frontend but also the API layer. This can be useful when you need to fetch localized data from the database or send emails in different languages.
Let's explore possibilities of this feature.
## Request-based localization
To get the locale for the current request, you can leverage the `localize` middleware:
```ts title="email/router.ts"
const emailRouter = new Hono().get("/", localize, (c) => {
const locale = c.var.locale;
// do something with the locale
});
```
Inside it, we're setting the `locale` variable in the current request context, making it available to the procedure.
## Error handling
When handling errors in an internationalized API, you'll want to ensure error messages are properly translated for your users. TurboStarter provides built-in support for localizing error messages using error codes and a special `onError` hook.
That's why it's recommended to use error codes instead of direct messages in your throw statements:
```ts
throw new HttpException(HttpStatusCode.UNAUTHORIZED, {
code: "auth:error.unauthorized",
/* 👇 optional */
message: "You are not authorized to access this resource.",
});
```
The error code will then be used to retrieve the localized message, and the returned response from your API will look like this:
```json
{
"code": "auth:error.unauthorized",
/* 👇 localized based on request's locale */
"message": "You are not authorized to access this resource.",
"path": "/api/auth/login",
"status": 401,
"timestamp": "2024-01-01T00:00:00.000Z"
}
```
Then, you can either use the returned code to get the localized message in your frontend, or simply use the returned message as is.

View File

@@ -0,0 +1,131 @@
---
title: Mutations
description: Learn how to mutate data on the server.
url: /docs/web/api/mutations
---
# Mutations
As we saw in [adding new endpoint](/docs/web/api/new-endpoint#maybe-mutation), mutations allow us to modify data on the server, like creating, updating, or deleting resources. They can be defined similarly to queries using our API client.
Just like queries, mutations can be executed either server-side or client-side depending on your needs. Let's explore both approaches.
## Server actions
Next.js provides [server actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) as a powerful way to handle mutations directly on the server. They're particularly well-suited for form submissions and other data modifications.
Using our `api` client with server actions is straightforward - you simply call the API function on the server.
Here's an example of how you can define an action to create a new post:
<Tabs items={["With helper", "Without helper"]}>
<Tab value="With helper">
```tsx
// [!code word:handle]
"use server";
import { revalidatePath } from "next/cache";
import { handle } from "@turbostarter/api/utils";
import { api } from "~/lib/api/server";
export async function createPost(post: PostInput) {
try {
await handle(api.posts.$post)(post);
} catch (error) {
onError(error);
}
revalidatePath("/posts");
}
```
</Tab>
<Tab value="Without helper">
```tsx
"use server";
import { revalidatePath } from "next/cache";
import { api } from "~/lib/api/server";
export async function createPost(post: PostInput) {
const response = await api.posts.$post(post);
if (!response.ok) {
return { error: "Failed to create post" };
}
revalidatePath("/posts");
}
```
</Tab>
</Tabs>
In the above example we're also using `revalidatePath` to revalidate the path `/posts` to fetch the updated list of posts.
<Cards>
<Card title="Server actions and mutation" description="nextjs.org" href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations" />
<Card title="revalidatePath" description="nextjs.org" href="https://nextjs.org/docs/app/api-reference/functions/revalidatePath" />
</Cards>
## useMutation hook
On the other hand, if you want to perform a mutation on the client-side, you can use the `useMutation` hook that comes straight from the integration with [React Query](https://tanstack.com/query).
<Tabs items={["With helper", "Without helper"]}>
<Tab value="With helper">
```tsx
// [!code word:handle]
import { handle } from "@turbostarter/api/utils";
import { api } from "~/lib/api/react";
export function CreatePost() {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: handle(api.posts.$post),
onSuccess: () => {
toast.success("Post created successfully!");
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
return <form onSubmit={...} />;
}
```
</Tab>
<Tab value="Without helper">
```tsx
import { api } from "~/lib/api/react";
export function CreatePost() {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: async (post: PostInput) => {
const response = await api.posts.$post(post);
if (!response.ok) {
throw new Error("Failed to create post!");
}
},
onSuccess: () => {
toast.success("Post created successfully!");
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
return <form onSubmit={...} />;
}
```
</Tab>
</Tabs>
<Cards>
<Card title="useMutation hook" description="tanstack.com" href="https://tanstack.com/query/latest/docs/framework/react/reference/useMutation" />
<Card title="Query invalidation" description="tanstack.com" href="https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation" />
</Cards>

View File

@@ -0,0 +1,105 @@
---
title: Adding new endpoint
description: How to add new endpoint to the API.
url: /docs/web/api/new-endpoint
---
# Adding new endpoint
To define a new API endpoint, you can either extend an existing entity (e.g. add new customer route) or create a new, separate module.
<Steps>
<Step>
## Create new module
To create a new module you can create a new folder in the `modules` folder. For example `modules/posts`.
Then you would need to create a router declaration for this module. We're following a convention with the filename describing its purpose, so you would need to create a file named `router.ts` in the `modules/posts` folder.
```typescript title="modules/posts/router.ts"
import { Hono } from "hono";
import { validate } from "../../middleware";
export const postsRouter = new Hono().get(
"/",
validate("query", filtersSchema),
(c) => getAllPosts(c.req.valid("query")),
);
```
As you can see we're implementing a `.get` method without any additional middlewares for the router. This is a simple way to define a new GET endpoint.
Also, we're using a [zod](https://zod.dev/) validator to ensure that input passed to the endpoint is correct.
</Step>
<Step>
### Maybe mutation?
The same way you can define a mutation for the new entity, just by changing the `get` to `post`:
```ts title="modules/posts/router.ts"
// [!code word:.post]
export const postsRouter = new Hono().post(
"/",
enforceAuth,
validate("json", postSchema),
(c) => createPost(c.req.valid("json")),
);
```
Hono supports all [HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods), so you can define a new endpoint for any method you need (e.g. `put`, `delete`, etc.).
The `enforceAuth` middleware ensures that only authenticated users can access the endpoint, while the zod validator checks if the input data matches the expected schema. This combination provides both authentication and data validation in a single, clean setup.
[Read more about protected routes](/docs/web/api/protected-routes).
</Step>
<Step>
## Implement logic
Then you would need to create a controller for this module. There is a place, where the logic happens, e.g. for the `GET /` endpoint we would need to create a `getAllPosts` function which will fetch posts from the database.
```typescript title="modules/posts/queries.ts"
import { db } from "@turbostarter/db/server";
import { posts } from "@turbostarter/db/schema";
export const getAllPosts = (filters: Filters) => {
return db.select().from(posts).all().where(/* your filter logic here */);
};
```
</Step>
<Step>
## Register router
To make the module and its endpoints available in the API you need to register a router for this module in the `index.ts` file:
```ts title="index.ts"
import { postsRouter } from "./modules/posts/router";
const appRouter = new Hono()
.basePath("/api")
.route("/posts", postsRouter)
/* other routers from your app logic */
.onError(onError);
type AppRouter = typeof appRouter;
export type { AppRouter };
export { appRouter };
```
The `basePath` method sets a prefix for all routes in this router. While optional, using it helps organize API endpoints. This modular approach makes the API structure clearer and easier to maintain.
</Step>
</Steps>
That's it! You've just created a new API endpoint - it's now available at `/api/posts` 🎉
<Callout title="It's fully type-safe!">
By exporting the `AppRouter` type you get fully type-safe RPC calls in the
client. It's important because without producing a huge amount of code, we're
fully type-safe from the frontend code. It helps avoid passing incorrect data
to the procedure and streamline consuming returned types without a need to
define these types by hand.
</Callout>

View File

@@ -0,0 +1,52 @@
---
title: Overview
description: Get started with the API.
url: /docs/web/api/overview
---
# Overview
TurboStarter is designed to be a scalable and production-ready full-stack starter kit. One of its core features is a dedicated and extensible API layer. To enable this in a type-safe manner, we chose [Hono](https://hono.dev) as the API server and client library.
<Callout title="Why Hono?">
Hono is a small, simple, and ultrafast web framework that gives you a way to
define your API endpoints with full type safety. It provides built-in
middleware for common needs like validation, caching, and CORS.
It also
includes an [RPC client](https://hono.dev/docs/guides/rpc) for making
type-safe function calls from the frontend. Being edge-first, it's optimized
for serverless environments and offers excellent performance.
</Callout>
All API endpoints and their resolvers are defined in the `packages/api` package. Here you will find a `modules` folder that contains the different feature modules of the API. Each module has its own folder and exports all its resolvers.
For each module, we create a separate Hono router and then aggregate all sub-routers into one main router in the `index.ts` file.
The API is then exposed as a route handler that will be provided as a Next.js API route:
```ts title="apps/web/src/app/api/[...route]/route.ts"
import { handle } from "hono/vercel";
import { appRouter } from "@turbostarter/api";
const handler = handle(appRouter);
export {
handler as GET,
handler as POST,
handler as OPTIONS,
handler as PUT,
handler as PATCH,
handler as DELETE,
handler as HEAD,
};
```
<Callout type="warn" title="API availability">
The API is a part of web, serverless Next.js app. It means that you **must**
deploy it to use the API in other apps (e.g. mobile app, browser extension),
even if you don't need web app itself. It's very simple, as you're just
deploying the Next.js app and the API is just a part of it.
</Callout>
Learn more about API in the following sections:

View File

@@ -0,0 +1,124 @@
---
title: Protected routes
description: Learn how to protect your API routes.
url: /docs/web/api/protected-routes
---
# Protected routes
Hono has built-in support for [middlewares](https://hono.dev/docs/guides/middleware), which are functions that can be used to modify the context or execute code before or after a route handler is executed.
That's how we can secure our API endpoints from unauthorized access. Below are some examples of you can leverage middlewares to protect your API routes.
## Authenticated access
After validating the user's authentication status, we store their data in the context using [Hono's built-in context](https://hono.dev/docs/api/context). This allows us to access the user's information in subsequent middleware and procedures without having to re-validate the session.
Here's an example of middleware that validates whether the user is currently logged in and stores their data in the context:
```ts title="middleware.ts"
export const enforceAuth = createMiddleware<{
Variables: {
user: User;
};
}>(async (c, next) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
const user = session?.user ?? null;
if (!user) {
throw new HTTPException(HttpStatusCode.UNAUTHORIZED, {
message: "You need to be logged in to access this feature!",
});
}
c.set("user", user);
await next();
});
```
Then we can use our defined middleware to protect endpoints by adding it before the route handler:
```ts title="billing/router.ts"
export const billingRouter = new Hono().get(
"/customer",
enforceAuth,
async (c) => c.json(await getCustomerByUserId(c.var.user.id)),
);
```
## Role-based access
In most cases, you will want to restrict access to certain endpoints based on the user's role.
You can achieve this by creating a middleware that will check if the user has the required role and then pass the execution to the next middleware or procedure.
E.g. for admin endpoints we want to ensure that the user has the `admin` role:
```ts title="middleware.ts"
export const enforceAdmin = createMiddleware<{
Variables: {
user: User;
};
}>(async (c, next) => {
const user = c.var.user;
if (!hasAdminPermission(user)) {
throw new HttpException(HttpStatusCode.FORBIDDEN, {
message: "You need to be an admin to access this feature!",
});
}
await next();
});
```
Then we can use our defined middleware to protect endpoints by adding it before the route handler:
```ts title="admin/router.ts"
export const adminRouter = new Hono().get(
"/users",
enforceAuth,
enforceAdmin,
(c) => c.json(...),
);
```
## Feature-based access
When developing your API you may want to restrict access to certain features based on the user's current subscription plan. (e.g. only users with "Pro" plan can access teams).
You can achieve this by creating a middleware that will check if the user has access to the feature and then pass the execution to the next middleware or procedure:
```ts title="middleware.ts"
export const enforceFeatureAvailable = (feature: Feature) =>
createMiddleware<{
Variables: {
user: User;
};
}>(async (c, next) => {
const { data: customer } = await getCustomerById(c.var.user.id);
const hasFeature = isFeatureAvailable(customer, feature);
if (!hasFeature) {
throw new HTTPException(HttpStatusCode.PAYMENT_REQUIRED, {
message: "Upgrade your plan to access this feature!",
});
}
await next();
});
```
Use it within your procedure the same way as we did with `enforceAuth` middleware:
```ts title="teams/router.ts"
export const teamsRouter = new Hono().get(
"/",
enforceAuth,
enforceFeatureAvailable(FEATURES.PRO.TEAMS),
(c) => c.json(...),
);
```
These are just examples of what you can achieve with Hono middlewares. You can use them to add any kind of logic to your API (e.g. [logging](https://hono.dev/docs/middleware/builtin/logger), [caching](https://hono.dev/docs/middleware/builtin/cache), etc.)

View File

@@ -0,0 +1,111 @@
---
title: Two-Factor Authentication (2FA)
description: Add an extra layer of security with two-factor authentication.
url: /docs/web/auth/2fa
---
# Two-Factor Authentication (2FA)
TurboStarter uses [Better Auth's 2FA plugin](https://www.better-auth.com/docs/plugins/2fa) to provide multi-factor authentication (MFA) capabilities. Two-factor authentication adds an extra layer of security by requiring users to provide a second form of verification alongside their password.
## Available methods
TurboStarter supports multiple 2FA verification methods through Better Auth:
* **TOTP (Time-based One-Time Password)** - codes generated by authenticator apps
* **OTP (One-Time Password)** - codes sent via email or SMS
* **Backup codes** - single-use recovery codes for account recovery
You can use any TOTP-compatible authenticator app, such as:
* [Google Authenticator](https://support.google.com/accounts/answer/1066447)
* [Authy](https://authy.com/)
* [Microsoft Authenticator](https://www.microsoft.com/en-us/security/mobile-authenticator-app)
* [1Password](https://1password.com/features/authenticator/)
* [Bitwarden](https://bitwarden.com/help/authenticator-keys/)
## Enabling 2FA
<Steps>
<Step>
### Enable in settings
Users enable two-factor authentication in their account security settings.
![Enable 2FA](/images/docs/web/auth/two-factor/enable.png)
</Step>
<Step>
### Setup authenticator
A QR code is displayed for users to scan with their authenticator app.
![Setup authenticator](/images/docs/web/auth/two-factor/authenticator-app.png)
</Step>
<Step>
### Verify setup
Users enter a verification code from their authenticator to confirm setup.
</Step>
<Step>
### Backup codes
Users receive single-use backup codes for account recovery.
![Backup codes](/images/docs/web/auth/two-factor/backup-codes.png)
</Step>
</Steps>
<Callout type="info">
Recovery codes are essential for account recovery if users lose access to
their authenticator device. Make sure to educate users about safely storing
their backup codes.
</Callout>
## Using 2FA
<Steps>
<Step>
### Sign in normally
Users enter their email and password or other methods as usual.
</Step>
<Step>
### 2FA prompt
After successful password verification, users are prompted for their 2FA code.
![2FA prompt](/images/docs/web/auth/two-factor/sign-in-prompt.png)
</Step>
<Step>
### Enter verification code
Users input the 6-digit code from their authenticator app.
</Step>
<Step>
### Access granted
Upon successful verification, users gain access to their account.
</Step>
</Steps>
### Trusted devices
Users can mark devices as trusted during 2FA verification. Trusted devices won't require 2FA verification for 60 days, providing a balance between security and convenience.
## Configuration
2FA is configured through Better Auth's plugin system. The plugin handles:
* Secure secret generation and storage
* QR code generation for authenticator setup
* TOTP code validation
* Backup code generation and management
* Trusted device management
For detailed implementation instructions, refer to the [Better Auth 2FA documentation](https://www.better-auth.com/docs/plugins/2fa).

View File

@@ -0,0 +1,138 @@
---
title: Configuration
description: Configure authentication for your application.
url: /docs/web/auth/configuration
---
# Configuration
TurboStarter supports multiple different authentication methods:
* **Password** - the traditional email/password method
* **Magic Link** - passwordless email link authentication
* **Passkey** - passkeys as an alternative to passwords
* **Anonymous** - guest mode for unauthenticated users
* **OAuth** - OAuth providers, [Apple](https://www.better-auth.com/docs/authentication/apple), [Google](https://www.better-auth.com/docs/authentication/google) and [Github](https://www.better-auth.com/docs/authentication/github) are set up by default
All authentication methods are enabled by default, but you can easily customize them to your needs. You can enable or disable any method, and configure them according to your requirements.
<Callout>
Remember that you can mix and match these methods or add new ones - for
example, you can have both password and magic link authentication enabled at
the same time, giving your users more flexibility in how they authenticate.
</Callout>
Authentication configuration can be customized through a simple configuration file. The following sections explain the available options and how to configure each authentication method based on your requirements.
## API
The **server-side** authentication configuration is set at `packages/auth/src/server.ts`. It confgures [Better Auth](https://better-auth.com) package to use the correct providers and settings:
```ts title="server.ts"
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendResetPassword: () => {},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: () => {},
},
database: drizzleAdapter(db, {
provider: "pg",
schema,
}),
plugins: [
magicLink({
sendMagicLink: () => {},
}),
passkey(),
anonymous(),
expo(),
nextCookies(),
],
socialProviders: {
[SocialProvider.APPLE]: {
clientId: env.APPLE_CLIENT_ID,
clientSecret: env.APPLE_CLIENT_SECRET,
appBundleIdentifier: env.APPLE_APP_BUNDLE_IDENTIFIER,
},
[SocialProvider.GOOGLE]: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
[SocialProvider.GITHUB]: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
},
},
/* other configuration options */
});
```
The configuration is validated against Better Auth's schema at runtime, providing immediate feedback if any settings are incorrect or insecure. This validation ensures your authentication setup remains robust and properly configured.
All authentication routes and handlers are centralized within the [Hono API](/docs/web/api/overview), giving you a single source of truth and complete control over the authentication flow. This centralization makes it easier to maintain, debug, and customize the authentication process as needed.
[Read more about it in the official documentation](https://www.better-auth.com/docs/basic-usage).
## UI
We have separate configuration that determines what is displayed to your users in the **UI**. It's set at `apps/web/config/auth.ts`.
The recommendation is to **not update this directly** - instead, please define the environment variables and override the default behavior.
```ts title="apps/web/config/auth.ts"
import env from "env.config";
import { SocialProvider, authConfigSchema } from "@turbostarter/auth";
import type { AuthConfig } from "@turbostarter/auth";
export const authConfig = authConfigSchema.parse({
providers: {
password: env.NEXT_PUBLIC_AUTH_PASSWORD,
magicLink: env.NEXT_PUBLIC_AUTH_MAGIC_LINK,
passkey: env.NEXT_PUBLIC_AUTH_PASSKEY,
anonymous: env.NEXT_PUBLIC_AUTH_ANONYMOUS,
oAuth: [SocialProvider.APPLE, SocialProvider.GOOGLE, SocialProvider.GITHUB],
},
}) satisfies AuthConfig;
```
The configuration is also validated using the Zod schema, so if something is off, you'll see the errors.
For example, if you want to switch from password to magic link, you'd change the following environment variables:
```dotenv title=".env.local"
NEXT_PUBLIC_AUTH_PASSWORD=false
NEXT_PUBLIC_AUTH_MAGIC_LINK=true
```
To display third-party providers in the UI, you need to set the `oAuth` array to include the provider you want to display. The default is Apple, Google and Github:
```tsx title="apps/web/config/auth.ts"
providers: {
...
oAuth: [SocialProvider.APPLE, SocialProvider.GOOGLE, SocialProvider.GITHUB],
...
},
```
## Third party providers
To enable third-party authentication providers, you'll need to:
1. Set up an OAuth application in the provider's developer console (like [Apple Developer Portal](https://developer.apple.com/account/), [Google Cloud Console](https://console.cloud.google.com/), [Github Developer Settings](https://github.com/settings/developers) or any other provider you want to use)
2. Configure the corresponding environment variables in your TurboStarter application
Each OAuth provider requires its own set of credentials and environment variables. Please refer to the [Better Auth documentation](https://better-auth.com/docs/concepts/oauth) for detailed setup instructions for each supported provider.
<Callout title="Multiple environments">
Make sure to set both development and production environment variables
appropriately. Your OAuth provider may require different callback URLs for
each environment.
</Callout>

View File

@@ -0,0 +1,53 @@
---
title: User flow
description: Discover the authentication flow in Turbostarter.
url: /docs/web/auth/flow
---
# User flow
TurboStarter ships with a fully functional authentication system. Most of the views and components are preconfigured and easily customizable to your needs.
Here you will find a quick walkthrough of the authentication flow.
## Sign up
The sign-up page is where users can create an account. They need to provide their email address and password.
![Sign up](/images/docs/web/auth/sign-up.png)
Once successful, users are asked to confirm their email address. This is enabled by default - and due to security reasons, it's not possible to disable it.
<Callout type="warn" title="Sending authentication emails">
Make sure to configure the [email provider](/docs/web/emails/configuration) together with the [auth hooks](/docs/web/emails/sending#authentication-emails) to be able to send emails from your app.
</Callout>
![Confirm email](/images/docs/web/auth/confirm-email.png)
## Sign in
The sign-in page is where users can log in to their account. They need to provide their email address and password, use magic link (if enabled) or third-party providers.
![Sign in](/images/docs/web/auth/sign-in.png)
## Sign out
The sign out button is located in the user menu.
![Sign out](/images/docs/web/auth/sign-out.png)
## Forgot password
The forgot password page is where users can reset their password. They need to provide their email address and follow the instructions in the email.
![Forgot password](/images/docs/web/auth/forgot-password.png)
The reset password page is where users land from a forgot email. There they can reset their password by providing new password and confirming it.
![Reset password](/images/docs/web/auth/update-password.png)
## Two-factor authentication
Two-factor authentication is a security feature that requires users to provide a code sent to their email or phone number in addition to their password when logging in.
![Two-factor authentication](/images/docs/web/auth/two-factor/sign-in-prompt.png)

View File

@@ -0,0 +1,56 @@
---
title: OAuth
description: Get started with social authentication.
url: /docs/web/auth/oauth
---
# OAuth
Better Auth supports over **30** (!) different [OAuth providers](https://www.better-auth.com/docs/concepts/oauth). They can be easily configured and enabled in the kit without any additional configuration needed.
<Callout title="Everything configured out of the box!">
TurboStarter provides you with all the configuration required to handle OAuth providers responses from your app:
* redirects
* middleware
* confirmation API routes
You just need to configure one of the below providers on their side and set correct credentials as environment variables in your TurboStarter app.
</Callout>
![OAuth providers](/images/docs/web/auth/social-providers.png)
Third Party providers need to be configured, managed and enabled fully on the provider's side. TurboStarter just needs the correct credentials to be set as environment variables in your app and passed to the [authentication API configuration](/docs/web/auth/configuration#api).
To enable OAuth providers in your TurboStarter app, you need to:
1. Set up an OAuth application in the provider's developer console (like [Apple Developer Portal](https://developer.apple.com/account/), [Google Cloud Console](https://console.cloud.google.com/), [Github Developer Settings](https://github.com/settings/developers) or any other provider you want to use)
2. Configure the provider's credentials as environment variables in your app. For example, for Google OAuth:
```dotenv title="apps/web/.env.local"
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
```
Then, pass it to the authentication configuration in `packages/auth/src/server.ts`:
```ts title="server.ts"
export const auth = betterAuth({
...
socialProviders: {
[SocialProvider.GOOGLE]: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
},
...
});
```
<Callout title="Missing provider?">
Better Auth provides a [generic OAuth plugin](https://www.better-auth.com/docs/plugins/generic-oauth) that allows you to add any OAuth provider to your app.
It supports both OAuth 2.0 and OpenID Connect (OIDC) flows, allowing you to easily add social login or custom OAuth authentication to your application.
</Callout>

View File

@@ -0,0 +1,39 @@
---
title: Overview
description: Get started with authentication.
url: /docs/web/auth/overview
---
# Overview
TurboStarter uses [Better Auth](https://better-auth.com) to handle authentication. It's a secure, production-ready authentication solution that integrates seamlessly with many frameworks and provides enterprise-grade security out of the box.
<Callout title="Why Better Auth?">
One of the core principles of TurboStarter is to do things **as simple as possible**, and to make everything **as performant as possible**.
Better Auth provides an excellent developer experience with minimal configuration required, while maintaining enterprise-grade security standards. Its framework-agnostic approach and focus on performance makes it the perfect choice for TurboStarter.
Recently, Better Auth [announced](https://www.better-auth.com/blog/authjs-joins-better-auth) an incorporation of [Auth.js (27k+ stars on Github)](https://authjs.dev/), making it even more powerful and flexible.
</Callout>
![Better Auth](/images/docs/better-auth.png)
You can read more about Better Auth in the [official documentation](https://better-auth.com/docs).
TurboStarter supports multiple authentication methods:
* **Password** - the traditional email/password method
* **Magic Link** - magic links
* **Passkey** - passkeys ([WebAuthn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API))
* **Anonymous** - allowing users to proceed anonymously
* **OAuth** - OAuth social providers ([Apple](https://www.better-auth.com/docs/authentication/apple), [Google](https://www.better-auth.com/docs/authentication/google) and [Github](https://www.better-auth.com/docs/authentication/github) preconfigured)
As well as common applications flows, with ready-to-use views and components:
* **Sign in** - sign in with email/password or OAuth providers
* **Sign up** - sign up with email/password or OAuth providers
* **Sign out** - sign out
* **Password recovery** - forgot and reset password
* **Email verification** - verify email
You can construct your auth flow like LEGO bricks - plug in needed parts and customize them to your needs.

View File

@@ -0,0 +1,122 @@
---
title: Overview
description: Learn about background tasks & cron jobs and how they can power your application.
url: /docs/web/background-tasks/overview
---
# Overview
Background tasks and cron jobs are long-running processes that execute outside of your main application flow, allowing you to handle time-intensive operations and scheduled workflows without blocking user interactions or hitting serverless function timeouts.
<Callout title="Perfect for time-intensive & scheduled operations">
Background tasks are ideal for operations that take longer than typical serverless function timeouts (10-60 seconds), such as processing large files, sending batch emails, or making multiple API calls.
Cron jobs are perfect for recurring operations like daily reports, cleanup tasks, or periodic data synchronization.
</Callout>
## What are background tasks?
**Background tasks** are asynchronous processes that run separately from your main application thread. Instead of forcing users to wait for lengthy operations to complete, you can offload these tasks to run in the background while your application remains responsive.
**Cron jobs** are scheduled background tasks that run automatically at specific times or intervals. They're perfect for maintenance operations, reports, and recurring workflows that need to happen without user intervention.
Think of background tasks as your application's *worker threads* - they handle the heavy lifting while your main application stays fast and responsive for users.
<ThemedImage alt="Background tasks architecture diagram" light="/images/docs/web/background-tasks/light.png" dark="/images/docs/web/background-tasks/dark.png" width={1335} height={285} zoomable />
## Why use background tasks?
<Cards className="grid-cols-1">
<Card title="Avoid timeouts">
Most serverless platforms have strict execution limits:
* **[Vercel (Hobby)](https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration)**: 300 seconds
* **[Vercel (Pro)](https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration)**: 800 seconds
* **[Vercel (Enterprise)](https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration)**: 800 seconds
* **[AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html)**: 900 seconds
* **[Netlify Functions](https://docs.netlify.com/functions/overview/#default-deployment-options)**: 30 seconds
Background tasks let you bypass these limitations entirely.
</Card>
<Card title="Better user experience">
Users don't have to wait for long-running processes. They can continue using
your application while tasks complete in the background.
</Card>
<Card title="Automated workflows">
Cron jobs enable hands-off automation of recurring tasks like daily backups,
weekly reports, or monthly user engagement analysis - all running reliably
without manual intervention.
</Card>
<Card title="Improved reliability">
Background tasks can be automatically retried if they fail, ensuring your
critical processes eventually complete successfully.
</Card>
<Card title="Resource optimization">
Your main application servers stay available to handle user requests instead of being tied up with heavy processing tasks.
</Card>
</Cards>
## Common use cases
Here are some typical scenarios where background tasks shine:
<Accordions>
<Accordion title="File processing">
* **Video transcoding**: Converting uploaded videos to different formats or resolutions
* **Image optimization**: Batch processing user-uploaded images
* **Document parsing**: Extracting text from PDFs or generating thumbnails
</Accordion>
<Accordion title="Data operations">
* **Database migrations**: Moving or transforming large datasets
* **Report generation**: Creating complex analytics reports
* **Data synchronization**: Syncing data between different systems
</Accordion>
<Accordion title="Communication">
* **Email campaigns**: Sending personalized emails to large user lists
* **Notification processing**: Delivering push notifications across multiple platforms
* **SMS campaigns**: Bulk SMS sending with rate limiting
</Accordion>
<Accordion title="AI and ML tasks">
* **Content generation**: Using AI models to generate text, images, or videos
* **Data analysis**: Running machine learning models on large datasets
* **Natural language processing**: Analyzing text content for insights
</Accordion>
<Accordion title="Third-party integrations">
* **API synchronization**: Syncing data with external services
* **Webhook processing**: Handling incoming webhooks that trigger complex workflows
* **Social media automation**: Posting content across multiple platforms
</Accordion>
<Accordion title="Scheduled operations (Cron jobs)">
* **Daily reports**: Generating and emailing daily analytics or performance reports
* **Database maintenance**: Cleaning up old records, optimizing indexes, or running backups
* **User engagement**: Sending weekly newsletters or monthly account summaries
* **System monitoring**: Health checks, performance monitoring, and alert notifications
* **Content management**: Auto-publishing scheduled content or archiving old posts
</Accordion>
</Accordions>
## When not to use background tasks?
Background tasks and cron jobs aren't always the right solution. Consider alternatives for:
* **Real-time operations**: Tasks that users need immediate results from
* **Simple, fast operations**: Tasks that complete in under 5-10 seconds
* **Database queries**: Standard CRUD operations that should remain synchronous
* **User authentication**: Login/logout processes should be immediate
<Callout type="warn" title="Keep it simple">
Start with synchronous processing for simple tasks and manual processes for infrequent operations. Only move to background tasks when you hit timeout limitations or user experience issues, and only use cron jobs when you need reliable automation.
</Callout>
## Getting started
Ready to add background tasks to your TurboStarter application? Check out our [Trigger.dev integration guide](/docs/web/background-tasks/trigger) or [Upstash QStash integration guide](/docs/web/background-tasks/qstash) to learn how to implement background tasks using one of the most developer-friendly background job frameworks available.

View File

@@ -0,0 +1,648 @@
---
title: Upstash QStash
description: Integrate Upstash QStash with your TurboStarter application for serverless-first background task processing.
url: /docs/web/background-tasks/qstash
---
# Upstash QStash
[Upstash QStash](https://upstash.com/docs/qstash) is a serverless message queue and task scheduler designed specifically for serverless and edge environments. It uses HTTP endpoints instead of persistent connections, making it perfect for modern web applications.
<Callout title="Why QStash?">
QStash is built for the serverless world - no infrastructure to manage, automatic scaling, and pay-per-use pricing. It delivers messages to your HTTP endpoints with built-in retries, delays, and scheduling capabilities.
</Callout>
<Steps>
<Step>
## Setup
Visit [Upstash Console](https://console.upstash.com) and create a free account. Create a new QStash project and note down your credentials.
Add your QStash credentials to your root environment variables:
```dotenv title=".env.local"
QSTASH_URL=https://qstash.upstash.io
QSTASH_TOKEN=your_qstash_token_here
QSTASH_CURRENT_SIGNING_KEY=your_current_signing_key_here
QSTASH_NEXT_SIGNING_KEY=your_next_signing_key_here
```
You can find these values in your Upstash Console under the QStash project settings.
For production, make sure to add these environment variables to your deployment platform.
</Step>
<Step>
## Install dependencies
Add the QStash SDK to your API package:
```bash
pnpm add --filter api @upstash/qstash
```
</Step>
<Step>
## Create the QStash client
Create a utility file to initialize the QStash client in your API package:
```ts title="packages/api/src/lib/qstash.ts"
import { Client } from "@upstash/qstash";
import { env } from "~/env";
export const qstashClient = new Client({
baseUrl: env.QSTASH_URL,
token: env.QSTASH_TOKEN,
});
```
</Step>
<Step>
## Create task handlers
QStash delivers messages to HTTP endpoints, so you'll create API routes to handle your background tasks.
Let's create task handlers for common operations:
<Tabs items={["Task router", "Process user data", "Daily cleanup", "Verification middleware"]}>
<Tab value="Task router">
```ts title="packages/api/src/modules/tasks/router.ts"
import { Hono } from "hono";
import * as z from "zod";
import { qstashVerifyMiddleware } from "../../middleware/qstash-verify";
import { dailyCleanupHandler } from "./handlers/daily-cleanup";
import { processUserDataHandler } from "./handlers/process-user-data";
const processUserDataSchema = z.object({
userId: z.string(),
operation: z.enum(["export", "analyze", "cleanup"]),
});
export const tasksRouter = new Hono()
.basePath("/tasks")
// Apply QStash signature verification to all task routes
.use(qstashVerifyMiddleware)
.post("/process-user-data", processUserDataHandler)
.post("/daily-cleanup", dailyCleanupHandler);
```
</Tab>
<Tab value="Process user data">
```ts title="packages/api/src/modules/tasks/handlers/process-user-data.ts"
import type { Context } from "hono";
import * as z from "zod";
const ProcessUserDataSchema = z.object({
userId: z.string(),
operation: z.enum(["export", "analyze", "cleanup"]),
});
export async function processUserDataHandler(c: Context) {
try {
const payload = ProcessUserDataSchema.parse(await c.req.json());
const { userId, operation } = payload;
console.log("Starting user data processing", { userId, operation });
switch (operation) {
case "export":
// Simulate data export
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log("User data exported successfully");
return c.json({
success: true,
result: "Data exported to CSV",
});
case "analyze":
// Simulate data analysis
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log("User data analysis completed");
return c.json({
success: true,
result: { totalActions: 156, avgSessionTime: "4m 32s" },
});
case "cleanup":
// Simulate data cleanup
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("User data cleanup completed");
return c.json({
success: true,
result: "Removed 23 obsolete records",
});
default:
throw new Error(`Unknown operation: ${operation}`);
}
} catch (error) {
console.error("Task failed:", error);
return c.json({ error: "Task failed" }, 500);
}
}
```
</Tab>
<Tab value="Daily cleanup">
```ts title="packages/api/src/modules/tasks/handlers/daily-cleanup.ts"
import type { Context } from "hono";
export async function dailyCleanupHandler(c: Context) {
try {
console.log("Starting daily cleanup");
// Cleanup old logs
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log("Logs cleaned up");
// Cleanup temporary files
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Temp files cleaned up");
// Generate daily reports
await new Promise((resolve) => setTimeout(resolve, 8000));
console.log("Reports generated");
return c.json({
success: true,
cleanupTime: new Date().toISOString(),
itemsProcessed: 1247,
});
} catch (error) {
console.error("Daily cleanup failed:", error);
return c.json({ error: "Daily cleanup failed" }, 500);
}
}
```
</Tab>
<Tab value="Verification middleware">
```ts title="packages/api/src/middleware/qstash-verify.ts"
import { Receiver } from "@upstash/qstash";
import { createMiddleware } from "hono/factory";
export const qstashVerifyMiddleware = createMiddleware(async (c, next) => {
const currentSigningKey = process.env.QSTASH_CURRENT_SIGNING_KEY;
const nextSigningKey = process.env.QSTASH_NEXT_SIGNING_KEY;
if (!currentSigningKey || !nextSigningKey) {
return c.json({ error: "QStash signing keys not configured" }, 500);
}
const signature = c.req.header("upstash-signature");
if (!signature) {
return c.json({ error: "Missing QStash signature" }, 401);
}
try {
const body = await c.req.text();
const receiver = new Receiver({
currentSigningKey,
nextSigningKey,
});
const isValid = receiver.verify({
body,
signature,
});
if (!isValid) {
return c.json({ error: "Invalid QStash signature" }, 401);
}
// Re-create the request with the body for the next handler
const newRequest = new Request(c.req.url, {
method: c.req.method,
headers: c.req.headers,
body,
});
c.req = newRequest;
await next();
} catch (error) {
console.error("QStash signature verification failed:", error);
return c.json({ error: "Invalid signature" }, 401);
}
});
```
</Tab>
</Tabs>
</Step>
<Step>
## Register task routes
Add the tasks router to your main API:
```ts title="packages/api/src/index.ts"
import { tasksRouter } from "./modules/tasks/router";
const appRouter = new Hono()
.basePath("/api")
.route("/tasks", tasksRouter)
// ... other existing routers
.onError(onError);
export { appRouter };
```
</Step>
<Step>
## Triggering tasks
You can trigger tasks from your TurboStarter application by publishing messages to QStash, which will then deliver them to your task endpoints.
Create a service to handle task triggering:
```ts title="packages/api/src/modules/tasks/service.ts"
import { qstashClient } from "../../lib/qstash";
function getTaskUrl(taskName: string): string {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
return `${baseUrl}/api/tasks/${taskName}`;
}
export class TaskService {
static async processUserData(
userId: string,
operation: "export" | "analyze" | "cleanup",
) {
return await qstashClient.publishJSON({
url: getTaskUrl("process-user-data"),
body: { userId, operation },
});
}
static async scheduleUserDataProcessing(
userId: string,
operation: "export" | "analyze" | "cleanup",
delaySeconds: number,
) {
return await qstashClient.publishJSON({
url: getTaskUrl("process-user-data"),
body: { userId, operation },
delay: `${delaySeconds}s`,
});
}
static async scheduleDailyCleanup() {
return await qstashClient.schedules.create({
destination: getTaskUrl("daily-cleanup"),
cron: "0 2 * * *", // Daily at 2 AM
});
}
}
```
</Step>
<Step>
## Create API endpoints for triggering
Create endpoints to trigger tasks from your application:
```ts title="packages/api/src/modules/tasks/trigger/router.ts"
import { Hono } from "hono";
import * as z from "zod";
import { enforceAuth, validate } from "../../middleware";
import { TaskService } from "./service";
const triggerUserDataSchema = z.object({
userId: z.string(),
operation: z.enum(["export", "analyze", "cleanup"]),
delaySeconds: z.number().optional(),
});
export const taskTriggerRouter = new Hono()
.post(
"/trigger/process-user-data",
enforceAuth,
validate("json", triggerUserDataSchema),
async (c) => {
const { userId, operation, delaySeconds } = c.req.valid("json");
const result = delaySeconds
? await TaskService.scheduleUserDataProcessing(
userId,
operation,
delaySeconds,
)
: await TaskService.processUserData(userId, operation);
return c.json({
success: true,
messageId: result.messageId,
message: delaySeconds
? `Task scheduled to run in ${delaySeconds} seconds`
: "Task queued for immediate processing",
});
},
)
.post("/trigger/daily-cleanup", enforceAuth, async (c) => {
const result = await TaskService.scheduleDailyCleanup();
return c.json({
success: true,
scheduleId: result.scheduleId,
message: "Daily cleanup scheduled",
});
});
```
Add it to your main router:
```ts title="packages/api/src/index.ts"
import { taskTriggerRouter } from "./modules/tasks/trigger/router";
const appRouter = new Hono()
.basePath("/api")
.route("/tasks", tasksRouter)
.route("/", taskTriggerRouter) // Trigger routes at root level
// ... other existing routers
.onError(onError);
export { appRouter };
```
</Step>
<Step>
## Using tasks in your application
### From the client
```tsx title="apps/web/src/modules/tasks/process-data-button.tsx"
"use client";
import { handle } from "@turbostarter/api/utils";
import { useMutation } from "@tanstack/react-query";
import { api } from "~/lib/api/client";
export function ProcessDataButton({ userId }: { userId: string }) {
const { mutate: processData, isPending } = useMutation({
mutationFn: handle(api.trigger["process-user-data"].$post),
onSuccess: (data) => {
console.log("Task queued:", data.messageId);
},
});
return (
<button
onClick={() =>
processData({
json: {
userId,
operation: "analyze",
delaySeconds: 30, // Optional delay
},
})
}
disabled={isPending}
>
{isPending ? "Queueing..." : "Analyze User Data"}
</button>
);
}
```
### From a server action
```ts title="apps/web/src/app/actions/user-actions.ts"
"use server";
import { handle } from "@turbostarter/api/utils";
import { api } from "~/lib/api/server";
export async function processUserData(
userId: string,
operation: "export" | "analyze" | "cleanup",
) {
try {
const result = await handle(api.trigger["process-user-data"].$post)({
json: { userId, operation },
});
return {
success: true,
messageId: result.messageId,
};
} catch (error) {
console.error("Failed to queue background task:", error);
throw new Error("Failed to queue background task");
}
}
```
</Step>
</Steps>
## Advanced features
### Cron jobs & scheduling
QStash makes it easy to schedule recurring tasks:
```ts
// Schedule a task to run every day at 2 AM
await qstashClient.schedules.create({
destination: `${baseUrl}/api/tasks/daily-cleanup`,
cron: "0 2 * * *",
});
// Schedule a task to run every Monday at 9 AM
await qstashClient.schedules.create({
destination: `${baseUrl}/api/tasks/weekly-report`,
cron: "0 9 * * 1",
});
// One-time delayed task
await qstashClient.publishJSON({
url: `${baseUrl}/api/tasks/reminder`,
body: { userId: "123", type: "follow-up" },
delay: "3d", // 3 days from now
});
```
### Topics (Fanout pattern)
Create topics to send messages to multiple endpoints:
```ts
// Create a topic
await qstashClient.topics.upsert({
name: "user-events",
endpoints: [
{ url: `${baseUrl}/api/tasks/update-analytics` },
{ url: `${baseUrl}/api/tasks/send-notification` },
{ url: `${baseUrl}/api/tasks/update-crm` },
],
});
// Publish to topic - all endpoints will receive the message
await qstashClient.publishJSON({
topic: "user-events",
body: {
userId: "123",
event: "user-registered",
timestamp: new Date().toISOString(),
},
});
```
### Queues (Sequential processing)
Create queues for ordered task processing:
```ts
// Create a queue
const queue = qstashClient.queue({ queueName: "user-onboarding" });
// Add tasks to queue (they'll run in order)
await queue.enqueueJSON({
url: `${baseUrl}/api/tasks/send-welcome-email`,
body: { userId: "123" },
});
await queue.enqueueJSON({
url: `${baseUrl}/api/tasks/setup-user-profile`,
body: { userId: "123" },
});
await queue.enqueueJSON({
url: `${baseUrl}/api/tasks/trigger-onboarding-sequence`,
body: { userId: "123" },
});
```
## Monitoring and debugging
### QStash Dashboard
Visit the [Upstash Console](https://console.upstash.com) to monitor your tasks:
* **Message tracking**: See all messages, their status, and delivery attempts
* **Logs**: View detailed logs for each message delivery
* **Analytics**: Monitor throughput, success rates, and error patterns
* **Schedules**: Manage and monitor your cron jobs
* **Dead letter queue**: Handle messages that failed after all retries
### Local development
During development, you can:
1. **Use ngrok** for local testing:
```bash
# Install ngrok
npm install -g ngrok
# Expose your local server
ngrok http 3000
# Use the ngrok URL in your QStash configuration
```
2. **Check message delivery** in the Upstash Console
3. **Use console.log** in your task handlers for debugging
## Best practices
<Accordions>
<Accordion title="Always verify signatures">
Use the QStash signature verification middleware to ensure messages are authentic:
```ts
// ✅ Good - Always verify QStash signatures
.use(qstashVerifyMiddleware)
// ❌ Not secure - Accepting unverified requests
.post("/tasks/sensitive-operation", handler)
```
</Accordion>
<Accordion title="Handle errors gracefully">
Return appropriate HTTP status codes so QStash knows whether to retry:
```ts
// ✅ Good - Clear error handling
try {
await processTask(payload);
return c.json({ success: true });
} catch (error) {
console.error("Task failed:", error);
// 5xx = QStash will retry, 4xx = won't retry
return c.json({ error: "Task failed" }, 500);
}
```
</Accordion>
<Accordion title="Use idempotent operations">
Make your tasks safe to run multiple times in case of retries:
```ts
// ✅ Good - Check if work already done
const existingResult = await db.findProcessedResult(payload.id);
if (existingResult) {
return c.json({ success: true, result: existingResult });
}
// Proceed with processing...
```
</Accordion>
<Accordion title="Set appropriate timeouts">
Configure timeouts based on your expected processing time:
```ts
// For quick tasks
await qstashClient.publishJSON({
url: taskUrl,
body: payload,
timeout: "30s",
});
// For longer tasks
await qstashClient.publishJSON({
url: taskUrl,
body: payload,
timeout: "300s", // 5 minutes
});
```
</Accordion>
<Accordion title="Use structured logging">
Include relevant context in your logs:
```ts
console.log("Task started", {
taskType: "process-user-data",
userId: payload.userId,
operation: payload.operation,
timestamp: new Date().toISOString(),
});
```
</Accordion>
</Accordions>
## Next steps
With QStash integrated into your TurboStarter application, you can now:
* **Process background tasks** without worrying about serverless timeouts
* **Schedule recurring operations** with reliable cron job functionality
* **Handle high-volume messaging** with automatic retries and scaling
* **Build complex workflows** using topics, queues, and delays
Ready to explore more advanced features? Check out the official documentation for webhooks, batch operations, and advanced routing patterns.
<Cards>
<Card title="Documentation" description="upstash.com" href="https://upstash.com/docs/qstash" />
<Card title="Dashboard" description="console.upstash.com" href="https://console.upstash.com" />
</Cards>

View File

@@ -0,0 +1,505 @@
---
title: trigger.dev
description: Integrate trigger.dev with your TurboStarter application for reliable background task processing.
url: /docs/web/background-tasks/trigger
---
# trigger.dev
[trigger.dev](https://trigger.dev) is an open-source background jobs framework that lets you write reliable workflows in plain async code.
<Callout title="Why trigger.dev?">
trigger.dev provides automatic retries, real-time monitoring, and seamless scaling - all while letting you write background tasks in familiar JavaScript/TypeScript code directly in your TurboStarter project.
</Callout>
<Steps>
<Step>
## Setup
Visit [trigger.dev](https://trigger.dev) and create a free account. Create a new project and note down your API key.
Add your trigger.dev API key to your root environment variables:
```dotenv title=".env.local"
TRIGGER_SECRET_KEY=your_secret_key_here
```
For production, make sure to add the production API key to your deployment environment.
</Step>
<Step>
## Create a new package in your repository
You can use the [Turbo generator](/docs/web/customization/add-package) to quickly scaffold the package structure:
```bash
turbo gen package
```
When prompted, name your package `tasks`. This will create the basic structure for you.
Alternatively, create a new folder `tasks` in the `/packages` directory and add the following files:
<Tabs items={["package.json", "tsconfig.json", "trigger.config.ts"]}>
<Tab value="package.json">
```json
{
"name": "@turbostarter/tasks",
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"clean": "git clean -xdf .cache .turbo dist node_modules",
"dev": "pnpm dlx trigger.dev@latest dev",
"deploy": "pnpm dlx trigger.dev@latest deploy",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/sdk": "3.3.17"
},
"devDependencies": {
"@trigger.dev/build": "3.3.17",
"@turbostarter/eslint-config": "workspace:*",
"@turbostarter/prettier-config": "workspace:*",
"@turbostarter/tsconfig": "workspace:*",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:"
},
"prettier": "@turbostarter/prettier-config"
}
```
</Tab>
<Tab value="tsconfig.json">
```json
{
"extends": "@turbostarter/tsconfig/base.json",
"include": ["**/*.ts"],
"exclude": ["dist", "build", "node_modules"]
}
```
</Tab>
<Tab value="trigger.config.ts">
```ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "your_project_id", // Replace with your actual project ID
runtime: "node",
logLevel: "log",
maxDuration: 300,
dirs: ["./src/trigger"],
});
```
</Tab>
</Tabs>
</Step>
<Step>
## Create your first task
Now create your first task in the `packages/tasks/src/trigger` directory:
<Tabs items={["process-user-data.ts", "daily-cleanup.ts", "src/index.ts"]}>
<Tab value="process-user-data.ts">
```ts title="packages/tasks/src/trigger/process-user-data.ts"
import { task, logger, wait } from "@trigger.dev/sdk";
import * as z from "zod";
const ProcessUserDataSchema = z.object({
userId: z.string(),
operation: z.enum(["export", "analyze", "cleanup"]),
});
export const processUserDataTask = task({
id: "process-user-data",
run: async (payload: z.infer<typeof ProcessUserDataSchema>) => {
const { userId, operation } = payload;
logger.info("Starting user data processing", { userId, operation });
switch (operation) {
case "export":
await wait.for({ seconds: 2 });
logger.info("User data exported successfully");
return { success: true, result: "Data exported to CSV" };
case "analyze":
await wait.for({ seconds: 5 });
logger.info("User data analysis completed");
return {
success: true,
result: { totalActions: 156, avgSessionTime: "4m 32s" },
};
case "cleanup":
await wait.for({ seconds: 3 });
logger.info("User data cleanup completed");
return { success: true, result: "Removed 23 obsolete records" };
default:
throw new Error(`Unknown operation: ${operation}`);
}
},
});
```
</Tab>
<Tab value="daily-cleanup.ts">
```ts title="packages/tasks/src/trigger/daily-cleanup.ts"
import { schedules, task, logger, wait } from "@trigger.dev/sdk";
export const dailyCleanupTask = task({
id: "daily-cleanup",
run: async () => {
logger.info("Starting daily cleanup");
// Cleanup old logs
await wait.for({ seconds: 5 });
logger.info("Logs cleaned up");
// Cleanup temporary files
await wait.for({ seconds: 3 });
logger.info("Temp files cleaned up");
// Generate daily reports
await wait.for({ seconds: 8 });
logger.info("Reports generated");
return {
success: true,
cleanupTime: new Date().toISOString(),
itemsProcessed: 1247,
};
},
});
// Schedule the task to run daily at 2 AM
schedules.create({
task: "daily-cleanup",
cron: "0 2 * * *",
});
```
</Tab>
<Tab value="src/index.ts">
```ts title="packages/tasks/src/index.ts"
export * from "./trigger/process-user-data";
export * from "./trigger/daily-cleanup";
```
</Tab>
</Tabs>
</Step>
<Step>
## Test your task
You can test your tasks locally by running:
```bash
# Start the development server
pnpm --filter @turbostarter/tasks dev
```
This will deploy your tasks to trigger.dev in the development environment, allowing you to trigger them from the dashboard or programmatically.
</Step>
<Step>
## Deploy your tasks
To deploy your tasks to production on trigger.dev, run:
```bash
pnpm --filter @turbostarter/tasks deploy
```
You can also add this command as an automated deployment step in your CI/CD pipeline by creating a new GitHub action.
Add the `TRIGGER_ACCESS_TOKEN` secret to your repository secrets, which you can create in the trigger.dev dashboard.
```yml title=".github/workflows/deploy-tasks.yml"
name: Deploy to trigger.dev (prod)
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install
- name: Deploy trigger tasks
env:
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
run: |
pnpm --filter @turbostarter/tasks deploy
```
</Step>
<Step>
## Triggering tasks
You can trigger tasks from your TurboStarter application using the API layer.
<Callout type="warning" title="Direct task triggering not recommended">
While you can trigger tasks directly from your frontend or server components using the trigger.dev SDK, it's recommended to use the API layer approach shown below.
This provides better security, validation, and separation of concerns.
</Callout>
First, add the `@turbostarter/tasks` package as a dependency to your API package:
```json title="packages/api/package.json"
{
"dependencies": {
"@turbostarter/tasks": "workspace:*"
}
}
```
### From an API endpoint
Create a new API module to handle task triggering:
```ts title="packages/api/src/modules/tasks/tasks.router.ts"
import { tasks } from "@trigger.dev/sdk";
import { Hono } from "hono";
import * as z from "zod";
import type { processUserDataTask } from "@turbostarter/tasks";
import { enforceAuth, validate } from "../../middleware";
const processUserDataSchema = z.object({
userId: z.string(),
operation: z.enum(["export", "analyze", "cleanup"]),
});
export const tasksRouter = new Hono().post(
"/process-user-data",
enforceAuth,
validate("json", processUserDataSchema),
async (c) => {
const { userId, operation } = c.req.valid("json");
const handle = await tasks.trigger<typeof processUserDataTask>(
"process-user-data",
{ userId, operation },
);
return c.json({
success: true,
taskId: handle.id,
message: "Background task started successfully",
});
},
);
```
Then register it in your main API router:
```ts title="packages/api/src/index.ts"
import { tasksRouter } from "./modules/tasks/tasks.router";
const appRouter = new Hono()
.basePath("/api")
.route("/tasks", tasksRouter)
// ... other existing routers
.onError(onError);
export { appRouter };
```
### From the client
You can call the task endpoint from your web app using TurboStarter's API client:
```tsx title="apps/web/src/modules/tasks/process-data-button.tsx"
"use client";
import { handle } from "@turbostarter/api/utils";
import { useMutation } from "@tanstack/react-query";
import { api } from "~/lib/api/client";
export function ProcessDataButton({ userId }: { userId: string }) {
const { mutate: processData, isPending } = useMutation({
mutationFn: handle(api.tasks["process-user-data"].$post),
onSuccess: (data) => {
console.log("Task started:", data.taskId);
},
});
return (
<button
onClick={() =>
processData({
json: { userId, operation: "analyze" },
})
}
disabled={isPending}
>
{isPending ? "Processing..." : "Analyze User Data"}
</button>
);
}
```
### From a server action
```ts title="apps/web/src/app/actions/user-actions.ts"
"use server";
import { handle } from "@turbostarter/api/utils";
import { api } from "~/lib/api/server";
export async function processUserData(userId: string, operation: string) {
try {
const result = await handle(api.tasks["process-user-data"].$post)({
json: { userId, operation },
});
return {
success: true,
taskId: result.taskId,
};
} catch (error) {
console.error("Failed to trigger background task:", error);
throw new Error("Failed to start background task");
}
}
```
</Step>
</Steps>
## Monitoring and debugging
### Dashboard access
Visit the [trigger.dev dashboard](https://trigger.dev) to monitor your tasks:
* View task execution logs and performance metrics
* Track success and failure rates across all your tasks
* Monitor task duration and resource usage
* Replay failed tasks with a single click
* Set up alerts for task failures or performance issues
### Local development
During development, run your tasks locally while connected to trigger.dev:
```bash
# Start everything in the workspace
pnpm dev
# or start the tasks package only
pnpm --filter @turbostarter/tasks dev
```
This allows you to:
* Test tasks locally with real data
* Debug with breakpoints and console logs
* See immediate feedback as you develop
## Best practices
<Accordions>
<Accordion title="Use descriptive task IDs">
```ts
// ✅ Good - Clear and descriptive
id: "user-data-export-csv";
id: "weekly-newsletter-campaign";
id: "cleanup-temp-files";
// ❌ Not so good - Generic and unclear
id: "task1";
id: "job";
id: "process";
```
</Accordion>
<Accordion title="Include proper error handling">
```ts
run: async (payload) => {
try {
const result = await processData(payload);
logger.info("Task completed successfully", { result });
return result;
} catch (error) {
logger.error("Task failed:", error.message);
throw error; // Re-throw to trigger retry logic
}
},
```
</Accordion>
<Accordion title="Use structured logging">
```ts
logger.info("Processing started", {
userId: payload.userId,
operation: payload.operation,
timestamp: new Date().toISOString(),
});
```
</Accordion>
<Accordion title="Keep tasks focused">
Instead of one massive task, create focused, single-purpose tasks that can be composed together for complex workflows.
</Accordion>
<Accordion title="Configure appropriate retries">
Set retry policies based on your task's requirements:
```ts
// For critical operations
retry: {
maxAttempts: 5,
minTimeoutInMs: 2000,
maxTimeoutInMs: 30000,
factor: 2,
}
// For less critical operations
retry: {
maxAttempts: 2,
minTimeoutInMs: 1000,
maxTimeoutInMs: 5000,
factor: 1.5,
}
```
</Accordion>
</Accordions>
## Next steps
With trigger.dev integrated into your TurboStarter application, you can now:
* **Handle long-running operations** that would timeout in serverless functions
* **Schedule recurring tasks** like reports, cleanups, and maintenance
* **Process background jobs** reliably with automatic retries
* **Scale your application** without worrying about task execution infrastructure
Ready to explore more advanced features? Check out the official documentation for additional capabilities like webhooks, batching, and custom integrations.
<Cards>
<Card title="Documentation" description="trigger.dev" href="https://trigger.dev/docs" />
<Card title="Examples" description="trigger.dev" href="https://trigger.dev/docs/guides/introduction" />
</Cards>

View File

@@ -0,0 +1,290 @@
---
title: Configuration
description: Configure billing for your application.
url: /docs/web/billing/configuration
---
# Configuration
The billing configuration schema replicates your billing provider's schema, so that:
* we can display the data in the UI (pricing table, billing section, etc.)
* create the correct checkout session
* make some features work correctly - such as feature-based access
It is common to all billing providers and placed in `packages/billing/src/config/index.ts`. Some billing providers have some differences in what you can or cannot do. In these cases, the schema will try to validate and enforce the rules - but it's up to you to make sure the data is correct.
The schema is based on few entities:
* **Plans:** The main product you are selling (e.g., "Pro Plan", "Starter Plan", etc.)
* **Prices:** The pricing plan for the product (e.g., "Monthly", "Yearly", etc.)
* **Discounts:** The discount for the price (e.g., "10% off", "20% off", etc.)
```ts title="index.ts"
type BillingConfig = {
plans: PlanWithPrices[];
discounts: Discount[];
};
```
<Callout title="Getting the schema right is important!" type="error">
Getting the IDs of your plans is **extremely important** - as these are used to:
* create the correct checkout
* manage your customers billing data
Please take it easy while you configure this, do one step at a time, and test it thoroughly.
</Callout>
## Billing provider
To set the billing provider, you need to modify the exports in the `packages/billing/src/providers` directory. It defaults to [Stripe](/docs/web/billing/stripe).
<Tabs items={["index.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
// [!code word:stripe]
export * from "./stripe";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:stripe]
export * from "./stripe/env";
```
</Tab>
</Tabs>
It's important to set it correctly, as this is used to determine the correct API calls and environment variables used during the communication with the billing provider.
## Billing model
To set the billing model, you need to modify the `BILLING_MODEL` environment variable. It defaults to `recurring` as it's the most common model for SaaS apps.
```dotenv
BILLING_MODEL="recurring"
```
This field will be used to display corresponding data in the UI (e.g. in pricing tables) and to create the correct checkout session.
<Callout title="Available billing models">
For now, TurboStarter supports two billing models:
* `recurring` - for subscription-based models
* `one-time` - for one-time payments
When changing it, make sure to also update corresponding data on the provider side to match it with the correct billing model.
</Callout>
## Plans
Plans are the main products you are selling. They are defined by the following fields:
```ts title="index.ts"
export const config = billingConfigSchema.parse({
...
plans: [
{
id: PricingPlanType.PREMIUM,
name: "Premium",
description: "Become a power user and gain benefits",
badge: "Bestseller",
prices: [],
},
],
...
}) satisfies BillingConfig;
```
Let's break down the fields:
* `id`: The unique identifier for the plan (e.g., `free`, `pro`, `enterprise`, etc.). **This is chosen by you, it doesn't need to be the same one as the one in the provider.** It's also used to determine the access level of the plan.
* `name`: The name of the plan
* `description`: The description of the plan
* `badge`: A badge to display on the product (e.g., "Bestseller", "Popular", etc.)
The majority of these fields are going to populate the pricing table in the UI.
### Prices
Prices are the pricing plans for the plan. They are defined by the following fields:
```ts title="index.ts"
export const config = billingConfigSchema.parse({
...
plans: [
{
id: PricingPlanType.PREMIUM,
name: "Premium",
description: "Become a power user and gain benefits",
badge: "Bestseller",
prices: [
{
/* 👇 This is the `priceId` from the provider (e.g. Stripe), `variantId` (e.g. Lemon Squeezy) or `productId` (e.g. Polar) */
id: "price_1PpZAAFQH4McJDTlig6Fxsyy",
amount: 1900,
currency: "usd",
interval: RecurringInterval.MONTH,
trialDays: 7,
type: BillingModel.RECURRING,
},
],
},
],
...
}) satisfies BillingConfig;
```
Let's break down the fields:
* `id`: The unique identifier for the price. **This must match the price ID in the billing provider**
* `amount`: The amount of the price (displayed values will be divided by 100)
* `currency`: The currency of the price (only currencies from the [current locale](/docs/web/internationalization/overview) will be displayed - defaults to `usd`)
<Callout title="Set the correct currency on your billing provider">
Make sure to have the same currency set on your third-party billing provider (e.g. as a [store currency](https://docs.lemonsqueezy.com/help/payments/currencies) on Lemon Squeezy)
</Callout>
* `interval`: The interval of the price (e.g., `month`, `year`, etc.)
* `trialDays`: The number of trial days for the price
* `type`: The type of the price (e.g., `recurring`, `one-time`, etc.)
The amount is set for UI purposes. The billing provider will handle the actual billing - therefore, please make sure the amount is correctly set in the billing provider.
<Callout title="Set the correct price ID!" type="error">
Make sure to set the correct price ID that corresponds to the price in the billing provider. This is very important - as this is used to identify the correct price when creating a checkout session.
</Callout>
### One-off payments
One-off payments are a type of price that is used to create a checkout session for a one-time payment. They are defined by the following fields:
```ts title="index.ts"
export const config = billingConfigSchema.parse({
...
plans: [
{
id: PricingPlanType.PREMIUM,
name: "Premium",
description: "Become a power user and gain benefits",
badge: "Bestseller",
prices: [
{
/* 👇 This is the `priceId` from the provider (e.g. Stripe), `variantId` (e.g. Lemon Squeezy) or `productId` (e.g. Polar) */
id: "price_1PpUagFQH4McJDTlHCzOmyT6",
amount: 29900,
currency: "usd",
type: BillingModel.ONE_TIME,
},
],
},
],
...
}) satisfies BillingConfig;
```
Let's break down the fields:
* `id`: The unique identifier for the price. **This must match the price ID in the billing provider**
* `amount`: The amount of the price (displayed values will be divided by 100)
* `currency`: The currency of the price (only currencies from the [current locale](/docs/web/internationalization/overview) will be displayed - defaults to `usd`)
* `type`: The type of the price (e.g. `recurring`, `one-time`, etc.). In this case it's `one-time` as it's a one-off payment.
Please remember that the cost is set for UI purposes. **The billing provider will handle the actual billing - therefore, please make sure the cost is correctly set in the billing provider.**
### Custom prices
Sometimes - you want to display a price in the pricing table - but not actually have it in the billing provider. This is common for custom plans, free plans that don't require the billing provider subscription, or plans that are not yet available.
To do so, let's add the `custom` flag to the price:
```ts title="index.ts"
{
id: "enterprise-monthly",
label: "Contact us!",
href: "/contact",
interval: RecurringInterval.MONTH,
custom: true,
type: BillingModel.RECURRING,
}
```
Here's the full example:
```ts title="index.ts"
export const config = billingConfigSchema.parse({
...
plans: [
{
id: PricingPlanType.PREMIUM,
name: "Premium",
description: "Become a power user and gain benefits",
badge: "Bestseller",
prices: [
{
id: "premium-monthly",
label: "Contact us!",
href: "/contact",
interval: RecurringInterval.MONTH,
custom: true,
type: BillingModel.RECURRING,
},
],
},
],
...
}) satisfies BillingConfig;
```
As you can see, the plan is now a custom plan. The UI will display the plan in the pricing table, but it won't be available for purchase.
We do this by adding the following fields:
* `custom`: A flag to indicate that the plan is custom. This will prevent the plan from being available for purchase. It's set to `false` by default.
* `label`: This is used to display the label in the pricing table instead of the price.
* `href`: The link to the page where the user can contact you. This is used in the pricing table.
<Callout title="Translations supported!">
All labels and descriptions can be translated using the [internationalization](/docs/web/internationalization/overview) feature. The UI will display the correct translation based on the user's locale.
```ts title="index.ts"
label: "common:contactUs",
```
To make strings translatable, make sure to provide the translation key in the config.
</Callout>
### Discounts
Sometimes, you want to offer a discount to your users. This is done by adding a discount to the price in `discounts` field.
```ts title="index.ts"
export const config = billingConfigSchema.parse({
...
discounts: [
{
code: "50OFF",
type: BillingDiscountType.PERCENT,
off: 50,
appliesTo: [
"price_1PpUagFQH4McJDTlHwsCzOmyT6",
],
},
],
...
}) satisfies BillingConfig;
```
Let's break down the fields:
* `code`: The code of the discount (e.g., "50OFF", "10% off", etc.) **This must match the code configured in the billing provider**
* `type`: The type of the discount (e.g., `percent`, `amount`, etc.)
* `off`: The amount of the discount (e.g., 50 for 50% off)
* `appliesTo`: The list of prices that the discount applies to. This is the price ID that you've configured above for the price.
This data will allow to display the correct banner in the UI e.g. "10% off for the first 100 customers!" and to apply the discount to the correct price at checkout.
## Adding more products, plans and discounts
Simply add more plans, prices and discounts to the arrays. The UI **should** be able to handle it in most traditional cases. If you have a more complex billing schema, you may need to adjust the UI accordingly.

View File

@@ -0,0 +1,13 @@
---
title: Creem
description: Manage your customers data and subscriptions using Creem.
url: /docs/web/billing/creem
---
# Creem
<Callout title="Creem integration is coming soon!">
We are working on adding [Creem](https://www.creem.io/) integration to our platform. As soon as it's ready, we will update this page with the necessary information.
[See roadmap](https://github.com/orgs/turbostarter/projects/1)
</Callout>

View File

@@ -0,0 +1,160 @@
---
title: Lemon Squeezy
description: Manage your customers data and subscriptions using Lemon Squeezy.
url: /docs/web/billing/lemon-squeezy
---
# Lemon Squeezy
[Lemon Squeezy](https://lemonsqueezy.com/) is another billing provider available within TurboStarter. Here we'll go through the configuration and how to set it up as a provider for your app.
To switch to Lemon Squeezy, you need to update the exports in:
<Tabs items={["index.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
// [!code word:lemon-squeezy]
export * from "./lemon-squeezy";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:lemon-squeezy]
export * from "./lemon-squeezy/env";
```
</Tab>
</Tabs>
Then, let's configure the integration:
<Steps>
<Step>
## Get API keys
After you have created your account and a store for [Lemon Squeezy](https://lemonsqueezy.com/), you will need to create a new API key. You can do this by going to the [API page](https://app.lemonsqueezy.com/settings/api) in the settings and clicking on the plus button. You will need to give your API key a name and then click on the *Create* button. Once you have created your API key, you will need to copy the API key to use it in the setup of the integration.
For local development, make sure to use [Test Mode](https://docs.lemonsqueezy.com/help/getting-started/test-mode) to not mess with the real transactions.
</Step>
<Step>
## Set environment variables
You need to set the following environment variables:
```dotenv title="apps/web/.env.local"
LEMONSQUEEZY_API_KEY="" # Your Lemon Squeezy API key
LEMONSQUEEZY_SIGNING_SECRET="" # Your Lemon Squeezy webhook signing secret
LEMONSQUEEZY_STORE_ID="" # Your Lemon Squeezy store ID (can be found under Settings > Stores next to your store url, e.g #12345)
```
**Please do not add the secret keys to the .env file in production.** During development, you can place them in `.env.local` as it's not committed to the repository. In production, you can set them in the environment variables of your hosting provider.
</Step>
<Step>
## Create products
For your users to choose from the available subscription plans, you need to create those Products first on the [Products page](https://app.lemonsqueezy.com/products). You can create as many products as you want.
Create one product per plan you want to offer. You can add multiple variant within the product to offer multiple models or different billing intervals.
![Lemon Squeezy Products](/images/docs/web/billing/lemon-squeezy/products.webp)
To offer multiple intervals for each plan, you can use the [Variant](https://docs.lemonsqueezy.com/help/products/variants) feature of Lemon Squeezy. Just create one variant for each interval/model you want to offer.
![Lemon Squeezy Variants](/images/docs/web/billing/lemon-squeezy/variants.png)
<Callout type="warn" title="Match the variant id with configuration">
You need to make sure that the price ID you set in the configuration matches the ID of the variant you created in Lemon Squeezy.
[See configuration](/docs/web/billing/configuration#prices) for more information.
</Callout>
</Step>
<Step>
## Create a webhook
To sync the current subscription status or checkout conclusion and other information to your database, you need to set up a webhook.
The webhook handling code comes ready to use with TurboStarter, you just have to create the webhook in the Lemon Squeezy dashboard and insert the URL for your project.
To configure a new webhook, go to the [Webhooks page](https://app.lemonsqueezy.com/settings/webhooks) in the Lemon Squeezy settings and click the *Plus* button.
![Lemon Squeezy Webhook](/images/docs/web/billing/lemon-squeezy/webhook.png)
Select the following events:
* For subscriptions:
* `subscription_created`
* `subscription_updated`
* `subscription_cancelled`
* For one-off payments:
* `order_created`
You will also have to enter a *Signing secret* which you can get by running the following command in your terminal:
```bash
openssl rand -base64 32
```
Copy the generated string and paste it into the *Signing secret* field.
You also need to add this secret to your environment variables:
```dotenv title="apps/web/.env.local"
LEMONSQUEEZY_WEBHOOK_SECRET=your-signing-secret
```
To get the callback URL for the webhook, you can either use a local development URL or the URL of your deployed app:
### Local development
If you want to test the webhook locally, you can use [ngrok](https://ngrok.com) to create a tunnel to your local machine. Ngrok will then give you a URL that you can use to test the webhook locally.
To do so, install ngrok and run it with the following command (while your TurboStarter web development server is running):
```bash
ngrok http 3000
```
![Ngrok](/images/docs/web/billing/stripe/ngrok.png)
This will give you a URL (see the *Forwarding* output) that you can use to create a webhook in Lemon Squeezy. Just use that url and add `/api/billing/webhook` to it.
<Card title="Lemon Squeezy Webhooks" description="docs.lemonsqueezy.com" href="https://docs.lemonsqueezy.com/api/webhooks" />
### Production deployment
When going to production, you will need to set the webhook URL and the events you want to listen to in Lemon Squeezy.
The webhook path is `/api/billing/webhook`. If your app is hosted at `https://myapp.com` then you need to enter `https://myapp.com/api/billing/webhook` as the URL.
All the relevant events are automatically handled by TurboStarter, so you don't need to do anything else. If you want to handle more events please check [Webhooks](/docs/web/billing/webhooks) for more information.
</Step>
</Steps>
## Add discount
You can add a discount for your customers that will apply on a specific price.
You can create the discount on [Discounts page](https://app.lemonsqueezy.com/discounts).
![Lemon Squeezy Discounts](/images/docs/web/billing/lemon-squeezy/discount.png)
You can set there a details of discount such as products that it should apply to, amount off, duration, max redemptions and more.
<Card title="Lemon Squeezy Discounts" description="lemonsqueezy.com" href="https://www.lemonsqueezy.com/marketing/discount-codes" />
You need to add also the discount code and details to TurboStarter billing configuration to enable displaying it in the UI, creating checkout sessions with it and calculate prices.
[See discounts configuration](/docs/web/billing/configuration#discounts) for more details.
That's it! 🎉 You have now set up Lemon Squeezy as a billing provider for your app.
Feel free to add more products, prices, discounts and manage your customers data and subscriptions using Lemon Squeezy.
<Callout type="warn" title="Ensure configuration matches">
Make sure that the data you set in the configuration matches the details of things you created in Lemon Squeezy.
[See configuration](/docs/web/billing/configuration) for more information.
</Callout>

View File

@@ -0,0 +1,38 @@
---
title: Overview
description: Get started with billing in TurboStarter.
url: /docs/web/billing/overview
---
# Overview
The `@turbostarter/billing` package is used to manage subscriptions, one-off payments, and more.
Inside, we're making an abstraction layer that allows us to use different billing providers without breaking our code nor changing the API calls.
![Billing Providers](/images/docs/billing-providers.webp)
## Providers
TurboStarter implements multiple providers for managing billing:
* [Stripe](/docs/web/billing/stripe)
* [Lemon Squeezy](/docs/web/billing/lemon-squeezy)
* [Polar](/docs/web/billing/polar)
* [Creem](/docs/web/billing/creem) (coming soon)
All configuration and setup is built-in with a unified API, so you can switch between providers by simply changing the exports and even introduce your own provider without breaking any billing-related logic.
## Subscriptions vs. One-off payments
TurboStarter supports both one-off payments and subscriptions. You have the choice to use one or both. What TurboStarter cannot assume with certainty is the billing mode you want to use. By default, we assume you want to use subscriptions, as this is the most common billing mode for SaaS applications.
This means that - by default - TurboStarter will be looking for a subscription plan when visiting the billing section or pricing page.
**It's easily customizable** - [take a look at configuration](/docs/web/billing/configuration).
### But I want to use both
Perfect - you can, but you need to customize the pages to display the correct data.
Depending on the service you use, you will need to set the environment variables accordingly. By default - the billing package uses [Stripe](/docs/web/billing/stripe). Alternatively, you can use [Lemon Squeezy](/docs/web/billing/lemon-squeezy) or [Polar](/docs/web/billing/polar). In the future, we will also add [Creem](/docs/web/billing/creem).

View File

@@ -0,0 +1,167 @@
---
title: Polar
description: Manage your customers data and subscriptions using Polar.
url: /docs/web/billing/polar
---
# Polar
[Polar](https://www.polar.com/) is another billing provider available within TurboStarter. Here we'll go through the configuration and how to set it up as a provider for your app.
To switch to Polar, you need to update the exports in:
<Tabs items={["index.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
// [!code word:polar]
export * from "./polar";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:polar]
export * from "./polar/env";
```
</Tab>
</Tabs>
Then, let's configure the integration:
<Steps>
<Step>
## Get the access token
After you have created your account for [Polar](https://www.polar.com/) and created your store, you will need to get the API key.
Under the *Settings*, scroll to *Developers* and click "New token". Enter a name for the token, set the expiration duration and select the scopes you want the token to have.
To keep it simple, you can select all scopes.
![Polar Access Token](/images/docs/web/billing/polar/access-token.png)
For local development, make sure to use [Sandbox Mode](https://docs.polar.sh/integrate/sandbox) to not mess with the real transactions.
</Step>
<Step>
## Set environment variables
You need to set the following environment variables:
```dotenv title="apps/web/.env.local"
POLAR_ACCESS_TOKEN="" # Your Polar access token
POLAR_WEBHOOK_SECRET="" # Your Polar webhook secret
POLAR_ORGANIZATION_SLUG="" # Your Polar organization slug (can be found under Settings > Organization)
```
**Please do not add the secret keys to the .env file in production.** During development, you can place them in `.env.local` as it's not committed to the repository. In production, you can set them in the environment variables of your hosting provider.
</Step>
<Step>
## Create products
For your users to choose from the available subscription plans, you need to create those Products first on the [Products page](https://docs.polar.sh/features/products). You can create as many products as you want.
![Polar Products](/images/docs/web/billing/polar/products.png)
Polar takes a different approach to product variants. Instead of having one product with multiple pricing options, Polar treats each pricing option as a separate product. This simplifies the user experience and API while giving you full flexibility.
At checkout, customers can choose between different products (like monthly or yearly plans), each with its own pricing and benefits.
![Polar Product Variants](/images/docs/web/billing/polar/variants.png)
<Callout type="warn" title="Match the product id with configuration">
You need to make sure that the price ID you set in the configuration matches the ID of the product you created in Polar.
[See configuration](/docs/web/billing/configuration#prices) for more information.
</Callout>
</Step>
<Step>
## Create a webhook
To sync the current subscription status or checkout conclusion and other information to your database, you need to set up a webhook.
The webhook handling code comes ready to use with TurboStarter, you just have to create the webhook in the Polar dashboard and insert the URL for your project.
To configure a new webhook, go to the [Webhooks page](https://docs.polar.sh/integrate/webhooks/endpoints) in the Polar settings and click the *Add endpoint* button.
![Polar Webhook](/images/docs/web/billing/polar/webhook.png)
Select the following events:
* For subscriptions:
* `subscription.created`
* `subscription.updated`
* `subscription.canceled`
* `subscription.revoked`
* For one-off payments:
* `order.created`
You will also have to enter a *Secret* which you can get by running the following command in your terminal:
```bash
openssl rand -base64 32
```
Copy the generated string and paste it into the *Secret* field.
You also need to add this secret to your environment variables:
```dotenv title="apps/web/.env.local"
POLAR_WEBHOOK_SECRET=your-generated-secret
```
To get the URL for the webhook, you can either use a local development URL or the URL of your deployed app:
### Local development
If you want to test the webhook locally, you can use [ngrok](https://ngrok.com) to create a tunnel to your local machine. Ngrok will then give you a URL that you can use to test the webhook locally.
To do so, install ngrok and run it with the following command (while your TurboStarter web development server is running):
```bash
ngrok http 3000
```
![Ngrok](/images/docs/web/billing/stripe/ngrok.png)
This will give you a URL (see the *Forwarding* output) that you can use to create a webhook in Polar. Just use that url and add `/api/billing/webhook` to it.
<Card title="Polar Webhooks" description="docs.polar.sh" href="https://docs.polar.sh/integrate/webhooks/delivery" />
### Production deployment
When going to production, you will need to set the webhook URL and the events you want to listen to in Polar.
The webhook path is `/api/billing/webhook`. If your app is hosted at `https://myapp.com` then you need to enter `https://myapp.com/api/billing/webhook` as the URL.
All the relevant events are automatically handled by TurboStarter, so you don't need to do anything else. If you want to handle more events please check [Webhooks](/docs/web/billing/webhooks) for more information.
</Step>
</Steps>
## Add discount
You can add a discount for your customers that will apply on a specific price.
You can create the discount under the *Products* page on *Discounts* tab in the Polar dashboard.
![Polar Discount](/images/docs/web/billing/polar/discount.png)
You can set there a details of discount such as products that it should apply to, amount off, duration, max redemptions and more.
<Card title="Polar Discounts" description="docs.polar.sh" href="https://docs.polar.sh/features/discounts" />
You need to add also the discount code and details to TurboStarter billing configuration to enable displaying it in the UI, creating checkout sessions with it and calculate prices.
[See discounts configuration](/docs/web/billing/configuration#discounts) for more details.
That's it! 🎉 You have now set up Polar as a billing provider for your app.
Feel free to add more products, prices, discounts and manage your customers data and subscriptions using Polar.
<Callout type="warn" title="Ensure configuration matches">
Make sure that the data you set in the configuration matches the details of things you created in Polar.
[See configuration](/docs/web/billing/configuration) for more information.
</Callout>

View File

@@ -0,0 +1,205 @@
---
title: Stripe
description: Manage your customers data and subscriptions using Stripe.
url: /docs/web/billing/stripe
---
# Stripe
[Stripe](https://stripe.com) is the default billing provider for TurboStarter. Here we'll go through the configuration and how to set it up as a provider for your app.
<Steps>
<Step>
## Get API keys
After you have created your account for [Stripe](https://stripe.com), you will need to get the API key. You can do this by going to the [API page](https://dashboard.stripe.com/apikeys) in the dashboard. Here you will find the *Secret key* and the *Publishable key*. You will need the *Secret key* for the integration to work.
For local development, make sure to use [Test Mode](https://docs.stripe.com/test-mode) to not mess with the real transactions.
</Step>
<Step>
## Set environment variables
You need to set the following environment variables:
```dotenv title="apps/web/.env.local"
STRIPE_SECRET_KEY="" # Your Stripe secret key
STRIPE_WEBHOOK_SECRET="" # The secret key of the webhook you created (see below)
```
**Please do not add the secret keys to the .env file in production.** During development, you can place them in `.env.local` as it's not committed to the repository. In production, you can set them in the environment variables of your hosting provider.
</Step>
<Step>
## Create products
For your users to choose from the available subscription plans, you need to create those Products first on the [Products page](https://dashboard.stripe.com/products). You can create as many products as you want.
Create one product per plan you want to offer. You can add multiple prices within this product to offer multiple models or different billing intervals.
![Stripe Products](/images/docs/web/billing/stripe/products.webp)
<Callout type="warn" title="Match the price id with configuration">
You need to make sure that the price ID you set in the configuration matches the ID of the price you created in Stripe.
[See configuration](/docs/web/billing/configuration) for more information.
</Callout>
</Step>
<Step>
## Create a webhook
To sync the current subscription status or checkout conclusion and other information to your database, you need to set up a webhook.
The webhook code comes ready to use with TurboStarter, you just have to create the webhook in the Stripe dashboard and insert the URL for your project.
To configure a new webhook, go to the [Webhooks page](https://dashboard.stripe.com/webhooks) in the Stripe settings and click the Add endpoint button.
![Stripe Webhook](/images/docs/web/billing/stripe/webhook.png)
Select the following events:
* For subscriptions:
* `customer.subscription.created`
* `customer.subscription.updated`
* `customer.subscription.deleted`
* For one-off payments:
* `checkout.session.completed`
To get the URL for the webhook, you can either use a local development URL or the URL of your deployed app:
### Local development
There are two ways to test the webhook during local development:
<Tabs items={["Stripe CLI", "Tunnel"]}>
<Tab value="Stripe CLI">
The Stripe CLI which allows you to listen to Stripe events straight to your own localhost. You can install and use the CLI using a variety of methods, but we recommend using official way to do it.
[Install the Stripe CLI](https://docs.stripe.com/stripe-cli)
Then - login to your Stripe account using the project you want to run:
```bash
stripe login
```
Copy the webhook secret displayed in the terminal and set it as the `STRIPE_WEBHOOK_SECRET` environment variable in your `apps/web/.env.local` file:
```dotenv title="apps/web/.env.local"
STRIPE_WEBHOOK_SECRET=*your-secret-key*
```
Now, you can listen to Stripe events running the following command:
```bash
stripe listen --forward-to localhost:3000/api/billing/webhook
```
This will forward all the Stripe events to your local endpoint.
<Callout type="warn" title="Not receiving events?">
**If you have not logged in** - the first time you set it up, you are required to sign in. This is a one-time process. Once you sign in, you can use the CLI to listen to Stripe events.
**Please sign in and then re-run the command.** Now, you can listen to Stripe events.
If you're not receiving events, please make sure that:
* the webhook secret is correct
* the account you signed in is the same as the one you're using in your app
</Callout>
You can even trigger the event manually for testing purposes:
```bash
stripe trigger customer.subscription.created
```
<Card title="Stripe CLI" description="docs.stripe.com" href="https://docs.stripe.com/stripe-cli" />
</Tab>
<Tab value="Tunnel">
If you want to test the webhook locally, you can use [ngrok](https://ngrok.com) to create a tunnel to your local machine. Ngrok will then give you a URL that you can use to test the webhook locally.
To do so, install ngrok and run it with the following command (while your TurboStarter web development server is running):
```bash
ngrok http 3000
```
![Ngrok](/images/docs/web/billing/stripe/ngrok.png)
This will give you a URL (see the *Forwarding* output) that you can use to create a webhook in Stripe. Just use that url and add `/api/billing/webhook` to it.
<Card title="Stripe Webhooks" description="docs.stripe.com" href="https://docs.stripe.com/webhooks" />
</Tab>
</Tabs>
### Production deployment
When going to production, you will need to set the webhook URL and the events you want to listen to in Stripe.
The webhook path is `/api/billing/webhook`. If your app is hosted at `https://myapp.com` then you need to enter `https://myapp.com/api/billing/webhook` as the URL.
All the relevant events are automatically handled by TurboStarter, so you don't need to do anything else. If you want to handle more events please check [Webhooks](/docs/web/billing/webhooks) for more information.
</Step>
<Step>
## Configure Stripe Customer Portal
Stripe requires you to set up the Customer Portal so that users can manage their billing information, invoices and plan settings from there.
You can do it [under the following link.](https://dashboard.stripe.com/settings/billing/portal)
![Stripe Customer Portal](/images/docs/web/billing/stripe/customer-portal.png)
1. Please make sure to enable the setting that lets users switch plans
2. Configure the behavior of the cancellation according to your needs
</Step>
</Steps>
## Add discount
You can add a discount for your customers that will apply on a specific price.
<Steps>
<Step>
### Create coupon
First, you'd need to create a coupon on the [Coupons page](https://dashboard.stripe.com/coupons).
![Stripe Coupons](/images/docs/web/billing/stripe/coupon.png)
You can set there a details of discount such as prices that it should apply to, amount off, duration, max redemptions and more.
</Step>
<Step>
### Add promotion code
To enable using code during checkout you need to get a promotion code. You can define it on the same page as the coupon and give some user-friendly name to it.
![Stripe Promotion Code](/images/docs/web/billing/stripe/promotion-code.png)
This code will be auto-applied at new checkout sessions.
<Card title="Stripe Discounts" description="docs.stripe.com" href="https://docs.stripe.com/checkout/custom-checkout/add-discounts" />
</Step>
<Step>
### Configure discount
You need to add also the discount code and details to TurboStarter billing configuration to enable displaying it in the UI, creating checkout sessions with it and calculate prices.
[See discounts configuration](/docs/web/billing/configuration#discounts) for more details.
</Step>
</Steps>
That's it! 🎉 You have now set up Stripe as a billing provider for your app.
Feel free to add more products, prices, discounts and manage your customers data and subscriptions using Stripe.
<Callout type="warn" title="Ensure configuration matches">
Make sure that the data you set in the configuration matches the details of things you created in Stripe.
[See configuration](/docs/web/billing/configuration) for more information.
</Callout>

View File

@@ -0,0 +1,41 @@
---
title: Webhooks
description: Handle webhooks from your billing provider.
url: /docs/web/billing/webhooks
---
# Webhooks
TurboStarter handles billing webhooks to update customer data based on events received from the billing provider.
Occasionally, you may need to set up additional webhooks or perform custom actions with webhooks.
In such cases, you can customize the billing webhook handler in the billing router at `packages/api/src/modules/billing/router.ts`.
By default, the webhook handler is configured to be as straightforward as possible:
```ts title="router.ts"
import { webhookHandler } from "@turbostarter/billing/server";
export const billingRouter = new Hono().post("/webhook", (c) =>
webhookHandler(c.req.raw),
);
```
However, you can extend it using the callbacks provided from `@turbostarter/billing` package:
```ts title="router.ts"
import { webhookHandler } from "@turbostarter/billing/server";
export const billingRouter = new Hono().post("/webhook", (c) =>
webhookHandler(c.req.raw, {
onCheckoutSessionCompleted: (sessionId) => {},
onSubscriptionCreated: (subscriptionId) => {},
onSubscriptionUpdated: (subscriptionId) => {},
onSubscriptionDeleted: (subscriptionId) => {},
onEvent: (rawEvent) => {},
}),
);
```
You can provide one or more of the callbacks to handle the events you are interested in.

View File

@@ -0,0 +1,92 @@
---
title: CLI
description: Start your new project with a single command.
url: /docs/web/cli
---
# CLI
<CliDemo />
To help you get started with TurboStarter **as quickly as possible**, we've developed a [CLI](https://www.npmjs.com/package/@turbostarter/cli) that enables you to create a new project (with all the configuration) in seconds.
The CLI is a set of commands that will help you create a new project, generate code, and manage your project efficiently.
Currently, the following action is available:
* **Starting a new project** - Generate starter code for your project with all necessary configurations in place (billing, database, emails, etc.)
**The CLI is in beta**, and we're actively working on adding more commands and actions. Soon, the following features will be available:
* **Translations** - Translate your project, verify translations, and manage them effectively
* **Installing plugins** - Easily install plugins for your project
* **Dynamic code generation** - Generate dynamic code based on your project structure
## Installation
You can run commands using `npx`:
```bash
npx turbostarter <command>
npx @turbostarter/cli@latest <command>
```
<Callout>
If you don't want to install the CLI globally, you can simply replace the examples below with `npx @turbostarter/cli@latest` instead of `turbostarter`.
This also allows you to always run the latest version of the CLI without having to update it.
</Callout>
## Usage
Running the CLI without any arguments will display the general information about the CLI:
```bash
Usage: turbostarter [options] [command]
Your Turbo Assistant for starting new projects, adding plugins and more.
Options:
-v, --version display the version number
-h, --help display help for command
Commands:
new create a new TurboStarter project
help [command] display help for command
```
You can also display help for it or check the actual version.
### Starting a new project
Use the `new` command to initialize configuration and dependencies for a new project.
```bash
npx turbostarter new
```
You will be asked a few questions to configure your project:
```bash
✔ All prerequisites are satisfied, let's start! 🚀
? What do you want to ship?
◉ Web app
◉ Mobile app
◯ Browser extension
? Enter your project name.
? How do you want to use database?
Local (powered by Docker)
Cloud
? What do you want to use for billing?
Stripe
Lemon Squeezy
...
🎉 You can now get started. Open the project and just ship it! 🎉
Problems? https://www.turbostarter.dev/docs
```
It will create a new project, configure providers, install dependencies and start required services in development mode.

View File

@@ -0,0 +1,86 @@
---
title: Blog
description: Learn how to manage your blog content.
url: /docs/web/cms/blog
---
# Blog
TurboStarter comes with a pre-configured blog implementation that allows you to manage your blog content.
## Creating a new blog post
To create a new blog post, you need to create a new directory (its name will be used as the slug of the blog post) with `.mdx` files in the `packages/cms/src/collections/blog/content` directory. Each file in this directory should be named after the locale it belongs to (e.g `en.mdx`, `es.mdx`, etc.).
The file will start with a [frontmatter](https://mdxjs.com/guides/frontmatter/) block, which is a yaml-like block that contains metadata about the post. The frontmatter block should be surrounded by three dashes (`---`).
```mdx title="packages/cms/src/collections/blog/content/my-first-blog-post/en.mdx"
---
title: Quick Tips to Improve Your Skills Right Away
description: Whether you're learning a new technical skill or working on personal development, these quick tips can help you improve right away. Learn how to break down your goals, practice consistently, and track your progress using Markdown.
publishedAt: 2023-04-19
tags: [learning, skills, progress]
thumbnail: https://images.unsplash.com/photo-1483639130939-150975af84e5?q=80&w=2370&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D
status: published
---
```
Let's break down the frontmatter fields:
* `title`: The title of the blog post (it will be also used to generate a slug for the blog post)
* `description`: The description of the blog post
* `publishedAt`: The date when the blog post was published
* `tags`: The tags of the blog post
* `thumbnail`: The thumbnail of the blog post
* `status`: The status of the blog post (could be `published` or `draft`)
After the frontmatter block, you can add the content of the blog post:
```mdx title="packages/cms/src/collections/blog/content/my-first-blog-post/en.mdx"
# Quick Tips to Improve Your Skills Right Away
Awesome paragraph!
[Link](https://www.turbostarter.dev)
<Callout>This is a callout component.</Callout>
...
```
You can consume the content the same as it's described in [Content Collections](/docs/web/cms/content-collections).
## BONUS: Using custom components
As you're using MDX, you can use **any React component** in your blog posts. Just define it as a normal React component and pass it to `<MdxContent />` in `components` prop:
```tsx title="apps/web/src/app/content/page.tsx"
import { MyComponent } from "~/modules/common/my-component";
export default function Page() {
return (
<MDXContent
code={data.body}
components={{ ...defaultMdxComponents, MyComponent }}
/>
);
}
```
Then, you would be able to use it in your document content and it will rendered on the page as a result:
```mdx title="packages/cms/src/collections/blog/content/my-first-blog-post/en.mdx"
...
# Heading
Excellent paragraph!
<MyComponent />
1. First item
2. Second item
3. Third item
```
TurboStarter ships with a set of default components that you can use in your blog posts, e.g. `<Callout />`, `<Card />` etc. Use them or define your own to make your blog posts more engaging.

View File

@@ -0,0 +1,98 @@
---
title: Content Collections
description: Get started with Content Collections.
url: /docs/web/cms/content-collections
---
# Content Collections
By default, TurboStarter uses [Content Collections](https://www.content-collections.dev/) to store and retrieve content from the MDX files.
Content from there is used to populate data in the following places:
* **Blog**
* **Legal pages**
* **Documentation**
<Callout title="Why content-collections?">
It is a great alternative to headless CMS like Contentful or Prismic based on MDX (a more powerful version of markdown). It is free, open source and the content is located right in your repository.
</Callout>
Of course, you can add more collections and views, as it's very flexible.
## Defining new collection
To define a new collection, you need to create a new file in the `packages/cms/src/collections` directory:
```ts title="packages/cms/src/collections/legal/index.ts"
import { defineCollection } from "@content-collections/core";
export const legal = defineCollection({
name: "legal",
directory: "src/collections/legal/content",
include: "**/*.mdx",
schema: (z) => ({
title: z.string(),
description: z.string(),
}),
transform: async (doc, context) => {
const mdx = await transformMDX(doc, context);
return {
...mdx,
slug: doc._meta.directory,
locale: doc._meta.fileName.split(".")[0],
};
},
});
```
Then it's passed to the config in `packages/cms/content-collections.ts` file which is used to generate types and parse content from MDX files.
```tsx title="packages/cms/content-collections.ts"
import { defineConfig } from "@content-collections/core";
import { legal } from "./src/collections/legal";
export default defineConfig({
collections: [legal],
});
```
When you run a development server, content collections will be automatically rebuilt (in `.content-collections` directory) and you will be able to import the content and metadata of each file in your application.
<Callout title="It's fully type-safe!">
By exporting the generated content you get fully type-safe API to interact
with the content. We can have type safety on the data that we're receiving
from the MDX files.
</Callout>
## Using content collections
To get some content from `@turbostarter/cms` package, you need to use the exposed API that we described in the [Overview section](/docs/web/cms/overview#api):
```tsx title="apps/web/src/app/[locale](marketing)/legal/[slug]/page.tsx"
import { content } from "@turbostarter/cms";
export default async function Page({
params,
}: {
params: Promise<{ slug: string; locale: string }>;
}) {
const item = getContentItemBySlug({
collection: CollectionType.LEGAL,
slug: (await params).slug,
locale: (await params).locale,
});
return <h1>{title}</h1>;
}
```
Voila! You can now access the content from the MDX files.
<Cards>
<Card title="Content Collections" description="content-collections.dev" href="https://www.content-collections.dev/" />
<Card title="MDX" description="mdxjs.com" href="https://mdxjs.com/" />
</Cards>

View File

@@ -0,0 +1,67 @@
---
title: Overview
description: Manage your content in TurboStarter.
url: /docs/web/cms/overview
---
# Overview
TurboStarter implements a CMS interface that abstracts the implementation from where you store your data. It provides a simple API to interact with your data, and it's easy to extend and customize.
By default, the starter kit ships with these implementations in place:
1. [Content Collections](https://www.content-collections.dev/) - a headless CMS that uses [MDX](https://mdxjs.com/) files to store your content.
The implementation is available under `@turbostarter/cms` package, here we'll go over how to use it.
## API
The CMS package provides a simple, unified API to interact with the content. It's the same for all the providers, so you can easily use it with any of the implementations without changing the code.
### Fetching content items
To fetch items from your colletions, you can use the `getContentItems` function.
```ts
import { getContentItems } from "@turbostarter/cms";
const { items, count } = getContentItems({
collection: CollectionType.BLOG,
tags: [ContentTag.SKILLS],
sortBy: "publishedAt",
sortOrder: SortOrder.DESCENDING,
status: ContentStatus.PUBLISHED,
locale: "en",
});
```
It accepts an object with the following properties:
* `collection`: The collection to fetch the items from.
* `tags`: The tags to filter the items by.
* `sortBy`: The field to sort the items by.
* `sortOrder`: The order to sort the items in.
* `status`: The status of the items to fetch. It can be `published` or `draft`. By default, only `published` items are fetched.
* `locale`: The locale to fetch the items in. By default, all locales are fetched.
### Fetching a single content item
To fetch a single content item, you can use the `getContentItemBySlug` function.
```ts
import { getContentItemBySlug } from "@turbostarter/cms";
const item = getContentItemBySlug({
collection: CollectionType.BLOG,
slug: "my-first-blog-post",
status: ContentStatus.PUBLISHED,
locale: "en",
});
```
It accepts an object with the following properties:
* `collection`: The collection to fetch the item from.
* `slug`: The slug of the item to fetch.
* `status`: The status of the item to fetch. It can be `published` or `draft`. By default, only `published` items are fetched.
* `locale`: The locale to fetch the item in. By default, all locales are fetched.

View File

@@ -0,0 +1,40 @@
---
title: App configuration
description: Learn how to setup the overall settings of your app.
url: /docs/web/configuration/app
---
# App configuration
The application configuration is set at `apps/web/src/config/app.ts`. This configuration stores some overall variables for your application.
This allows you to host multiple apps in the same monorepo, as every application defines its own configuration.
The recommendation is to **not update this directly** - instead, please define the environment variables and override the default behavior. The configuration is strongly typed so you can use it safely accross your codebase - it'll be validated at build time.
```ts title="apps/web/src/config/app.ts"
import env from "env.config";
export const appConfig = {
name: env.NEXT_PUBLIC_PRODUCT_NAME,
url: env.NEXT_PUBLIC_URL,
locale: env.NEXT_PUBLIC_DEFAULT_LOCALE,
theme: {
mode: env.NEXT_PUBLIC_THEME_MODE,
color: env.NEXT_PUBLIC_THEME_COLOR,
},
} as const;
```
For example, to set the product name and default locale, you'd update the following variables:
```dotenv title=".env.local"
NEXT_PUBLIC_PRODUCT_NAME="TurboStarter"
NEXT_PUBLIC_DEFAULT_LOCALE="en"
```
<Callout type="warn" title="Do NOT use process.env!">
Do NOT use `process.env` to get the values of the variables. Variables
accessed this way are not validated at build time, and thus the wrong variable
can be used in production.
</Callout>

View File

@@ -0,0 +1,104 @@
---
title: Environment variables
description: Learn how to configure environment variables.
url: /docs/web/configuration/environment-variables
---
# Environment variables
Environment variables are defined in the `.env` file in the root of the repository and in the root of the `apps/web` package.
* **Shared environment variables**: Defined in the **root** `.env` file. These are shared between environments (e.g., development, staging, production) and apps (e.g., web, mobile).
* **Environment-specific variables**: Defined in `.env.development` and `.env.production` files. These are specific to the development and production environments.
* **App-specific variables**: Defined in the app-specific directory (e.g., `apps/web`). These are specific to the app and are not shared between apps.
* **Secret keys**: Not stored in the `.env` file. Instead, they are stored in the environment variables of the CI/CD system.
* **Local secret keys**: If you need to use secret keys locally, you can store them in the `.env.local` file. This file is not committed to Git, making it safe for sensitive information.
## Shared variables
Here you can add all the environment variables that are shared across all the apps. This file should be located in the **root** of the project.
To override these variables in a specific environment, please add them to the specific environment file (e.g. `.env.development`, `.env.production`).
```dotenv title=".env.local"
# Shared environment variables
# The database URL is used to connect to your database.
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# The name of the product. This is used in various places across the apps.
PRODUCT_NAME="TurboStarter"
# The url of the web app. Used mostly to link between apps.
URL="http://localhost:3000"
...
```
If you're using Supabase for your database, the [Supabase recipe](/docs/web/recipes/supabase#configure-environment-variables) shows the exact `DATABASE_URL` format and how to set it in your `.env.local`.
## App-specific variables
Here you can add all the environment variables that are specific to the app (e.g. `apps/web`).
You can also override the shared variables defined in the root `.env` file.
```dotenv title="apps/web/.env.local"
# App-specific environment variables
# Env variables extracted from shared to be exposed to the client in Next.js app
NEXT_PUBLIC_PRODUCT_NAME="${PRODUCT_NAME}"
NEXT_PUBLIC_URL="${URL}"
NEXT_PUBLIC_DEFAULT_LOCALE="${DEFAULT_LOCALE}"
# Theme mode and color
NEXT_PUBLIC_THEME_MODE="system"
NEXT_PUBLIC_THEME_COLOR="orange"
...
```
<Callout title="NEXT_PUBLIC_ prefix">
To make environment variables available in the Next.js **client-side** app code, you need to prefix them with `NEXT_PUBLIC_`. They will be injected to the code during the build process.
Only environment variables prefixed with `NEXT_PUBLIC_` will be injected, so don't use this prefix for environment variables that should be used only in the server-side code.
[Read more about Next.js environment variables.](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables)
</Callout>
## Secret keys
Secret keys and sensitive information are to be never stored in the `.env` file. Instead, **they are stored in the environment variables of the CI/CD system.**
<Callout title="What does this mean?">
It means that you will need to add the secret keys to the environment
variables of your CI/CD system (e.g., GitHub Actions, Vercel, Cloudflare, your
VPS, Netlify, etc.). This is not a TurboStarter-specific requirement, but a
best practice for security for any application. Ultimately, it's your choice.
</Callout>
Below is some examples of "what is a secret key?" in practice.
```dotenv title=".env.local"
# Secret keys
# The database URL is used to connect to your database.
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# Stripe server config - required only if you use Stripe as a billing provider
STRIPE_WEBHOOK_SECRET=""
STRIPE_SECRET_KEY=""
# Lemon Squeezy server config - required only if you use Lemon Squeezy as a billing provider
LEMON_SQUEEZY_API_KEY=""
LEMON_SQUEEZY_SIGNING_SECRET=""
LEMON_SQUEEZY_STORE_ID=""
...
```
<Callout title="Secrets used locally">
If you need to use secret keys locally, you can store them in the `.env.local`
file. This file is not committed to Git, therefore it is safe to store
sensitive information in it.
</Callout>

View File

@@ -0,0 +1,52 @@
---
title: Paths configuration
description: Learn how to configure the paths of your app.
url: /docs/web/configuration/paths
---
# Paths configuration
The paths configuration is set at `apps/web/config/paths.ts`. This configuration stores all the paths that you'll be using in your application. It is a convenient way to store them in a central place rather than scatter them in the codebase using magic strings.
It is **unlikely you'll need to change** this unless you're heavily editing the codebase.
```ts title="apps/web/config/paths.ts"
const pathsConfig = {
index: "/",
marketing: {
pricing: "/pricing",
contact: "/contact",
blog: {
index: BLOG_PREFIX,
post: (slug: string) => `${BLOG_PREFIX}/${slug}`,
},
legal: (slug: string) => `${LEGAL_PREFIX}/${slug}`,
},
auth: {
login: `${AUTH_PREFIX}/login`,
register: `${AUTH_PREFIX}/register`,
join: `${AUTH_PREFIX}/join`,
forgotPassword: `${AUTH_PREFIX}/password/forgot`,
updatePassword: `${AUTH_PREFIX}/password/update`,
error: `${AUTH_PREFIX}/error`,
},
dashboard: {
user: {
index: DASHBOARD_PREFIX,
ai: `${DASHBOARD_PREFIX}/ai`,
settings: {
index: `${DASHBOARD_PREFIX}/settings`,
security: `${DASHBOARD_PREFIX}/settings/security`,
billing: `${DASHBOARD_PREFIX}/settings/billing`,
},
},
...
},
...,
} as const;
```
<Callout title="Fully type-safe">
By declaring the paths as constants, we can use them safely throughout the
codebase. There is no risk of misspelling or using magic strings.
</Callout>

View File

@@ -0,0 +1,83 @@
---
title: Adding apps
description: Learn how to add apps to your Turborepo workspace.
url: /docs/web/customization/add-app
---
# Adding apps
<Callout title="Advanced topic" type="warn">
This is an **advanced topic** - you should only follow these instructions if you are sure you want to add a new app to your TurboStarter project within your monorepo and want to keep pulling updates from the TurboStarter repository.
</Callout>
In some ways - creating a new repository may be the easiest way to manage your application. However, if you want to keep your application within the monorepo and pull updates from the TurboStarter repository, you can follow these instructions.
To pull updates into a separate application outside of `web` - we can use [git subtree](https://www.atlassian.com/git/tutorials/git-subtree).
Basically, we will create a subtree at `apps/web` and create a new remote branch for the subtree. When we create a new application, we will pull the subtree into the new application. This allows us to keep it in sync with the `apps/web` folder.
To add a new app to your TurboStarter project, you need to follow these steps:
<Steps>
<Step>
## Create a subtree
First, we need to create a subtree for the `apps/web` folder. We will create a branch named `web-branch` and create a subtree for the `apps/web` folder.
```bash
git subtree split --prefix=apps/web --branch web-branch
```
</Step>
<Step>
## Create a new app
Now, we can create a new application in the `apps` folder.
Let's say we want to create a new app `ai-chat` at `apps/ai-chat` with the same structure as the `apps/web` folder (which acts as the template for all new apps).
```bash
git subtree add --prefix=apps/ai-chat origin web-branch --squash
```
You should now be able to see the `apps/ai-chat` folder with the contents of the `apps/web` folder.
</Step>
<Step>
## Update the app
When you want to update the new application, follow these steps:
### Pull the latest updates from the TurboStarter repository
The command below will update all the changes from the TurboStarter repository:
```bash
git pull upstream main
```
### Push the `web-branch` updates
After you have pulled the updates from the TurboStarter repository, you can split the branch again and push the updates to the web-branch:
```bash
git subtree split --prefix=apps/web --branch web-branch
```
Now, you can push the updates to the `web-branch`:
```bash
git push origin web-branch
```
### Pull the updates to the new application
Now, you can pull the updates to the new application:
```bash
git subtree pull --prefix=apps/ai-chat origin web-branch --squash
```
</Step>
</Steps>
That's it! You now have a new application in the monorepo 🎉

View File

@@ -0,0 +1,109 @@
---
title: Adding packages
description: Learn how to add packages to your Turborepo workspace.
url: /docs/web/customization/add-package
---
# Adding packages
<Callout title="Advanced topic" type="warn">
This is an **advanced topic** - you should only follow these instructions if you are sure you want to add a new package to your TurboStarter application instead of adding a folder to your application in `apps/web` or modify existing packages under `packages`. You don't need to do this to add a new page or component to your application.
</Callout>
To add a new package to your TurboStarter application, you need to follow these steps:
<Steps>
<Step>
## Generate a new package
First, enter the command below to create a new package in your TurboStarter application:
```bash
turbo gen package
```
Turborepo will ask you to enter the name of the package you want to create. Enter the name of the package you want to create and press enter.
If you don't want to add dependencies to your package, you can skip this step by pressing enter.
The command will have generated a new package under packages named `@turbostarter/<package-name>`. If you named it `example`, the package will be named `@turbostarter/example`.
Finally, to make fast refresh work when you make changes to the package, you need to add the package to the `next.config.ts` file in the root of your TurboStarter application `apps/web`.
```ts title="next.config.ts"
const INTERNAL_PACKAGES = [
// all internal packages,
"@turbostarter/example",
];
```
</Step>
<Step>
## Export a module from your package
By default, the package exports a single module using the `index.ts` file. You can add more exports by creating new files in the package directory and exporting them from the `index.ts` file or creating export files in the package directory and adding them to the `exports` field in the `package.json` file.
### From `index.ts` file
The easiest way to export a module from a package is to create a new file in the package directory and export it from the `index.ts` file.
```ts title="packages/example/src/module.ts"
export function example() {
return "example";
}
```
Then, export the module from the `index.ts` file.
```ts title="packages/example/src/index.ts"
export * from "./module";
```
### From `exports` field in `package.json`
**This can be very useful for tree-shaking.** Assuming you have a file named `module.ts` in the package directory, you can export it by adding it to the `exports` field in the `package.json` file.
```json title="packages/example/package.json"
{
"exports": {
".": "./src/index.ts",
"./module": "./src/module.ts"
}
}
```
**When to do this?**
1. when exporting two modules that don't share dependencies to ensure better tree-shaking. For example, if your exports contains both client and server modules.
2. for better organization of your package
For example, create two exports `client` and `server` in the package directory and add them to the `exports` field in the `package.json` file.
```json title="packages/example/package.json"
{
"exports": {
".": "./src/index.ts",
"./client": "./src/client.ts",
"./server": "./src/server.ts"
}
}
```
1. The `client` module can be imported using `import { client } from '@turbostarter/example/client'`
2. The `server` module can be imported using `import { server } from '@turbostarter/example/server'`
</Step>
<Step>
## Use the package in your application
You can now use the package in your application by importing it using the package name:
```ts title="apps/web/src/app/page.tsx"
import { example } from "@turbostarter/example";
console.log(example());
```
</Step>
</Steps>
Et voilà! You have successfully added a new package to your TurboStarter application. 🎉

View File

@@ -0,0 +1,120 @@
---
title: Components
description: Manage and customize your app components.
url: /docs/web/customization/components
---
# Components
For the components part, we're using [shadcn/ui](https://ui.shadcn.com) for atomic, accessible and highly customizable components.
<Callout type="info" title="Why shadcn/ui?">
shadcn/ui is a powerful tool that allows you to generate pre-designed
components with a single command. It's built with Tailwind CSS and Radix UI,
and it's highly customizable.
</Callout>
TurboStarter defines two packages that are responsible for the UI part of your app:
* `@turbostarter/ui` - shared styles, [themes](/docs/web/customization/styling#themes) and assets (e.g. icons)
* `@turbostarter/ui-web` - pre-built UI web components, ready to use in your app
## Adding a new component
There are basically two ways to add a new component:
<Tabs items={["Using the CLI", "Copy-pasting"]}>
<Tab value="Using the CLI">
TurboStarter is fully compatible with [shadcn CLI](https://ui.shadcn.com/docs/cli), so you can generate new components with single command.
Run the following command from the **root** of your project:
```bash
pnpm --filter @turbostarter/ui-web ui:add
```
This will launch an interactive command-line interface to guide you through the process of adding a new component where you can pick which component you want to add.
```bash
Which components would you like to add? > Space to select. A to toggle all.
Enter to submit.
◯ accordion
◯ alert
◯ alert-dialog
◯ aspect-ratio
◯ avatar
◯ badge
◯ button
◯ calendar
◯ card
◯ checkbox
```
Newly created components will appear in the `packages/ui/web/src` directory.
</Tab>
<Tab value="Copy-pasting">
You can always copy-paste a component from the [shadcn/ui](https://ui.shadcn.com/docs/components) website and modify it to your needs.
This is possible, because the components are headless and don't need (in most cases) any additional dependencies.
Copy code from the website, create a new file in the `packages/ui/web/src` directory and paste the code into the file.
</Tab>
</Tabs>
<Callout title="Keep it atomic" type="warn">
Keep in mind that you should always try to keep shared components as atomic as possible. This will make it easier to reuse them and to build specific views by composition.
E.g. include components like `Button`, `Input`, `Card`, `Dialog` in shared package, but keep specific components like `LoginForm` in your app directory.
</Callout>
## Using components
Each component is a standalone entity which has a separate export from the package. It helps to keep things modular, avoid unnecessary dependencies and make tree-shaking possible.
To import a component from the UI package, use the following syntax:
```tsx title="components/my-component.tsx"
// [!code word:card]
import {
Card,
CardContent,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
} from "@turbostarter/ui-web/card";
```
Then you can use it to build a component specific to your app:
```tsx title="components/my-component.tsx"
export function MyComponent() {
return (
<Card>
<CardHeader>
<CardTitle>My Component</CardTitle>
</CardHeader>
<CardContent>
<p>My Component Content</p>
</CardContent>
<CardFooter>
<Button>Click me</Button>
</CardFooter>
</Card>
);
}
```
<Callout title="Recommendation: use v0 to generate layouts">
We recommend using [v0](https://v0.dev) to generate layouts for your app. It's a powerful tool that allows you to generate layouts from the natural language instructions.
Of course, **it won't replace a designer**, but it can be a good starting point for your layout.
</Callout>
<Cards>
<Card href="https://ui.shadcn.com/" title="shadcn/ui" description="ui.shadcn.com" />
<Card href="https://v0.dev/chat" title="v0 by Vercel" description="v0.dev" />
</Cards>

View File

@@ -0,0 +1,153 @@
---
title: Styling
description: Get started with styling your app.
url: /docs/web/customization/styling
---
# Styling
To build the web user interface, TurboStarter comes with [Tailwind CSS](https://tailwindcss.com/) and [Radix UI](https://www.radix-ui.com/) pre-configured.
<Callout title="Why Tailwind CSS and Radix UI?" type="info">
The combination of Tailwind CSS and Radix UI gives ready-to-use, accessible UI components that can be fully customized to match your brand's design.
</Callout>
## Tailwind configuration
In the `packages/ui/shared/src/styles` directory, you will find shared CSS files with Tailwind CSS configuration. To change global styles, you can edit the files in this folder.
Here is an example of a shared CSS file that includes the Tailwind CSS configuration:
```css title="packages/ui/shared/src/styles/globals.css"
@import "tailwindcss";
@import "./themes.css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.65rem;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
...
}
```
For colors, we rely strictly on [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) in [OKLCH](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch) format to allow for easy theme management without the need for any JavaScript.
Also, each app has its own `globals.css` file, which extends the shared config and allows you to override the global styles.
Here is an example of an app's `globals.css` file:
```css title="apps/web/src/assets/styles/globals.css"
@import "@turbostarter/ui/globals.css";
@import "@turbostarter/ui-web/globals.css";
@theme inline {
/* Overridden theme variables for the app */
--background: oklch(0.98 0.01 80);
--foreground: oklch(0.22 0.03 120);
--card: oklch(0.97 0.02 50);
--card-foreground: oklch(0.18 0.01 280);
...
}
```
This way, we maintain a separation of concerns and a clear structure for the Tailwind CSS configuration.
## Themes
TurboStarter comes with **9+** predefined themes, which you can use to quickly change the look and feel of your app.
They're defined in the `packages/ui/shared/src/styles/themes` directory. Each theme is a set of variables that can be overridden:
```ts title="packages/ui/shared/src/styles/themes/colors/orange.ts"
export const orange = {
light: {
background: [1, 0, 0],
foreground: [0.141, 0.005, 285.823],
card: [1, 0, 0],
"card-foreground": [0.141, 0.005, 285.823],
...
}
} satisfies ThemeColors;
```
Each variable is stored as a [OKLCH](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch) array, which is then converted to a CSS variable at build time (by our custom build script). That way we can ensure full type-safety and reuse themes across different parts of our apps (e.g. use the same theme in emails).
Feel free to add your own themes or override the existing ones to match your brand's identity.
To apply a theme to your app, you can use the `data-theme` attribute on the `html` element:
```tsx title="apps/web/src/app/layout.tsx"
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body data-theme="orange">{children}</body>
</html>
);
}
```
## Dark mode
TurboStarter comes with built-in dark mode support.
Each theme has a corresponding set of dark mode variables, which are used to switch the theme to its dark mode counterpart.
```ts title="packages/ui/shared/src/styles/themes/colors/orange.ts"
export const orange = {
light: {},
dark: {
background: [0.141, 0.005, 285.823],
foreground: [0.985, 0, 0],
card: [0.21, 0.006, 285.885],
"card-foreground": [0.985, 0, 0],
...
}
} satisfies ThemeColors;
```
Because the dark variant is defined to use a class (`@custom-variant dark (&:is(.dark *))`) in the shared Tailwind configuration, we need to add the `dark` class to the `html` element to apply dark mode styles.
For this purpose, we're using the [next-themes](https://github.com/pacocoursey/next-themes) package under the hood to handle user preference management.
```tsx title="apps/web/src/providers/theme.tsx"
export const ThemeProvider = memo<ThemeProviderProps>(({ children }) => {
return (
<NextThemeProvider
attribute="class"
defaultTheme={appConfig.theme.mode}
enableSystem
>
{children}
<ThemeConfigProvider />
</NextThemeProvider>
);
});
```
You can also define the default theme mode and color in the [app configuration](/docs/web/configuration/app).
<Cards>
<Card title="Tailwind CSS" description="tailwindcss.com" href="https://tailwindcss.com/" />
<Card title="Radix UI" description="radix-ui.com" href="https://www.radix-ui.com/" />
</Cards>

View File

@@ -0,0 +1,79 @@
---
title: Database client
description: Use database client to interact with the database.
url: /docs/web/database/client
---
# Database client
The database client is an export of the Drizzle client. It is automatically typed by Drizzle based on the schema and is exposed as the db object from the database package (`@turbostarter/db`) in the monorepo.
This guide covers how to initialize the client and also basic operations, such as querying, creating, updating, and deleting records. To learn more about the Drizzle client, check out the [official documentation](https://orm.drizzle.team/kit-docs/overview).
## Initializing the client
Pass the validated `DATABASE_URL` to the client to initialize it.
```ts title="server.ts"
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { env } from "../env";
const client = postgres(env.DATABASE_URL);
export const db = drizzle(client);
```
Now it's exported from the `@turbostarter/db` package and can be used across the codebase (server-side).
## Querying data
To query data, you can use the `db` object and its methods:
```ts title="query.ts"
import { eq } from "@turbostarter/db";
import { db } from "@turbostarter/db/server";
import { customer } from "@turbostarter/db/schema";
export const getCustomerByUserId = async (userId: string) => {
const [data] = await db
.select()
.from(customer)
.where(eq(customer.userId, userId));
return data ?? null;
};
```
<Cards className="sm:grid-cols-3">
<Card title="Select" description="orm.drizzle.team" href="https://orm.drizzle.team/docs/select" />
<Card title="Filters" description="orm.drizzle.team" href="https://orm.drizzle.team/docs/operators" />
<Card title="Joins" description="orm.drizzle.team" href="https://orm.drizzle.team/docs/joins" />
</Cards>
## Mutating data
You can use the exported utilities to mutate data. Insert, update or delete records in fast and fully type-safe way:
```ts title="mutation.ts"
import { eq } from "@turbostarter/db";
import { db } from "@turbostarter/db/server";
import { customer } from "@turbostarter/db/schema";
export const upsertCustomer = (data: InsertCustomer) => {
return db.insert(customer).values(data).onConflictDoUpdate({
target: customer.userId,
set: data,
});
};
```
<Cards className="sm:grid-cols-3">
<Card title="Insert" description="orm.drizzle.team" href="https://orm.drizzle.team/docs/insert" />
<Card title="Update" description="orm.drizzle.team" href="https://orm.drizzle.team/docs/update" />
<Card title="Delete" description="orm.drizzle.team" href="https://orm.drizzle.team/docs/delete" />
</Cards>

View File

@@ -0,0 +1,49 @@
---
title: Migrations
description: Migrate your changes to the database.
url: /docs/web/database/migrations
---
# Migrations
You have your schema in place, and you want to apply your changes to the database. TurboStarter provides you a convenient way to do so with pre-configured CLI commands.
## Generating migration
To generate a migration, from the schema you need to run the following command:
```bash
pnpm with-env turbo db:generate
```
This will create a new `.sql` file in the `migrations` directory.
<Callout>
Drizzle will also generate a `.json` representation of the migration in the `meta` directory, but it's for its internal purposes and you shouldn't need to touch it.
</Callout>
## Applying migrations
To apply the migrations to the database, you need to run the following command:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:migrate
```
This will apply all the migrations that have not been applied yet. If any conflicts arise, you can resolve them by modifying the generated migration file.
## Pushing changes
To push changes directly to the database, you can use the following command:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:push
```
This lets you push your schema changes directly to the database and omit managing SQL migration files.
<Callout type="warn" title="Use with caution!">
Pushing changes directly to the database (without using migrations) could be risky. Please be careful when using it; we recommend it only for local development and local databases.
[Read more about it in the Drizzle docs](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push).
</Callout>

View File

@@ -0,0 +1,83 @@
---
title: Overview
description: Get started with the database.
url: /docs/web/database/overview
---
# Overview
We're using [Drizzle ORM](https://orm.drizzle.team) to interact with the database. It basically adds a little layer of abstraction between our code and the database.
> If you know SQL, you know Drizzle.
For the database we're leveraging [PostgreSQL](https://www.postgresql.org), but you could use any other database that Drizzle ORM supports (basically any SQL database e.g. [MySQL](https://orm.drizzle.team/docs/get-started-mysql), [SQLite](https://orm.drizzle.team/docs/get-started-sqlite), etc.).
<Callout title="Why Drizzle ORM?">
Drizzle ORM is a powerful tool that allows you to interact with the database in a type-safe manner. It ships with **0** (!) dependencies and is designed to be fast and easy to use.
</Callout>
## Setup
To start interacting with the database you first need to ensure that your database service instance is up and running.
<Tabs items={["Local development", "Cloud instance"]}>
<Tab value="Local development">
For local development we recommend using the [Docker](https://hub.docker.com/_/postgres) container.
You can start the container with the following command:
```bash
pnpm services:setup
```
This will start all the services (including the database container) and initialize the database with the latest schema.
**Where is DATABASE\_URL?**
`DATABASE_URL` is a connection string that is used to connect to the database. When the command will finish it will be displayed in the console and setup to your environment variables.
</Tab>
<Tab value="Cloud instance">
You can also use a cloud instance of database (e.g. [Supabase](/docs/web/recipes/supabase), [Neon](https://neon.tech/), [Turso](https://turso.tech/), etc.), although it's not recommended for local development.
If you choose Supabase as your provider, follow the [Supabase recipe](/docs/web/recipes/supabase#configure-environment-variables) for details on configuring `DATABASE_URL` and running migrations.
**Where is DATABASE\_URL?**
It's available in your provider's project dashboard. You'll need to copy the connection string from there and add it to your `.env.local` file. The format will look something like:
* Neon: `postgresql://user:password@ep-xyz-123.region.aws.neon.tech/dbname`
* Turso: `libsql://your-db-xyz.turso.io`
Make sure to keep this URL secure and never commit it to version control.
</Tab>
</Tabs>
Then, you need to set `DATABASE_URL` environment variable in **root** `.env.local` file.
```dotenv title=".env.local"
# The database URL is used to connect to your database.
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
```
You're ready to go! 🥳
## Studio
TurboStarter provides you also with an interactive UI where you can explore your database and test queries called Studio.
To run the Studio, you can use the following command:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:studio
```
This will start the Studio on [https://local.drizzle.studio](https://local.drizzle.studio).
![Drizzle Studio](/images/docs/db-studio.webp)
## Next steps
* [Update schema](/docs/web/database/schema) - learn about schema and how to update it.
* [Generate & run migrations](/docs/web/database/migrations) - migrate your changes to the database.
* [Initialize client](/docs/web/database/client) - initialize the database client and start interacting with the database.

View File

@@ -0,0 +1,75 @@
---
title: Schema
description: Learn about the database schema.
url: /docs/web/database/schema
---
# Schema
Creating a schema for your data is one of the primary tasks when building a new application.
You can find the schema of each table in `packages/db/src/db/schema` directory. The schema is basically organized by entity and each file is a separate table.
## Defining schema
The schema is defined using SQL-like utilities from [drizzle-orm](https://orm.drizzle.team/docs/sql-schema-declaration).
It supports all the SQL features, such as enums, indexes, foreign keys, extensions and more.
<Callout title="Code-first approach">
We're relying on the [code-first approach](https://orm.drizzle.team/docs/migrations), where we define the schema in code and then generate the SQL from it. That way we can approach full type-safety and the simplest flow for database updates and migrations.
</Callout>
## Example
Let's take a look at the `customer` table, where we store information about our customers.
```typescript title="customer.ts"
export const customer = pgTable("customer", {
id: text().primaryKey().$defaultFn(generateId),
userId: text()
.references(() => user.id, {
onDelete: "cascade",
})
.notNull()
.unique(),
customerId: text().notNull().unique(),
status: billingStatusEnum(),
plan: pricingPlanTypeEnum(),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp()
.notNull()
.$onUpdate(() => new Date()),
});
```
We're using a few native SQL utilities here, such as:
* `pgTable` - a table definition.
* `primaryKey` - a primary key.
* `defaultFn` - a default function.
* `$onUpdate` - an on update function.
* `notNull` - a not null constraint.
* `defaultNow` - a default now function.
* `timestamp` - a timestamp.
* `text` - a text.
* `unique` - a unique constraint.
* `references` - a reference to another table.
What's more, Drizzle gives us the ability to export the TypeScript types for the table, which we can reuse e.g. for the API calls.
Also, we can use the drizzle extension [drizzle-zod](https://orm.drizzle.team/docs/zod) to generate the Zod schemas for the table.
```typescript title="customer.ts"
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
export const insertCustomerSchema = createInsertSchema(customer);
export const selectCustomerSchema = createSelectSchema(customer);
export const updateCustomerSchema = createUpdateSchema(customer);
export type InsertCustomer = z.infer<typeof insertCustomerSchema>;
export type SelectCustomer = z.infer<typeof selectCustomerSchema>;
export type UpdateCustomer = z.infer<typeof updateCustomerSchema>;
```
Then we can use the generated schemas in our API handlers and frontend forms to validate the data.

View File

@@ -0,0 +1,125 @@
---
title: AWS Amplify
description: Learn how to deploy your TurboStarter app to AWS Amplify.
url: /docs/web/deployment/amplify
---
# AWS Amplify
[AWS Amplify](https://aws.amazon.com/amplify/) is a fully managed service that makes it easy to build, deploy, and host modern web applications. It provides features like continuous deployment, serverless functions, authentication, and more - all integrated into a seamless developer experience.
This guide explains how to deploy your TurboStarter app on AWS Amplify. You'll learn how to set up your repository for automated deployments, configure build settings, manage environment variables, and ensure your application runs smoothly in production. **AWS Amplify handles the infrastructure management, allowing you to focus on developing your application.**
<Callout type="warn" title="Prerequisite: AWS account">
To deploy to AWS Amplify, you need to have an AWS account. You can create one [here](https://aws.amazon.com/amplify/).
</Callout>
<Steps>
<Step>
## Create configuration file
To deploy your TurboStarter app to AWS Amplify, you need to create a config file. This file will contain the necessary information to connect your repository to AWS Amplify and deploy your application.
Let's create a new file called `amplify.yml` in the root of your project:
```yaml title="amplify.yml"
version: 1
applications:
- frontend:
buildPath: "/"
phases:
preBuild:
commands:
- npm install -g pnpm
- pnpm install
build:
commands:
- pnpm dlx turbo build --filter=web
artifacts:
baseDirectory: apps/web/.next
files:
- "**/*"
cache:
paths:
- node_modules/**/*
- apps/web/.next/cache/**/*
appRoot: apps/web
```
This configuration file tells AWS Amplify how to build and deploy your application:
* The `version` field specifies the Amplify configuration version
* Under `applications`, we define the build settings for our web app:
* `buildPath` indicates where to run the build commands
* `preBuild` phase installs pnpm and project dependencies
* `build` phase runs the Turborepo build command for the web app
* `artifacts` specifies which files to deploy (the Next.js build output)
* `cache` configures which directories to cache between builds
* `appRoot` points to the web application directory
AWS Amplify will use this configuration to automatically build and deploy your app whenever you push changes to your repository. It also useful to define other resources that you can use and link to your project.
</Step>
<Step>
## Create a new Amplify project
We'll use the [AWS Amplify](https://aws.amazon.com/amplify/) web interface to deploy our app. First, let's create a new project.
![Amplify create project](/images/docs/web/deployment/amplify/create-project.png)
Proceed with the option to *Deploy an app*.
</Step>
<Step>
## Connect repository
Choose the Git provider of your project and select the repository you want to deploy.
![Amplify connect repository](/images/docs/web/deployment/amplify/connect-repository.png)
<Callout title="Authorization needed">
If your repository is private you need to authorize Amplify to access it. It's recommended to follow a *least privileged access* approach, so to only grant access to the repository you want to deploy, not the entire account.
</Callout>
Select the branch you want to deploy and make sure to enable the *My app is a monorepo* option - configure it with the path to the app that you want to deploy (e.g. `apps/web`).
![Amplify repository and branch](/images/docs/web/deployment/amplify/repository.png)
</Step>
<Step>
## Configure build settings
Finalize your deployment by configuring the build settings to match your project's specific needs. Refer to the points below to ensure a seamless deployment process.
![Amplify build settings](/images/docs/web/deployment/amplify/build-settings.png)
Make sure that the build command and build output directory is set to the correct values (it should be defined based on your configuration file from Step 1.).
### Environment variables
In the *Advanced settings* section, you can define environment variables that will be available to your application at runtime.
![Amplify environment variables](/images/docs/web/deployment/amplify/environment-variables.png)
Verify that all required environment variables are defined, so your app can be build and deployed successfully.
</Step>
<Step>
## Review and deploy!
On the next step, you'll be able to review the configuration that you've created and deploy your app. It's the right time to make sure that everything is set up correctly.
![Amplify review and deploy](/images/docs/web/deployment/amplify/review.png)
After making sure that everything is set up correctly, you can click on the *Save and deploy* button to start the deployment process.
When your app is deployed, you'll be able to access it via the URL provided in the Amplify console:
![Amplify deployed app](/images/docs/web/deployment/amplify/deployed.png)
That's it! Your app is now deployed to AWS Amplify, congratulations! 🎉
</Step>
</Steps>
Feel free to scale your deployment to multiple regions, add custom domains, and use other Amplify features to make your app more robust and scalable.
Check out the [AWS Amplify documentation](https://docs.aws.amazon.com/amplify/latest/userguide/welcome.html) for more information on how to use Amplify to its full potential.

View File

@@ -0,0 +1,198 @@
---
title: Standalone API
description: Learn how to deploy your API as a dedicated service.
url: /docs/web/deployment/api
---
# Standalone API
Sometimes you want to deploy your API as a standalone service. This is useful if you want to deploy your API to a different domain or to deploy it as a microservice. You can also follow this approach if you don't need a web app, but still need API service for [mobile app](/docs/mobile) or [browser extension](/docs/extension).
Deploying your API as a standalone service provides enhanced flexibility and scalability. This allows you to independently scale your API from your web app. It's particularly beneficial for executing "long-running" tasks on your backend, such as report generation, real-time data processing, or background tasks that are likely to timeout in a serverless environment.
This guide explains how to deploy your TurboStarter API as a standalone service. As Hono has multiple deployment options (e.g. [Deno](https://hono.dev/docs/getting-started/deno), [Bun](https://hono.dev/docs/getting-started/bun)), this guide will focus primarily on the [Node.js](https://hono.dev/docs/getting-started/nodejs) deployment.
<Steps>
<Step>
## Create separate API app
We have a [dedicated guide](/docs/web/customization/add-app) on how to add another app to your project. However, in this case, only a few files need to be added, so we can do it quickly here.
First, let's create an `api` directory inside the `apps` directory - it will be the root of your API app.
Next, add the following files into the `apps/api` directory:
<Tabs items={["package.json", "tsconfig.json", "src/index.ts"]}>
<Tab value="package.json">
```json
{
"name": "api",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.js",
"clean": "git clean -xdf dist .turbo node_modules",
"dev": "dotenv -c -- tsx watch src/index.ts",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@hono/node-server": "1.13.7",
"@turbostarter/api": "workspace:*"
},
"devDependencies": {
"@turbostarter/tsconfig": "workspace:*",
"@types/node": "20.16.10",
"esbuild": "0.24.2",
"tsx": "4.19.2",
"typescript": "catalog:"
}
}
```
</Tab>
<Tab value="tsconfig.json">
```json
{
"extends": "@turbostarter/tsconfig/base.json",
"include": ["src"],
"exclude": ["node_modules"]
}
```
</Tab>
<Tab value="src/index.ts">
```ts
import { serve } from "@hono/node-server";
import { appRouter } from "@turbostarter/api";
serve(
{
fetch: appRouter.fetch,
port: Number(process.env.PORT) || 3001,
},
({ port }) => {
console.log(`Server is running on ${port} 🚀`);
},
);
```
</Tab>
</Tabs>
This will enable you to have a minimal configuration required to run your API as a standalone service. For sure, you can add more configuration (e.g. ESLint or Prettier) if needed, we just want to keep it minimal for the sake of this guide.
</Step>
<Step>
## Connect web app to API
The API will be running on a different URL than your web app. For the minimal setup and to avoid handling [cross-origin resource sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) issues, we will rewrite the API URL in the web app.
To do this, you will need to change your `next.config.ts` file to include the API URL rewrite:
```js title="apps/web/next.config.ts"
import type { NextConfig } from "next";
const config: NextConfig = {
rewrites: async () => [
{
source: "/api/:path*",
destination: `${env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001"}/api/:path*`,
},
],
};
```
<Callout title="Use environment variable to set API url">
It's recommended to use an environment variable (e.g. `NEXT_PUBLIC_API_URL`) to set the API URL. This is a good practice to make it easier to change the API URL in different environments (e.g. development, staging, production).
</Callout>
Now you should be able to run your API as a standalone service. When you run the project with `pnpm dev`, you will see the new app called `api` with your API server running on [http://localhost:3001](http://localhost:3001).
</Step>
<Step>
## Deploy!
You can basically deploy your API as any other Node.js project. We will quickly go through the two most popular options: [PaaS](https://en.wikipedia.org/wiki/Platform_as_a_service) and [Docker](https://www.docker.com/).
### Platform as a Service (PaaS)
PaaS providers like [Vercel](https://vercel.com/), [Heroku](https://www.heroku.com/), or [Netlify](https://www.netlify.com/) allow you to deploy your Node.js app with a few clicks. You can follow our [dedicated guides](/docs/web/deployment/checklist#deploy-web-app-to-production) for the most popular providers. Every process is similar, and will contains a few crucial steps:
1. Connecting your repository to the PaaS provider
2. Setting up build settings (e.g. build command, output directory)
3. Setting up environment variables
4. Deploying the project
<Callout title="Ensure correct commands">
To make sure your API is built and run correctly, you will need to ensure that appropriate commands are correctly set up. In our case, the following commands will need to be configured:
<Tabs items={["Build command", "Start command"]}>
<Tab value="Build command">
```bash
pnpm turbo build --filter=api
```
</Tab>
<Tab value="Start command">
```bash
pnpm --filter=api start
```
</Tab>
</Tabs>
This is required to ensure that the PaaS provider of your choice will be able to build and run your application correctly.
</Callout>
### Docker
Deploying your API as a Docker container is a good option if you want to have more control over the deployment process. You can follow our [dedicated guide](/docs/web/deployment/docker) to learn how to deploy your API as a Docker container.
For the API application, the `Dockerfile` will be located in the `apps/api` directory and it could look like this:
```dockerfile title="apps/api/Dockerfile"
FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS pruner
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY . .
RUN pnpm dlx turbo prune api --docker
FROM base AS builder
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile --ignore-scripts --prefer-offline && pnpm store prune
ENV SKIP_ENV_VALIDATION=1 \
NODE_ENV=production
COPY --from=pruner /app/out/full/ .
RUN pnpm dlx turbo build --filter=api
FROM base AS runner
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S api -u 1001 -G nodejs
COPY --from=builder --chown=api:nodejs /app/apps/api/dist/ ./
USER api
EXPOSE 3001
CMD ["node", "index.js"]
```
To test if everything works correctly, you can run a [container](https://docs.docker.com/get-started/03_run_your_app/) locally with the following commands:
```bash
docker build -f ./apps/api/Dockerfile . -t turbostarter-api
docker run -p 3001:3001 turbostarter-api
```
Make sure to also [pass](https://docs.docker.com/reference/cli/docker/container/run/#env) all the required environment variables to the container, so your API can start without any issues.
Deploying your API as a Docker container is a great way to isolate your API from the host environment, making it easier to deploy and scale. It also simplifies the workflow if you're working with a team, as you can easily share the Docker image with your colleagues and they will run the API in the **exact same** environment.
</Step>
</Steps>
That's it! You can now grow your API layer as a standalone service, separated from other apps in your project, and deploy it anywhere you want.

View File

@@ -0,0 +1,193 @@
---
title: Checklist
description: Let's deploy your TurboStarter app to production!
url: /docs/web/deployment/checklist
---
# Checklist
When you're ready to deploy your project to production, follow this checklist.
This process may take a few hours and some trial and error, so buckle up - you're almost there!
<Steps>
<Step>
## Create database instance
**Why it's necessary?**
A production-ready database instance is essential for storing your application's data securely and reliably in the cloud. [PostgreSQL](https://www.postgresql.org/) is the recommended database for TurboStarter due to its robustness, features, and wide support.
**How to do it?**
You have several options for hosting your PostgreSQL database:
* [Supabase](/docs/web/recipes/supabase) - Provides a fully managed Postgres database with additional features
* [Vercel Postgres](https://vercel.com/storage/postgres) - Serverless SQL database optimized for Vercel deployments
* [Neon](https://neon.tech/) - Serverless Postgres with automatic scaling
* [Turso](https://turso.tech/) - Edge database built on libSQL with global replication
* [DigitalOcean](https://www.digitalocean.com/products/managed-databases) - Managed database clusters with automated failover
Choose a provider based on your needs for:
* Pricing and budget
* Geographic region availability
* Scaling requirements
* Additional features (backups, monitoring, etc.)
</Step>
<Step>
## Migrate database
**Why it's necessary?**
Pushing database migrations ensures that your database schema in the remote database instance is configured to match TurboStarter's requirements. This step is crucial for the application to function correctly.
**How to do it?**
You basically have two possibilities of doing a migration:
<Tabs items={["Using Github Actions (recommended)", "Running locally"]}>
<Tab value="Using Github Actions (recommended)">
TurboStarter comes with predefined Github Actions workflow to handle database migrations. You can find its definition in the `.github/workflows/publish-db.yml` file.
What you need to do is to set your `DATABASE_URL` as a [secret for your Github repository](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions).
Then, you can run the workflow which will publish the database schema to your remote database instance.
[Check how to run Github Actions workflow.](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow)
</Tab>
<Tab value="Running locally">
You can also run your migrations locally, although this is not recommended for production.
To do so, set the `DATABASE_URL` environment variable to your database URL (that comes from your database provider) in `.env.local` file and run the following command:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:migrate
```
This command will run the migrations and apply them to your remote database.
[Learn more about database migrations.](/docs/web/database/migrations)
</Tab>
</Tabs>
</Step>
<Step>
## Configure OAuth Providers
**Why it's necessary?**
Configuring OAuth providers like [Google](https://www.better-auth.com/docs/authentication/google) or [Github](https://www.better-auth.com/docs/authentication/github) ensures that users can log in using their existing accounts, enhancing user convenience and security. This step involves setting up the OAuth credentials in the provider's developer console, configuring the necessary environment variables, and setting up callback URLs to point to your production app.
**How to do it?**
1. Follow the provider-specific guides to set up OAuth credentials for the providers you want to use. For example:
* [Apple OAuth setup guide](https://www.better-auth.com/docs/authentication/apple)
* [Google OAuth setup guide](https://www.better-auth.com/docs/authentication/google)
* [Github OAuth setup guide](https://www.better-auth.com/docs/authentication/github)
2. Once you have the credentials, set the corresponding environment variables in your project. For the example providers above:
* For Apple: `APPLE_CLIENT_ID` and `APPLE_CLIENT_SECRET`
* For Google: `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`
* For Github: `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`
3. Ensure that the callback URLs for each provider are set to point to your production app. **This is crucial for the OAuth flow to work correctly.**
You can add or remove OAuth providers based on your needs. Just make sure to follow the provider's setup guide, set the required environment variables, and configure the callback URLs correctly.
</Step>
<Step>
## Setup billing provider
**Why it's necessary?**
Well - you want to get paid, right? Setting up billing ensures that you can charge your users for using your SaaS application, enabling you to monetize your service and cover operational costs.
**How to do it?**
* Create a [Stripe](/docs/web/billing/stripe), [Lemon Squeezy](/docs/web/billing/lemon-squeezy) or [Polar](/docs/web/billing/polar) account.
* Update the environment variables with the correct values for your billing service.
* Point webhooks from Stripe, Lemon Squeezy or Polar to `/api/billing/webhook`.
* Refer to the [relevant documentation](/docs/web/billing/overview) for more details on setting up billing.
</Step>
<Step>
## Setup emails provider
**Why it's necessary?**
Setting up an email provider is crucial for your SaaS application to send notifications, confirmations, and other important messages to your users. This enhances user experience and engagement, and is a standard practice in modern web applications.
**How to do it?**
* Create an account with an email service provider of your choice. See [available providers](/docs/web/emails/configuration#providers) for more information.
* Update the environment variables with the correct values for your email service.
* Refer to the [relevant documentation](/docs/web/emails/overview) for more details on setting up email.
</Step>
<Step>
## Setup storage provider
**Why it's necessary?**
Don't forget to configure your storage provider, if you want to operate on files in your app. By default, this is optional — the app can run without a storage provider — but some features could be unavailable (e.g., avatar uploads and other file-related actions).
**How to do it?**
* Review the [Storage overview](/docs/web/storage/overview).
* Follow [Storage configuration](/docs/web/storage/configuration) to choose and set up a provider.
* Add any required environment variables in your **hosting provider**.
</Step>
<Step>
## Environment variables
**Why it's necessary?**
Setting the correct environment variables is essential for the application to function correctly. These variables include API keys, database URLs, and other configuration details required for your app to connect to various services.
**How to do it?**
Use our `.env.example` files to get the correct environment variables for your project. Then add them to your **hosting provider's environment variables**. Redeploy the app once you have the URL to set in the environment variables.
</Step>
<Step>
## Deploy web app to production
**Why it's necessary?**
Because your users are waiting! Deploying your Next.js app to a hosting provider makes it accessible to users worldwide, allowing them to interact with your application.
**How to do it?**
Deploy your Next.js app to chosen hosting provider. **Copy the deployment URL and set it as an environment variable in your project's settings.** Feel free to check out our dedicated guides for the most popular hosting providers:
<Cards>
<Card title="Vercel" description="Deploy your TurboStarter web app to Vercel platform." href="/docs/web/deployment/vercel" />
<Card title="Netlify" description="Deploy your TurboStarter web app to Netlify platform." href="/docs/web/deployment/netlify" />
<Card title="Render" description="Deploy your TurboStarter web app to Render platform." href="/docs/web/deployment/render" />
<Card title="Railway" description="Deploy your TurboStarter web app to Railway platform." href="/docs/web/deployment/railway" />
<Card title="AWS Amplify" description="Deploy your TurboStarter web app to AWS Amplify platform." href="/docs/web/deployment/amplify" />
<Card title="Docker" description="Containerize your TurboStarter web app using Docker." href="/docs/web/deployment/docker" />
<Card title="Fly.io" description="Deploy your TurboStarter web app to Fly.io platform." href="/docs/web/deployment/fly" />
</Cards>
We also have a dedicated guide for [deploying your API as a standalone service](/docs/web/deployment/api).
</Step>
</Steps>
That's it! Your app is now live and accessible to your users, good job! 🎉
<Callout title="Other things to consider">
* Update the legal pages with your company's information (privacy policy, terms of service, etc.).
* Remove the placeholder blog and documentation content / or replace it with your own.
* Customize authentication emails and other email templates.
* Update the favicon and logo with your own branding.
* Update the FAQ and other static content with your own information.
</Callout>

View File

@@ -0,0 +1,93 @@
---
title: Docker
description: Learn how to containerize your TurboStarter app with Docker.
url: /docs/web/deployment/docker
---
# Docker
[Docker](https://docker.com) is a popular platform for containerizing applications, making it easy to package your app with all its dependencies for consistent performance across environments. It simplifies development, testing, and deployment.
This guide explains how to containerize your TurboStarter app using Docker. You'll learn to create a Dockerfile, build a container image, and run your app in a container for a reliable and portable setup.
<Steps>
<Step>
## Configure Next.js for Docker
First of all, we need to configure Next.js to output the build files in the [standalone format](https://nextjs.org/docs/pages/api-reference/config/next-config-js/output) - it's required for the Docker image to work. To do this, we need to add the following to our `next.config.ts` file:
```js title="apps/web/next.config.ts"
import type { NextConfig } from "next";
const config: NextConfig = {
output: "standalone",
...
};
```
</Step>
<Step>
## Create a Dockerfile
[Dockerfile](https://docs.docker.com/get-started/02_our_app/) is a text file that contains the instructions for building a [Docker image](https://docs.docker.com/get-started/02_our_app/). It defines the environment, dependencies, and commands needed to run your app. You can safely copy the following Dockerfile to your project:
```dockerfile title="apps/web/Dockerfile"
FROM node:22-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS pruner
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY . .
RUN pnpm dlx turbo prune web --docker
FROM base AS builder
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile --ignore-scripts --prefer-offline && pnpm store prune
ENV SKIP_ENV_VALIDATION=1 \
NODE_ENV=production
COPY --from=pruner /app/out/full/ .
RUN pnpm dlx turbo build --filter=web
FROM base AS runner
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S web -u 1001 -G nodejs
COPY --from=builder --chown=web:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=web:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=web:nodejs /app/apps/web/public ./apps/web/public
USER web
EXPOSE 3000
CMD ["node", "apps/web/server.js"]
```
Feel free to check out our [self-hosting guide](/blog/self-host-your-nextjs-turborepo-app-with-docker-in-5-minutes) for more details on how each stage of the Dockerfile works.
And that's all we need! You can now build and run your Docker image to deploy your app anywhere you want in an [isolated environment](https://docs.docker.com/get-started/workshop/04_sharing_app/).
</Step>
<Step>
## Run a container
To test if everything works correctly, you can run a [container](https://www.docker.com/resources/what-container/) locally with the following commands:
```bash
docker build -f ./apps/web/Dockerfile . -t turbostarter
docker run -p 3000:3000 turbostarter
```
Make sure to also [pass](https://docs.docker.com/reference/cli/docker/container/run/#env) all the required environment variables to the container, so your app can start without any issues.
If everything works correctly, you should be able to access your app at [http://localhost:3000](http://localhost:3000).
</Step>
</Steps>
That's it! You can now build and deploy your app as a Docker container to any supported hosting (e.g. [Fly.io](/docs/web/deployment/fly)).
Using Docker containers is a great way to isolate your app from the host environment, making it easier to deploy and scale. It also simplifies the workflow if you're working with a team, as you can easily share the Docker image with your colleagues and they will run the app in the **exact same** environment.

View File

@@ -0,0 +1,112 @@
---
title: Fly.io
description: Learn how to deploy your TurboStarter app to Fly.io.
url: /docs/web/deployment/fly
---
# Fly.io
[Fly.io](https://fly.io) makes deploying web applications to the cloud easy and efficient. It handles scaling, monitoring, and logging so you can focus on building your app.
This guide explains how to deploy your TurboStarter app on Fly.io. You'll learn how to leverage [Docker](/docs/web/deployment/docker) containers to deploy your app, set up builds, and manage environment variables for a smooth and reliable deployment.
<Callout type="warn" title="Prerequisite: Fly account and Docker configured">
To deploy to Fly.io, you need to have an account. You can create one [here](https://fly.io/app/sign-up).
You also need to have [Docker](/docs/web/deployment/docker) configured in your project.
</Callout>
<Steps>
<Step>
## Setup Fly CLI
As we will be using Fly CLI to launch and manage our app, you need to install and setup it on your machine.
[Check the official documentation on how to install Fly CLI](https://fly.io/docs/flyctl/install/).
After you've installed Fly CLI, you need to login to your Fly account and connect it with your machine:
```bash
fly auth login
```
[Read more about authenticating CLI](https://fly.io/docs/flyctl/auth/#available-commands).
Now you're ready to launch your app!
</Step>
<Step>
## Launch project
Use a [Dockerfile](/docs/web/deployment/docker) to launch your app with [Fly CLI](https://fly.io/docs/flyctl/). You can use the following command to do this from your local machine:
```bash
fly launch --dockerfile apps/web/Dockerfile
```
Make sure to set all the required configuration in the CLI steps (e.g. set port to `3000`, setup additional services, choose billing plan, etc.).
![Fly launch](/images/docs/web/deployment/fly/launch.png)
<Callout title="Customize region for better performance">
If you want to achieve better performance and lower latency in your API requests, you can customize the region of your Render service. Make sure to set it to the region closest to your database and users.
</Callout>
After the launch is complete, Fly will output your project configuration into `fly.toml` file. The configuration of your project is stored there, feel free to customize it to your needs:
```toml title="fly.toml"
app = 'web-aged-sky-5596'
primary_region = 'ams'
[build]
dockerfile = 'apps/web/Dockerfile'
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[vm]]
memory = '512mb'
cpu_kind = 'shared'
cpus = 1
```
See [Fly.io documentation](https://fly.io/docs/reference/configuration) for more information on how to use this file.
</Step>
<Step>
## Set up secrets
To make your app fully functional, you need to set up required environment variables. You can do this by running the following command:
```bash
fly secrets set --app <your-app-name> DATABASE_URL=...
```
They will be automatically added to your app's runtime environment.
</Step>
<Step>
## Deploy!
Each time you make changes to `fly.toml` or secrets, you need to re-deploy your app to apply changes to the running app.
To do this, just run the following command in your project directory:
```bash
fly deploy
```
This will build your app and deploy it to Fly.io with the latest code version.
![Fly deploy](/images/docs/web/deployment/fly/deploy.png)
That's it! Your app is now deployed to Fly.io, congratulations! 🎉
</Step>
</Steps>
Fly is a platform that allows you to deploy and manage applications in the cloud. It provides a simple and intuitive way to deploy your app, with features such as automatic scaling, load balancing, and rolling updates. With Fly, you can focus on building your app without worrying about the underlying infrastructure.

View File

@@ -0,0 +1,67 @@
---
title: Netlify
description: Learn how to deploy your TurboStarter app to Netlify.
url: /docs/web/deployment/netlify
---
# Netlify
[Netlify](https://netlify.com) is a powerful platform for deploying modern web applications. It offers continuous deployment, serverless functions, and a global CDN to ensure your application is fast and reliable.
In this guide, we will walk through the steps to deploy your TurboStarter app to Netlify. You will learn how to connect your repository, configure build settings, and manage environment variables to ensure a smooth deployment process.
<Callout type="warn" title="Prerequisite: Netlify account">
To deploy to Netlify, you need to have an account. You can create one [here](https://netlify.com/signup).
</Callout>
<Steps>
<Step>
## Create new site
Once you've created your account and logged in, the Netlify dashboard will display an option to add a new site. Click on the *Import from Git* button to begin connecting your Git repository.
![Create new site](/images/docs/web/deployment/netlify/create-site.png)
If you've already had a Netlify account, you can get to this step by clicking on the *Sites* tab in the navigation menu.
</Step>
<Step>
## Connect your repository
Choose the Git provider of your project and select the repository you want to deploy.
![Connect repository](/images/docs/web/deployment/netlify/connect-repository.png)
<Callout title="Authorization needed">
To connect your repository, you need to authorize Netlify to access it. It's recommended to follow a *least privileged access* approach, so to only grant access to the repository you want to deploy, not the entire account.
</Callout>
</Step>
<Step>
## Configure build settings
Last step before deploying! Configure the build settings according to your project configuration. Use the screenshots provided below for reference to ensure a smooth deployment process.
![Netlify build settings](/images/docs/web/deployment/netlify/build-settings.png)
Also, add all environment variables under *Environment variables* section - it's required to make the build process work.
</Step>
<Step>
## Deploy!
Click on the *Deploy* button to start the deployment process.
![Netlify deploy](/images/docs/web/deployment/netlify/deploy.png)
That's it! Your app is now deployed to Netlify, congratulations! 🎉
</Step>
</Steps>
<Callout title="Customize region for better performance">
If you want to achieve better performance and lower latency in your API requests, you can customize the region of your Netlify serverless functions. Make sure to set it to the region closest to your database and users.
![Netlify region](/images/docs/web/deployment/netlify/region.png)
Unfortunately, it's a paid feature, so you need to upgrade your Netlify account to be able to change it.
</Callout>

View File

@@ -0,0 +1,82 @@
---
title: Railway
description: Learn how to deploy your TurboStarter app to Railway.
url: /docs/web/deployment/railway
---
# Railway
[Railway](https://railway.app) is a platform that allows you to deploy your web applications to a cloud environment. It provides a simple and efficient way to manage your application's infrastructure, including scaling, monitoring, and logging.
This guide provides a step-by-step walkthrough for deploying your TurboStarter app on Railway, and taking advantage of its features in production environment. You'll discover how to link your repository, tailor build settings, and oversee environment variables, ensuring a smooth and optimized deployment process that leverages Railway's capabilities.
<Callout type="warn" title="Prerequisite: Railway account">
To deploy to Railway, you need to have an account. You can create one [here](https://railway.app/signup).
</Callout>
<Steps>
<Step>
## Create new project
We'll use [Railway](https://railway.app) web app to deploy our project. First, let's create a new project.
![Railway create project](/images/docs/web/deployment/railway/create-project.png)
Proceed with the option to *Deploy from Github repo*.
</Step>
<Step>
## Connect repository
Choose the Git provider of your project and select the repository you want to deploy.
![Connect repository](/images/docs/web/deployment/railway/connect-repository.png)
<Callout title="Authorization needed">
If your repository is private you need to authorize Railway to access it. It's recommended to follow a *least privileged access* approach, so to only grant access to the repository you want to deploy, not the entire account.
</Callout>
</Step>
<Step>
## Configure project settings
Finalize your deployment by configuring the build settings to match your project's specific needs. Refer to the points below to ensure a seamless deployment process.
### Commands
Configure the build and start commands to ensure that your project is built and started correctly.
![Railway project commands](/images/docs/web/deployment/railway/commands.png)
Make sure to set them to the following values:
* **Build command** - `pnpm dlx turbo build --filter=web`
* **Start command** - `pnpm --filter=web start`
### Environment variables
Last, but not least, you need to set the environment variables for your project. Make sure to check if all the required variables are set.
![Railway environment variables](/images/docs/web/deployment/railway/environment-variables.png)
<Callout title="Customize region for better performance and reliability">
If you want to achieve better performance, lower latency in your API requests or add some replicas of your application, you can customize the region of your Railway instance. Make sure to set it to the region closest to your database and users.
![Railway region](/images/docs/web/deployment/railway/region.png)
</Callout>
You can also use a [Railway config file](https://docs.railway.com/guides/config-as-code) to manage your project's settings in one place, as a code.
</Step>
<Step>
## Deploy!
Click on the *Deploy* button to start the deployment process.
![Railway deploy](/images/docs/web/deployment/railway/deploy.png)
That's it! Your app is now deployed to Railway, congratulations! 🎉
</Step>
</Steps>
Feel free to scale your deployment to multiple regions or isolate it in the separate network. Check out the [Railway documentation](https://docs.railway.app) for more information about which services are available.

View File

@@ -0,0 +1,94 @@
---
title: Render
description: Learn how to deploy your TurboStarter app to Render.
url: /docs/web/deployment/render
---
# Render
[Render](https://render.com) offers a unique combination of features that make it an ideal platform for deploying modern web applications. With Render, you can leverage continuous deployment, managed databases, and a global CDN to ensure your application is not only fast and reliable but also scalable and secure.
In this guide, we will walk through the steps to deploy your TurboStarter app to Render, highlighting the benefits of using Render's platform. You will learn how to connect your repository, configure build settings, and manage environment variables to ensure a seamless and efficient deployment process that takes advantage of Render's features.
<Callout type="warn" title="Prerequisite: Render account">
To deploy to Render, you need to have an account. You can create one [here](https://dashboard.render.com/register).
</Callout>
<Steps>
<Step>
## Create a new service
Navigate to the [Render dashboard](https://dashboard.render.com) and click on the *New* button.
![Create new service](/images/docs/web/deployment/render/create-service.png)
Pick the *Web Service* option and proceed to the next step.
</Step>
<Step>
## Connect your repository
Choose the Git provider of your project and select the repository you want to deploy.
![Connect repository](/images/docs/web/deployment/render/connect-repository.png)
<Callout title="Authorization needed">
If your repository is private you need to authorize Render to access it. It's recommended to follow a *least privileged access* approach, so to only grant access to the repository you want to deploy, not the entire account.
</Callout>
</Step>
<Step>
## Configure service settings
Finalize your deployment by configuring the build settings to match your project's specific needs. Refer to the screenshots below to ensure a seamless deployment process.
![Render service settings](/images/docs/web/deployment/render/general-settings.png)
You can also group your service with other services (e.g. [databases](https://render.com/docs/postgresql-creating-connecting) or [cron jobs](https://render.com/docs/cronjobs)) in a [Project](https://render.com/docs/projects), which will help you manage them together.
[Read official documentation for more information](https://render.com/docs/projects).
<Callout title="Customize region for better performance">
If you want to achieve better performance and lower latency in your API requests, you can customize the region of your Render service. Make sure to set it to the region closest to your database and users.
</Callout>
### Commands
Configure the build and start commands to ensure that your project is built and started correctly.
![Render service commands](/images/docs/web/deployment/render/commands.png)
Make sure to set them to the following values:
* **Build command** - `pnpm install --frozen-lockfile; pnpm dlx turbo build --filter=web`
* **Start command** - `pnpm --filter=web start`
### Instance type
Select a plan that fits your project's needs.
![Render instance type](/images/docs/web/deployment/render/instance-type.png)
For testing purposes or MVPs, you can safely use the *Free* plan. Although, for the production version, it's recommended to upgrade your plan, as it offers more resources and your project won't be paused after periods of inactivity.
### Environment variables
Last, but not least, you need to set the environment variables for your project. Make sure to check if all the required variables are set.
![Render environment variables](/images/docs/web/deployment/render/environment-variables.png)
You can also modify *Advanced settings* to set e.g. [health checks](https://render.com/docs/deploys#health-checks) or modify [auto deploy](https://render.com/docs/deploys#automatic-git-deploys) triggers.
</Step>
<Step>
## Deploy!
Click on the *Deploy Web Service* button to start the deployment process.
![Render deploy](/images/docs/web/deployment/render/deploy.png)
That's it! Your app is now deployed to Render, congratulations! 🎉
</Step>
</Steps>
Render is a powerful platform with a lot of integrations and features. Feel free to check out the [official documentation](https://render.com/docs) for more information.

View File

@@ -0,0 +1,215 @@
---
title: Vercel
description: Learn how to deploy your TurboStarter app to Vercel.
url: /docs/web/deployment/vercel
---
# Vercel
In general you can deploy the application to any hosting provider that supports Node.js, but we recommend using [Vercel](https://vercel.com) for the best experience.
Vercel is the easiest way to deploy Next.js apps. It's the company behind Next.js and has first-class support for Next.js.
<Callout type="warn" title="Prerequisite: Vercel account">
To deploy to Vercel, you need to have an account. You can create one [here](https://vercel.com/signup).
</Callout>
TurboStarter has two, separate ways to deploy to Vercel, each ships with **one-click deployment**. Choose the one that best fits your needs.
<Tabs items={["Connecting repository", "Github Actions"]}>
<Tab value="Connecting repository">
Deploying with this method is the easiest and fastest way to get your app up and running on the cloud provider. Follow these steps:
<Steps>
<Step>
## Connect your git repository
After signing up you will be promted to import a git repository. Select the git provider of your project and connect your git account with Vercel.
![Vercel import project](/images/docs/web/deployment/vercel/connect-repository.webp)
</Step>
<Step>
## Configure project settings
As we're working in monorepo, some additional settings are required to make the build process work.
Make sure to set the following settings:
* **Build command**: `pnpm turbo build --filter=web` - to build only the web app
* **Root directory**: `apps/web` - to make sure Vercel uses the web folder as the root directory (make sure to check *Include files outside the root directory in the Build Step* option, it will ensure that all packages from your monorepo are included in the build process)
![Vercel project settings](/images/docs/web/deployment/vercel/project-settings.png)
<Cards>
<Card title="Build and development settings" description="vercel.com" href="https://vercel.com/docs/deployments/configure-a-build#build-and-development-settings" />
<Card title="Root directory" description="vercel.com" href="https://vercel.com/docs/deployments/configure-a-build#root-directory" />
</Cards>
</Step>
<Step>
## Configure environment variables
Please make sure to set all the environment variables required for the project to work correctly. You can find the list of required environment variables in the `.env.example` file in the `apps/web` directory.
The environment variables can be set in the Vercel dashboard under *Project Settings* > *Environment Variables*. Make sure to set them for all environments (Production, Preview, and Development) as needed.
**Failure to set the environment variables will result in the project not working correctly.**
If the build fails, deep dive into the logs to see what is the issue. Our Zod configuration will validate and report any missing environment variables. To find out which environment variables are missing, please check the logs.
<Callout title="First deployment may fail">
The first time this may fail if you don't yet have a custom domain connected since you cannot place it in the environment variables yet. It's fine. Make the first deployment fail, then pick the domain and add it. Redeploy.
</Callout>
</Step>
<Step>
## Deploy!
Click on the *Deploy* button to start the deployment process.
![Vercel deploy](/images/docs/web/deployment/vercel/success.png)
That's it! Your app is now deployed to Vercel, congratulations! 🎉
</Step>
</Steps>
</Tab>
<Tab value="Github Actions">
Despite connecting your repository is the easiest way to deploy to Vercel, we recommend using preconfigured Github Actions for the most granular control over your deployments.
We'll leverage [Vercel CLI](https://vercel.com/docs/cli) to deploy the application on the CI/CD pipeline. [See official documentation on deploying to Github Actions](https://vercel.com/guides/how-can-i-use-github-actions-with-vercel).
<Steps>
<Step>
## Get Vercel Access Token
To deploy the application, we need to get Vercel access token.
Please, follow [this guide](https://vercel.com/guides/how-do-i-use-a-vercel-api-access-token) to create one.
![Vercel access token](/images/docs/web/deployment/vercel/access-token.avif)
</Step>
<Step>
## Install Vercel CLI
We need to install [Vercel CLI](https://vercel.com/docs/cli) locally to be able to get required credentials for our Github Actions.
You can install it using following command:
```bash
pnpm i -g vercel
```
Then, login to Vercel using following command:
```bash
vercel login
```
</Step>
<Step>
## Get credentials
Inside your folder, run following command to create a new project:
```bash
vercel link
```
This will generate a `.vercel` folder, where you can find `project.json` file with `projectId` and `orgId`.
</Step>
<Step>
## Configure Github Actions
Inside GitHub, add `VERCEL_TOKEN`, `VERCEL_ORG_ID`, and `VERCEL_PROJECT_ID` as [secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) to your repository.
![Github secrets](/images/docs/web/deployment/vercel/github-tokens.png)
This will allow Github Actions to access your settings and deploy the application to Vercel.
</Step>
<Step>
## Configure project settings
As we're working in monorepo, some additional settings are required to make the build process work.
Make sure to set the following settings:
* **Build command**: `pnpm turbo build --filter=web` - to build only the web app
* **Root directory**: `apps/web` - to make sure Vercel uses the web folder as the root directory (make sure to check *Include files outside the root directory in the Build Step* option, it will ensure that all packages from your monorepo are included in the build process)
![Vercel project settings](/images/docs/web/deployment/vercel/project-settings.png)
<Cards>
<Card title="Build and development settings" description="vercel.com" href="https://vercel.com/docs/deployments/configure-a-build#build-and-development-settings" />
<Card title="Root directory" description="vercel.com" href="https://vercel.com/docs/deployments/configure-a-build#root-directory" />
</Cards>
</Step>
<Step>
## Configure environment variables
Please make sure to set all the environment variables required for the project to work correctly. You can find the list of required environment variables in the `.env.example` file in the `apps/web` directory.
The environment variables can be set in the Vercel dashboard under *Project Settings* > *Environment Variables*. Make sure to set them for all environments (Production, Preview, and Development) as needed.
**Failure to set the environment variables will result in the project not working correctly.**
If the build fails, deep dive into the logs to see what is the issue. Our Zod configuration will validate and report any missing environment variables. To find out which environment variables are missing, please check the logs.
<Callout title="First deployment may fail">
The first time this may fail if you don't yet have a custom domain connected since you cannot place it in the environment variables yet. It's fine. Make the first deployment fail, then pick the domain and add it. Redeploy.
</Callout>
</Step>
<Step>
## Deploy!
By default, TurboStarter comes with a Github Actions workflow that can be [triggered manually](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow).
The configuration is located in `.github/workflows/publish-web.yml`, you can easily customize it to your needs, for example to trigger a deployment from `main` branch.
```diff title=".github/workflows/publish-web.yml"
on:
- workflow_dispatch:
+ push:
+ branches:
+ - main
```
Then, every time you push to `main` branch, the workflow will be triggered and the application will be deployed to Vercel.
![Vercel deploy](/images/docs/web/deployment/vercel/success.png)
That's it! Your app is now deployed to Vercel, congratulations! 🎉
</Step>
</Steps>
</Tab>
</Tabs>
<Card title="Vercel" href="https://vercel.com" description="vercel.com" />
## Troubleshooting
In some cases, users have reported issues with the deployment to Vercel using the default parameters. If you encounter problems, try these troubleshooting steps:
1. **Check root directory settings**
* Set the root directory to `apps/web`
* Enable *Include source files outside of the Root Directory* option
2. **Verify build configuration**
* Ensure the framework preset is set to Next.js
* Set build command to `pnpm turbo build --filter=web`
* Set install command to `pnpm install`
3. **Review deployment logs**
* If deployment fails, carefully review the build logs
* Look for any error messages about missing dependencies or environment variables
* Verify that all required environment variables are properly configured
If issues persist after trying these steps, check the [deployment troubleshooting guide](/docs/web/troubleshooting/deployment) for additional help.

View File

@@ -0,0 +1,391 @@
---
title: Configuration
description: Learn how to configure your emails in TurboStarter.
url: /docs/web/emails/configuration
---
# Configuration
The `@turbostarter/email` package provides a simple and flexible way to send emails using various email providers. It abstracts the complexity of different email services and offers a consistent interface for sending emails with pre-defined templates.
To configure the email service, you need to set an `EMAIL_FROM` environment variable. It will be used as the sender of the emails. **Please make sure that the mail address and domain are verified in your mail provider.**
```dotenv
EMAIL_FROM="hello@resend.dev"
```
The email provider is configured by modifying the exports in `packages/email` package. By default, [Nodemailer](/docs/web/emails/configuration#nodemailer) is used.
Configuration will be validated against the schema, so you will see the error messages in the console if something is not right.
## Providers
TurboStarter supports multiple email providers, each with its own configuration. Below, you'll find detailed information on how to set up and use each supported provider. Choose the one that best fits your needs and follow the instructions in the respective accordion section.
<Accordions>
<Accordion title="Resend" id="resend">
To use Resend as your email provider, you need to [create an account](https://resend.com/) and [obtain your API key](https://resend.com/docs/dashboard/api-keys/introduction).
Then, set it as an environment variable in your `.env.local` file in `apps/web` directory and your deployment environment:
```dotenv
RESEND_API_KEY="your-api-key"
```
Also, make sure to activate Resend as your email provider by updating the exports in:
<Tabs items={["index.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
// [!code word:resend]
export * from "./resend";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:resend]
export * from "./resend/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/email/src/providers/resend` directory.
For more information, please refer to the [Resend documentation](https://resend.com/docs).
</Accordion>
<Accordion title="SendGrid" id="sendgrid">
To use SendGrid as your email provider, you need to [create an account](https://sendgrid.com/) and [obtain your API key](https://sendgrid.com/docs/ui/account-and-settings/api-keys/).
Then, set it as an environment variable in your `.env.local` file in `apps/web` directory and your deployment environment:
```dotenv
SENDGRID_API_KEY="your-api-key"
```
Also, make sure to activate SendGrid as your email provider by updating the exports in:
<Tabs items={["index.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
// [!code word:sendgrid]
export * from "./sendgrid";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:sendgrid]
export * from "./sendgrid/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/email/src/providers/sendgrid` directory.
For more information, please refer to the [SendGrid documentation](https://sendgrid.com/docs).
</Accordion>
<Accordion title="Postmark" id="postmark">
To use Postmark as your email provider, you need to [create an account](https://postmarkapp.com/) and [obtain your server API token](https://postmarkapp.com/support/article/1008-what-are-the-account-and-server-api-tokens).
Then, set it as an environment variable in your `.env.local` file in `apps/web` directory and your deployment environment:
```dotenv
POSTMARK_API_KEY="your-secret-api-token"
```
Also, make sure to activate Postmark as your email provider by updating the exports in:
<Tabs items={["index.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
export * from "./postmark";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:postmark]
export * from "./postmark/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/email/src/providers/postmark` directory.
For more information, please refer to the [Postmark documentation](https://postmarkapp.com/developer).
</Accordion>
<Accordion title="Plunk" id="plunk">
To use Plunk as your email provider, you need to [create an account](https://plunk.dev/) and [obtain your API key](https://docs.useplunk.com/api-reference/authentication).
Then, set it as an environment variable in your `.env.local` file in `apps/web` directory and your deployment environment:
```dotenv
PLUNK_API_KEY="your-api-key"
```
Also, make sure to activate Plunk as your email provider by updating the exports in:
<Tabs items={["index.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
// [!code word:plunk]
export * from "./plunk";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:plunk]
export * from "./plunk/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/email/src/providers/plunk` directory.
For more information, please refer to the [Plunk documentation](https://docs.useplunk.com).
</Accordion>
<Accordion title="nodemailer" id="nodemailer">
If you're using the `nodemailer` as your email provider, you'll need to set the following SMTP configuration in your environment variables:
```dotenv
NODEMAILER_HOST="your-smtp-host"
NODEMAILER_PORT="your-smtp-port"
NODEMAILER_USER="your-smtp-user"
NODEMAILER_PASSWORD="your-smtp-password"
```
The variables are:
* `NODEMAILER_HOST`: The host of your SMTP server.
* `NODEMAILER_PORT`: The port of your SMTP server.
* `NODEMAILER_USER`: The email address user of your SMTP server.
* `NODEMAILER_PASSWORD`: The password for the email account.
Also, make sure to activate nodemailer as your email provider by updating the exports in:
<Tabs items={["index.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
// [!code word:nodemailer]
export * from "./nodemailer";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:nodemailer]
export * from "./nodemailer/env";
```
</Tab>
</Tabs>
To customize the provider, you can find its definition in `packages/email/src/providers/nodemailer` directory.
For more information, please refer to the [nodemailer documentation](https://nodemailer.com/smtp/).
</Accordion>
</Accordions>
## Templates
In the `@turbostarter/email` package, we provide a set of pre-defined templates for you to use. You can find them in the `packages/email/src/templates` directory.
When you run your development server, you will be able to preview all available templates in the browser under [http://localhost:3005](http://localhost:3005).
![Email preview](/images/docs/web/emails/development.png)
Next to the templates, you can also find some shared components that you can use in your emails. The file structure looks like this:
<Files>
<Folder name="templates" defaultOpen>
<Folder name="_components - Shared components used in emails" />
<Folder name="auth - Authentication related emails" />
<File name="index.ts - Main entrypoint for the templates" />
</Folder>
</Files>
Feel free to add your own templates and components or modify existing ones to match them with your brand and style.
### How to add a new template?
We'll go through the process of adding a new template, as it requires a few steps to make sure everything works correctly.
<Steps>
<Step>
#### Define types
Let's assume that we want to add a **welcome email**, that new users will receive after signing up.
We'll start with defining new template type in `packages/email/src/types/templates.ts` file:
```ts title="templates.ts"
export const EmailTemplate = {
...AuthEmailTemplate,
WELCOME: "welcome",
} as const;
```
Also, we would need to add types for variables that we'll pass to the template (if any), in our case it will be just a `name` of the user:
```ts title="templates.ts"
type WelcomeEmailVariables = {
welcome: {
name: string;
};
};
export type EmailVariables = AuthEmailVariables | WelcomeEmailVariables;
```
By doing this, we ensure that payload passed to the template will have all required properties and we won't end up with an email that tells your user "Hey, undefined!".
</Step>
<Step>
#### Create template
Next up, we need to create a file with the template itself. We'll create an `welcome.tsx` file in `packages/email/src/templates` directory.
```tsx title="welcome.tsx"
import { Heading, Preview, Text } from "@react-email/components";
import { Button } from "../_components/button";
import { Layout } from "../_components/layout/layout";
import type { EmailTemplate, EmailVariables } from "../../types";
type Props = EmailVariables[typeof EmailTemplate.WELCOME];
export const Welcome = ({ name }: Props) => {
return (
<Layout>
<Preview>Welcome to TurboStarter!</Preview>
<Heading>Hi, {name}!</Heading>
<Text>Start your journey with our app by clicking the button below.</Text>
<Button>Start</Button>
</Layout>
);
};
Welcome.subject = "Welcome to TurboStarter!";
Welcome.PreviewProps = {
name: "John Doe",
};
export default Welcome;
```
As you can see, by defining appropriate types for the template, we can safely use the variables as a props in the template.
To learn more about supported components, please refer to the [React Email documentation](https://react.email/docs/components).
</Step>
<Step>
#### Register template
We have to register the template in the main entrypoint of the templates in `packages/email/src/templates/index.ts` file:
```ts title="index.ts"
import { Welcome } from "./welcome";
export const templates = {
...
[EmailTemplate.WELCOME]: Welcome,
} as const;
```
That way, it will be available in the `sendEmail` function, enabling us to send it from the server-side of your application.
```ts
import { sendEmail } from "@turbostarter/email/server";
sendEmail({
to: "user@example.com",
template: EmailTemplate.WELCOME,
variables: {
name: "John Doe",
},
});
```
Learn more about sending emails in the [dedicated section](/docs/web/emails/sending).
</Step>
</Steps>
Et voilà! You've just added a new email template to your application 🎉
### Translating templates
You can also translate your templates to support multiple languages. Each mail template is passed the `locale` property, which you can use to get the translation for the current locale. This allows you to maintain consistent translations across your application and emails.
The translation system [uses the same i18n setup](/docs/web/internationalization/overview) as your main application, so you can reuse your existing translation files and namespaces. The translations are loaded server-side when the email is generated, ensuring the correct language is used based on the user's preferences.
Here's how you can implement translations in your email templates:
```tsx
import { Heading, Preview, Text } from "@react-email/components";
import { getTranslation } from "@turbostarter/i18n/server";
import { Button } from "../_components/button";
import { Layout } from "../_components/layout/layout";
import type {
EmailTemplate,
EmailVariables,
CommonEmailProps,
} from "../../types";
type Props = EmailVariables[typeof EmailTemplate.WELCOME] & CommonEmailProps;
export const Welcome = async ({ name, locale }: Props) => {
const { t } = await getTranslation({ locale, ns: "auth" });
return (
<Layout locale={locale}>
<Preview>{t("account.welcome.preview")}</Preview>
<Heading>{t("account.welcome.heading", { name })}</Heading>
<Text>{t("account.welcome.body")}</Text>
<Button>{t("account.welcome.cta")}</Button>
</Layout>
);
};
Welcome.subject = async ({ locale }: CommonEmailProps) => {
const { t } = await getTranslation({ locale, ns: "auth" });
return t("account.welcome.subject");
};
Welcome.PreviewProps = {
name: "John Doe",
locale: "en",
};
export default Welcome;
```
To send the email in the specified language, you can pass the optional `locale` argument to the `sendEmail` function:
```ts
sendEmail({
to: "user@example.com",
template: EmailTemplate.WELCOME,
variables: {
name: "John Doe",
},
locale: "en", // [!code highlight]
});
```
Learn more about translations in the [dedicated section](/docs/web/internationalization/translations).

View File

@@ -0,0 +1,45 @@
---
title: Overview
description: Get started with emails in TurboStarter.
url: /docs/web/emails/overview
---
# Overview
For mailing functionality, TurboStarter integrates [React Email](https://react.email/docs/introduction) which enables you to build your emails from composable React components.
<Callout title="Why React Email?">
It's a simple, yet powerful library that allows you to **write your emails in React**.
It also allows you to use **Tailwind CSS for styling**, which is a huge advantage, as we can share almost everything from the main app with the emails package, keeping them consistent with rest of the app.
</Callout>
You can read more about `react-email` package in the [official documentation](https://react.email/docs/introduction).
## Providers
TurboStarter implements multiple providers for managing and sending emails. To learn more about each provider and how to configure them, see the respective section:
<Cards>
<Card title="Resend" href="/docs/web/emails/configuration#resend" />
<Card title="SendGrid" href="/docs/web/emails/configuration#sendgrid" />
<Card title="Postmark" href="/docs/web/emails/configuration#postmark" />
<Card title="Plunk" href="/docs/web/emails/configuration#plunk" />
<Card title="Nodemailer" href="/docs/web/emails/configuration#nodemailer" />
</Cards>
All configuration and setup is built-in with a unified API, so you can switch between providers by simply changing the exports and even introduce your own provider without breaking any sending-related logic.
## Development
When you [setup your development environment](/docs/web/installation/development) and run `pnpm dev` command a new app will start at [http://localhost:3005](http://localhost:3005).
![Email preview](/images/docs/web/emails/development.png)
There you'll be able to check your email templates and send test emails from your app. It includes hot-reloading, so when you make change in the code - it will be reflected in the browser.
Learn more about configuration and setup of the emails in TurboStarter in the following sections.

View File

@@ -0,0 +1,114 @@
---
title: Sending emails
description: Learn how to send emails in TurboStarter.
url: /docs/web/emails/sending
---
# Sending emails
The strategy for sending emails, that every provider has to implement, is **extremely simple**:
```ts
export interface EmailProviderStrategy {
send: (args: {
to: string;
subject: string;
text: string;
html?: string;
}) => Promise<void>;
}
```
<Callout>
You don't need to worry much about it, as all the providers are already configured for you. Just be aware of it if you want to add your custom provider.
</Callout>
Then, we define a general `sendEmail` function that you can use as an API for sending emails in your app:
```ts
const sendEmail = async <T extends EmailTemplate>({
to,
template,
variables,
locale,
}: {
to: string;
template: T;
variables: EmailVariables[T];
locale?: string;
}) => {
const { html, text, subject } = await getTemplate({
id: template,
variables,
locale,
});
return send({ to, subject, html, text });
};
```
The arguments are:
* `to`: The recipient's email address.
* `template`: The email template to use.
* `variables`: The variables to pass to the template.
* `locale`: The locale to use for the email.
It returns a promise that resolves when the email is sent successfully. If there is an error, the promise will be rejected with an error message.
To send an email, just invoke the `sendEmail` with the correct arguments from the **server-side** of your application:
```ts
import { sendEmail } from "@turbostarter/email/server";
sendEmail({
to: "user@example.com",
template: EmailTemplate.WELCOME,
variables: {
name: "John Doe",
},
locale: "en",
});
```
And that's it! You're ready to send emails in your application 🚀
## Authentication emails
TurboStarter comes with a set of pre-configured authentication emails for various purposes, including magic links and password reset functionality.
To handle the sending of these emails at the right time, we use [Better Auth Hooks](https://www.better-auth.com/docs/concepts/email), which trigger when specific authentication events occur.
The logic for determining which email to send is already implemented for you in the `packages/auth/src/server.ts` file, alongside your [authentication configuration](/docs/web/auth/configuration):
```ts title="server.ts"
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
sendResetPassword: async ({ user, url }) =>
sendEmail({
to: user.email,
template: EmailTemplate.RESET_PASSWORD,
variables: {
url,
},
}),
},
emailVerification: {
sendVerificationEmail: async ({ user, url }) =>
sendEmail({
to: user.email,
template: EmailTemplate.CONFIRM_EMAIL,
variables: {
url,
},
}),
},
/* other options */
});
```
As you can see, the authentication emails are automatically sent when needed (e.g. when user requests password reset or needs to verify their email address).
You can customize authentication templates by modifying them in the `packages/email/src/templates` directory, or create your own templates for other use cases in your application.

View File

@@ -0,0 +1,66 @@
---
title: Extras
description: See what you get together with the code.
url: /docs/web/extras
---
# Extras
## Tips and Tricks
In many places, next to the code you will find some marketing tips, design suggestions, and potential risks. This is to help you build a better product and avoid common pitfalls.
```tsx title="Hero.tsx"
return (
<header>
{/* 💡 Use something that user can visualize e.g.
"Ship your startup while on the toilet" */}
<h1>Best startup on the world</h1>
</header>
);
```
### Submission tips
When it comes to mobile app and browser extension, you must submit your product to review from Apple/Google etc. We have some tips for you to make sure your submission goes smoothly.
```json title="app.json"
{
"ios": {
"infoPlist": {
/* 🍎 add descriptive justification of using this permission on iOS */
"NSCameraUsageDescription": "This app uses the camera to scan barcodes on event tickets."
}
}
}
```
As well as providing you with the info on how to make your store listings better:
```json title="package.json"
{
"manifest": {
/* 💡 Use localized messages to get more visibility in web stores */
"name": "__MSG_extensionName__",
"default_locale": "en"
}
}
```
## Discord community
We have a Discord community where you can ask questions and share your projects. It's a great place to get help and meet other developers. Check more details at [/discord](/discord).
<DiscordCta source="extras" />
![Discord](/images/docs/discord.png)
## 25+ SaaS Ideas
Not sure what to build? We have a list of **25+** SaaS ideas that you can use to get started 🔥
Grouped by category, these ideas are a great way to get inspired and start building your next project.
Including design, copies, marketing tips and potential risks, this list is a great resource for anyone looking to build a SaaS product.
![SaaS Ideas](/images/docs/saas-ideas.png)

View File

@@ -0,0 +1,92 @@
---
title: FAQ
description: Find answers to common technical questions.
url: /docs/web/faq
---
# FAQ
## Why isn't everything hidden and configured with one BIG config file?
TurboStarter intentionally exposes the underlying code rather than hiding it behind configuration files (like some starters do). This design choice follows our **you own your code** philosophy, giving you full control and flexibility over your codebase.
While a single config file might seem simpler initially, it often becomes restrictive when you need to customize functionality beyond what the config allows. With direct access to the code, you can modify any part of the system to match your specific requirements.
## I don't know some technology! Should I buy TurboStarter?
You should be prepared for a learning curve or consider learning it first. However, TurboStarter will still work for you if you're willing to learn.
Even without knowing some technologies, you can still use the rest of the features.
## I don't need mobile app or browser extension, what should I do?
You can simply ignore the mobile app and browser extension parts of the project. You can remove the `apps/mobile` and `apps/extension` directories from the project.
The modular nature of TurboStarter allows you to remove parts of the project that you don't need without affecting the rest of the stack.
## I want to use a different provider for X
Sure! TurboStarter is designed to be modular, so configuring new provider (e.g. for emails, billing or any other service) is straightforward. You just need to make sure your configuration is compatible with common interface to be able to plug it into the codebase.
## Will you add more packages in the future?
Yes, we will keep updating TurboStarter with new packages and features. This kit is designed to be modular, allowing for new features and packages to be added without interfering with your existing code. You can always [update your project](/docs/web/installation/update) to the latest version.
## Can I use this kit for a non-SaaS project?
This kit is mainly designed for SaaS projects. If you're building something other than a SaaS, the Next.js SaaS Boilerplate might include features you don't need. You can still use it for non-SaaS projects, but you may need to remove or modify features that are specific to SaaS use cases.
## Can I use personal accounts only?
Yes! You can disable team accounts and have personal accounts only by setting a feature flag.
## Does it set up the production instance for me?
No, TurboStarter does not set up the production instance for you. This includes setting up databases, Stripe, or any other services you need. TurboStarter does not have access to your Stripe or Resend accounts, so setup on your end is required. TurboStarter provides the codebase and documentation to help you set up your SaaS project.
## Does the starter include Solito?
No. Solito will not be included in this repo. It is a great tool if you want to share code between your Next.js and Expo app. However, the main purpose of this repo is not the integration between Next.js and Expo — it's the code splitting of your SaaS platforms into a monorepo. You can utilize the monorepo with multiple apps, and it can be any app such as Vite, Electron, etc.
Integrating Solito into this repo isn't hard, and there are a few [official templates](https://github.com/nandorojo/solito/tree/master/example-monorepos) by the creators of Solito that you can use as a reference.
## Does this pattern leak backend code to my client applications?
No, it does not. The `api` package should only be a production dependency in the Next.js application where it's served. The Expo app, browser extension, and all other apps you may add in the future should only add the `api` package as a dev dependency. This lets you have full type safety in your client applications while keeping your backend code safe.
If you need to share runtime code between the client and server, you can create a separate `shared` package for this and import it on both sides.
## How do I get support if I encounter issues?
For support, you can:
1. Visit our [Discord](https://discord.gg/KjpK2uk3JP)
2. Contact us via support email ([hello@turbostarter.dev](mailto:hello@turbostarter.dev))
## Are there any example projects or demos?
Yes - feel free to check out our demo app at [demo.turbostarter.dev](https://demo.turbostarter.dev). Also, you can get inspired by projects built by our customers - take a look at [Showcase](/#showcase).
## How do I deploy my application?
Please check the [production checklist](/docs/web/deployment/checklist) for more information.
## How do I update my project when a new version of the boilerplate is released?
Please read the [documentation for updating your TurboStarter code](/docs/web/installation/update).
## Can I use the React package X with this kit?
Yes, you can use any React package with this kit. The kit is based on React, so you are generally only constrained by the underlying technologies and not by the kit itself. Since you own and can edit all the code, you can adapt the kit to your needs. However, if there are limitations with the underlying technology, you might need to work around them.
## Can I integrate TurboStarter into an existing project?
TurboStarter is a full-stack starter intended to be used as the foundation of your app. You can copy individual modules or patterns into an existing codebase, but retrofitting the entire starter into a mature project is typically not recommended and is not officially supported. If you choose to copy parts, prefer isolating boundaries (e.g., `packages/` modules) and aligning interfaces first.
## Where can I deploy my application?
TurboStarter targets modern Node.js/Next.js runtimes. You can deploy to providers that support these environments, such as [Vercel](/docs/web/deployment/vercel), [Railway](/docs/web/deployment/railway), [Render](/docs/web/deployment/render), [Fly](/docs/web/deployment/fly), or [Netlify](/docs/web/deployment/netlify) - following their Next.js guidance. Review our [production checklist](/docs/web/deployment/checklist) before going live.
## Can I easily swap providers (billing, email, etc.)?
Yes. The starter organizes integrations behind clear interfaces so you can replace providers (e.g., billing or email) with minimal surface changes. Keep your implementation behind a module boundary and adapt to the existing types to avoid ripple effects.

View File

@@ -0,0 +1,336 @@
---
title: Introduction
description: Get started with TurboStarter web kit.
url: /docs/web
---
# Introduction
Welcome to the TurboStarter documentation. This is your starting point for learning about the starter kit, its structure, features, and how to use it for your app development.
<ThemedImage light="/images/docs/demo/light.webp" dark="/images/docs/demo/dark.webp" alt="TurboStarter demo" width={2311} height={1562} zoomable priority />
## What is TurboStarter?
TurboStarter is a fullstack starter kit that helps you build scalable and production-ready web apps, mobile apps, and browser extensions in minutes.
Looking to bootstrap your project quickly? Check out our [TurboStarter CLI guide](/blog/the-only-turbo-cli-you-need-to-start-your-next-project-in-seconds) to get started in seconds.
## Demo apps
TurboStarter provides a suite of live demo applications you can try instantly - right in your browser, on your phone, or via browser extensions. Try them live by clicking the buttons below.
<DemoBadges
urls={{
android:
"https://play.google.com/store/apps/details?id=com.turbostarter.core",
ios: "https://apps.apple.com/app/id6754278899",
chrome:
"https://chromewebstore.google.com/detail/bcjmonmlfbnngpkllpnpmnjajaciaboo",
firefox: "https://addons.mozilla.org/addon/turbostarter_",
edge: "https://microsoftedge.microsoft.com/addons/detail/turbostarter/ianbflanmmoeleokihabnmmcahhfijig",
web: "https://demo.turbostarter.dev",
}}
/>
## Principles
TurboStarter is built with the following principles:
* **As simple as possible** - It should be easy to understand, easy to use, and strongly avoid overengineering things.
* **As few dependencies as possible** - It should have as few dependencies as possible to allow you to take full control over every part of the project.
* **As performant as possible** - It should be fast and light without any unnecessary overhead.
## Features
Before diving into the technical details, let's overview the features TurboStarter provides.
### Multi-platform development
* [Web](/docs/web/stack): Build web apps with React, Next.js, and Tailwind CSS.
* [Mobile](/docs/mobile/stack): Build mobile apps with React Native and Expo.
* [Browser extension](/docs/extension/stack): Build browser extensions with React and WXT.
For those interested in AI development, check out our dedicated [TurboStarter AI documentation](/ai/docs) with specialized features for building AI-powered applications.
<Callout title="Available. Everywhere.">
Most features are available on all platforms. You can use the **same codebase** to build web, mobile, and browser extension apps.
</Callout>
### Authentication
<Cards>
<Card title="Ready-to-use components and views" description="Pre-built authentication components and pages that match your brand's unique style." className="shadow-none" />
<Card title="Email/password authentication" description="Traditional email and password auth implementing validation and security best practices." className="shadow-none" />
<Card title="Magic links" description="Passwordless authentication through secure email-based magic links, including rate limiting." className="shadow-none" />
<Card title="Password recovery" description="Complete password reset flow including email verification and secure token handling." className="shadow-none" />
<Card title="Multi-factor authentication (MFA)" description="Increase account security with support for 2FA (authenticator apps, TOTP), ready to use and customizable in your app." className="shadow-none" />
<Card title="Passkeys (passwordless)" description="Passwordless authentication using Passkeys (FIDO2/WebAuthn) for seamless, phishing-resistant sign-ins." className="shadow-none" />
<Card title="Anonymous" description="Allow users to proceed anonymously without requiring authentication." className="shadow-none" />
<Card title="OAuth providers" description="Pre-configured social authentication for Google and GitHub, ready to use." className="shadow-none" />
</Cards>
### Organizations/teams
<Cards>
<Card title="Multi-tenancy" description="Multi-tenant organization model with ownership and membership." className="shadow-none" />
<Card title="Teams and members" description="Create teams, invite members, assign roles and manage seats." className="shadow-none" />
<Card title="Invitations" description="Email-based invites with role presets and expiry." className="shadow-none" />
<Card title="Roles per organization" description="Role-based permissions scoped to each organization." className="shadow-none" />
</Cards>
### Billing
<Cards>
<Card title="Subscriptions" description="Recurring billing system supporting multiple plans, pricing tiers and usage metrics." className="shadow-none" />
<Card title="One-time payments" description="Simple payment processing featuring secure checkout and payment confirmation." className="shadow-none" />
<Card title="Webhooks" description="Real-time billing event handling and payment provider data synchronization." className="shadow-none" />
<Card title="Custom plans" description="Create and manage custom pricing plans offering flexible billing options." className="shadow-none" />
<Card title="Billing components" description="Ready-made components for pricing, checkout and billing management." className="shadow-none" />
<Card title="Multiple providers" description="Seamless integration of Stripe, LemonSqueezy and Polar payment systems." className="shadow-none" />
</Cards>
### Database
<Cards>
<Card title="Advanced querying" description="Type-safe SQL queries, relational joins, filters, ordering, pagination and more." className="shadow-none" />
<Card title="Schema migrations" description="Automated schema migrations with version control, rollback, and auto-generation." className="shadow-none" />
<Card title="Connection pooling" description="Standalone or serverless database connections with optimal pooling strategies." className="shadow-none" />
<Card title="Data validation" description="End-to-end data validation using shared types and schema definitions." className="shadow-none" />
</Cards>
### API
<Cards>
<Card title="Serverless architecture" description="Modern serverless infrastructure offering auto-scaling and high availability." className="shadow-none" />
<Card title="Single source of truth" description="Unified data management across all apps through shared types and validation." className="shadow-none" />
<Card title="Protected routes" description="Secure API endpoints implementing role-based access control and rate limiting." className="shadow-none" />
<Card title="Feature-based access" description="Access control based on features and subscription plans." className="shadow-none" />
<Card title="Typesafe client" description="Fully typesafe frontend client featuring automatic type generation." className="shadow-none" />
</Cards>
### Admin
<Cards>
<Card title="Super admin UI" description="Centralized admin workspace with overview metrics and quick actions." className="shadow-none" />
<Card title="User management" description="Search, filter and manage users, status, auth methods and MFA." className="shadow-none" />
<Card title="Roles and permissions" description="Granular access control for admins, moderators and support staff." className="shadow-none" />
<Card title="Impersonation" description="Securely impersonate users to reproduce issues and provide support." className="shadow-none" />
</Cards>
### AI
<Cards>
<Card title="Multiple providers" className="shadow-none">
Seamless integration of OpenAI, Anthropic, Groq, Mistral, and Gemini. For more advanced AI features, check out [TurboStarter AI](/ai/docs).
</Card>
<Card title="Ready-to-use components" description="Pre-built chatbot and assistant components supporting real-time streaming." className="shadow-none" />
<Card title="Streaming responses" description="Real-time AI response delivery including progress indicators." className="shadow-none" />
<Card title="Custom rules" description="Includes custom rules and prompts for AI editors and models to make you ship faster." className="shadow-none" />
</Cards>
### Internationalization
<Cards>
<Card title="Locale routing" description="Smart routing based on user locale and automatic language detection." className="shadow-none" />
<Card title="Multiple languages" description="Comprehensive multi-language support and translation management." className="shadow-none" />
<Card title="Language switching" description="One-click language changes and persistent preferences." className="shadow-none" />
<Card title="Mail templates" description="Multi-language email templates including fallback options." className="shadow-none" />
</Cards>
### Emails
<Cards>
<Card title="Transactional emails" description="Automated email delivery including tracking and analytics capabilities." className="shadow-none" />
<Card title="Marketing emails" description="Create and send marketing campaigns using beautiful templates." className="shadow-none" />
<Card title="Email templates" description="Responsive email templates supporting dark mode customization." className="shadow-none" />
<Card title="Multiple providers" description="Easy integration of SendGrid, Resend, and Nodemailer services." className="shadow-none" />
</Cards>
### Landing page
<Cards>
<Card title="Hero section" description="Dynamic hero with subtle animations, platform links, and primary/secondary CTAs." className="shadow-none" />
<Card title="Feature highlights" description="Grid and list layouts to present key features and differentiators." className="shadow-none" />
<Card title="Pricing plans" description="Billing-integrated pricing table with intervals, discounts and footer notes." className="shadow-none" />
<Card title="Testimonials" description="Social proof section with avatars, star ratings and counts." className="shadow-none" />
<Card title="FAQ" description="Structured FAQ with SEO schema for rich results." className="shadow-none" />
<Card title="Re-usable CTAs" description="Buttons, badges and links to docs, pricing and community." className="shadow-none" />
</Cards>
### Marketing
<Cards>
<Card title="SEO" description="Complete SEO toolkit including automatic sitemap generation." className="shadow-none" />
<Card title="Meta tags" description="Flexible meta tag system supporting social media previews." className="shadow-none" />
<Card title="Tips and tricks" description="Comprehensive tips and tricks for optimizing marketing of your app." className="shadow-none" />
<Card title="Mobile onboarding" description="Onboarding flow for mobile apps including custom steps, authentication, and more." className="shadow-none" />
<Card title="Blog" description="Full-featured blog system including categories and RSS feed." className="shadow-none" />
<Card title="Legal pages" description="Pre-built legal templates including version control." className="shadow-none" />
<Card title="Contact form" description="Smart contact form featuring spam protection and auto-responses." className="shadow-none" />
</Cards>
### Storage
<Cards>
<Card title="File uploads" description="Complete file upload system including progress tracking and validation." className="shadow-none" />
<Card title="S3 storage" description="S3-compatible storage offering automatic file optimization." className="shadow-none" />
</Cards>
### CMS
<Cards>
<Card title="Blog pages" description="Complete blog management system including categories and tags." className="shadow-none" />
<Card title="MDX content collections" description="Organized content structure using MDX-based collections and custom frontmatter." className="shadow-none" />
</Cards>
### Theming
<Cards>
<Card title="Built-in themes" description="10+ pre-built themes offering customizable color schemes." className="shadow-none" />
<Card title="Dark mode" description="Built-in dark mode supporting system preference detection." className="shadow-none" />
<Card title="Components CLI" description="Component generation tools following best practices and TypeScript standards." className="shadow-none" />
<Card title="Design system" description="Complete atomic design system including accessibility features." className="shadow-none" />
</Cards>
### Analytics
<Cards>
<Card title="Event tracking" description="Custom event tracking plus automatic session management." className="shadow-none" />
<Card title="Page views" description="Automatic page view capture including bounce rate metrics." className="shadow-none" />
<Card title="User identification" description="Cross-device user tracking and session management." className="shadow-none" />
<Card title="Multiple providers" description="Seamless integration with multiple platforms (e.g. Google Analytics, PostHog, Plausible, Umami, Open Panel, Vemetric)." className="shadow-none" />
</Cards>
### Monitoring
<Cards>
<Card title="Auto-capture exceptions" description="Automatically capture exceptions and errors in your application." className="shadow-none" />
<Card title="Track performance metrics" description="Track performance metrics such as page views, user sessions, and more." className="shadow-none" />
<Card title="Source maps" description="Automatically generate source maps for your application to improve error reporting." className="shadow-none" />
<Card title="Multiple providers" description="Seamless integration with multiple platforms (e.g. Sentry, PostHog)." className="shadow-none" />
</Cards>
### Deployment
<Cards>
<Card title="One-click deploy" description="Simple deployment to your preferred cloud provider." className="shadow-none" />
<Card title="Submission guide" description="Comprehensive guide for app store submissions and requirements." className="shadow-none" />
<Card title="CI/CD workflows" description="Pre-configured deployment pipelines including automated testing." className="shadow-none" />
<Card title="Over-the-air updates" description="Instantly push code or config updates to users without resubmitting to the app store." className="shadow-none" />
</Cards>
### Testing
<Cards>
<Card title="Unit tests" description="Write and run fast unit tests for individual functions and components with instant feedback." className="shadow-none" />
<Card title="Code coverage" description="See precise coverage metrics that show what code is and isn't tested, giving you valuable insight to improve your test suite." className="shadow-none" />
<Card title="Integration tests" description="Test the interaction between different modules or services to ensure everything works together as intended." className="shadow-none" />
<Card title="E2E tests" description="Simulate real user scenarios across the entire stack with automated end-to-end test tools and examples." className="shadow-none" />
</Cards>
## Use like LEGO blocks
The biggest advantage of TurboStarter is its modularity. You can use the entire stack or just the parts you need. It's like LEGO blocks - you can build anything you want with it.
If you don't need a specific feature, feel free to remove it without affecting the rest of the stack.
This approach allows for:
* **Easy feature integration** - plug new features into the kit with minimal changes.
* **Simplified maintenance** - keep the codebase clean and maintainable.
* **Core feature separation** - distinguish between core features and custom features.
* **Additional modules** - easily add modules like billing, CMS, monitoring, logger, mailer, and more.
## Scope of this documentation
While building a SaaS application involves many moving parts, this documentation focuses specifically on TurboStarter. For in-depth information on the underlying technologies, please refer to their respective official documentation.
This documentation will guide you through configuring, running, and deploying the kit, and will provide helpful links to the official documentation of technologies where necessary.
## `llms.txt`
You can access the entire TurboStarter documentation in Markdown format at [/llms.txt](/llms.txt). This can be used to ask any LLM (assuming it has a big enough context window) questions about the TurboStarter based on the most up-to-date documentation.
### Example usage
For instance, to prompt an LLM with questions about the TurboStarter:
1. Copy the documentation contents from [/llms.txt](/llms.txt)
2. Use the following prompt format:
```
Documentation:
{paste documentation here}
---
Based on the above documentation, answer the following:
{your question}
```
## Enjoy!
This documentation is designed to be easy to follow and understand. If you have any questions or need help, feel free to reach out to us at [hello@turbostarter.dev](mailto:hello@turbostarter.dev).
Explore new features, build amazing apps, and have fun! 🚀

View File

@@ -0,0 +1,65 @@
---
title: Cloning repository
description: Get the code to your local machine and start developing.
url: /docs/web/installation/clone
---
# Cloning repository
<Callout type="info" title="Prerequisite: Git installed">
Ensure you have Git installed on your local machine before proceeding. You can download Git from [here](https://git-scm.com).
</Callout>
## Git clone
Clone the repository using the following command:
```bash
git clone git@github.com:turbostarter/core
```
By default, we're using [SSH](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) for all Git commands. If you don't have it configured, please refer to the [official documentation](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) to set it up.
Alternatively, you can use HTTPS to clone the repository:
```bash
git clone https://github.com/turbostarter/core
```
Another alternative could be to use the [Github CLI](https://cli.github.com/) or [Github Desktop](https://desktop.github.com/) for Git operations.
<Card title="Git clone" description="git-scm.com" href="https://git-scm.com/docs/git-clone" />
## Git remote
After cloning the repository, remove the original origin remote:
```bash
git remote rm origin
```
Add the upstream remote pointing to the original repository to pull updates:
```bash
git remote add upstream git@github.com:turbostarter/core
```
Once you have your own repository set up, add your repository as the origin:
```bash
git remote add origin <your-repository-url>
```
<Card title="Git remote" description="git-scm.com" href="https://git-scm.com/docs/git-remote" />
## Staying up to date
To pull updates from the upstream repository, run the following command daily (preferably with your morning coffee ☕):
```bash
git pull upstream main
```
This ensures your repository stays up to date with the latest changes.
Check [Updating codebase](/docs/web/installation/update) for more details on updating your codebase.

View File

@@ -0,0 +1,353 @@
---
title: Common commands
description: Learn about common commands you need to know to work with the project.
url: /docs/web/installation/commands
---
# Common commands
<Callout>
For sure, you don't need these commands to kickstart your project, but it's useful to know they exist for when you need them.
</Callout>
<Callout title="Want shorter commands?">
You can set up aliases for these commands in your shell configuration file. For example, you can set up an alias for `pnpm` to `p`:
```bash title="~/.bashrc"
alias p='pnpm'
```
Or, if you're using [Zsh](https://ohmyz.sh/), you can add the alias to `~/.zshrc`:
```bash title="~/.zshrc"
alias p='pnpm'
```
Then run `source ~/.bashrc` or `source ~/.zshrc` to apply the changes.
You can now use `p` instead of `pnpm` in your terminal. For example, `p i` instead of `pnpm install`.
</Callout>
<Callout title="Injecting environment variables">
To inject environment variables into the command you run, prefix it with `with-env`:
```bash
pnpm with-env <command>
```
For example, `pnpm with-env pnpm build` will run `pnpm build` with the environment variables injected.
Some commands, like `pnpm dev`, automatically inject the environment variables for you.
</Callout>
## Installing dependencies
To install the dependencies, run:
```bash
pnpm install
```
## Starting development server
Start development server by running:
```bash
pnpm dev
```
## Building project
To build the project (including all apps and packages), run:
```bash
pnpm build
```
## Building specific app/package
To build a specific app/package, run:
```bash
pnpm turbo build --filter=<package-name>
```
## Cleaning project
To clean the project, run:
```bash
pnpm clean
```
Then, reinstall the dependencies:
```bash
pnpm install
```
## Formatting code
To check for formatting errors using Prettier, run:
```bash
pnpm format
```
To fix formatting errors using Prettier, run:
```bash
pnpm format:fix
```
## Linting code
To check for linting errors using ESLint, run:
```bash
pnpm lint
```
To fix linting errors using ESLint, run:
```bash
pnpm lint:fix
```
## Typechecking
To typecheck the code using TypeScript for any type errors, run:
```bash
pnpm typecheck
```
## Adding UI components
<Tabs items={["Web", "Mobile"]}>
<Tab value="Web">
To add a new web component, run:
```bash
pnpm --filter @turbostarter/ui-web ui:add
```
This command will add and export a new component to `@turbostarter/ui-web` package.
</Tab>
<Tab value="Mobile">
To add a new mobile component, run:
```bash
pnpm --filter @turbostarter/ui-mobile ui:add
```
This command will add and export a new component to `@turbostarter/ui-mobile` package.
</Tab>
</Tabs>
## Services commands
<Callout title="Prerequisite: Docker installed">
To run the services containers locally, you need to have [Docker](https://www.docker.com/) installed on your machine.
You can always use the cloud-hosted solution (e.g. [Neon](https://neon.tech/), [Turso](https://turso.tech/) for database) for your projects.
</Callout>
We have a few commands to help you manage the services containers (for local development).
### Starting containers
To start the services containers, run:
```bash
pnpm services:start
```
It will run all the services containers. You can check their configs in `docker-compose.yml`.
### Setting up services
To setup all the services, run:
```bash
pnpm services:setup
```
It will start all the services containers and run necessary setup steps.
### Stopping containers
To stop the services containers, run:
```bash
pnpm services:stop
```
### Displaying status
To check the status and logs of the services containers, run:
```bash
pnpm services:status
```
### Displaying logs
To display the logs of the services containers, run:
```bash
pnpm services:logs
```
### Database commands
We have a few commands to help you manage the database leveraging [Drizzle CLI](https://orm.drizzle.team/kit-docs/commands).
#### Generating migrations
To generate a new migration, run:
```bash
pnpm with-env turbo db:generate
```
It will create a new migration `.sql` file in the `packages/db/migrations` folder.
#### Running migrations
To run the migrations against the db, run:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:migrate
```
It will apply all the pending migrations.
#### Pushing changes directly
<Callout type="warn" title="Don't mess up with your schema!">
Make sure you know what you're doing before pushing changes directly to the db.
</Callout>
To push changes directly to the db, run:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:push
```
It lets you push your schema changes directly to the database and omit managing SQL migration files.
#### Checking database status
To check the status of the database, run:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:status
```
It will display the status of the applied migrations and the pending ones.
```bash
Applied migrations:
- 0000_cooing_vargas
- 0001_curious_wallflower
- 0002_good_vertigo
- 0003_peaceful_devos
- 0004_fat_mad_thinker
- 0005_yummy_bucky
- 0006_glorious_vargas
Pending migrations:
- 0007_nebulous_havok
```
#### Resetting database
To reset the database, run:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:reset
```
It will reset the database to the initial state.
#### Seeding database
To seed the database with some example data (for development purposes), run:
```bash
pnpm with-env turbo db:seed
```
It will populate your database with some example data.
#### Checking database
To check the database schema consistency, run:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:check
```
#### Studying database
To study the database schema in the browser, run:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:studio
```
This will start the Studio on [https://local.drizzle.studio](https://local.drizzle.studio).
## Tests commands
### Running tests
To run the tests, run:
```bash
pnpm test
```
This will run all the tests in the project using Turbo tasks. As it leverages Turbo caching, it's [recommended](/docs/web/tests/unit#configuration) to run it in your CI/CD pipeline.
### Running tests projects
To run tests for all Vitest [Test Projects](https://vitest.dev/guide/projects), run:
```bash
pnpm test:projects
```
This will run all the tests in the project using Vitest.
### Watching tests
To watch the tests, run:
```bash
pnpm test:projects:watch
```
This will watch the tests for all [Test Projects](https://vitest.dev/guide/projects) and run them automatically when you make changes.
### Generating code coverage
To generate code coverage report, run:
```bash
pnpm turbo test:coverage
```
This will generate a code coverage report in the `coverage` directory under `tooling/vitest` package.
### Viewing code coverage
To preview the code coverage report in the browser, run:
```bash
pnpm turbo test:coverage:view
```
This will launch the report's `.html` file in your default browser.

View File

@@ -0,0 +1,86 @@
---
title: Conventions
description: Some standard conventions used across TurboStarter codebase.
url: /docs/web/installation/conventions
---
# Conventions
You're not required to follow these conventions: they're simply a standard set of practices used in the core kit. If you like them - we encourage you to keep these during your usage of the kit - so to have consistent code style that you and your teammates understand.
## Turborepo Packages
In this project, we use [Turborepo packages](https://turbo.build/repo/docs/core-concepts/internal-packages) to define reusable code that can be shared across multiple applications.
* **Apps** are used to define the main application, including routing, layout, and global styles.
* **Packages** shares reusable code add functionalities across multiple applications. They're configurable from the main application.
<Callout title="Should I create a new package?">
**Recommendation:** Do not create a package for your app code unless you plan to reuse it across multiple applications or are experienced in writing library code.
If your application is not intended for reuse, keep all code in the app folder. This approach saves time and reduces complexity, both of which are beneficial for fast shipping.
**Experienced developers:** If you have the experience, feel free to create packages as needed.
</Callout>
## Imports and Paths
When importing modules from packages or apps, use the following conventions:
* **From a package:** Use `@turbostarter/package-name` (e.g., `@turbostarter/ui`, `@turbostarter/api`, etc.).
* **From an app:** Use `~/` (e.g., `~/components`, `~/config`, etc.).
## Enforcing conventions
* [Prettier](https://prettier.io/) is used to enforce code formatting.
* [ESLint](https://eslint.org/) is used to enforce code quality and best practices.
* [TypeScript](https://www.typescriptlang.org/) is used to enforce type safety.
<Cards className="grid-cols-2 sm:grid-cols-3">
<Card title="Prettier" href="https://prettier.io/" description="prettier.io" />
<Card title="ESLint" href="https://eslint.org/" description="eslint.org" />
<Card title="TypeScript" href="https://www.typescriptlang.org/" description="typescriptlang.org" />
</Cards>
## Code health
TurboStarter provides a set of tools to ensure code health and quality in your project.
### Github Actions
By default, TurboStarter sets up Github Actions to run tests on every push to the repository. You can find the Github Actions configuration in the `.github/workflows` directory.
The workflow has multiple stages:
* `format` - runs Prettier to format the code.
* `lint` - runs ESLint to check for linting errors.
* `typecheck` - runs TypeScript to check for type errors.
### Git hooks
Together with TurboStarter we have set up a `commit-msg` hook which will check if your commit message follows the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) message format. This is important for generating changelogs and keeping a clean commit history.
Although we didn't ship any pre-commit hooks (we believe in shipping fast with moving checking code responsibility to CI), you can easily add them by using [Husky](https://typicode.github.io/husky/#/).
#### Setting up the Pre-Commit Hook
To do so, create a `pre-commit` file in the `./..husky` directory with the following content:
```bash
#!/bin/sh
pnpm typecheck
pnpm lint
```
Turborepo will execute the commands for all the affected packages - while skipping the ones that are not affected.
#### Make the Pre-Commit Hook Executable
```bash
chmod +x ./.husky/pre-commit
```
To test the pre-commit hook, try to commit a file with linting errors or type errors. The commit should fail, and you should see the error messages in the console.

View File

@@ -0,0 +1,73 @@
---
title: Managing dependencies
description: Learn how to manage dependencies in your project.
url: /docs/web/installation/dependencies
---
# Managing dependencies
As the package manager we chose [pnpm](https://pnpm.io/).
<Callout title="Why pnpm?">
It is a fast, disk space efficient package manager that uses hard links and symlinks to save one version of a module only ever once on a disk. It also has a great [monorepo support](https://pnpm.io/workspaces). Of course, you can change it to use [Bun](https://bunpkg.com), [yarn](https://yarnpkg.com) or [npm](https://www.npmjs.com) with minimal effort.
</Callout>
## Install dependency
To install a package you need to decide whether you want to install it to the root of the monorepo or to a specific workspace. Installing it to the root makes it available to all packages, while installing it to a specific workspace makes it available only to that workspace.
To install a package globally, run:
```bash
pnpm add -w <package-name>
```
To install a package to a specific workspace, run:
```bash
pnpm add --filter <workspace-name> <package-name>
```
For example:
```bash
pnpm add --filter @turbostarter/ui motion
```
It will install `motion` to the `@turbostarter/ui` workspace.
## Remove dependency
Removing a package is the same as installing but with the `remove` command.
To remove a package globally, run:
```bash
pnpm remove -w <package-name>
```
To remove a package from a specific workspace, run:
```bash
pnpm remove --filter <workspace-name> <package-name>
```
## Update a package
Updating is a bit easier since there is a nice way to update a package in all workspaces at once:
```bash
pnpm update -r <package-name>
```
<Callout title="Semantic versioning">
When you update a package, pnpm will respect the [semantic versioning](https://docs.npmjs.com/about-semantic-versioning) rules defined in the `package.json` file. If you want to update a package to the latest version, you can use the `--latest` flag.
</Callout>
## Renovate bot
By default, TurboStarter comes with [Renovate](https://www.npmjs.com/package/renovate) enabled. It is a tool that helps you manage your dependencies by automatically creating pull requests to update your dependencies to the latest versions. You can find its configuration in the `.github/renovate.json` file. Learn more about it in the [official docs](https://docs.renovatebot.com/configuration-options/).
When it creates a pull request, it is treated as a normal PR, so all tests and preview deployments will run. **It is recommended to always preview and test the changes in the staging environment before merging the PR to the main branch to avoid breaking the application.**
<Card href="https://docs.renovatebot.com" title="Renovate" description="renovatebot.com" />

View File

@@ -0,0 +1,89 @@
---
title: Development
description: Get started with the code and develop your SaaS.
url: /docs/web/installation/development
---
# Development
## Prerequisites
To get started with TurboStarter, ensure you have the following installed and set up:
* [Node.js](https://nodejs.org/en) (22.x or higher)
* [Docker](https://www.docker.com) (only if you want to use local services e.g. database)
* [pnpm](https://pnpm.io)
## Project development
<Steps>
<Step>
### Install dependencies
Install the project dependencies by running the following command:
```bash
pnpm i
```
<Callout title="Why pnpm?">
It is a fast, disk space efficient package manager that uses hard links and symlinks to save one version of a module only ever once on a disk. It also has a great [monorepo support](https://pnpm.io/workspaces). Of course, you can change it to use [Bun](https://bunpkg.com), [yarn](https://yarnpkg.com) or [npm](https://www.npmjs.com) with minimal effort.
</Callout>
</Step>
<Step>
### Setup environment variables
Create a `.env.local` files from `.env.example` files and fill in the required environment variables.
You can use the following command to recursively copy the `.env.example` files to the `.env.local` files:
<Tabs items={["Unix (MacOS/Linux)", "Windows"]}>
<Tab value="Unix (MacOS/Linux)">
```bash
find . -name ".env.example" -exec sh -c 'cp "$1" "${1%.example}.local"' _ {} \;
```
</Tab>
<Tab value="Windows">
```bash
Get-ChildItem -Recurse -Filter ".env.example" | ForEach-Object {
Copy-Item $_.FullName ($\_.FullName -replace '\.example$', '.local')
}
```
</Tab>
</Tabs>
Check [Environment variables](/docs/web/configuration/environment-variables) for more details on setting up environment variables.
</Step>
<Step>
### Setup services
If you want to use local services like [database](/docs/web/database/overview) (**recommended for development purposes**), ensure Docker is running, then setup them with:
```bash
pnpm services:setup
```
This command initiates the containers and runs necessary setup steps, ensuring your services are up to date and ready to use.
</Step>
<Step>
### Start development server
To start the application development server, run:
```bash
pnpm dev
```
Your app should now be up and running at [http://localhost:3000](http://localhost:3000) 🎉
</Step>
<Step>
### Deploy to Production
When you're ready to deploy the project to production, follow the [checklist](/docs/web/deployment/checklist) to ensure everything is set up correctly.
</Step>
</Steps>

View File

@@ -0,0 +1,69 @@
---
title: Editor setup
description: Learn how to set up your editor for the fastest development experience.
url: /docs/web/installation/editor-setup
---
# Editor setup
Of course you can use every IDE you like, but you will have the best possible developer experience with this starter kit when using **VSCode-based** editor with the suggested settings and extensions.
## Settings
We have included most recommended settings in the `.vscode/settings.json` file to make your development experience as smooth as possible. It include mostly configs for tools like Prettier, ESLint and Tailwind which are used to enforce some conventions across the codebase. You can adjust them to your needs.
## LLM rules
We exposed a special endpoint that will scan all the docs and return the content as a text file which you can use to train your LLM or put in a prompt. You can find it at [/llms.txt](/llms.txt).
The repository also includes a custom rules for most popular AI editors and agents to ensure that AI completions are working as expected and following our conventions.
### AGENTS.md
We've integrated specific rules that help maintain code quality and ensure AI-assisted completions align with our project standards.
You can find them in the `AGENTS.md` file at the root of the project. This format is a standardized way to instruct AI agents to follow project conventions when generating code and it's used by over **20,000** open-source projects - including [Cursor](https://cursor.sh/), [Aider](https://aider.chat/), [Codex from OpenAI](https://openai.com/blog/openai-codex/), [Jules from Google](https://jules.ai/), [Windsurf](https://windsurf.dev/), and many others.
```md title="AGENTS.md"
### Code Style and Structure
- Write concise, technical TypeScript code with accurate examples
- Use functional and declarative programming patterns; avoid classes
- Prefer iteration and modularization over code duplication
### Naming Conventions
....
```
To learn more about `AGENTS.md` rules check out the [official documentation](https://agents.md/).
## Extensions
Once you cloned the project and opened it in VSCode you should be promted to install suggested extensions which are defined in the `.vscode/extensions.json` automatically. In case you rather want to install them manually you can do so at any time later.
These are the extensions we recommend:
### ESLint
Global extension for static code analysis. It will help you to find and fix problems in your JavaScript code.
<Card title="Download ESLint" href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint" description="marketplace.visualstudio.com" />
### Prettier
Global extension for code formatting. It will help you to keep your code clean and consistent.
<Card title="Download Prettier" href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode" description="marketplace.visualstudio.com" />
### Pretty TypeScript Errors
Improves TypeScript error messages shown in the editor.
<Card title="Download Pretty TypeScript Errors" href="https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors" description="marketplace.visualstudio.com" />
### Tailwind CSS IntelliSense
Adds IntelliSense for Tailwind CSS classes to enable autocompletion and linting.
<Card title="Download Tailwind CSS IntelliSense" href="https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss" description="marketplace.visualstudio.com" />

View File

@@ -0,0 +1,116 @@
---
title: Project structure
description: Learn about the project structure and how to navigate it.
url: /docs/web/installation/structure
---
# Project structure
The main directories in the project are:
* `apps` - the location of the main apps
* `packages` - the location of the shared code and the API
### `apps` Directory
This is where the apps live. It includes web app (Next.js), mobile app (React Native - Expo), and the browser extension (WXT - Vite + React). Each app has its own directory.
### `packages` Directory
This is where the shared code and the API for packages live. It includes the following:
* shared libraries (database, mailers, cms, billing, etc.)
* shared features (auth, mails, billing, ai etc.)
* UI components (buttons, forms, modals, etc.)
All apps can use and reuse the API exported from the packages directory. This makes it easy to have one, or many apps in the same codebase, sharing the same code.
## Repository structure
By default the monorepo contains the following apps and packages:
<Files>
<Folder name="apps" defaultOpen>
<Folder name="web - Web app (Next.js)" />
<Folder name="mobile - Mobile app (React Native - Expo)" />
<Folder name="extension - Browser extension (WXT - Vite + React)" />
</Folder>
<Folder name="packages" defaultOpen>
<Folder name="analytics - Analytics setup" />
<Folder name="api - API server (including all features logic)" />
<Folder name="auth - Authentication setup" />
<Folder name="billing - Billing config and providers" />
<Folder name="cms - CMS setup and providers" />
<Folder name="db - Database setup" />
<Folder name="email - Mail templates and providers" />
<Folder name="i18n - Internationalization setup" />
<Folder name="shared - Shared utilities and helpers" />
<Folder name="storage - Storage setup" />
<Folder name="ui - Atomic UI components">
<Folder name="shared" />
<Folder name="web" />
<Folder name="mobile" />
</Folder>
</Folder>
<Folder name="tooling" defaultOpen>
<Folder name="eslint - ESLint config" />
<Folder name="github - Github actions" />
<Folder name="prettier - Prettier config" />
<Folder name="typescript - TypeScript config" />
</Folder>
</Files>
## Web application structure
The web application is located in the `apps/web` folder. It contains the following folders:
<Files>
<Folder name="public - Static assets" />
<Folder name="src" defaultOpen>
<Folder name="app - Main application" />
<Folder name="assets - Optimized static assets" />
<Folder name="config - Global app config" />
<Folder name="modules - Application modules" />
<Folder name="lib - Communication with third-party packages" />
<Folder name="utils - Shared utilities" />
</Folder>
<File name=".env.local" />
<File name="env.config.ts" />
<File name="eslint.config.js" />
<File name="next.config.ts" />
<File name="package.json" />
<File name="tsconfig.json" />
<File name="turbo.json" />
</Files>

View File

@@ -0,0 +1,97 @@
---
title: Updating codebase
description: Learn how to update your codebase to the latest version.
url: /docs/web/installation/update
---
# Updating codebase
If you've been following along with our previous guides, you should already have a Git repository set up for your project, with an `upstream` remote pointing to the original repository.
Updating your project involves fetching the latest changes from the `upstream` remote and merging them into your project. Let's dive into the steps!
<Steps>
<Step>
## Stash changes
<Callout title="Don't have changes?">
If you don't have any changes to stash, you can skip this step and proceed with the update process.
Alternatively, you can [commit](https://git-scm.com/docs/git-commit) your changes.
</Callout>
If you have any uncommitted changes, stash them before proceeding. It will allow you to avoid any conflicts that may arise during the update process.
```bash
git stash
```
This command will save your changes in a temporary location, allowing you to retrieve them later. Once you're done updating, you can apply the stash to your working directory.
```bash
git stash apply
```
</Step>
<Step>
## Pull changes
Pull the latest changes from the `upstream` remote.
```bash
git pull upstream main
```
When prompted the first time, please opt for merging instead of rebasing.
Don't forget to run `pnpm i` in case there are any updates in the dependencies.
</Step>
<Step>
## Resolve conflicts
If there are any conflicts during the merge, Git will notify you. You can resolve them by opening the conflicting files in your code editor and making the necessary changes.
<Callout title="Conflicts in pnpm-lock.yaml?">
If you find conflicts in the `pnpm-lock.yaml file`, accept either of the two changes (avoid manual edits), then run:
```bash
pnpm i
```
Your lock file will now reflect both your changes and the updates from the upstream repository.
</Callout>
</Step>
<Step>
## Run a health check
After resolving the conflicts, it's time to test your project to ensure everything is working as expected. Run your project locally and navigate through the various features to verify that everything is functioning correctly.
For a quick health check, you can run:
```bash
pnpm lint
pnpm typecheck
```
If everything looks good, you're all set! Your project is now up to date with the latest changes from the `upstream` repository.
</Step>
<Step>
## Commit and push
Once everything is working fine, don't forget to commit your changes using:
```bash
git commit -m "<your-commit-message>"
```
and push them to your remote repository with:
```bash
git push origin <your-branch-name>
```
</Step>
</Steps>

View File

@@ -0,0 +1,136 @@
---
title: Configuration
description: Learn how to configure internationalization in TurboStarter.
url: /docs/web/internationalization/configuration
---
# Configuration
The default global configuration is defined in the `@turbostarter/i18n` package and shared across all applications. You can override it in each app to customize the internationalization setup for that specific app.
The configuration is defined in the `packages/i18n/src/config.ts` file:
```ts title="packages/i18n/src/config.ts"
export const config = {
locales: ["en", "es"],
defaultLocale: "en",
namespaces: [
"common",
"admin",
"organization",
"dashboard",
"auth",
"billing",
"marketing",
"validation",
],
cookie: "locale",
} as const;
```
Let's break down the configuration options:
* `locales`: An array of all supported locales.
* `defaultLocale`: The default locale to use if no other locale is detected.
* `namespaces`: An array of all namespaces used in the application.
* `cookie`: The name of the cookie to store the detected locale (acts as a cache).
## Translation files
The core of the whole internationalization setup is the translation files. They are stored in the `packages/i18n/src/translations` directory and are used to store the translations for each locale and namespace.
Each directory represents a locale and contains a set of files, each corresponding to a specific namespace (e.g. `en/common.json`). Inside we define the keys and values for the translations.
```ts title="packages/i18n/src/translations/en/common.json"
{
"hello": "Hello, world!"
}
```
That way we can ensure that we have a single source of truth for the translations and we can use them consistently in all the applications.
## Locales
The `locales` array in the configuration defines the list of supported languages in your application. Each locale is represented by a string that uniquely identifies the language.
To add a new locale, you need to:
1. Add the new locale to the `locales` array in the configuration.
2. Create a new directory in the `packages/i18n/src/translations` directory.
3. Create a new file in the new directory for each namespace and add the translations for the new locale.
For example, if you want to add the `fr` locale, you need to:
1. Add `fr` to the `locales` array in the configuration.
2. Create a new directory in the `packages/i18n/src/translations` directory.
3. Create a new file for each namespace in the created directory and add the translations for the new locale.
### Fallback locale
The `defaultLocale` option in the configuration defines the fallback locale. If a translation is not found for a specific locale, the fallback locale will be used.
We can also override this setting in each [app configuration](/docs/web/configuration/app) by configuring the `locale` property.
## Namespaces
`namespaces` are used to group translations by feature or module. This helps in organizing the translations and makes it easier to maintain them.
### Why not one big namespace?
Using multiple namespaces instead of one large namespace helps with:
1. **Performance:** load translations on-demand instead of all at once, reducing the initial bundle size.
2. **Organization:** group translations by feature (e.g., `auth`, `common`, `dashboard`).
3. **Maintenance:** easier to update and manage smaller translation files.
4. **Development:** better TypeScript support and team collaboration.
For example, you might structure your namespaces like this:
<Tabs items={["Common", "Auth", "Billing"]}>
<Tab value="Common">
```ts title="packages/i18n/src/translations/en/common.json"
{
"hello": "Hello, world!"
}
```
</Tab>
<Tab value="Auth">
```ts title="packages/i18n/src/translations/en/auth.json"
{
"login": "Login",
"register": "Register"
}
```
</Tab>
<Tab value="Billing">
```ts title="packages/i18n/src/translations/en/billing.json"
{
"invoice": "Invoice",
"payment": "Payment",
"subscription": "Subscription"
}
```
</Tab>
</Tabs>
Remember that while you can create as many namespaces as needed, it's important to maintain a balance - too many namespaces can lead to unnecessary complexity, while too few might defeat the purpose of separation.
## Routing
TurboStarter implements locale-based routing by placing pages under the `[locale]` folder. However, the default locale (usually `en`) is not prefixed in the URL for better SEO and user experience.
For example, with English as the default locale and Polish as an additional language:
* `/dashboard` → English version (default locale)
* `/pl/dashboard` → Polish version
The app also automatically detects the user's preferred language through cookies, HTML `lang` attribute, and browser's `Accept-Language` header.
This ensures a seamless experience where users get content in their preferred language while maintaining clean URLs for the default locale.
<Callout>
You can override the locale by manually setting the cookie or by navigating to
a URL with a different locale prefix.
</Callout>

View File

@@ -0,0 +1,40 @@
---
title: Overview
description: Get started with internationalization in TurboStarter.
url: /docs/web/internationalization/overview
---
# Overview
TurboStarter uses [i18next](https://www.i18next.com/) for internationalization, which is one of the most popular and mature (over 10 years of development!) i18n frameworks for JavaScript.
<Callout title="Why i18next?">
With i18next, you can easily translate your application into multiple
languages, handle complex pluralization rules, format dates and numbers
according to locale, and much more. The framework is highly extensible through
plugins and provides excellent TypeScript support out of the box.
</Callout>
You can read more about `i18next` package in the [official documentation](https://www.i18next.com/overview/getting-started).
![i18next logo](/images/docs/i18next.jpg)
## Getting started
TurboStarter comes with `i18next` pre-configured and abstracted behind the `@turbostarter/i18n` package. This abstraction layer ensures that any future changes to the underlying translation library won't impact your application code. The internationalization setup is ready to use out of the box and includes:
* Multiple language support out of the box
* Type-safe translations with generated types
* Automatic language detection
* Easy-to-use React hooks for translations
* Built-in number and date formatting
* Support for nested translation keys
* Pluralization handling
To start using internationalization in your app, you'll need to:
1. Configure your supported languages
2. Add translation files
3. Use translation hooks in your components
Check out the following sections to learn more about each step:

View File

@@ -0,0 +1,147 @@
---
title: Translating app
description: Learn how to translate your application to multiple languages.
url: /docs/web/internationalization/translations
---
# Translating app
TurboStarter provides a flexible and powerful translation system that works seamlessly across your entire application. Whether you're working with React Server Components (RSC), client-side components, or server-side rendering, you can easily integrate translations to create a fully internationalized experience.
The translation system supports:
* **Server components (RSC)** for efficient server-side translations
* **Client components** for dynamic language switching
* **Server-side rendering** for SEO-friendly translated content
## Server components (RSC)
To get the translations in a server component, you can use the `getTranslation` method:
```tsx
import { getTranslation } from "@turbostarter/i18n";
export default async function MyComponent() {
const { t } = await getTranslation();
return <div>{t("common:hello")}</div>;
}
```
There is also a possibility to use the [Trans](https://react.i18next.com/latest/trans-component) component, which could be useful e.g. for interpolating variables:
```tsx
import { Trans } from "@turbostarter/i18n";
import { withI18n } from "@turbostarter/i18n/with-i18n";
const Page = () => {
return <Trans i18nKey="common:hello" components={{ bold: <b /> }} />;
};
export default withI18n(Page);
```
Although, to make it available in the server component, you need to wrap it with the `withI18n` HOC.
Given that server components are rendered in parallel, it's uncertain which one will render first. Therefore, it's crucial to initialize the translations before rendering the server component on each page/layout.
## Client components
For client components, you can use the `useTranslation` hook from the `@turbostarter/i18n` package:
```tsx
"use client";
import { useTranslation } from "@turbostarter/i18n";
export default function MyComponent() {
const { t } = useTranslation();
return <div>{t("common:hello")}</div>;
}
```
That's the simplest way to get the translations in a client component.
## Server-side
In all other places (e.g. metadata, API routes, sitemaps etc.) you can use the `getTranslation` method to get the translations server-side:
```ts
import { getTranslation } from "@turbostarter/i18n";
export const generateMetadata = async () => {
const { t } = await getTranslation();
return {
title: t("common:title"),
};
};
```
It automatically checks the user's preferred locale and uses the correct translation.
## Language switcher
TurboStarter ships with a language customizer component that allows you to switch between languages. You can import and use the `LocaleCustomizer` component and drop it anywhere in your application to allow users to change the language seamlessly.
```tsx
import { LocaleCustomizer } from "@turbostarter/ui-web/i18n";
export default function MyComponent() {
return <LocaleCustomizer />;
}
```
The component automatically displays all languages configured in your i18n settings. When a user switches languages, it will:
1. Update the URL to include the new locale prefix (e.g. `/es/dashboard`)
2. Store the selected locale in a cookie for persistence
3. Refresh translations across the entire application
4. Preserve the current page/route during the language switch
This provides a seamless localization experience without requiring any additional configuration.
## Best practices
Here are some recommended best practices for managing translations in your application:
* Use descriptive translation keys that follow a logical hierarchy
```ts
// ✅ Good
"auth.login.title";
// ❌ Bad
"loginTitleForAuth";
```
* Keep translations organized in separate namespaces/files based on features or sections
```
translations/
├── en/
│ ├── auth.json
│ └── common.json
└── pl/
├── auth.json
└── billing.json
```
* Avoid hardcoding text strings - always use translation keys even for seemingly static content
* Always provide a fallback language (usually English) for when translations are missing
* Use pluralization and interpolation features when dealing with dynamic content
```ts
// Pluralization
t("items", { count: 2 }); // "2 items"
// Interpolation
t("welcome", { name: "John" }); // "Welcome, John!"
```
* Regularly review and clean up unused translation keys to keep files maintainable
* Use TypeScript for type-safe translation keys

View File

@@ -0,0 +1,98 @@
---
title: Legal pages
description: Learn how to create and update legal pages
url: /docs/web/marketing/legal
---
# Legal pages
Legal pages are defined in the `apps/web/src/app/[locale]/(marketing)/legal` directory.
TurboStarter comes with the following legal pages:
* **Terms and Conditions**: to define the terms and conditions of your application
* **Privacy Policy**: to define the privacy policy of your application
* **Cookie Policy**: to define the cookie policy of your application
For obvious reasons, **these pages are empty and you need to fill in the content.**
## Content from CMS
Content for legal pages are stored as [MDX](https://mdxjs.com/) files in [content collection](/docs/web/cms/content-collections) in `packages/cms/src/content/collections/legal` directory.
Then it's parsed and rendered as a Next.js page under corresponding slug:
```tsx title="apps/web/src/app/[locale]/(marketing)/legal/[slug]/page.tsx"
import {
CollectionType,
getContentItemBySlug,
getContentItems,
} from "@turbostarter/cms";
export default async function Page({ params }: PageParams) {
const item = getContentItemBySlug({
collection: CollectionType.LEGAL,
slug: (await params).slug,
locale: (await params).locale,
});
if (!item) {
return notFound();
}
return <Mdx mdx={item.mdx} />;
}
export function generateStaticParams() {
return getContentItems({ collection: CollectionType.LEGAL }).items.map(
({ slug, locale }) => ({
slug,
locale,
}),
);
}
```
As it's fully typesafe it also allows us to generate metadata for each page based on the frontmatter that you define in the MDX file:
```tsx title="apps/web/src/app/[locale]/(marketing)/legal/[slug]/page.tsx"
export async function generateMetadata({ params }: PageParams) {
const item = getContentItemBySlug({
collection: CollectionType.LEGAL,
slug: (await params).slug,
locale: (await params).locale,
});
if (!item) {
return notFound();
}
return getMetadata({
title: item.title,
description: item.description,
})({ params });
}
```
Read more about it in the [CMS section](/docs/web/cms/overview).
## ChatGPT prompts
Each `.mdx` file with legal content include a set of useful prompts that you can use to generate the content.
<Callout type="warn" title="Please, be aware of this!">
Please, be aware that **ChatGPT is not a lawyer** and the content generated by it should be reviewed by one before publishing. Take your time and treat the generated content as a starting point not a final document.
</Callout>
```mdx title="privacy-policy.mdx"
---
title: Privacy Policy
description: Our privacy policy outlines how we collect, use, and protect your personal information.
---
{/* 💡 You can use one of the following ChatGPT prompts to generate this 💡 */}
...
```
Feel free to add your own content or even additional pages to the `legal` collection.

View File

@@ -0,0 +1,42 @@
---
title: Marketing pages
description: Discover which marketing pages are available out of the box and how to add a new one.
url: /docs/web/marketing/pages
---
# Marketing pages
TurboStarter comes with pre-defined marketing pages to help you get started with your SaaS application. These pages are built with Next.js and Tailwind CSS and are located in the `apps/web/src/app/[locale]/(marketing)` directory.
TurboStarter comes with the following marketing pages:
* **Home**: conversions-optimized [landing page](https://demo.turbostarter.dev) with [hero section](https://demo.turbostarter.dev#hero), [features](https://demo.turbostarter.dev#features), [pricing](https://demo.turbostarter.dev#pricing), [testimonials](https://demo.turbostarter.dev#testimonials), [FAQ](https://demo.turbostarter.dev#faq) and more
* [Blog](/docs/web/cms/blog): to display your blog posts
* **Pricing**: to display your pricing plans
* **Contact**: to enable users to contact you with a contact form
## Contact form
To make the contact form work, you need to add the following environment variable:
```dotenv
CONTACT_EMAIL=
```
Set this variable to the email address where you want to receive contact form submissions. The sender's email address will match what you configured in your [mailing configuration](/docs/web/emails/configuration).
## Adding a new marketing page
To add a new marketing page, create a new directory in `apps/web/src/app/[locale]/(marketing)` with the desired route name.
The page will automatically become available in your application at the corresponding URL path.
For example, to create a page accessible at `/about`, create a directory named `about` and add a `page.tsx` file inside it. The complete path would be `apps/web/src/app/[locale]/(marketing)/about/page.tsx`.
```tsx title="apps/web/src/app/[locale]/(marketing)/about/page.tsx"
export default function AboutPage() {
return <div>About</div>;
}
```
This page inherits the layout at `apps/web/src/app/[locale]/(marketing)/layout.tsx`. You can customize the layout by editing this file - but remember that it will affect all marketing pages.

View File

@@ -0,0 +1,188 @@
---
title: SEO
description: Learn how to optimize your app for search engines.
url: /docs/web/marketing/seo
---
# SEO
SEO is an important part of building a website. It helps search engines understand your website and rank it higher in search results. In this guide, you'll learn how to improve your SaaS application's search engine optimization (SEO).
<Callout title="Already optimized!">
TurboStarter is already optimized for SEO out of the box (including meta tags, sitemaps, robots files and many more). However, there are a few things you can do to improve your application's SEO.
</Callout>
**Content:** High-quality, relevant content is the cornerstone of effective SEO. Focus on **creating valuable, engaging content** that addresses your customers' needs and questions. Regularly update your content to keep it fresh and relevant.
**Keyword optimization:** Conduct thorough keyword research to identify terms your target audience is searching for. Incorporate these keywords naturally into your content, titles, meta descriptions, and headers. Avoid keyword stuffing; prioritize readability and user experience.
**On-Page SEO:**
* Use descriptive, keyword-rich titles and meta descriptions for each page.
* Implement a clear heading structure (H1, H2, H3) to organize your content.
* Optimize images with descriptive file names and alt text.
* Ensure your URLs are clean, descriptive, and include relevant keywords.
**Technical SEO:**
* Improve website loading speed by optimizing images, minifying CSS and JavaScript, and leveraging browser caching.
* Ensure your website is mobile-friendly and responsive across all devices.
* Implement schema markup to help search engines better understand your content.
* Use HTTPS to secure your website and boost search rankings.
**User experience:**
* Design an intuitive site structure and navigation to improve user engagement.
* Reduce bounce rates by creating compelling, easy-to-read content.
* Implement internal linking to guide users through your site and distribute page authority.
**Link building:**
* Create high-quality, shareable content to naturally attract backlinks.
* Engage in guest posting on reputable sites within your industry.
* Participate in industry forums and discussions, providing valuable insights and linking to your content when relevant.
* Leverage social media to increase content visibility and encourage sharing.
**Local SEO (if applicable):**
* Claim and optimize your Google My Business listing.
* Ensure consistent NAP (Name, Address, Phone) information across all online directories.
* Encourage customer reviews on Google and other relevant platforms.
**Monitor and analyze:**
* Use [Google Search Console](https://search.google.com/search-console/about) to monitor your site's performance in search results and identify issues.
* Regularly analyze your SEO efforts using tools like Google Analytics to understand user behavior and refine your strategy.
**Stay updated:**
* Keep abreast of SEO best practices and algorithm updates to continually refine your strategy.
* Regularly audit your website to identify and fix any SEO issues.
## Sitemap
Generally speaking, Google will find your pages without a sitemap as it follows the link in your website. However, you can add pages to the sitemap by adding them to the `apps/web/src/app/sitemap.ts` file, which is used to generate the sitemap.
If you add more static pages to your website, you can add them to the sitemap by adding them to the `apps/web/src/app/sitemap.ts` returned array.
```tsx title="sitemap.ts"
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
...getEntry(pathsConfig.index),
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1,
},
...getContentItems({
collection: CollectionType.BLOG,
locale: appConfig.locale,
}).items.map<MetadataRoute.Sitemap[number]>((post) => ({
...getEntry(pathsConfig.marketing.blog.post(post.slug)),
lastModified: new Date(post.lastModifiedAt),
changeFrequency: "monthly",
priority: 0.7,
})),
/* other pages */
];
}
```
All the existing pages are already added to the sitemap. You don't need to add them manually.
## Meta tags
TurboStarter provides a helper function called `getMetadata` to easily set meta tags for your pages. This helper ensures consistent metadata formatting across your site and includes essential SEO tags like title, description, and Open Graph tags. You can use it in any page's metadata export:
```tsx title="page.tsx"
export const generateMetadata = getMetadata({
title: "My Page Title",
description: "My Page Description",
});
```
This will generate the following meta tags:
```html
<meta name="description" content="My Page Description" />
<meta property="og:title" content="My Page Title" />
<meta property="og:description" content="My Page Description" />
```
The `getMetadata` helper is really useful for generating consistent meta tags across your site, making SEO optimization simpler and more reliable.
<Callout title="Translations supported!">
`getMetadata` also supports translations. You can pass a translation key to the `title` and `description` parameters, and it will automatically use the correct translation for the current locale.
```tsx
export const generateMetadata = getMetadata({
title: "billing:title",
description: "billing:description",
});
```
In this example, the `title` and `description` will be fetched from the `billing` namespace for the current locale and placed in the meta tags.
</Callout>
## Backlinks
Backlinks are said to be the **most important factor** in modern SEO. The more backlinks you have from high-quality websites, the higher your website will rank in search results - and the more traffic you'll get.
How do you acquire backlinks? The most effective strategy is to create high-quality, valuable content that naturally attracts links from other websites. However, there are several other methods to build backlinks:
1. **Guest blogging:** Contribute articles to reputable websites within your industry. This not only provides backlinks but also exposes your brand to a new audience.
2. **Strategic outreach:** Identify websites that could benefit from linking to your content. Reach out with a personalized pitch, explaining the value your content adds to their audience.
3. **Digital PR:** Create newsworthy content or conduct original research that journalists and bloggers will want to reference and link to.
4. **Broken link building:** Find broken links on relevant websites and suggest your content as a replacement.
5. **Resource page link building:** Find resource pages in your niche and suggest your content for inclusion.
6. **Social media engagement:** While not directly impacting SEO, active social media presence can increase content visibility and indirectly lead to more backlinks.
7. **Create linkable assets:** Develop infographics, tools, or comprehensive guides that others in your industry will want to reference.
8. **Participate in industry forums and discussions:** Contribute meaningfully to conversations in your field, including your website when relevant.
Remember, the quality of backlinks is more important than quantity. Focus on acquiring links from authoritative, relevant websites in your niche. Avoid any black-hat techniques or link schemes that could result in penalties from search engines.
## Adding your website to Google Search Console
Once you've optimized your website for SEO, you can add it to Google Search Console. Google Search Console is a free tool that helps you monitor and maintain your website's presence in Google search results.
You can use it to check your website's indexing status, submit sitemaps, and get insights into how Google sees your website.
The first thing you need to do is verify your website in Google Search Console. You can do this by adding a meta tag to your website's HTML or by uploading an HTML file to your website.
Once you've verified your website, you can submit your sitemap to Google Search Console. This will help Google find and index your website's pages faster.
Please submit your sitemap to Google Search Console by going to the `Sitemaps` section and adding the URL of your sitemap. The URL of your sitemap is `https://your-website.com/sitemap.xml`.
Of course, please replace `your-website.com` with your actual website URL.
## Content
When it comes to internal factors, **content is king**. Make sure your content is relevant, useful, and engaging. Make sure it's updated regularly and optimized for SEO.
<Callout title="What should you write about?">
Most importantly, you want to think about how your customers will search for the problem your SaaS is solving. For example, if you're building a project management tool, you might want to write about project management best practices, how to manage a remote team, or how to use your tool to improve productivity.
</Callout>
You can use the blog and documentation features in TurboStarter to create high-quality content that will help your website rank higher in search results - and help your customers find what they're looking for.
## Indexing and ranking take time
New websites can take a while to get indexed by search engines. It can take anywhere from a few days to a few weeks (in some cases, even months!) for your website to show up in search results. Be patient and keep updating your content and optimizing your website for search engines.
Also, you can edit `robots.ts` file to control which pages are indexed by search engines:
```tsx title="robots.ts"
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/api", "/dashboard", "/auth"],
},
sitemap: appConfig.url + "/sitemap.xml",
};
}
```
Remember, **SEO is an ongoing process.** Consistently apply these practices and adapt your strategy based on performance data and industry changes to improve your search engine visibility over time.

View File

@@ -0,0 +1,161 @@
---
title: Overview
description: Get started with web monitoring in TurboStarter.
url: /docs/web/monitoring/overview
---
# Overview
TurboStarter includes lightweight monitoring hooks so you can quickly answer: **what's failing**, **where it's failing**, and **who it's affecting**. Out of the box, the web app can report exceptions from both the client and the server, and it's designed to be easy to extend with your preferred provider.
## Capturing exceptions
Monitoring starts with capturing exceptions reliably in the places that matter most:
* **Client-side errors**: the Next.js App Router error boundary reports unexpected runtime errors so you get visibility without leaving users stuck on a broken screen.
* **Server-side errors**: API failures (for example, Hono errors in production) can be reported with a stable, anonymous distinct id so you can spot recurring issues and correlate them with sessions.
* **Manual reporting**: you can also report exceptions from your own `try/catch` blocks to add extra context around critical flows (payments, onboarding, imports, etc.).
<Tabs items={["Client-side", "Server-side"]}>
<Tab value="Client-side">
```tsx
"use client";
import { captureException } from "@turbostarter/monitoring-web";
export default function ExampleComponent() {
const handleClick = () => {
try {
/* some risky operation */
} catch (error) {
captureException(error);
}
};
return <button onClick={handleClick}>Trigger Exception</button>;
}
```
</Tab>
<Tab value="Server-side">
```ts
import { captureException } from "@turbostarter/monitoring-web/server";
try {
/* do something */
} catch (error) {
captureException(error);
}
```
</Tab>
</Tabs>
<Callout type="error" title="Ensure correct import!">
Make sure to use the correct import for the `captureException` function. We're using the same name for both client and server monitoring, but they are different functions. For server-side, just add `/server` to the import path (`@turbostarter/monitoring-web/server`).
<Tabs items={["Client-side", "Server-side"]}>
<Tab value="Client-side">
```tsx
import { captureException } from "@turbostarter/monitoring-web";
```
</Tab>
<Tab value="Server-side">
```tsx
// [!code word:server]
import { captureException } from "@turbostarter/monitoring-web/server";
```
</Tab>
</Tabs>
</Callout>
## Identifying users
Exception reports become dramatically more actionable once they're tied to a real user. TurboStarter automatically identifies signed-in users (based on the current auth session), which allows your monitoring provider to associate exceptions and sessions with a user profile.
If you want richer debugging, identify users with traits (like email, plan, or role) so you can filter and segment issues by the people impacted.
```tsx title="monitoring.tsx"
"use client";
import { useEffect } from "react";
import { identify } from "@turbostarter/monitoring-web";
import { authClient } from "~/lib/auth/client";
export const MonitoringProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const session = authClient.useSession();
useEffect(() => {
if (session.isPending) {
return;
}
identify(session.data?.user ?? null);
}, [session]);
return <>{children}</>;
};
```
<Callout title="Identifying users on the server" type="warn">
On the server, there are no dedicated identification helper. Most providers that support user-level tracking expect you to pass an identifier or traits directly within the `captureException` call (for example, as a `userId` or similar property), so make sure to check your specific provider's documentation for the recommended way to include user information.
</Callout>
## Providers
The starter implements multiple providers for managing monitoring. To learn more about each provider and how to configure them, see their respective sections:
<Cards>
<Card title="Sentry" href="/docs/web/monitoring/sentry" />
<Card title="PostHog" href="/docs/web/monitoring/posthog" />
</Cards>
Configuration and setup are handled for you via a unified API, making it easy to switch monitoring providers by just updating the exports. You can also add custom providers without disrupting any monitoring-related logic.
## Best practices
Below are some guidelines to keep monitoring useful, low-noise, and privacy-safe.
<Cards>
<Card title="Capture actionable errors" className="shadow-none">
Report unexpected exceptions and failed business-critical operations; avoid
logging “expected” states (validation errors, user cancellations, missing
optional data).
</Card>
<Card title="Add context" className="shadow-none">
Include what the user was doing (route/action), relevant IDs (request id,
order id), and a clear message so you can reproduce and triage quickly.
</Card>
<Card title="Identify users, but avoid PII" className="shadow-none">
Identify with stable IDs; only attach traits that are necessary for
debugging. Dont send secrets or sensitive fields (tokens, passwords, raw
payment details).
</Card>
<Card title="Deduplicate and rate-limit" className="shadow-none">
If a loop or retry can fire many times, guard your capture calls so you
dont spam your provider (and your budget).
</Card>
<Card title="Separate environments" className="shadow-none">
Keep dev/staging/prod isolated (separate projects or environment tags) so
production alerts stay meaningful.
</Card>
<Card title="Alert on symptoms that matter" className="shadow-none">
Set alerts for spikes in error rate, degraded performance, and failures in
critical flows (auth, checkout, billing webhooks), not for every single
exception.
</Card>
</Cards>
Application monitoring helps you track errors, exceptions, and performance issues for better app reliability. With multiple provider support, you can quickly spot and resolve problems.
Focus on actionable errors, useful context, and user privacy to get the most value from your monitoring.

View File

@@ -0,0 +1,153 @@
---
title: PostHog
description: Learn how to setup PostHog as your web monitoring provider.
url: /docs/web/monitoring/posthog
---
# PostHog
[PostHog](https://posthog.com/) is a comprehensive product analytics platform that includes error tracking, session replay, feature flags, and more. It helps developers identify, diagnose, and fix issues in their applications by capturing and reporting errors and exceptions in real time.
With features like automatic error reporting, stack trace visualization, and user/session context, PostHog provides deep insight into how your application is behaving in production so you can quickly resolve problems and improve reliability.
<Callout type="warn" title="Prerequisite: PostHog account">
To use PostHog as your monitoring provider, you need to have an account. You can create one [here](https://app.posthog.com/signup) or [self-host](https://posthog.com/docs/self-host) it.
</Callout>
<Callout type="info" title="You can also use it for analytics!">
PostHog is also one of pre-configured providers for [analytics](/docs/web/analytics/overview) in TurboStarter. You can learn more about it [here](/docs/web/analytics/configuration#posthog).
</Callout>
![PostHog banner](/images/docs/web/monitoring/posthog/banner.jpg)
## Configuration
PostHog integrates seamlessly with TurboStarter, enabling you to monitor application errors and performance from development to production. By configuring PostHog as your monitoring provider, you'll be able to detect, track, and resolve issues proactively, leading to a more stable and reliable app.
Follow the simple setup instructions below to get started with PostHog in your TurboStarter project.
<Steps>
<Step>
### Create a project
First, you need to create a [project](https://app.posthog.com/project/settings) in PostHog. You can do it directly from your [dashboard](https://app.posthog.com) by clicking on the *New Project* button.
</Step>
<Step>
### Activate PostHog as your monitoring provider
The monitoring provider to use is determined by the exports in `packages/monitoring/web` package. To activate PostHog as your monitoring provider, you need to update the exports in:
<Tabs items={["index.ts", "server.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
// [!code word:posthog]
export * from "./posthog";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:posthog]
export * from "./posthog/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:posthog]
export * from "./posthog/env";
```
</Tab>
</Tabs>
If you want to customize the provider, you can find its definition in `packages/monitoring/web/src/providers/posthog` directory.
</Step>
<Step>
### Set environment variables
Based on your [project settings](https://app.posthog.com/project/settings), fill the following environment variables in your `.env.local` file in `apps/web` directory and your deployment environment:
```dotenv title="apps/web/.env.local"
NEXT_PUBLIC_POSTHOG_KEY="your-posthog-api-key"
NEXT_PUBLIC_POSTHOG_HOST="https://us.i.posthog.com"
```
</Step>
</Steps>
That's it! You can now start your app and see the errors and exceptions in your [PostHog dashboard](https://app.posthog.com/project/error_tracking).
![PostHog error](/images/docs/web/monitoring/posthog/error.png)
Feel free to customize the configuration to your needs. For more information, please refer to the [PostHog documentation](https://posthog.com/docs/error-tracking/installation/nextjs).
<Cards>
<Card title="Error tracking" href="https://posthog.com/docs/error-tracking" description="posthog.com" />
<Card title="Next.js error tracking installation" href="https://posthog.com/docs/error-tracking/installation/nextjs" description="posthog.com" />
</Cards>
## Uploading source maps
**Source maps** are files that map your minified or transpiled code (such as the JavaScript code generated by frameworks like Next.js) back to your original source code (for example, TypeScript or unbundled JavaScript). When your app is running in production, the code is often bundled and minified to improve performance, which makes stack traces and error messages hard to read and debug.
<Callout>
With source maps enabled and uploaded to your monitoring provider (like PostHog), error reports include references to the original lines of your source code, not just the processed/minified output.
</Callout>
PostHog can automatically provide readable stack traces for errors using source maps. The `@posthog/nextjs-config` package handles source map generation and upload automatically during the build process.
To start using source maps, install the package `@posthog/nextjs-config` in `apps/web/package.json` as a dependency.
```bash
pnpm i @posthog/nextjs-config --filter web
```
Next, extend your app's Next.js options by adding `withPostHogConfig` into the `next.config.ts` file:
```ts title="apps/web/next.config.ts"
import { withPostHogConfig } from "@posthog/nextjs-config";
const config = {
/* existing Next.js configuration options */
};
export default withPostHogConfig(config, {
personalApiKey: process.env.POSTHOG_API_KEY,
envId: process.env.POSTHOG_ENV_ID,
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
sourcemaps: {
enabled: true, // Enable sourcemaps generation and upload
project: "my-application", // Optional: Project name, defaults to repository name
version: "1.0.0", // Optional: Release version, defaults to current git commit
deleteAfterUpload: true, // Delete sourcemaps after upload, defaults to true
},
});
```
Make sure you have set the following environment variables locally and in your deployment environment:
* `POSTHOG_ERROR_TRACKING_API_KEY` - Your [Personal API Key](https://app.posthog.com/settings/user-api-keys#variables) with write access on error tracking
* `POSTHOG_PROJECT_ID` - Project ID from [project settings](https://app.posthog.com/settings/environment#variables)
* `NEXT_PUBLIC_POSTHOG_HOST` - Your PostHog instance URL
<Callout type="warn" title="Verify source map generation, upload and injection">
Before proceeding, confirm that source maps are being generated by checking for `.js.map` files in your `dist` directory. These are the symbol sets that will be used to unminify stack traces in PostHog.
Next, confirm that source maps are successfully uploaded to PostHog by checking the [symbol sets](https://app.posthog.com/project/settings/symbol-sets) section in your project settings.
Finally, confirm that the served files are injected with the correct source map comment in production. You can do this by inspecting your deployed app in browser dev tools and looking for a comment like this at the end of your JavaScript bundles:
```js
//# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c
```
</Callout>
Once everything is set up, PostHog will provide you with detailed, easy-to-read error reports that link directly back to your original source code - even after your code has been bundled or minified. This makes diagnosing and fixing production issues much simpler.
<Cards>
<Card title="What are source maps?" href="https://web.dev/articles/source-maps" description="web.dev" />
<Card title="Upload source maps for Next.js" href="https://posthog.com/docs/error-tracking/upload-source-maps/nextjs" description="posthog.com" />
</Cards>

View File

@@ -0,0 +1,157 @@
---
title: Sentry
description: Learn how to setup Sentry as your web monitoring provider.
url: /docs/web/monitoring/sentry
---
# Sentry
[Sentry](https://sentry.io/) is a popular error monitoring and performance tracking platform. It helps developers identify, diagnose, and fix issues in their applications by capturing and reporting errors and exceptions in real time.
With features like automatic error reporting, stack trace visualization, and user/session context, Sentry provides deep insight into how your application is behaving in production so you can quickly resolve problems and improve reliability.
<Callout type="warn" title="Prerequisite: Sentry account">
To use Sentry as your monitoring provider, you need to have an account. You can create one [here](https://sentry.io/signup).
</Callout>
![Sentry banner](/images/docs/web/monitoring/sentry/banner.png)
## Configuration
Sentry integrates seamlessly with TurboStarter, enabling you to monitor application errors and performance from development to production. By configuring Sentry as your monitoring provider, youll be able to detect, track, and resolve issues proactively, leading to a more stable and reliable app.
Follow the simple setup instructions below to get started with Sentry in your TurboStarter project.
<Steps>
<Step>
### Create a project
First, you need to create a [project](https://docs.sentry.io/product/projects/) in Sentry. You can do it directly from your [dashboard](https://sentry.io/settings/account/projects/) by clicking on the *Create Project* button.
</Step>
<Step>
### Activate Sentry as your monitoring provider
The monitoring provider to use is determined by the exports in `packages/monitoring/web` package. To activate Sentry as your monitoring provider, you need to update the exports in:
<Tabs items={["index.ts", "server.ts", "env.ts"]}>
<Tab value="index.ts">
```ts
// [!code word:sentry]
export * from "./sentry";
```
</Tab>
<Tab value="server.ts">
```ts
// [!code word:sentry]
export * from "./sentry/server";
```
</Tab>
<Tab value="env.ts">
```ts
// [!code word:sentry]
export * from "./sentry/env";
```
</Tab>
</Tabs>
If you want to customize the provider, you can find its definition in `packages/monitoring/web/src/providers/sentry` directory.
</Step>
<Step>
### Set environment variables
Based on your [project settings](https://sentry.io/project/settings), fill the following environment variables in your `.env.local` file in `apps/web` directory and your deployment environment:
```dotenv title="apps/web/.env.local"
NEXT_PUBLIC_SENTRY_DSN="your-sentry-dsn"
NEXT_PUBLIC_PROJECT_ENVIRONMENT="your-project-environment"
```
</Step>
<Step>
### Apply instrumentation to your app
Install the package `@sentry/nextjs` in `apps/web/package.json` as a dependency.
```bash
pnpm i @sentry/nextjs --filter web
```
Next, extend your app's Next.js options by adding `withSentryConfig` into the `next.config.ts` file:
```ts title="apps/web/next.config.ts"
import { withSentryConfig } from "@sentry/nextjs";
const config = {
/* existing Next.js configuration options */
};
export default withSentryConfig(config, {
org: "your-sentry-org",
project: "your-sentry-project",
});
```
</Step>
</Steps>
That's it! You can now start your app and see the errors and exceptions in your [Sentry dashboard](https://sentry.io/settings/account/projects/).
![Sentry error](/images/docs/web/monitoring/sentry/error.jpg)
Feel free to customize the configuration to your needs. For more information, please refer to the [Sentry documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/).
<Cards>
<Card title="Quick Start" href="https://docs.sentry.io/platforms/javascript/guides/nextjs/" description="docs.sentry.io" />
<Card title="Manual Setup" href="https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/" description="docs.sentry.io" />
</Cards>
## Uploading source maps
**Source maps** are files that map your minified or transpiled code (such as the JavaScript code generated by frameworks like Next.js) back to your original source code (for example, TypeScript or unbundled JavaScript). When your app is running in production, the code is often bundled and minified to improve performance, which makes stack traces and error messages hard to read and debug.
<Callout>
With source maps enabled and uploaded to your monitoring provider (like Sentry), error reports include references to the original lines of your source code, not just the processed/minified output.
</Callout>
Sentry can automatically provide readable stack traces for errors using source maps, requiring a [Sentry auth token](https://docs.sentry.io/account/auth-tokens/).
Update your `next.config.ts` file with the following options:
```ts title="apps/web/next.config.ts"
import { withSentryConfig } from "@sentry/nextjs";
const config = {
/* existing Next.js configuration options */
};
export default withSentryConfig(config, {
org: "your-sentry-org",
project: "your-sentry-project",
// An auth token is required for uploading source maps.
authToken: process.env.SENTRY_AUTH_TOKEN, // [!code ++]
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true, // [!code ++]
});
```
Then, set the `SENTRY_AUTH_TOKEN` environment variable in your `.env.local` file in `apps/web` directory and your deployment environment:
```dotenv title="apps/web/.env.local"
SENTRY_AUTH_TOKEN="your-sentry-auth-token"
```
With these steps, your Sentry integration will give you clear, actionable error reports tied directly to your source code - even after bundling and minification. This makes it much easier to debug and resolve production issues.
Take a moment to test your setup and ensure source maps are correctly resolving stack traces in your [Sentry dashboard](https://sentry.io/settings/account/projects/). For deeper customization or additional troubleshooting, always consult the [official Sentry documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/sourcemaps/).
<Cards>
<Card title="What are source maps?" href="https://web.dev/articles/source-maps" description="web.dev" />
<Card title="Source maps" href="https://docs.sentry.io/platforms/javascript/guides/nextjs/sourcemaps/" description="docs.sentry.io" />
</Cards>

View File

@@ -0,0 +1,130 @@
---
title: Active organization
description: Set and switch the current organization context within your application.
url: /docs/web/organizations/active-organization
---
# Active organization
The active organization is tracked based on the **URL slug** and the **session state**. We made it **as simple as possible** to use, introducing our custom hooks and an abstraction to sync it both ways.
Below you can find more details about how to access the active organization across different contexts in your application.
You can customize the behavior to your needs—for example, to restrict users to at most one organization at a time.
## Server component
You have two separate ways to access the active organization of the currently logged-in user on the server:
* from the URL slug (organization-scoped routes)
* from the session (when no slug is present or you don't want to use it)
We recommend always using the URL slug when you're doing something inside an organization-scoped route. This keeps the URL as the source of truth and works seamlessly with SSR and caching.
```tsx title="page.tsx"
import { getOrganization } from "~/lib/auth/server";
export default async function Page({
params,
}: {
params: Promise<{
organization: string;
}>;
}) {
const organization = (await params).organization;
const activeOrganization = await getOrganization({ slug: organization });
return <>{activeOrganization?.name}</>;
}
```
Alternatively, you can use the session to access the active organization. This reads `session.activeOrganizationId` and resolves the organization by its stable ID.
```tsx title="page.tsx"
import { getOrganization, getSession } from "~/lib/auth/server";
export default async function Page() {
const { session } = await getSession();
const activeOrganization = await getOrganization({
id: session.activeOrganizationId,
});
return <>{activeOrganization?.name}</>;
}
```
Be aware that sometimes you might encounter synchronization issues between the URL slug and the session, for example when a user opens multiple tabs to different organizations. More on this in the [Edge cases](#edge-cases) section.
## Client component
On the client side, we designed a dedicated hook to access the active organization - `useActiveOrganization`. It's a simple wrapper around the API that returns the active organization based on the URL slug or the session. It also helps keep the state in sync with the server session.
```tsx title="client.tsx"
"use client";
import { useActiveOrganization } from "~/lib/hooks/use-active-organization";
export default function ClientComponent() {
const { activeOrganization, activeMember } = useActiveOrganization();
return (
<>
<p>{activeOrganization?.name}</p>
<p>{activeMember?.role}</p>
</>
);
}
```
Using the hook is recommended over direct API calls, as it will keep the state in sync with the server session.
It also returns the active member of the active organization, so you can access the user's role and other member-specific data.
## API route
To access the active organization data in an API route, you can read it from the session that is appended to the context when you use [authentication middleware](/docs/web/api/protected-routes).
```ts title="action/router.ts"
export const actionRouter = new Hono().post("/", enforceAuth, async (c) => {
const organizationId = c.var.user.activeOrganizationId;
const organization = await getOrganization({ id: organizationId });
return c.json(organization);
});
```
Although it's the simplest way, we recommend directly passing the `organizationId` together with the payload when you need to perform an action.
```ts title="action/router.ts"
export const actionRouter = new Hono().post(
"/",
enforceAuth,
validate(
"json",
z.object({
organizationId: z.string(),
/* rest of the payload */
}),
),
async (c) => {
const { organizationId, ...payload } = c.req.valid("json");
const organization = await getOrganization({ id: organizationId });
return c.json(await performAction(organization, payload));
},
);
```
This ensures that the action is performed on the correct organization, even if the user has multiple organizations open in different tabs. See [Edge cases](#edge-cases) for more details.
## Edge cases
* **Expected and harmless:** Short periods where the URL slug and server session differ can happen (for example, with multiple tabs or quick switching). The active tab always treats the slug as the source of truth and the session catches up.
* **Multiple tabs:** Each tab maintains its own org context from its slug. As you switch focus, the shared session updates; brief divergence is normal and safe.
* **Rapid switching/slow network:** During fast navigation or poor connectivity, you may momentarily see the previous org while the session updates. Show a small loading state; cancel in-flight requests tied to the old org.
* **Missing/invalid slug:** If the slug is missing or invalid, we fall back to the sessions `activeOrganizationId` or redirect to a safe default.
* **Access or permission changes:** If a user loses access to the org theyre viewing, the data is cleared from the session and the user is redirected to a valid organization or personal dashboard.
<Callout type="warn" title="Invalidation">
Whenever the active organization changes, the server session is updated and the client is redirected to the new organization scope.
All caches keyed by organization are invalidated to avoid leaking data between organizations.
</Callout>

View File

@@ -0,0 +1,93 @@
---
title: Data model
description: Entities and relationships for organizations and multi-tenancy.
url: /docs/web/organizations/data-model
---
# Data model
Our multi-tenant model is organized around the concept of an **organization**. An organization represents a single tenant and is the primary boundary for data isolation, access control, and routing.
Users can belong to multiple organizations through a membership. Invitations let organization admins onboard new members by email with a specific role.
<OrganizationsDbFlow />
## Entities
### Organization
The tenant. Stores human-friendly `name`, unique `slug` (used in URLs and lookups), optional `logo`, and optional `metadata` for extensibility (feature flags, billing context, UI preferences, etc.). `createdAt` provides auditability. The `slug` is globally unique to keep URLs stable and predictable.
### User
The identity of a person. Users are global and can join many organizations. Account-level fields (e.g., `name`, `email`, verification, avatar, security flags) live here.
<Callout type="warn">
A user's application-wide properties (like a global `role` or moderation flags) are distinct from their per-organization role.
</Callout>
### Member (Membership)
The join between a `user` and an `organization`. This is where multi-tenancy permissions are enforced. Each membership stores the `role` the user holds in that specific organization (for example, `member`, `admin`).
Memberships include timestamps for auditing and can be cascaded when a user or organization is removed.
### Invitation
Represents an invite to join an organization by `email` with an intended `role`. It includes `status` (e.g., pending, accepted, revoked), `expiresAt`, and `inviterId` for traceability.
On acceptance, an invitation creates a corresponding membership if one does not already exist.
## Relationships and constraints
<Accordions type="multiple">
<Accordion title="Many-to-many">
Users and organizations are related many-to-many through memberships. A user
can join multiple organizations; an organization has multiple members.
</Accordion>
<Accordion title="Uniqueness">
We keep `organization.slug` unique across the system to ensure
consistent routing and discoverability. Within a single organization, each
`userId` should only appear once in memberships; enforce this
at the application layer or with a composite unique index
`(organizationId, userId)`.
</Accordion>
<Accordion title="Cascades">
* Deleting an organization removes its dependent memberships and invitations.
* Deleting a user removes their memberships and invitations.
These cascades preserve referential integrity and prevent orphaned records.
</Accordion>
</Accordions>
## Tenancy and isolation
### Tenant separator
`organizationId` is the tenant key. All tenant-scoped data should either live under the organization or reference it directly. Every read/write path in the application should be constrained by the current `organizationId`.
### Query guardrails
Derive the active `organizationId` from authenticated context (session or URL slug → lookup → id). Apply `organizationId` filters at the repository/service layer to avoid crosstenant reads. Add composite indexes that include `organizationId` on frequently queried relations.
### Isolation level
All organizations share the same database and schema, separated by `organizationId`. This keeps operations simple and costeffective. If stricter isolation is needed, evolve toward schemapertenant or databasepertenant with care, as operational overhead increases.
<Callout title="Rename organizations">
The term "organizations" is used throughout the starter kit to identify a group of users. However, depending on your application's needs, you might want to represent these groups with a different name, such as "Teams" or "Workspaces."
If that's the case, we suggest retaining "organization" as the internal term within your codebase (to avoid the complexity of renaming it everywhere), while customizing the UI labels to your preferred terminology. To do this, simply update all user-facing instances of "Organization" in your interface to reflect the term that best fits your application.
</Callout>
## Lifecycle flows
* **Create organization**: Create an organization (with `name`, `slug`, optional `logo`/`metadata`) and immediately create a membership for the creator with an elevated role (commonly `owner`).
* **Invite member**:
1. Admin creates an invitation specifying `email` and intended `role`.
2. The invite is sent by email with an expiring token.
3. On acceptance, if the user exists they are added as a member; otherwise they register and then join.
4. Handle idempotency so repeated accepts dont duplicate memberships.
* **Leave or remove**: Members can leave an organization and admins can remove members. The policy that "at least one owner must remain" is enforced at the application layer.

View File

@@ -0,0 +1,92 @@
---
title: Invitations
description: Send, track, and accept organization invites.
url: /docs/web/organizations/invitations
---
# Invitations
You can invite teammates **by email** to join an organization straight from your organization settings.
Acceptance is frictionless: we verify the invite, create (or reuse) the membership with the intended role, and activate the organization in the user's session.
The implementation is based on the [Better Auth plugin](https://www.better-auth.com/docs/plugins/organization) and designed to drive engagement, minimize back-and-forth and keep admins in control.
![Invitations list](/images/docs/web/organizations/invitations/list.png)
## Model
As we can see inside our [data model](/docs/web/organizations/data-model), an invitation targets an `email`, carries the intended `role`, records the `inviterId`, and is scoped to an `organizationId`.
```ts
export const invitation = pgTable("invitation", {
id: text().primaryKey(),
organizationId: text()
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
email: text().notNull(),
role: text(),
status: text().default("pending").notNull(),
inviterId: text()
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: timestamp().defaultNow().notNull(),
expiresAt: timestamp().notNull(),
});
```
The invitations expire at `expiresAt` to keep links shortlived.
## Status
An invitation can be in one of three states:
* **Pending**: created/sent, awaiting acceptance.
* **Accepted**: verified; membership created or reused.
* **Rejected**: manually invalidated or removed via cascades.
<Callout>
Expiration is controlled by `expiresAt` (not a separate status). After this timestamp, the link is invalid and should be resent.
</Callout>
## Flow
1. Admin creates an invite with `email` and `role`. The `organizationId` is inferred from the context.
2. System generates a signed, single-use token bound to the invite and `expiresAt` and sends a CTA link.
3. Recipient opens the link; we verify the token and email.
4. On success, we proceed to acceptance.
## Onboarding
### Existing user
After verification, we create (or reuse) a membership with the invited role and set the active organization in the session.
![Join organization prompt](/images/docs/web/organizations/invitations/join.png)
### New user
We attach the invite context to signup; after registration, we create the membership and activate the organization - no detours required.
![Invitation disclaimer](/images/docs/web/organizations/invitations/sign-in-disclaimer.png)
You can fully customize the invitation flow to fit your organization's needs. For example, you can add extra onboarding steps, capture additional user information, or implement advanced verification logic as part of the invite process.
The system is designed to be extensible—tailor it to match your team's requirements and user experience preferences.
## Automatic invalidation
An invitation is automatically revoked in the following scenarios:
* **The user accepts the invitation:** Once accepted, the token becomes invalid.
* **The user changes their email address:** To prevent misuse, any changes to the associated email automatically invalidate the token.
* **The user deletes their account:** Invitations linked to a deleted account are revoked to maintain data integrity.
This ensures that invitations remain secure and aligned with the current state of user accounts.
## Invitation management
Admins of the organization and [super admins](/docs/web/admin/overview) can manage invitations via a dedicated section in the dashboard, where they can:
* View the status of all invitations (`pending`, `accepted`, `rejected`).
* Resend invitations who did not respond.
* Revoke invitations if they were sent to the wrong email or are no longer needed.
* Adjust the role of an invitation if not yet accepted

View File

@@ -0,0 +1,79 @@
---
title: Overview
description: Learn how to use organizations/teams/multi-tenancy in TurboStarter.
url: /docs/web/organizations/overview
---
# Overview
Organizations let you build teams and multi-tenant SaaS out of the box, which is a widely used pattern, especially in a [B2B](https://en.wikipedia.org/wiki/Business-to-business) apps. Users can create organizations, invite teammates, assign roles, and seamlessly switch between workspaces.
<Callout title="What is multi-tenancy?">
[Multi-tenancy](https://www.ibm.com/think/topics/multi-tenant) is a software architecture pattern where a single instance of an application serves multiple tenants, each with its own data and configuration.
</Callout>
The feature is mostly powered by the [Better Auth organization plugin](https://www.better-auth.com/docs/plugins/organization) and integrates with TurboStarter's API, routing, data layer, and UI components. This allows you to share most of the code between the web app, [mobile app](/docs/mobile/organizations/overview), and [extension](/docs/extension/organizations).
<ThemedImage light="/images/docs/web/organizations/multi-tenancy/light.png" dark="/images/docs/web/organizations/multi-tenancy/dark.png" alt="Architecture" width={1375} zoomable height={955} />
## Architecture
TurboStarter uses a pragmatic multi-tenant architecture:
* **Tenant context** lives in the session as the active organization ID (derived from the user's selection or defaults). Server handlers read this context to enforce scoping.
* **Data scoping** is performed via `organizationId` on tenant-owned tables and guard clauses in queries. Background tasks and API routes receive the same context.
* **Authorization** combines tenant scoping with role checks. We separate “can access this tenant?” from “can perform this action within the tenant?”.
* **Extensibility**: add new tenant-bound entities by including `organizationId` and using the provided helpers to read the active organization.
This keeps data isolated per organization while remaining simple to reason about and customize.
<Callout>
You can restrict who can create organizations, perform actions within it, and hook into
lifecycle events using our API.
Check dedicated [Data model](/docs/web/organizations/data-model), [RBAC](/docs/web/organizations/rbac) and [Invitations](/docs/web/organizations/invitations) sections or direct [Better Auth docs](https://www.better-auth.com/docs/plugins/organization) for more details.
</Callout>
## Concepts
To effectively use multi-tenancy in your app, we introduced a few core concepts that define how the whole system works:
| Concept | Description |
| ----------------------- | ----------------------------------------------------------------------------------------------- |
| **Organization** | A workspace that owns resources and settings, acting as an isolated tenant. |
| **Member** | A user assigned to an organization. |
| **Role** | Access level within an organization (see [RBAC](/docs/web/organizations/rbac)). |
| **Invitation** | Email request to join an organization (see [Invitations](/docs/web/organizations/invitations)). |
| **Active organization** | The currently selected organization in a user's session, used to scope data and permissions. |
These concepts provide the building blocks for flexible team management and secure, multi-tenant SaaS applications.
## Development data
In development, TurboStarter automatically [seeds](/docs/web/installation/commands#seeding-database) some example data when you [setup services](/docs/web/installation/commands#setting-up-services):
* One organization is created by default.
* All default roles are created and assigned within that organization.
* Sample invitations are generated so you can test the invite flow.
You can safely experiment with these sample organizations, roles, and invitations to understand multi-tenancy features - [reset](/docs/web/installation/commands#resetting-database) or [reseed](/docs/web/installation/commands#seeding-database) anytime to return to the default state.
The default credentials for demo users can be customized using the `SEED_EMAIL` and `SEED_PASSWORD` environment variables.
<Callout type="error" title="Never run in production">
The default development data and setup are intended for local development and
testing only. **Never** use these seeds or configurations in a production
environment - they are insecure and may expose sensitive functionality.
</Callout>
## Customization
You have flexibility to adapt organizations to fit your product. For example, you might rename labels (such as Organization to *Team* or *Workspace*), and update the UI copy accordingly.
You can adjust the available [roles and permissions](/docs/web/organizations/rbac) to suit your access model.
The [invitation flow](/docs/web/organizations/invitations) can be customized, including how verification, onboarding, or metadata capture work.
You may also want to introduce tenant-specific policies, like usage limits, feature flags, or billing rules.
Feel free to check how to configure all of these features in the dedicated sections below.

View File

@@ -0,0 +1,97 @@
---
title: RBAC (Roles & Permissions)
description: Manage roles, permissions, and access scopes.
url: /docs/web/organizations/rbac
---
# RBAC (Roles & Permissions)
Role-based access control (RBAC) lets you define who can do what in an organization.
<Callout title="New to RBAC?">
If you're new to the RBAC concept, a simple mental model is:
* Users belong to organizations.
* Users get roles.
* Roles map to permissions on resources.
</Callout>
In TurboStarter, we primarily rely on the [Better Auth plugin](https://www.better-auth.com/docs/plugins/organization) for the heavy lifting - roles, permissions, teams, and member management - while handling critical logic with our own code.
This provides a flexible access control system, letting you control user access based on their role in the organization. You can also define custom permissions per role.
<Callout title="Everything is configured out of the box!">
TurboStarter ships with the default RBAC system configured out of the box. This setup may be enough if you're not planning a very complex access control system, but you can also easily customize it to your needs.
It also includes [protecting routes](/docs/web/api/protected-routes) that users with specific roles can access by adding custom middlewares and disabling certain actions in the UI.
</Callout>
## Roles
Roles are named bundles of permissions. Keep them few and well-defined. By default, we have the following roles:
```ts
const MemberRole = {
MEMBER: "member",
ADMIN: "admin",
OWNER: "owner",
} as const;
```
A user can have multiple roles in an organization. For example, a user can be a member and an admin (if it makes sense for your application).
<Callout type="warn" title="Don't confuse organization admin with super admin">
The organization's `admin` role is **different** from the user's global `admin` role.
The organization `admin` governs permissions only inside the organization, whereas the global `admin` controls access to the [super admin dashboard](/docs/web/admin/overview).
</Callout>
To create additional roles with custom permissions, see the [official documentation](https://www.better-auth.com/docs/plugins/organization#create-access-control) for more details.
## Permissions
Permissions represent what actions a role can perform on which resources. To check if the current user has permission to perform an action, you can use the `hasPermission` function.
```ts
const canCreateProject = await authClient.organization.hasPermission({
permissions: {
project: ["create"],
},
});
```
Or, if you're performing the check on the server, you can use the `hasPermission` function from the `auth.api` object.
```ts
await auth.api.hasPermission({
headers: await headers(),
body: {
permissions: {
project: ["create"], // This must match the structure in your access control
},
},
});
```
Once your roles and permissions are defined, you can avoid server checks (e.g., to reduce API calls) by using the client-side `checkRolePermission` function.
```ts
const { activeMember } = useActiveOrganization();
const canUpdateProject = authClient.organization.checkRolePermission({
permission: {
project: ["update"],
},
role: activeMember.role,
});
```
We leverage the existing custom hook to retrieve the active member role within the [active organization](/docs/web/organizations/active-organization) context. That way, you can easily check whether a member has permission to perform an action without a server round trip.
<Callout type="warn">
This does not include any dynamic roles or permissions because everything runs synchronously on the client-side. Use the `hasPermission` APIs to include checks for dynamic roles and permissions.
</Callout>
If you need to add more granular permissions to existing roles, or create new ones, use the [`createAccessControl`](https://www.better-auth.com/docs/plugins/organization#custom-permissions) API.
For further customization - such as dynamic access control, lifecycle hooks, or team management - see the guidance in the [official documentation](https://www.better-auth.com/docs/plugins/organization).

View File

@@ -0,0 +1,221 @@
---
title: Supabase
description: Learn how to set up Supabase for your TurboStarter project.
url: /docs/web/recipes/supabase
---
# Supabase
[Supabase](https://supabase.com) is an open-source backend platform built on top of PostgreSQL that provides a managed database, storage, and other features out of the box.
You can adopt Supabase incrementally - start with just the pieces you need (for example, database only, or database + storage) and add more features over time. There's no requirement to integrate everything at once.
In this guide, we'll walk you through the process of setting up Supabase as a provider for your TurboStarter project. This could include using it as a [database](https://supabase.com/docs/guides/database), [storage](https://supabase.com/docs/guides/storage), [edge runtime for your API](https://supabase.com/docs/guides/functions) and more.
## Prerequisites
Before you start, make sure you have:
* **TurboStarter project** cloned locally with dependencies installed (you can use our [CLI](/docs/web/cli) to create a new project in seconds)
* **Supabase account** - you can create one at [supabase.com](https://supabase.com/sign-up)
* Basic familiarity with the core database docs:
* [Database overview](/docs/web/database/overview)
* [Migrations](/docs/web/database/migrations)
* [Database client](/docs/web/database/client)
<Steps>
<Step>
## Create a new Supabase project
1. Go to the [Supabase dashboard](https://supabase.com).
2. Create a **new project** (choose a strong database password and a region close to your users).
3. Supabase will automatically provision a **PostgreSQL database** for you.
![Create a new Supabase project](/images/docs/web/recipes/supabase/create-project.png)
Optionally, you can customize the **Security options** by choosing the **Only Connection String** option - it will opt out of autogenerating API for tables inside your database. It's not needed for TurboStarter setup, but of course you can still leverage it for your custom use-cases.
![Security options](/images/docs/web/recipes/supabase/security-options.png)
Once the project is ready, you can fetch the connection string.
</Step>
<Step>
## Get the database connection string
In the Supabase dashboard:
1. Open your project.
2. Click on the **Connect** button at the top.
3. Locate the **connection string** for your chosen ORM (it will be under the **ORMs** tab).
![Connect application](/images/docs/web/recipes/supabase/connect-app.png)
Copy this value - you'll use it as your `DATABASE_URL`.
<Callout title="Replace password placeholder" type="warn">
In your Supabase connection string, you can see a placeholder like `[YOUR-PASSWORD]`. Make sure to replace this with the actual password you set when creating your Supabase project.
</Callout>
</Step>
<Step>
## Configure environment variables
TurboStarter reads database connection settings from the **root** `.env.local` file and uses them inside the `@turbostarter/db` package.
Create (or update) the `.env.local` file in the **monorepo root**:
```dotenv title=".env.local"
DATABASE_URL="postgres://postgres.[YOUR-PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[aws-region].pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"
```
Replace:
* `YOUR-PROJECT-REF` with your Supabase project ref
* `YOUR-PASSWORD` with the database password you set when creating the project
* `aws-region` with the region shown in the Supabase connection string
<Callout>
These variables are validated in the `@turbostarter/db` package and used to create Drizzle client for your database.
</Callout>
For more background on how `DATABASE_URL` is used, see [Database overview](/docs/web/database/overview).
</Step>
<Step>
## Setup your Supabase database
With `DATABASE_URL` now pointing to Supabase, you can apply the existing TurboStarter schema to your Supabase database.
From the monorepo root, run:
```bash
pnpm with-env pnpm --filter @turbostarter/db db:migrate
```
This will:
* Use your Supabase `DATABASE_URL` from `.env.local`
* Run all pending SQL migrations from `packages/db/migrations`
* Create the full TurboStarter schema (users, billing, demo tables, etc.) in Supabase
If you're actively iterating on the schema, you can generate new migrations and apply them as described in [Migrations](/docs/web/database/migrations).
<Callout title="Seeding your database" type="info">
After running your migrations, you may want to seed your database with initial data (such as demo users or organizations). You can do this by running the following command:
```bash
pnpm with-env pnpm turbo db:seed
```
This will populate your Supabase database with some example data you can use to test your application.
</Callout>
</Step>
<Step>
## Use Supabase Storage as S3-compatible storage
TurboStarter's storage layer is designed to work seamlessly with **any S3-compatible provider**. In this section, we'll show how to use [Supabase Storage](/docs/web/storage/overview) as your application's file storage back-end.
Supabase Storage provides a simple, S3-compatible API and is a great choice if you're already using Supabase for your database.
### Create a storage bucket
1. In the Supabase dashboard, go to **Storage → Buckets**.
2. Click **Create bucket** (name it whatever you want, for example `avatars` or `uploads`).
3. Adjust settings based on your needs (e.g. limit the maximum file size, specify the allowed file types, etc.)
![Create a new bucket](/images/docs/web/recipes/supabase/create-bucket.png)
You can create multiple buckets (for documents, images, videos, etc.) if needed.
### Generate S3 access keys in Supabase dashboard
1. Go to **Storage → S3 → Access keys**.
2. Click **New access key**.
3. Give it a descriptive name and create the key.
4. Copy the **Access key ID** and **Secret access key** to use in your application.
![Generate S3 access keys](/images/docs/web/recipes/supabase/s3-keys.png)
### Configure S3 environment variables for Supabase Storage
In your weba application's `.env.local`, add (or update) the S3 configuration used by TurboStarter's storage layer:
```dotenv title=".env.local"
S3_REGION="us-east-1"
S3_BUCKET="avatars"
S3_ENDPOINT="https://[YOUR-PROJECT-REF].supabase.co/storage/v1/s3"
S3_ACCESS_KEY_ID="your-access-key-id"
S3_SECRET_ACCESS_KEY="your-secret-access-key"
```
These variables integrate directly with the storage configuration described in:
* [Storage overview](/docs/web/storage/overview)
* [Storage configuration](/docs/web/storage/configuration)
Once set, existing TurboStarter file upload flows (e.g. user avatars, organization logos) will use Supabase Storage via presigned URLs.
</Step>
<Step>
## Run your API on Supabase Edge Functions
As we're using a [Hono](https://hono.dev) as our API server, you can deploy it as a Supabase Edge Function so it runs close to your users.
At a high level:
1. Install the [Supabase CLI](https://supabase.com/docs/guides/cli) and initialize a Supabase project locally with `supabase init`.
2. Create a new [Edge Function](https://supabase.com/docs/guides/functions/quickstart) (for example `hono-backend`) with `supabase functions new hono-backend`.
3. Inside the generated function (for example `supabase/functions/hono-backend/index.ts`), set up a basic Hono app and export it via `Deno.serve(app.fetch)`:
```ts
import { Hono } from "jsr:@hono/hono";
// change this to your function name
const functionName = "hono-backend";
const app = new Hono().basePath(`/${functionName}`);
app.get("/hello", (c) => c.text("Hello from hono-server!"));
Deno.serve(app.fetch);
```
4. Run the function locally with `supabase start` and `supabase functions serve --no-verify-jwt`, then call it from your TurboStarter app using the local or deployed function URL.
5. When you're ready, deploy the function with `supabase functions deploy` (or `supabase functions deploy hono-backend`) and manage it using the Supabase dashboard, as described in the [Supabase Edge Functions docs](https://supabase.com/docs/guides/functions).
This is entirely optional, but it's a great fit for lightweight APIs, webhooks, and other serverless logic you want to run alongside your Supabase project.
</Step>
<Step>
## Explore additional Supabase features
Supabase is a full Postgres development platform, so beyond the database and storage pieces wired up above you can gradually add more features as your app grows ([see the Supabase homepage](https://supabase.com/) for an overview).
Some features that fit especially well with TurboStarter's design are:
* [Realtime](https://supabase.com/docs/guides/realtime) - built on [Postgres replication](https://www.postgresql.org/docs/current/runtime-config-replication.html), so you can stream changes from your existing TurboStarter tables (inserts, updates, deletes) into live UIs without changing how you manage schema or RLS. You still define tables and policies via `@turbostarter/db`, and opt into Realtime on top.
* [Vector](https://supabase.com/docs/guides/vector) - powered by the [pgvector](https://github.com/pgvector/pgvector) extension and stored in regular Postgres tables, making it easy to integrate semantic search or AI features while keeping everything in the same migrations and Drizzle models you already use in TurboStarter. We're using it extensively in our dedicated [AI Kit](/ai).
* [Cron](https://supabase.com/docs/guides/functions/cron) - enables you to schedule background jobs and periodic tasks with [pg\_cron](https://github.com/citusdata/pg_cron). You can define cron jobs for things like scheduled database cleanups, sending emails, report generation, or any recurring logic, all managed alongside your TurboStarter app with full Postgres integration.
Because these features are all layered on top of Postgres, you can introduce them incrementally and keep managing everything through your familiar workflow.
</Step>
<Step>
## Start the development server
With the database and other services configured to use Supabase, you can start TurboStarter as usual from the monorepo root:
```bash
pnpm dev
```
TurboStarter will now:
* Use **Supabase Postgres** as your database through `DATABASE_URL`
* Use **Supabase Storage** as your file storage through the S3-compatible endpoint
* Leverage **Supabase Edge Functions** (for example, with Hono) for your serverless backend
</Step>
</Steps>
That's it! You can now start building your application with Supabase as your main provider. Explore the [Supabase documentation](https://supabase.com/docs) for more features and best practices.

View File

@@ -0,0 +1,72 @@
---
title: Tech Stack
description: A detailed look at the technical details.
url: /docs/web/stack
---
# Tech Stack
## Turborepo
[Turborepo](https://turbo.build/) is a monorepo tool that helps you manage your project's dependencies and scripts. We chose a monorepo setup to make it easier to manage the structure of different features and enable code sharing between different packages.
<Card href="https://turbo.build/" title="Turborepo - Make Ship Happen" description="turbo.build" icon={<Turborepo />} />
## Next.js
[Next.js](https://nextjs.org) is one of the most popular [React](https://react.dev) frameworks that enables server-side rendering, static site generation, and more. We chose Next.js for its flexibility and ease of use. We're also using it to host our serverless API.
<Cards>
<Card href="https://react.dev" title="React" description="react.dev" icon={<React />} />
<Card href="https://nextjs.org" title="Next.js" description="nextjs.org" icon={<Next />} />
</Cards>
## Hono & React Query
[Hono](https://hono.dev) is a small, simple, and ultrafast web framework for the edge. It provides tools to help you build APIs and web applications faster. It includes an RPC client for making type-safe function calls from the frontend. We use Hono to build our serverless API endpoints.
To make data fetching and caching from our API easy and reliable, we pair Hono with [React Query](https://tanstack.com/query/latest). It helps manage asynchronous data, caching, and state synchronization between the client and backend, delivering a fast and seamless UX.
<Cards>
<Card href="https://hono.dev" title="Hono" description="hono.dev" icon={<Hono />} />
<Card
href="https://tanstack.com/query/latest"
title="React Query"
description="tanstack.com"
icon={
<img src="/images/icons/tanstack.png" alt="" width={32} height={32} />
}
/>
</Cards>
## Better Auth
[Better Auth](https://www.better-auth.com) is a modern authentication library for fullstack applications. It provides ready-to-use snippets for features like email/password login, magic links, OAuth providers, and more. We use Better Auth to handle all authentication flows in our application.
<Card href="https://www.better-auth.com" title="Better Auth" description="better-auth.com" icon={<BetterAuth />} />
## Tailwind CSS
[Tailwind CSS](https://tailwindcss.com) is a utility-first CSS framework that helps you build custom designs without writing any CSS. We also use [Radix UI](https://radix-ui.com) for our headless components library and [shadcn/ui](https://ui.shadcn.com), which enables you to generate pre-designed components with a single command.
<Cards className="grid-cols-2 sm:grid-cols-3">
<Card href="https://tailwindcss.com" title="Tailwind CSS" description="tailwindcss.com" icon={<Tailwind />} />
<Card href="https://radix-ui.com" title="Radix UI" description="radix-ui.com" icon={<Radix />} />
<Card href="https://ui.shadcn.com" title="shadcn/ui" description="ui.shadcn.com" icon={<Shadcn />} />
</Cards>
## Drizzle
[Drizzle](https://orm.drizzle.team/) is a super fast [ORM](https://orm.drizzle.team/docs/overview) (Object-Relational Mapping) tool for databases. It helps manage databases, generate TypeScript types from your schema, and run queries in a fully type-safe way.
We use [PostgreSQL](https://www.postgresql.org) as our default database, but thanks to Drizzle's flexibility, you can easily switch to MySQL, SQLite or any [other supported database](https://orm.drizzle.team/docs/connect-overview) by updating a few configuration lines.
<Cards>
<Card href="https://orm.drizzle.team/" title="Drizzle" description="orm.drizzle.team" icon={<Drizzle />} />
<Card href="https://www.postgresql.org" title="PostgreSQL" description="postgresql.org" icon={<Postgres />} />
</Cards>

View File

@@ -0,0 +1,31 @@
---
title: Configuration
description: Learn how to configure storage in TurboStarter.
url: /docs/web/storage/configuration
---
# Configuration
Currently, TurboStarter supports all S3-compatible storage providers, including [AWS S3](https://aws.amazon.com/s3/), [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces), [Cloudflare R2](https://www.cloudflare.com/products/r2/), and [Supabase Storage](https://supabase.com/storage).
For a concrete example using Supabase Storage as an S3-compatible provider, see the [Supabase recipe](/docs/web/recipes/supabase#use-supabase-storage-as-s3-compatible-storage).
The setup process is straightforward - you just need to configure a few environment variables in both your local environment and hosting provider:
```dotenv
S3_REGION=
S3_BUCKET=
S3_ENDPOINT=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
```
Let's break down each required variable:
* `S3_REGION`: The [AWS region](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/) where your storage is located - defaults to `us-east-1`
* `S3_BUCKET`: The default name of your storage bucket - you can pass different for each request
* `S3_ENDPOINT`: The S3 [endpoint URL](https://docs.aws.amazon.com/general/latest/gr/s3.html) for your storage provider - defaults to `https://s3.amazonaws.com`
* `S3_ACCESS_KEY_ID`: Your storage provider's [access key ID](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html)
* `S3_SECRET_ACCESS_KEY`: Your storage provider's [secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html)
You can learn more about S3 service configuration in the [official AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) or your specific storage provider's documentation.

View File

@@ -0,0 +1,178 @@
---
title: Managing files
description: Learn how to manage files in TurboStarter.
url: /docs/web/storage/managing-files
---
# Managing files
Before you start managing files, make sure you have [configured storage](/docs/web/storage/configuration).
## Permissions
Most S3-compatible storage providers allow you to configure bucket permissions and access policies. It's crucial to properly set these up to secure your files and control who can access them.
Here are some key security recommendations:
* Keep your bucket private by default
* Use IAM roles and policies to manage access
* Enable server-side encryption for sensitive data
* Configure CORS settings appropriately for client-side uploads
* Regularly audit bucket permissions and access logs
Making your bucket public is strongly discouraged as it can expose sensitive data and lead to unauthorized access and unexpected costs from bandwidth usage.
For detailed guidance on configuring bucket policies and permissions, refer to your storage provider's documentation:
* [AWS S3 Security Documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-policy-language-overview.html)
* [DigitalOcean Spaces Security](https://docs.digitalocean.com/products/spaces/how-to/manage-access/)
* [Cloudflare R2 Security](https://developers.cloudflare.com/r2/api/s3/tokens/)
* [Supabase Storage Security](https://supabase.com/docs/guides/storage/security/access-control)
## Uploading files
As explained in the [overview](/docs/web/storage/overview), TurboStarter uses presigned URLs to upload files to your storage provider.
We prepared a special endpoint to generate presigned URLs for your uploads to use in your client-side code.
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/upload",
enforceAuth,
validate("query", getObjectUrlSchema),
async (c) => c.json(await getUploadUrl(c.req.valid("query"))),
);
```
<Callout title="Expiration time" type="warn">
The signed URL is only valid for a limited time and will work for anyone who has access to it during that period. Make sure to handle the URL securely and avoid exposing it to unauthorized users.
</Callout>
Then, you can use it to upload files to the generated presigned URL from your frontend code:
```tsx title="upload.tsx"
const upload = useMutation({
mutationFn: async (data: { file?: File }) => {
const extension = data.file?.type.split("/").pop();
const path = `files/${crypto.randomUUID()}.${extension}`;
const { url: uploadUrl } = await handle(api.storage.upload.$get)({
query: { path },
});
const response = await fetch(uploadUrl, {
method: "PUT",
body: data.file,
headers: {
"Content-Type": data.file?.type ?? "",
},
});
if (!response.ok) {
throw new Error("Failed to upload file!");
}
},
onError: (error) => {
toast.error(error.message});
},
onSuccess: async ({ publicUrl, oldImage }, _b, context) => {
toast.success("File uploaded!");
},
});
```
The code above demonstrates how to implement file uploads in your application:
1. First, we have a server-side endpoint (`storageRouter`) that generates presigned URLs for uploads. This endpoint:
* [Requires authentication](/docs/web/api/protected-routes) via `enforceAuth`
* Validates the request parameters using `validate`
* Returns a presigned URL for uploading
2. Then, in the frontend code (`upload.tsx`), we use React Query's `useMutation` hook to handle the upload process:
* Requests a presigned URL from the server
* Uploads the file directly to the storage provider using the presigned URL
* Handles success and error cases with toast notifications
This approach ensures secure file uploads while avoiding server bandwidth costs and function timeout issues.
### Public uploads
Although **it's not recommended** to use public uploads in production, you can use the same endpoint to generate presigned URLs for public uploads:
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/upload",
validate("query", getObjectUrlSchema),
async (c) => c.json(await getUploadUrl(c.req.valid("query"))),
);
```
Just remove the `enforceAuth` middleware from the endpoint and keep rest of the logic the same.
## Displaying files
We provide dedicated endpoints for retrieving signed URLs specifically for displaying files. These URLs are time-limited to maintain security, so they cannot be used for permanent storage or long-term access:
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/signed",
enforceAuth,
validate("query", getObjectUrlSchema),
async (c) => c.json(await getSignedUrl(c.req.valid("query"))),
);
```
This endpoint is perfect for displaying files that should only be accessible to authorized users for a limited time.
### Public files
For displaying files publicly (without authorization and time limitations), you can use the `/public` endpoint:
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/public",
validate("query", getObjectUrlSchema),
async (c) => c.json(await getPublicUrl(c.req.valid("query"))),
);
```
This endpoint generates a public URL for the file that you can use to display in your application. Please ensure that your bucket policy allows public access to the files and verify that you're not exposing any sensitive information.
## Deleting files
Deleting files works almost the same way as uploading files. You just need to generate a presigned URL for deletion and then use it to remove the file:
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/delete",
validate("query", getObjectUrlSchema),
async (c) => c.json(await getDeleteUrl(c.req.valid("query"))),
);
```
Then, in the frontend code, we use React Query's `useMutation` hook to handle the deletion process:
```tsx title="delete.tsx"
const remove = useMutation({
mutationFn: async () => {
const path = file.split("/").pop();
if (!path) return;
const { url: deleteUrl } = await handle(api.storage.delete.$get)({
query: { path: `files/${path}` },
});
await fetch(deleteUrl, {
method: "DELETE",
});
},
onError: (error) => {
toast.error(error.message);
},
onSuccess: () => {
toast.success("File removed!");
},
});
```
Now that you understand how to manage files in TurboStarter, it's time to build something awesome! Try creating a file upload component, building a photo gallery, or implementing a document management system.

View File

@@ -0,0 +1,34 @@
---
title: Overview
description: Get started with storage in TurboStarter.
url: /docs/web/storage/overview
---
# Overview
With TurboStarter, you can easily upload and manage files (images, videos, documents, and more) in your application.
Currently, all S3-compatible storage providers are supported, including [AWS S3](https://aws.amazon.com/s3/), [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces), [Cloudflare R2](https://www.cloudflare.com/products/r2/), [Supabase Storage](https://supabase.com/storage), and others.
If you're using Supabase, you can follow the [Supabase recipe](/docs/web/recipes/supabase#use-supabase-storage-as-s3-compatible-storage) for a concrete example of configuring Supabase Storage as your S3-compatible backend.
## Uploading files
The most common approach to uploading files is to use client-side uploads. With client-side uploads, you avoid paying ingress/egress fees for transferring file binary data through your server.
Additionally, most hosting platforms like [Vercel](https://vercel.com/docs/functions/runtimes#size-limits) or [Netlify](https://answers.netlify.com/t/what-is-the-maximum-file-size-upload-limit-in-a-netlify-form-submission/108419) have limitations on file size and maximum serverless function execution time.
That's why TurboStarter utilizes the **presigned URLs** feature of storage providers to upload files. Instead of sending files to the serverless function, the client requests a time-limited presigned URL from the serverless function and then uploads the file directly to the storage provider.
<ThemedImage alt="Client side uploads" light="/images/docs/web/storage/light.png" dark="/images/docs/web/storage/dark.png" width={1294} height={654} zoomable />
1. Client **requests** a presigned URL from the serverless function.
2. Server parses the request, validates the payload, optionally saves the metadata, and **returns the presigned URL** to the client.
3. Client **uploads the file** to the presigned URL within the expiration time.
4. (Optional) Once the file is uploaded, the serverless function is notified about the upload event, and the file metadata is saved to the database.
<Callout>
This approach ensures that credentials remain secure, handles authorization and authentication properly, and avoids the limitations of serverless platforms.
</Callout>
The configuration and use of storage is straightforward and simple. We'll explore this in more detail in the following sections.

View File

@@ -0,0 +1,15 @@
---
title: E2E tests
description: Simulate real user scenarios across the entire stack with automated end-to-end test tools and examples.
url: /docs/web/tests/e2e
---
# E2E tests
<Callout title="E2E testing is coming soon">
End-to-end (E2E) tests will be available soon, allowing you to automate testing of real user flows and interactions across your application.
Stay tuned for updates as we roll out robust E2E testing resources and examples.
[See roadmap](https://github.com/orgs/turbostarter/projects/1)
</Callout>

View File

@@ -0,0 +1,136 @@
---
title: Unit tests
description: Write and run fast unit tests for individual functions and components with instant feedback.
url: /docs/web/tests/unit
---
# Unit tests
Unit tests are a type of automated test where individual units or components are tested. The "unit" in "unit test" refers to the smallest testable parts of an application. These tests are designed to verify that each unit of code performs as expected.
TurboStarter uses [Vitest](https://vitest.dev) as the unit testing framework. It's a blazing-fast test runner built on top of [Vite](https://vitejs.dev), designed for modern JavaScript and TypeScript projects.
<Callout title="Why Vitest?">
If you've used [Jest](https://jestjs.io) before, you already know Vitest - it shares the same API. But Vitest is built for speed: native TypeScript support without transpilation, parallel test execution, and a smart watch mode that only re-runs tests affected by your changes.
It comes with everything you need out of the box - code coverage, snapshot testing, mocking, and a slick UI for debugging. Fast feedback, zero configuration.
</Callout>
## Why write unit tests?
Unit tests give you **fast, focused feedback** on small pieces of your code - individual functions, hooks, or components. Instead of debugging an entire page or flow, you can verify just the logic you care about in isolation.
They also act as **living documentation**: a good test tells you how a function is supposed to behave, which edge cases are important, and what assumptions the code makes. This makes it much easier to safely refactor or extend features later.
In TurboStarter, unit tests are designed to be **cheap and quick to run**, so you can keep Vitest running in watch mode while you code. Every change you make is immediately checked, helping you catch regressions before they ever reach integration or endtoend tests.
## Configuration
TurboStarter configures Vitest to be **as simple as possible**, while still taking advantage of [Turborepo's caching](https://turborepo.com/docs/crafting-your-repository/caching) and Vitest's [Test Projects](https://vitest.dev/guide/projects).
```ts title="vitest.config.ts"
import { mergeConfig } from "vitest/config";
import baseConfig from "@turbostarter/vitest-config/base";
export default mergeConfig(baseConfig, {
test: {
/* your extended test configuration here */
},
});
```
* **Per-package tests**: each package that has unit tests defines its own `test` script. This keeps the configuration close to the code and makes it easy to add tests to any workspace.
* **Turbo tasks for CI**: the root `test` task (`pnpm test`) uses `turbo run test` to execute all package-level test scripts with smart caching, which is ideal for CI pipelines where you want to avoid re-running unchanged tests.
* **Vitest Test Projects for local dev**: a root Vitest configuration uses [Test Projects](https://vitest.dev/guide/projects) to run all unit test suites from a single command, which is perfect for local development when you want fast feedback across the whole monorepo.
This **hybrid setup** combines Turborepo and Vitest Projects in a way that fits TurboStarter's principles: cached, package-aware runs in CI, and a single, unified Vitest entry point for local development.
You can read more about this setup in the official documentation guides listed below.
<Cards>
<Card title="Vitest" description="turborepo.com" href="https://turborepo.com/docs/guides/tools/vitest" />
<Card title="Test Projects" description="vitest.dev" href="https://vitest.dev/guide/projects" />
</Cards>
## Running tests
There are a few different ways to run unit tests, depending on what you're doing:
* **CI / full test run** - at the root of the repo:
```bash
pnpm test
```
This runs `turbo run test`, which executes all `test` scripts in packages that define them, with Turborepo handling caching so unchanged packages are skipped. This is what you should use in your CI/CD pipeline.
* **One-off local run with Vitest Projects**:
```bash
pnpm test:projects
```
This uses Vitest [Test Projects](https://vitest.dev/guide/projects) to run all configured unit test suites from a single command, which is great when you want to quickly validate the whole monorepo locally.
* **Watch mode during development**:
```bash
pnpm test:projects:watch
```
This starts Vitest in watch mode across all Test Projects. As you edit files, only the affected tests are re-run, giving you fast feedback while you work.
## Code coverage
Unit test coverage helps you understand **how much** of your code is being tested. While it can't guarantee bug-free code, it shines a light on untested paths that could hide issues or regressions.
To generate a code coverage report for all unit tests, run:
```bash
pnpm turbo test:coverage
```
This command runs the coverage task across all relevant packages (using Turborepo) and collects the results into a single coverage output.
To open the coverage report in your browser:
```bash
pnpm turbo test:coverage:view
```
This will build the HTML report and launch it using your default browser, so you can explore which files and branches are covered.
<Callout title="Uploading coverage as an artifact">
You can also store the generated coverage report as a [GitHub Actions artifact](https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts) during your CI/CD pipeline, just add the following steps to your workflow job:
```yaml title=".github/workflows/ci.yml"
# your workflow job configuration here
- name: 📊 Generate coverage
run: pnpm turbo test:coverage
- name: 🗃️ Archive coverage report
uses: actions/upload-artifact@v5
with:
name: coverage-${{ github.sha }}
path: tooling/vitest/coverage/report
```
This will generate a test coverage report and upload it as an artifact, so you can access it from GitHub Actions tab for later inspection.
</Callout>
A high coverage percentage means your tests execute most lines and branches - but the quality and relevance of your tests matter more than the raw number. Use coverage reports to spot gaps and guide improvements, not as the sole metric of test health.
![Code coverage](/images/docs/code-coverage.png)
## Best practices
Unit tests should work **for you**, not the other way around. Focus on writing tests that make it easier to change code with confidence, not on satisfying arbitrary rules or reaching a magic number in a dashboard.
Code coverage is a **useful metric**, but it **SHOULD NOT** be the goal. It's better to have a smaller set of highvalue tests that cover critical paths and edge cases than a huge suite of fragile tests that are hard to maintain.
When in doubt, ask: *“Does this test give **me** confidence that I can change this code without breaking users?”* If the answer is no, refactor or remove it.
Finally, keep unit tests focused on **small, isolated pieces of logic**. More advanced flows — like multi-step user journeys, cross-service interactions, or full-page behavior — are better covered by [end-to-end (E2E) tests](/docs/web/tests/e2e), where you can verify the system as a whole.

View File

@@ -0,0 +1,25 @@
---
title: Billing
description: Find answers to common billing issues.
url: /docs/web/troubleshooting/billing
---
# Billing
## Checkout can't be created
This happen in the following cases:
1. The environment variables are not set correctly. Please make sure you have set the environment variables corresponding to your billing provider in `.env.local` if locally - or in your hosting provider's dashboard if in production
2. The price IDs used are incorrect. Make sure to use the exact price IDs as they are in the payment provider's dashboard.
[Read more about billing configuration](/docs/web/billing/configuration)
## Database is not updated after subscribing to a plan
This may happen if the webhook is not set up correctly. Please make sure you have set up the webhook in the payment provider's dashboard and that the URL is correct.
If working locally, make sure that:
1. If using Stripe, that the Stripe CLI or configured proxy is up and running ([see the Stripe documentation for more information](/docs/web/billing/stripe#create-a-webhook))
2. If using Lemon Squeezy, that the webhook set in Lemon Squeezy is correct and that the server is running. Additionally, make sure the proxy is set up correctly if you are testing locally ([see the Lemon Squeezy documentation for more information](/docs/web/billing/lemon-squeezy#create-a-webhook)).

View File

@@ -0,0 +1,45 @@
---
title: Deployment
description: Find answers to common web deployment issues.
url: /docs/web/troubleshooting/deployment
---
# Deployment
## Deployment build fails
This is most likely an issue related to the environment variables not being set correctly in the deployment environment. Please analyse the logs of the deployment provider to see what is the issue.
The kit is very defensive about incorrect environment variables, and will throw an error if any of the required environment variables are not set. In this way - the build will fail if the environment variables are not set correctly - instead of deploying a broken application.
Check our guides for the most popular hosting providers for more information on how to deploy your TurboStarter project correctly:
<Cards>
<Card title="Vercel" description="Deploy your TurboStarter web app to Vercel platform." href="/docs/web/deployment/vercel" />
<Card title="Netlify" description="Deploy your TurboStarter web app to Netlify platform." href="/docs/web/deployment/netlify" />
<Card title="Render" description="Deploy your TurboStarter web app to Render platform." href="/docs/web/deployment/render" />
<Card title="Railway" description="Deploy your TurboStarter web app to Railway platform." href="/docs/web/deployment/railway" />
<Card title="AWS Amplify" description="Deploy your TurboStarter web app to AWS Amplify platform." href="/docs/web/deployment/amplify" />
<Card title="Docker" description="Containerize your TurboStarter web app using Docker." href="/docs/web/deployment/docker" />
<Card title="Fly.io" description="Deploy your TurboStarter web app to Fly.io platform." href="/docs/web/deployment/fly" />
</Cards>
## What should I set as a URL before my first deployment?
That's very good question! For the first deployment you can set any URL, and then, after you (or your provider) assign a domain name, you can change it to the correct one. There's nothing wrong with redeploying your project multiple times.
## Sign in with OAuth provider doesn't work
This is most likely a settings issues in the provider's settings. To troubleshoot this issue, follow these steps:
1. **Verify provider settings**: Ensure that the OAuth provider's settings are correctly configured. Check that the client ID, client secret, and redirect URI are accurate and match the values in your application.
2. **Check environment variables**: Confirm that the environment variables for the OAuth provider are set correctly in your application production environment.
3. **Validate callback URLs**: Ensure that the callback URLs for each provider are set correctly and match the URLs in your application. This is crucial for the OAuth flow to work correctly.
Please read [Better Auth documentation](https://www.better-auth.com/docs/concepts/oauth) for more information on how to set up third-party providers.

View File

@@ -0,0 +1,44 @@
---
title: Emails
description: Find answers to common emails issues.
url: /docs/web/troubleshooting/emails
---
# Emails
## I want to use a different email provider
Of course! You can use any email provider that you want. All you need to do is to implement the `EmailProviderStrategy` and export it in your `index.ts` file.
[Read more about sending emails](/docs/web/emails/sending)
## My emails are landing in the spam folder
Emails landing in spam folders is a common issue. Here are key steps to improve deliverability:
1. **Configure proper domain setup**:
* Use a dedicated subdomain for sending emails (e.g., mail.yourdomain.com)
* Ensure [reverse DNS (PTR) records](https://www.cloudflare.com/learning/dns/dns-records/dns-ptr-record/) are properly configured
* Warm up your sending domain gradually
2. **Implement authentication protocols**:
* Set up [SPF records](https://www.cloudflare.com/learning/dns/dns-records/dns-spf-record/) to specify authorized sending servers
* Enable [DKIM signing](https://www.cloudflare.com/learning/dns/dns-records/dns-dkim-record/) to verify email authenticity
* Configure [DMARC policies](https://www.cloudflare.com/learning/dns/dns-records/dns-dmarc-record/) to prevent spoofing
3. **Follow deliverability best practices**:
* Include clear unsubscribe mechanisms in all marketing communications
* Personalize content appropriately
* Avoid excessive promotional language and spam triggers
* Maintain consistent HTML formatting and styling
* Only include links to verified domains
* Keep a regular sending schedule
* Clean your email lists regularly
* Use double opt-in for new subscribers
4. **Monitor and optimize**:
* Track key metrics like delivery rates, opens, and bounces
* Monitor spam complaint rates
* Review email authentication reports
* Test emails across different clients and devices
* Adjust sending practices based on performance data

View File

@@ -0,0 +1,99 @@
---
title: Installation
description: Find answers to common web installation issues.
url: /docs/web/troubleshooting/installation
---
# Installation
## Cannot clone the repository
Issues related to cloning the repository are usually related to a Git misconfiguration in your local machine. The commands displayed in this guide using SSH: these will work only if you have setup your SSH keys in Github.
If you run into issues, [please make sure you follow this guide to set up your SSH key in Github.](https://docs.github.com/en/authentication/connecting-to-github-with-ssh)
If this also fails, please use HTTPS instead. You will be able to see the commands in the repository's Github page under the "Clone" dropdown.
Please also make sure that the account that accepted the invite to TurboStarter, and the locally connected account are the same.
## My environment variables from `.env.local` file are not being loaded
Make sure you are running the `pnpm dev` command from the root directory of your project (where the `pnpm-workspace.yaml` file is located)
Also, ensure that the `.env.local` files are present in the apps that need them. For example, the `.env` file should be present in the `apps/web` directory for the web app.
<Callout>
TurboStarter uses the `dotenv-cli` to load environment variables from a `.env` files. The `dotenv-cli` is automatically used when running the `pnpm dev` command from the root directory.
</Callout>
## Next.js server doesn't start
This may happen due to some issues in the packages. Try to clean the workspace using the following command:
```bash
pnpm clean
```
Then, reinstall the dependencies:
```bash
pnpm i
```
You can now retry running the dev server.
## Local database doesn't start
If you cannot run the local database container, it's likely you have not started [Docker](https://docs.docker.com/get-docker/) locally. Our local database requires Docker to be installed and running.
Please make sure you have installed Docker (or compatible software such as [Colima](https://github.com/abiosoft/colima), [Orbstack](https://github.com/orbstack/orbstack)) and that is running on your local machine.
Also, make sure that you have enough [memory and CPU allocated](https://docs.docker.com/engine/containers/resource_constraints/) to your Docker instance.
## I don't see my translations
If you don't see your translations appearing in the application, there are a few common causes:
1. Check that your translation `.json` files are properly formatted and located in the correct directory
2. Verify that the language codes in your configuration match your translation files
3. Enable debug mode (`debug: true`) in your i18next configuration to see detailed logs
[Read more about configuration for translations](/docs/web/internationalization/configuration)
## "Module not found" error
This issue is mostly related to either dependency installed in the wrong package or issues with the file system.
The most common cause is incorrect dependency installation. Here's how to fix it:
1. Clean the workspace:
```bash
pnpm clean
```
2. Reinstall the dependencies:
```bash
pnpm i
```
If you're adding new dependencies, make sure to install them in the correct package:
```bash
# For main app dependencies
pnpm install --filter web my-package
# For a specific package
pnpm install --filter @turbostarter/ui my-package
```
If the issue persists, please check the file system for any issues.
### Windows OneDrive
OneDrive can cause file system issues with Node.js projects due to its file syncing behavior. If you're using Windows with OneDrive, you have two options to resolve this:
1. Move your project to a location outside of OneDrive-synced folders (recommended)
2. Disable OneDrive sync specifically for your development folder
This prevents file watching and symlink issues that can occur when OneDrive tries to sync Node.js project files.