# Docs
URL: /docs/apps/docs
Docs powered-by Fumadocs. Manage content with MDX.
[ship.pluv.io](https://ship.pluv.io) comes with a prebuilt docs powered by [Fumadocs](https://fumadocs.com/). Providing docs for your app can be a great way to help your users get started with your app.
## Writing content
The content for the docs are powered by [Content Collections](https://content-collections.dev/) and [MDX](https://mdxjs.com/). To write or edit content, you will need to update the content in the `/content/docs` directory. This directory uses file-based routing to determine the URL of the page:
* `index.mdx` files will map to a `/` URL path
* e.g. if you have `/content/docs/my-docs/index.mdx`, the URL will be `/docs/my-docs`
* `[name].mdx` files will map to a `/[name]` URL path
* e.g. if you have `/content/docs/my-docs/my-page.mdx`, the URL will be `/docs/my-docs/my-page`
* Folders will create a sidebar group. So if you have `/content/docs/my-docs/my-page/index.mdx`, the URL will be `/docs/my-docs/my-page` and the page will be grouped under the "My Docs" sidebar group (assuming that "My Docs" is the title you gave the group).
For any page, you can provide a `title` and `description` in the frontmatter. These will be used to display the title and description of the page in the sidebar and on the page itself.
You can also provide a `meta.json` file in the same directory to explicitly specify the order of the pages in the sidebar.
```json title="meta.json"
{
"pages": ["my-page", "my-other-page"]
}
```
To view the full fumadocs documentation on routing, see [their routing documentation](https://fumadocs.dev/docs/ui/page-conventions).
### Adding custom components
[MDX](https://mdxjs.com/) allows you to use custom React components in your content. To add your own custom components, simply append your component to the `useMDXComponents` function in `/src/mdx-components.tsx`.
```tsx title="src/mdx-components.tsx"
import { MyComponent } from "@/components/my-component";
import type { MDXComponents } from "mdx/types";
export const useMDXComponents = (components: MDXComponents = {}): MDXComponents => ({
...components,
MyComponent,
});
```
Then you can use your component in your content by importing it like so:
```mdx title="content/docs/my-docs/my-page.mdx"
## My custom title
Below is my custom component.
```
See that you do not need to import the component within your page's content.
# Apps
URL: /docs/apps
Understand the Next.js apps that make up the ship.pluv.io templates.
[ship.pluv.io](https://ship.pluv.io) monorepo templates include a Next.js app that is broken down into multiple [Next.js zones](https://nextjs.org/docs/app/guides/multi-zones).
Multi-zone Next.js apps do make deploying your app the first time more complex, but once setup, provides a number of long-term benefits:
1. **Lower complexity** - Each zone is a smaller, more focused app that is easier to understand and maintain.
2. **Faster deployments** - Zones have fewer dependencies and pages to build, making them smaller and faster to deploy.
3. **Deploys independently** - Zones deploy independently, allowing you to deploy changes to one zone without affecting the others.
Check out the Next.js apps included in the templates below:
# Blog
URL: /docs/apps/marketing/blog
Blog powered-by Notion. Manage content on Notion.
[ship.pluv.io](https://ship.pluv.io) comes with a prebuilt blog powered by [Notion X](https://github.com/NotionX).
## Setup Instructions
1. Create a [Notion database](https://www.notion.com/help/intro-to-databases)
2. Add the following columns to your Notion database:
1. Status
* Must have `Published` as a valid value
* Only blog pages with a status of `Published` will be made publicly viewable in your app.
2. Title
3. Publish Date
4. Category
5. Tags
6. SEO Title
7. Meta Description
Your notion database should look something like this:

3. Get the root notion page id. You can do this by getting a link to the Notion database page. You'll get a URL like so: `https://www.notion.so/1ded8f15373a80afa690cc7ca19c39fe`. The page id is the path parameter after the origin url (e.g. in this URL, it is `1ded8f15373a80afa690cc7ca19c39fe`).

4. Set the `NEXT_PUBLIC_NOTION_ROOT_PAGE_ID` environment variable in `/apps/marketing` to the page id you copied from step (3).
## Preview
### Blog root page

### Blog page

## Tips
* Consider configuring SEO Title and Meta Description to use AI autofill on Notion.
* You can do this by going to your Notion database, clicking on the SEO Title and/or Meta Description column header, then clicking "Set up AI autofill"
# Marketing
URL: /docs/apps/marketing
A Next.js app that is used to showcase your product or service, consisting of static pages and a blog.
The marketing app is a public-facing [Next.js zone](https://nextjs.org/docs/app/guides/multi-zones) designed to be used to showcase your product or service. It is designed to be SEO-friendly, and consists of the following pages:
A beautiful landing page to showcase your product or service.
A blog powered by Notion. Manage content on Notion.
# EnterprisePlanCard
URL: /docs/apps/marketing/landing-page/enterprise-plan-card
A pricing card where potential customers can contact your business if your plans aren't exactly fitting their needs.
## Preview

## Import
```tsx title="page.tsx"
import { EnterprisePlanCard } from "@/components/EnterprisePlanCard";
```
## Tips
* Include a copy that predicts what users might want to call about.
* If you have defined abuse/usage limits that can be increased for a particular plan, consider adding that detail to this card.
# FaqSection
URL: /docs/apps/marketing/landing-page/faq-section
A list of questions & answers you expect users to ask before buying or when using your product.
## Preview

## Import
```tsx title="page.tsx"
import { FaqSection } from "@/components/FaqSection";
```
## Tips
* Try to predict questions or objections users might have prior to purchasing your product. This helps to give users more confidence in their purchase.
* If you have a particular refund policy, it would be a good idea to include that in the FAQ.
# FeaturesCycler
URL: /docs/apps/marketing/landing-page/features-cycler
A features showcase with clickable tabs and descriptions. Each feature can be clicked to display its description.
## Preview

## Import
```tsx title="page.tsx"
import { FeaturesCycler } from "@/components/FeaturesCycler";
```
# HomeClosingCta
URL: /docs/apps/marketing/landing-page/home-closing-cta
A call-to-action section with a title, supporting headline and CTA. Gives your landing a final chance to convert a customer.
## Preview

## Import
```tsx title="page.tsx"
import { HomeClosingCta } from "@/components/HomeClosingCta";
```
## Tips
* Your title describe once more what value your product offers (i.e. why users should care about what you're offering).
# HomeHero
URL: /docs/apps/marketing/landing-page/home-hero
A hero element that users see when visiting your home page. Includes a title, supporting headline and CTA.
## Preview

## Import
```tsx title="page.tsx"
import { HomeHero } from "@/components/HomeHero";
```
## Tips
* Your `
` tag should be a catching title describing what value your product offers (i.e. why users should care about what you're offering).
* Your supporting headline should explain how your product provides the value outlined in your title.
# Landing Page
URL: /docs/apps/marketing/landing-page
A beautiful landing page to showcase your product or service.
## Home page in minutes
[ship.pluv.io](https://ship.pluv.io) comes with a growing number of components and a prebuilt landing page to help you get your MVP and/or verify your idea with users as quickly as possible.

## Custom components
# SubscriptionPlanCard
URL: /docs/apps/marketing/landing-page/subscription-plan-card
A pricing card that displays the price, features and limits.
## Preview

## Import
```tsx title="page.tsx"
import { SubscriptionPlanCard } from "@/components/SubscriptionPlanCard";
```
## Tips
* If you have different limits per plan, list them here.
* If you charge for overages, display the usage-based price where it is relevant within the card.
# Testimonial
URL: /docs/apps/marketing/landing-page/testimonial
A testimonial element to show that customers are receiving value.
## Preview

## Import
```tsx title="page.tsx"
import { Testimonial } from "@/components/Testimonial";
```
# Web
URL: /docs/apps/web
A Next.js app consists of the authenticated, product pages of your app.
The web app is a [Next.js zone](https://nextjs.org/docs/app/guides/multi-zones) that is protected behind authentication. It consists of the actual product pages of your app such as:
# SubscriptionGateDialog
URL: /docs/apps/web/misc/subscription-gate-dialog
An upsell dialog that can block actions until a user upgrades their plan.
## Preview

## Import
```tsx title="page.tsx"
import { SubscriptionGateDialog } from "@/components/SubscriptionGateDialog";
```
# Project API Keys
URL: /docs/apps/web/project-api-keys
API keys page where users can manage their project's API keys.
[ship.pluv.io](https://ship.pluv.io) comes with a prebuilt project API keys page. API secret keys are generated in a cryptographically secure way and are 1-way hashed into the database.

## Tips
* API public and secret keys should be treated as if they were usernames and passwords respectively.
* Public keys should not be sensitive (i.e. is fine to be exposed on the client-side), and should only be used for things where you just need to identify which project an API request was made for (e.g. Stripe public keys identify which Stripe account the payment transactions are for).
* Private keys should be treated like passwords (i.e. should be 1-way hashed into your database). API requests that you charge users for should be protected by secret keys (i.e. not public keys) to prevent abuse scenarios.
* Since public keys are not sensitive, you can have them be base-64 encoded metadata that helps with debugging.
# Project Chat
URL: /docs/apps/web/project-chat
Chat page where users can interact with your LLM product.
[ship.pluv.io](https://ship.pluv.io) comes with a prebuilt project chat page. This is mostly just to provide a LLM integration example for your product.

# Project Dashboard
URL: /docs/apps/web/project-dashboard
Dashboard where users can manage their project's API keys and use your product.
[ship.pluv.io](https://ship.pluv.io) comes with a prebuilt project dashboard. Add more pages as your product sees fit. Separate product usage within teams by projects.

# Team Billing
URL: /docs/apps/web/team-billing
Billing page where users can manage their team's current plan and view invoices.
[ship.pluv.io](https://ship.pluv.io) comes with a prebuilt team billing page - viewable only for team owners. Owners can manage their current plan and view past invoices.

# Team Dashboard
URL: /docs/apps/web/team-dashboard
Dashboard where users can manage their team's billing, members and projects.
[ship.pluv.io](https://ship.pluv.io) comes with a prebuilt team dashboard so you can focus on building the actual core of your product. Add more pages as your product sees fit. Members are not accessible for teams that represent user accounts (e.g. NeonDB accounts are functionally like teams as well).

# Team Members
URL: /docs/apps/web/team-members
Members page where users can manage their team's members.
[ship.pluv.io](https://ship.pluv.io) comes with a prebuilt team members page - viewable only for team owners. Owners can manage their current members and invite new members. Only teams that are not accounts can have members (i.e. teams that represent user accounts cannot have members).

# CLI Monorepo
URL: /docs/deployment/cli-monorepo
Learn how to deploy the CLI Monorepo template to Vercel and npm.
## Deploying to Vercel and npm
To deploy your project to Vercel, you will need to have a Vercel account. If you do not have a Vercel account, you can sign up for one [here on Vercel](https://vercel.com/signup).
To deploy your CLI to npm, you will need to have an npm account. If you do not have an npm account, you can sign up for one [here on npm](https://www.npmjs.com/signup).
## Set up your services
Follow the instructions in the [AI](/docs/features/ai), [Auth](/docs/features/auth/lucia), [NeonDB](/docs/features/database/neondb), [Payments](/docs/features/payments/one-time) documentations to set up services that you will need for your project.
## Deploy the marketing app
Follow the instructions in the [Setup](https://github.com/pluv-io/ship-cli-monorepo/tree/main/apps/marketing#setup) and [Deployment](https://github.com/pluv-io/ship-cli-monorepo/tree/main/apps/marketing#deployment) sections of the [README](https://github.com/pluv-io/ship-cli-monorepo/tree/main/apps/marketing) of the marketing app at `/apps/marketing/README.md` to deploy the marketing app to Vercel.
## Deploy the web app
Follow the instructions in the [Setup](https://github.com/pluv-io/ship-cli-monorepo/tree/main/apps/web#setup) and [Deployment](https://github.com/pluv-io/ship-cli-monorepo/tree/main/apps/web#deployment) sections of the [README](https://github.com/pluv-io/ship-cli-monorepo/tree/main/apps/web) of the web app at `/apps/web/README.md` to deploy the web app to Vercel.
## Deploying your CLI to npm
Follow the instructions in the [Publishing to npm](https://github.com/pluv-io/ship-cli-monorepo/tree/main?tab=readme-ov-file#publishing-to-npm) section of the [README](https://github.com/pluv-io/ship-cli-monorepo/tree/main?tab=readme-ov-file) of the CLI monorepo to deploy your CLI to npm.
# Cloudflare Monorepo
URL: /docs/deployment/cloudflare-monorepo
Learn how to deploy the Cloudflare Monorepo template to Cloudflare.
## Deploying to Cloudflare
To deploy your project to Cloudflare, you will need to have a Cloudflare account. If you do not have a Cloudflare account, you can sign up for one [here on Cloudflare](https://dash.cloudflare.com/sign-up).
## Update app configs
You will need to update the global config files located in the `/packages/configs` directory. You can view all of the available config files [here on GitHub](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/packages/configs).
## Set up your services
Follow the instructions in the [AI](/docs/features/ai), [Auth](/docs/features/auth/lucia), [D1 database](/docs/features/database/cloudflare-d1), [Emails](/docs/features/emails), [Payments](/docs/features/payments/metered), and [Real-time](/docs/features/real-time) documentations to set up services that you will need for your project.
## Deploy the docs app
Follow the instructions in the [Setup](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/apps/docs#setup) and [Deployment](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/apps/docs#deployment) sections of the [README](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/apps/docs) of the docs app at `/apps/docs/README.md` to deploy the docs app to Cloudflare.
Once you have deployed the docs app, note origin URL that you have deployed to. If you wish to use a custom domain, you can do so by going to the settings page of the Cloudflare Worker dashboard. You will need this origin URL for connecting the Next.js app zones.
## Deploy the marketing app
Follow the instructions in the [Setup](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/apps/marketing#setup) and [Deployment](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/apps/marketing#deployment) sections of the [README](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/apps/marketing) of the marketing app at `/apps/marketing/README.md` to deploy the marketing app to Cloudflare.
Once you have deployed the marketing app, note origin URL that you have deployed to. If you wish to use a custom domain, you can do so by going to the settings page of the Cloudflare Worker dashboard. You will need this origin URL for connecting the Next.js app zones.
## Deploy the web app
Follow the instructions in the [Setup](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/apps/web#setup) and [Deployment](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/apps/web#deployment) sections of the [README](https://github.com/pluv-io/ship-cloudflare-monorepo/tree/main/apps/web) of the web app at `/apps/web/README.md` to deploy the web app to Cloudflare.
When deploying the web app to Cloudflare, you will also need to update the build environment variables `NEXT_PUBLIC_ZONE_DOCS_URL` and `NEXT_PUBLIC_ZONE_MARKETING_URL` to the origin URLs of the docs and marketing apps that you deployed earlier.
# Vercel Monorepo
URL: /docs/deployment/vercel-monorepo
Learn how to deploy the Vercel Monorepo template to Vercel.
## Deploying to Vercel
To deploy your project to Vercel, you will need to have a Vercel account. If you do not have a Vercel account, you can sign up for one [here on Vercel](https://vercel.com/signup).
## Update app configs
You will need to update the global config files located in the `/packages/configs` directory. You can view all of the available config files [here on GitHub](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/packages/configs).
## Set up your services
Follow the instructions in the [AI](/docs/features/ai), [Auth](/docs/features/auth/better-auth), [NeonDB](/docs/features/database/neondb), [Emails](/docs/features/emails), [Payments](/docs/features/payments/flat-rate), and [Real-time](/docs/features/real-time) documentations to set up services that you will need for your project.
## Deploy the docs app
Follow the instructions in the [Setup](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/apps/docs#setup) and [Deployment](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/apps/docs#deployment) sections of the [README](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/apps/docs) of the docs app at `/apps/docs/README.md` to deploy the docs app to Vercel.
Once you have deployed the docs app, note origin URL that you have deployed to. If you wish to use a custom domain, you can do so by in the settings page of the Vercel dashboard. You will need this origin URL for connecting the Next.js app zones.
## Deploy the marketing app
Follow the instructions in the [Setup](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/apps/marketing#setup) and [Deployment](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/apps/marketing#deployment) sections of the [README](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/apps/marketing) of the marketing app at `/apps/marketing/README.md` to deploy the marketing app to Vercel.
## Deploy the web app
Follow the instructions in the [Setup](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/apps/web#setup) and [Deployment](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/apps/web#deployment) sections of the [README](https://github.com/pluv-io/ship-vercel-monorepo/tree/main/apps/web) of the web app at `/apps/web/README.md` to deploy the web app to Vercel.
When deploying the web app to Vercel, you will also need to update the build environment variables `NEXT_PUBLIC_ZONE_DOCS_URL` and `NEXT_PUBLIC_ZONE_MARKETING_URL` to the origin URLs of the docs and marketing apps that you deployed earlier.
# AI
URL: /docs/features/ai
Learn how to integrate AI into your project.
[ship.pluv.io](https://ship.pluv.io) templates include an AI integration for you to use out-of-the-box, and includes AI-powered utilities to help you ship your products faster.
## AI Integration
The `@workspace-apps/web` app includes an existing AI integration built with the [AI SDK](https://ai-sdk.dev/) and [OpenRouter](https://openrouter.ai/).
The [AI SDK](https://ai-sdk.dev/) is a TypeScript/React SDK for building conversational AI experiences using streaming AI responses. [OpenRouter](https://openrouter.ai/) is a unified API layer for accessing multiple LLM providers.
Using both of these together, you can build conversational AI features quickly with the AI SDK, then hot-swap the LLM providers with a different one without having to change any of your code.
### Setup
To get started, you will need to create an account on [OpenRouter](https://openrouter.ai/) and create an API key.
Once you have your API key, you can add it to your `.env` file.
```txt title="/apps/web/.env"
OPEN_ROUTER_API_KEY="your-api-key-here"
```
Then, because the templates currently use GPT-4o-mini, you will need to create an account on [OpenAI](https://openai.com/) and create an API key.
Once you have your API key, you can add it to your `.env` file.
```txt title="/apps/web/.env"
OPENAI_API_KEY="your-api-key-here"
```
With this, you can see the included AI integration in action in the following files:
1. `/apps/web/src/app/(app)/[teamSlug]/[projectSlug]/chat/page.tsx`
2. `/apps/web/src/server/router/chat/router.ts`
3. `/apps/web/src/server/router/projects/router.ts`
* In the `POST /:urlSlug/chat` route
## AI Utilities
### llms.txt
The `llms.txt` file is a publically accessible file that is intended to be parsed by AI agents to have a better understanding of the product(s) you offer. This can later be used to integrate with other AI-powered services to power things such as:
1. Product search
2. Automated live support
3. Automated guides and onboarding
It is automatically generated by the `@workspace-apps/docs` app. As you add more documentation to your product, the `llms.txt` file will be updated to include the new documentation.
### .cursor/rules
If you are using [Cursor](https://www.cursor.com/) to edit your code, you can use [Cursor Rules](https://docs.cursor.com/context/rules) to add rules for your AI agent to follow. They can be used to enforce coding standards, or to help the AI agent understand the codebase better.
The [ship.pluv.io](https://ship.pluv.io) templates include multiple `.cursor/rules` that explains each package and app in the monorepo. This should help you better use AI agents to generate code with better context.
### Helpful Prompts
The prompts provided do not consistute legal advice. Please consult a lawyer if you are unsure about any legal implications of your product.
The `/resources` directory contains helpful prompts that you can paste into chats like ChatGPT including:
1. A privacy policy builder
2. A terms of service builder
# Google Analytics
URL: /docs/features/analytics/google-analytics
Learn how to add Google Analytics to your project.
You can choose to skip these instructions if you are not interested in using Google Analytics.
You can view more complete instructions on how to setup [Google Analytics here](https://support.google.com/analytics/answer/9304153?hl=en).
## Setup
[Google Analytics](https://analytics.google.com/) is a web analytics service provided by Google.
To get started, go to [Google Analytics](https://analytics.google.com/) and sign in with your Google account.
Click on "Start measuring", and fill out the form to describe your website and define a property name (e.g. "My Website").
Then, create a data stream for your website, and copy the "Measurement ID" (e.g. "G-1234567890") to a separate file:
```txt title="env.txt"
NEXT_PUBLIC_GA_TRACKING_ID="your-measurement-id"
```
You do not need to use these environment variables in your local development environment. They are only used in your production environment.
When you are ready to deploy your project, you can add these values to your environment variables in your hosting provider.
# Analytics
URL: /docs/features/analytics
Learn how to add analytics to your project.
[ship.pluv.io](https://ship.pluv.io) has built-in analytics, powered by [PostHog](https://posthog.com/) and [Google Analytics](https://analytics.google.com/). Click on the links below to learn how to setup each analytics provider:
# PostHog
URL: /docs/features/analytics/posthog
Learn how to add PostHog analytics to your project.
You can choose to skip these instructions if you are not interested in using PostHog.
## Setup
[PostHog](https://posthog.com/) is a developer-centric product and web analytics platform including many features that can be useful to understand your users and their behavior.
To get started, you will need to create an account on [PostHog](https://posthog.com/) and create a new organization.
Once you have created your organization, select "Web Analytics" as the feature you want to use.
The templates are currently only using Web Analytics, but you can use PostHog for other features such as event tracking, feature flags, and more if you need them.
Then, click on the "Copy API key" button at the top of the page and save these values to a separate file:
```txt title="env.txt"
NEXT_PUBLIC_POSTHOG_KEY="your-api-key-here"
NEXT_PUBLIC_POSTHOG_HOST="https://us.i.posthog.com"
```
You do not need to use these environment variables in your local development environment. They are only used in your production environment.
When you are ready to deploy your project, you can add these values to your environment variables in your hosting provider.
# Better Auth
URL: /docs/features/auth/better-auth
Learn how to set up use Better Auth for auth in your app.
[Better Auth](https://better-auth.com/) is an auth library that handles most of the complexity of auth for you, but abstracts away much of the lower-level details and control of how your auth works.
## Setup
If you're using the [Vercel Monorepo](/docs/picking-a-template/vercel-monorepo) template, the `@workspace-apps/web` app is setup with [Better Auth](https://better-auth.com/) out of the box.
To get the auth setup working, you will need to create a new OAuth app in your GitHub account, and then update the environment variables in the `@workspace-apps/web` app with the necessary client ID and secret.
### Creating a new GitHub OAuth app
To create a new OAuth app in your GitHub account, you will need to visit the [GitHub Developer Settings](https://github.com/settings/developers) page. From there, you will need to click the "New OAuth App" button. Fill out the form with the following information:
* **Application name:** The name of your application.
* **Homepage URL:** The URL of your application's homepage.
* **Authorization callback URL:** The URL of your application's authorization callback.
* On local development, this will be `http://localhost:3000/api/auth/callback/github`.
* On production, this will be the same as the callback URL for local development, but the domain will be your production domain.
After creating the OAuth app, you will be given a Client ID and Client Secret. You will need to update the `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` environment variables in the `/apps/web/.env` file with the values you were given.
```txt title="/apps/web/.env"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
```
### Using other OAuth providers
If you want to use different/additional OAuth providers (e.g. Google OAuth), you will need to make updates to the following files within `@workspace-apps/web`:
* `/src/lib/auth.ts`
* Update the `socialProviders` object to include the new provider(s).
```ts title="/src/lib/auth.ts"
import { betterAuth } from "better-auth";
import { env } from "@/env";
export const auth = betterAuth({
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
},
// Add your other providers here...
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
},
});
```
* `/src/app/(general)/login/page.tsx`
* Add a new login button for the new provider(s).
* `/src/app/(general)/signup/page.tsx`
* Add a new signup button for the new provider(s).
# Authentication
URL: /docs/features/auth
Learn how to set up use the auth setup for your app.
[ship.pluv.io](https://ship.pluv.io) provides an auth setup using either [Lucia](https://lucia-auth.com/) or [Better Auth](https://better-auth.com/) out of the box, depending on which template you are building from. All included auth setups are built with GitHub OAuth, but configuring different/additional OAuth providers is also supported with minimal code changes.
Generally speaking, Lucia is more manual to set-up, but gives you more control over how your auth works; whereas Better Auth handles most of the complexity of auth for you, but abstracts away much of the lower-level details (i.e. less control).
In either case, because auth is already configured for you to start using right away, the choice is a matter of maintenance preferences.
# Lucia
URL: /docs/features/auth/lucia
Learn how to set up use Lucia for auth in your app.
[Lucia](https://lucia-auth.com/) is less an auth library than it is a framework for building your own auth system. It takes considerably more effort to set-up, but gives you more control over how your auth works.
## Why use Lucia for CLIs?
Doing OAuth for CLIs is more complex than for web apps. This is because CLIs are run in a terminal, and therefore cannot write to the browser (e.g. cookies) to store session data.
Due to this, Lucia is a good choice for CLIs because of the additional control that is necessary to handle the differences between web apps and CLIs.
## Setup
If you're using the [Cloudflare Monorepo](/docs/picking-a-template/cloudflare-monorepo) or [CLI Monorepo](/docs/picking-a-template/cli-monorepo) template, the `@workspace-apps/web` app is setup with [Lucia](https://lucia-auth.com/) out of the box.
To get the auth setup working, you will need to create a new OAuth app in your GitHub account, and then update the environment variables in the `@workspace-apps/web` app with the necessary client ID and secret.
### Creating a new GitHub OAuth app
To create a new OAuth app in your GitHub account, you will need to visit the [GitHub Developer Settings](https://github.com/settings/developers) page. From there, you will need to click the "New OAuth App" button. Fill out the form with the following information:
* **Application name:** The name of your application.
* **Homepage URL:** The URL of your application's homepage.
* **Authorization callback URL:** The URL of your application's authorization callback.
* On local development, this will be `http://localhost:3000/api/auth/callback/github`.
* On production, this will be the same as the callback URL for local development, but the domain will be your production domain.
After creating the OAuth app, you will be given a Client ID and Client Secret. You will need to update the `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` environment variables in `/apps/web` with the values you were given.
* For the Cloudflare Monorepo, this will be in the `/apps/web/.dev.vars` file.
```txt title="/apps/web/.dev.vars"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
```
* For the CLI Monorepo, this will be in the `/apps/web/.env` file.
```txt title="/apps/web/.env"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
```
### Using other OAuth providers
If you want to use different/additional OAuth providers (e.g. Google OAuth), you will need to make updates to the following files within `@workspace-apps/web`:
* `/src/server/auth/getAuth.ts`
* Update the `providers` object to include the new provider(s).
```ts title="/src/server/auth/getAuth.ts"
import { GitHub, Google } from "arctic";
// ...
export const getAuth = (params: GetAuthParams) => {
// ...
return {
// ...
providers: {
get github() {
return new GitHub(env.GITHUB_CLIENT_ID, env.GITHUB_CLIENT_SECRET);
},
// Add your other providers here...
get google() {
return new Google(env.GOOGLE_CLIENT_ID, env.GOOGLE_CLIENT_SECRET);
},
},
};
};
```
* `/src/server/router/auth/router.ts`
* Add a new GET endpoint for the new provider(s).
* e.g. `GET /api/auth/google`
* Simply copy the existing GET endpoint for GitHub, and update the provider that is used to the new provider.
* Add a new GET endpoint for the callback URL of the new provider(s).
* e.g. `GET /api/auth/google/callback`
* You can start with a copy of the existing callback endpoint for GitHub. But the new OAuth provider may return different user profile data than GitHub. This will require that you map the properties of that provider's profile data to the columns of the `User` table in your database.
* If you wish to track different fields for each provider, you will need to add new columns to the `User` table in your database.
* `/src/app/(general)/login/page.tsx`
* Add a new login button for the new provider(s).
* `/src/app/(general)/signup/page.tsx`
* Add a new signup button for the new provider(s).
# Cloudflare D1
URL: /docs/features/database/cloudflare-d1
Learn how to setup and use Cloudflare D1 and Drizzle ORM.
## Setup
If you're using the [Cloudflare Monorepo](/docs/picking-a-template/cloudflare-monorepo) template, the `@workspace/drizzle` package is setup with [Cloudflare D1](https://www.cloudflare.com/developer-platform/products/d1/) and Drizzle ORM out of the box.
To use D1, you will need to create a new D1 database for your project, update your wrangler config files to include your D1 database, then run migrations.
### Create a new D1 database
In the root of your project, run the following command to create a new D1 database (you can replace `my_example_db` with your desired database name):
```bash title="/"
pnpm wrangler d1 create my_example_db
```
This will create a new D1 database and output the database ID, like so:
```bash title="/"
> wrangler d1 create my_example_db
⛅️ wrangler 4.25.0
───────────────────
✅ Successfully created DB 'my_example_db' in region WNAM
Created your new D1 database.
{
"d1_databases": [
{
"binding": "DB",
"database_name": "my_example_db",
"database_id": "49d82112-f513-458a-bfcd-361a2434435e"
}
]
}
```
If you are using staging environments, you will need to create separate D1 databases for both staging and production. You can do this by running the above command with different database names.
### Update wrangler config files
Next, you will need to update all `wranger.jsonc` files to point to your new D1 database(s). Open the `wrangler.jsonc` files in the following locations:
1. `/apps/meter/wrangler.jsonc`
2. `/apps/realtime/wrangler.jsonc`
3. `/apps/web/wrangler.jsonc`
4. `/packages/drizzle/wrangler.jsonc`
In each of these files, you will need to replace the contents of the `d1_databases` array with the database name and ID you just created. For example:
```jsonc title="/apps/meter/wrangler.jsonc"
{
// ...
"d1_databases": [
{
"binding": "DB",
// Replace with your database name
"database_name": "my_example_db",
// Replace with your database ID
"database_id": "49d82112-f513-458a-bfcd-361a2434435e",
// ...
}
],
// ...
}
```
### Run migrations
Lastly, you will need to ensure that your D1 database contains the latest schema. Since your D1 databases are new and empty, you can first delete the existing migrations in the `@workspace/drizzle` package:
```bash title="/packages/drizzle"
rm -rf ./.drizzle
```
Then generate fresh migrations based on your current schema:
```bash title="/packages/drizzle"
pnpm run migrate:generate
```
Finally, run the migrations to apply them to your local D1 database:
```bash title="/packages/drizzle"
# Applies the migrations to your LOCAL D1 database
pnpm run migrate:apply:local
```
You can verify that the migrations have been applied by checking the `migrations` table in your D1 database. You can do this by using Drizzle Studio via the following command:
```bash title="/packages/drizzle"
pnpm run studio:local
```
Once you are ready to roll out your database changes to either your staging or production environments, you can run the following command to apply the migrations to your D1 database in the respective environment:
```bash title="/packages/drizzle"
# Applies the migrations to your STAGING D1 database
pnpm run migrate:apply:stage
# Applies the migrations to your PRODUCTION D1 database
pnpm run migrate:apply:prod
```
# Database
URL: /docs/features/database
Learn how to setup and use the database with Drizzle ORM for your app.
[ship.pluv.io](https://ship.pluv.io) provides a database setup using Drizzle ORM, which allows you to define your database schema in TypeScript and interact with it in a type-safe manner. However, depending on which template you are building from, the initial setup may vary slightly. Click on the database you are using below to see the specific instructions:
# NeonDB
URL: /docs/features/database/neondb
Learn how to setup and use NeonDB and Drizzle ORM.
## Setup
If you're using the [Vercel Monorepo](/docs/picking-a-template/vercel-monorepo) or [CLI Monorepo](/docs/picking-a-template/cli-monorepo) template, the `@workspace/drizzle` package is setup with [NeonDB](https://neon.com/) and Drizzle ORM out of the box.
To use NeonDB, you will need to create a new NeonDB database for your project, update your environment variables, then run migrations.
### Create a new NeonDB database
In your NeonDB dashboard, create a new database with any name that you like. Once you've created the database, you will be sent to NeonDB's Project Dashboard page. On this page, you will find a button at the top right that says "Connect". Click on it, select the "Connection string" option, ensure that the "Connection pooling" option is selected, and copy the connection string to a separate file to use later.
You should see something like this:
```txt title="Example NeonDB connection url"
postgresql://neondb_owner:my_password@ep-sweet-frog-aerm7yqw-pooler.c-2.us-east-2.aws.neon.tech/neondb?sslmode=require&channel_binding=require
```
Ensure that the connection string you copy is the raw, password-included version, and contains "pooler" in the hostname.
Then, we recommend creating new database branches for staging (i.e. preview) and local environments. You can do this simply by going to the "Branches" page in the NeonDB dashboard, and clicking on the "Create branch" button.
Save these connection strings in a separate txt file, as you will need these as well.
### Update environment variables
Next, we will need to update your local environment variables to include the connection string for your NeonDB branch that you've created for your local environment. Open and edit the `.env` files in the following locations:
1. `/apps/web/.env`
2. `/packages/drizzle/.env`
```txt title="/packages/drizzle/.env"
DATABASE_URL="postgresql://neondb_owner:my_password@ep-sweet-frog-aerm7yqw-pooler.c-2.us-east-2.aws.neon.tech/neondb?sslmode=require&channel_binding=require"
```
### Run migrations
Lastly, you will need to ensure that your NeonDB branch contains the latest schema. Since your NeonDB database should be new and empty, you can first delete the existing migrations in the `@workspace/drizzle` package:
```bash title="/packages/drizzle"
rm -rf ./.drizzle
```
Then generate fresh migrations based on your current schema:
```bash title="/packages/drizzle"
pnpm run migrate:generate
```
Finally, run the migrations to apply them to your database:
```bash title="/packages/drizzle"
pnpm run migrate:apply
```
You do not need to run migrations for your staging and production NeonDB branches, because we will have these automatically run during your deployments to Vercel later on.
## Using Docker for development
Disclaimer: The following documentation assumes that you already have [Docker](https://www.docker.com/) installed on your local environment.
We recommend using NeonDB branches for your local development so that you can make use of NeonDB's branches feature; however, it can be handy to setup a local Postgres instance instead if you want to work while offline.
For this, you can run the following command in the root of your project to create your local Postgres instance at `localhost:5432`:
```bash title="/"
docker compose up -d
```
Then in the `.env` files above, update the `DATABASE_URL` environment variables to be as follows:
```txt title=".env"
DATABASE_URL="postgres://postgres:postgres@db.localtest.me:5432/main"
```
Finally, ensure that your local database has the latest schema by running the migration commands above once more.
# Emails
URL: /docs/features/emails
Learn how to send emails with React Email and Resend.
## Setup
These instructions assume that you already have a domain on Cloudflare or a similar DNS provider. We recommend using Cloudflare if you want to make use of [Cloudflare Email Routing](https://www.cloudflare.com/developer-platform/products/email-routing/) to send and receive emails from your Gmail inbox without having to pay for a GSuite account.
1. Create a new account on [Resend](https://resend.com).
2. Navigate to [Domains](https://resend.com/domains) and add a new domain. This domain should either be the same as your app's domain or a subdomain of it.
* Resend recommends using a subdomain instead of the root domain so that you can segment your emails by purpose (e.g. transactional vs marketing).
3. Update your DNS records, and go through all of the DNS verification steps (see Resend's [documentation](https://resend.com/docs/knowledge-base/cloudflare) for more details).
4. Navigate to [API Keys](https://resend.com/api-keys) and create a new API key. This key will be used to authenticate your requests to Resend's API. You can leave all other settings as default.
5. Add the API key to your `.env` file:
```txt title="/apps/web/.env"
RESEND_API_KEY="{{re_resend_api_key}}"
```
## Sending emails
Within any of the API routes, the Resend client is available on the Hono context (i.e. `c.var.resend`). Use this to send emails from your server.
```ts title="apps/web/src/server/router/example/router.ts"
import { MyEmailTemplate } from "@workspace/emails";
import { appConfig } from "@/appConfig";
import { getFactory } from "@/server/router/factory";
import { isAuthenticated } from "@/server/router/middleware"
export const exampleRouter = getFactory()
.createApp()
.post("/", isAuthenticated({ id: "example" }), async (c) => {
/* ... */
const { resend, user } = c.var;
await resend.emails
.send({
from: appConfig.email.noReplyMailTo,
to: user.email,
subject: "This is a test email",
react: await MyEmailTemplate({
name: user.name,
message: "This is a test email sent from Hono!",
}),
})
return c.json({ ok: true }, 200);
});
```
### Creating email templates
The `MyEmailTemplate` is a React component that you can create in the template's `emails` package. This is located in `/packages/emails`.
```tsx title="packages/emails/templates/MyEmailTemplate.tsx"
import { Body, Head, Html, Row, Text } from "@react-email/components";
import type { EmailComponent } from "../../types";
import { Card, CardContent } from "../../components/Card";
import { TailwindProvider } from "../../components/TailwindProvider";
export interface MyEmailTemplateProps {
name: string;
message: string;
}
export const MyEmailTemplate: EmailComponent = ({
name,
message,
}) => {
return (
Hello {name}!
{message}
);
};
```
Once you have created your email templates, make sure to build the package so that the templates are compiled and ready to be used. You can do this by running:
```bash title="/packages/emails"
pnpm run build
```
You can preview your email templates in the `/emails` package by running the following command:
```bash title="/packages/emails"
pnpm run email:studio
```
## Avoiding the spam folder
To reduce the chances of your emails being marked as spam, there are several best-practices you can follow:
1. **Use a verified domain**: Make sure to use a domain that you own and have verified with Resend.
2. **Set up SPF, DKIM, and DMARC records**: These records help email providers verify that your emails are legitimate and not spoofed. Resend provides detailed instructions on how to set these up in their [documentation](https://resend.com/docs/dashboard/domains/dmarc).
3. **Add an unsubscribe link**: If you're sending marketing emails, make sure to include an unsubscribe link in your emails. This is not only a best practice but also a legal requirement in many jurisdictions.
4. **Avoid spammy content**: Be mindful of the content of your emails. Avoid using excessive capitalization, exclamation marks, and spammy keywords that might trigger spam filters (e.g. "free", "guaranteed", "urgent", etc.).
5. **Avoid links to suspicious domains**: Make sure that the links in your emails point to legitimate and trusted domains. Avoid using URL shorteners or links that redirect to unknown sites.
## Email routing to/from Gmail
The following documentation assumes you've already set up your domain within Cloudflare. If your domain is managed on another provider (e.g. Vercel), you will need to transfer your domain to Cloudflare before proceeding.
If you only care to send and receive emails from your domain, and don't want to pay for a GSuite account, you can use [Cloudflare Email Routing](https://www.cloudflare.com/developer-platform/products/email-routing/) and Gmail's SMTP server to achieve this.
### Receiving emails from your domain
If you want to receive emails from your domain to a Gmail inbox, you can set up [Cloudflare Email Routing](https://www.cloudflare.com/developer-platform/products/email-routing/).
To set up email routing, follow these steps:
1. Navigate to your domain on the Cloudflare dashboard.
* You can find your domains within the "Account Home" page.
* Click on the domain you want to set up email routing for.
2. On the domains page, click on the "Email" link in the left sidebar.
3. Go to the `Destination addresses` tab and add a new destination address.
* This should be your Gmail address that you wish to route emails to.

4. Go to the `Routing rules` tab and click on the `Create address` button.
* Enter the email address you want to route emails from
* Specify the destination address you added in the previous step.
* Click on the `Save` button to create the routing rule. Note that this may take a few minutes to propagate, so be patient.
5. Send a a test email to the address you set up in the previous step.
* You should receive the email in your Gmail inbox.
* If you don't receive the email, your email routing may not have fully propagated yet. Wait a few more minutes and try again later.
* If you still don't receive the test email, try changing the email address that you are using to send the test email. The email routing may not work if it detects that the email being sent will be to the same address that it is routing to.
### Sending emails from your domain
Cloudflare Email Routing allows you to receive emails from your domain, but it does not allow you to send emails from your domain. To send emails from your domain, you can use Gmail's SMTP server.
[This answer](https://community.cloudflare.com/t/solved-how-to-use-gmail-smtp-to-send-from-an-email-address-which-uses-cloudflare-email-routing/382769/2) from the Cloudflare community outlines the steps you need to follow to set this up.
Re-posting the steps here for convenience:
1. Make sure you have 2FA enabled on your Gmail account.
* If you don't already have 2FA enabled, you can follow this link to set it up: [Enable 2FA in your Google Account](https://myaccount.google.com/signinoptions/two-step-verification).
2. In your same Google Account, create an [App Password](https://myaccount.google.com/apppasswords).
* If you need to select an app and device, select "Mail" as the app, and your computer as the device.
* Copy the generated password, as you will need it later.
3. In your Gmail account, go to "Settings" -> "Accounts and Import" tab -> "Send mail as" section.
* The "Settings" can be found by clicking on the gear icon in the top right corner of Gmail. If you see the "Quick settings" menu, click on "See all settings" to access the full settings page.
4. Click on "Add another email address" and fill in the first form with your name and the email address you want to send emails from (i.e. the email address you set-up in step 4 of the previous section within Cloudflare Email Routing).
* Make sure to *uncheck* the "Treat as an alias" checkbox.
5. Fill in the second form with the following details:
* SMTP Server: `smtp.gmail.com`
* Port: `587`
* Username: your Gmail address (the destination address you specified in step 3 of the previous section)
* Password: the App Password you generated in step 2
* Check the "Secured connection using TLS" option
* Click on "Add Account" to finish the setup
6. You will receive a verification email from Gmail, asking you to confirm that you own the email with a code. Copy the code and paste it into the dialog (or click on the link in the email to verify your email address).
Now you can send emails from your domain using your Gmail account. When you compose a new email, you will see the email address you added in the "From" dropdown menu. You can select it to send emails from that address.
# Flat-rate
URL: /docs/features/payments/flat-rate
Learn how to setup and use flat-rate subscriptions in your project.
## Payment Structure
If you're using the [Vercel Monorepo](/docs/picking-a-template/vercel-monorepo) template, the `@workspace-apps/web` app defines a flat-rate subscription plan out of the box.
Payments are structured as follows:
1. Free trial for 7 days
2. Paid subscription for fixed price per month
* e.g. $10/month
Note that if you need to change the pricing structure (e.g. add another tier), you will need to make revisions to the `@workspace-apps/web` app in the following locations:
1. `/apps/web/src/server/router/payment/router.ts`.
2. `/apps/web/src/server/router/plans/router.ts`.
## Setup
Create a new Stripe account and activate payments.
### Business settings
Set up your business details so that you can start accepting payments.
1. Go to the [Settings](https://dashboard.stripe.com/settings) of the Stripe dashboard
2. Click on the [Business](https://dashboard.stripe.com/settings/account) section.
3. Click on the [Account details](https://dashboard.stripe.com/settings/account) tab and fill out the form as needed.
4. Click on the [Business details](https://dashboard.stripe.com/settings/business-details) tab and fill out the form as needed.
* You can do this later when you need to start accepting payments (i.e. are ready to switch off Stripe's test mode).
5. Click on the [Branding](https://dashboard.stripe.com/settings/branding) tab, and add your icon, logo, and colors.
6. Click on the [Customer emails](https://dashboard.stripe.com/settings/emails) tab and enable emails "Successful payments" and "Refunds".
### Payment settings
Recommendations to avoid potential abuse and fraud.
1. Go back to the [Settings](https://dashboard.stripe.com/settings) page.
2. Click on the [Payments](https://dashboard.stripe.com/settings/payments) section.
3. Click on the [Checkout and Payment Links](https://dashboard.stripe.com/settings/checkout) tab, and enable "Limit customers to 1 subscription".
* This will help prevent potential race conditions, where users might subscribe multiple times on accident.
4. Click on the [Payment methods](https://dashboard.stripe.com/settings/payment_methods) tab, and enable only the payment methods you want to offer.
* We recommend disabling "Cash App Pay" to mitigate abuse and fraud. Many customers using it tend to cancel transactions.
### Billing settings
Recommendations for avoiding failed payments.
1. Go back to the [Settings](https://dashboard.stripe.com/settings) page.
2. Click on the [Billing](https://dashboard.stripe.com/settings/billing/automatic) section.
3. Click on the [Subscriptions and emails](https://dashboard.stripe.com/settings/billing/automatic) tab.
4. Enable the following Customer emails:
* Send a reminder email 7 days before a free trial ends
* Send emails about upcoming renewals
* Send emails about expiring cards
* Send emails when card payments fail
* Send emails when bank debit payments fail
5. Within the "Payment method updates" section, select "Link to a Stripe-hosted page"
6. Within the "Subscription management" section, select "Include a link for customers to manage their subscriptions"
7. In the "Manage free trial messaging" section, enable the "Statement descriptor" option.
### API keys
Connect your Stripe account to your project.
1. Go to the [API keys](https://dashboard.stripe.com/apikeys) page. You can find this by clicking on the "Developers" link at the bottom of the left sidebar.
2. Create a "Secret key", and copy values for `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY`.
3. Add these values to your `.env` file in `/apps/web/.env`.
```txt title=".env"
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
```
4. Go to the [Webhooks](https://dashboard.stripe.com/workbench/webhooks) page. You can find this by clicking on the "Developers" link at the bottom of the left sidebar.
5. Click on "Add destination" and set the "Endpoint URL" to point to a URL that you want to receive webhooks from Stripe.
* Note that if you are running the project locally, you will need to tunnel your selected Endpoint URL to your local machine. This is because Stripe is unable to access localhost origins.
6. Enable the following events:
* `checkout.session.completed`
* `customer.subscription.created`
* `customer.subscription.deleted`
* `customer.subscription.paused`
* `customer.subscription.pending_update_applied`
* `customer.subscription.pending_update_expired`
* `customer.subscription.resumed`
* `customer.subscription.trial_will_end`
* `customer.subscription.updated`
* `invoice.marked_uncollectible`
* `invoice.paid`
* `invoice.payment_failed`
* `invoice.payment_succeeded`
7. Click on "Create destination", and copy the "Signing secret" value.
8. Add this value to your `.env` file in `/apps/web/.env` as `STRIPE_WEBHOOK_SECRET`.
```txt title=".env"
STRIPE_WEBHOOK_SECRET="whsec_..."
```
### Product and prices
Create your product and prices to offer to your customers.
1. Go to the [Products catalog](https://dashboard.stripe.com/products) page.
2. Click on "Create product" and fill out the name, description and image.
3. In the pricing section of the product
* Ensure "Recurring" is selected.
* Select a "Monthly" billing period, and set the price to your desired price.
* Click "Add product" to finish creating the product.
4. Copy the "Product ID" and "Price ID" and add them to your `.env` file at `/apps/web/.env`
```txt title="/apps/web/.env"
STRIPE_PRICE_ID: "price_...",
STRIPE_PRODUCT_ID: "prod_...",
```
### Set Product metadata
We assume that you may want to potentially use the same Stripe business account for multiple products, each with different Stripe webhooks and settings. Unfortunately, Stripe does not allow partitioning webhooks by product, so we use Stripe metadata to differentiate between products.
1. On your newly created product, click on the "Edit" button within the "Metadata" section.
* Create a key-value pair with the key `appId` and the value of your `appId` that you have chosen for your project in `/packages/configs/appConfig.js` under `stripe.appId`.
2. Click on your flat-rate price, and add the same `appId` key-value pair to the "Metadata" section.
# Payments
URL: /docs/features/payments
Learn how to setup and use payments in your project.
[ship.pluv.io](https://ship.pluv.io) provides a payments setup using Stripe, which allows you to accept payments in your project. There are 3 different payment types available, depending on which template you are building from:
1. Flat-rate subscriptions + usage-based billing
* Users can subscribe to a flat-rate plan, and then be charged based on usage (i.e. overages).
* Example: $10/month for 100GB of storage, $0.01/GB over 100GB.
2. Flat-rate subscriptions
* Users can subscribe to a flat-rate plan, and then be charged a fixed amount each month.
* Example: $20/month to watch movies on a streaming service in HD resolution.
3. One-time payments
* Users can pay a fixed amount for a one-time purchase.
* Example: $299 1-time payment to access [TailwindCSS Plus](https://tailwindcss.com/plus#pricing).
Click on the setup you are interested in below to see the specific instructions:
# Metered
URL: /docs/features/payments/metered
Learn how to setup and use metered payments in your project.
## Payment Structure
If you're using the [Cloudflare Monorepo](/docs/picking-a-template/cloudflare-monorepo) template, the `@workspace-apps/web` app defines a metered subscription plan out of the box.
Payments are structured as follows:
1. Free tier with low usage limits
* e.g. 20K tokens per month for $0
2. Pro tier with higher usage limits, with some limits that can be exceeded for a usage-based fee
* e.g. 10M tokens per month for $29. $3.5/1M tokens over 10M
Note that if you need to change the pricing structure (e.g. add another tier), you will need to make revisions to the `@workspace-apps/web` app in the following locations:
1. `/apps/web/src/server/router/payment/router.ts`.
2. `/apps/web/src/server/router/plans/router.ts`.
## Setup
Create a new Stripe account and activate payments.
### Business settings
Set up your business details so that you can start accepting payments.
1. Go to the [Settings](https://dashboard.stripe.com/settings) of the Stripe dashboard
2. Click on the [Business](https://dashboard.stripe.com/settings/account) section.
3. Click on the [Account details](https://dashboard.stripe.com/settings/account) tab and fill out the form as needed.
4. Click on the [Business details](https://dashboard.stripe.com/settings/business-details) tab and fill out the form as needed.
* You can do this later when you need to start accepting payments (i.e. are ready to switch off Stripe's test mode).
5. Click on the [Branding](https://dashboard.stripe.com/settings/branding) tab, and add your icon, logo, and colors.
6. Click on the [Customer emails](https://dashboard.stripe.com/settings/emails) tab and enable emails "Successful payments" and "Refunds".
### Payment settings
Recommendations to avoid potential abuse and fraud.
1. Go back to the [Settings](https://dashboard.stripe.com/settings) page.
2. Click on the [Payments](https://dashboard.stripe.com/settings/payments) section.
3. Click on the [Checkout and Payment Links](https://dashboard.stripe.com/settings/checkout) tab, and enable "Limit customers to 1 subscription".
* This will help prevent potential race conditions, where users might subscribe multiple times on accident.
4. Click on the [Payment methods](https://dashboard.stripe.com/settings/payment_methods) tab, and enable only the payment methods you want to offer.
* We recommend disabling "Cash App Pay" to mitigate abuse and fraud. Many customers using it tend to cancel transactions.
### Billing settings
Recommendations for avoiding failed payments.
1. Go back to the [Settings](https://dashboard.stripe.com/settings) page.
2. Click on the [Billing](https://dashboard.stripe.com/settings/billing/automatic) section.
3. Click on the [Subscriptions and emails](https://dashboard.stripe.com/settings/billing/automatic) tab.
4. Enable the following Customer emails:
* Send a reminder email 7 days before a free trial ends
* Send emails about upcoming renewals
* Send emails about expiring cards
* Send emails when card payments fail
* Send emails when bank debit payments fail
5. Within the "Payment method updates" section, select "Link to a Stripe-hosted page"
6. Within the "Subscription management" section, select "Include a link for customers to manage their subscriptions"
7. In the "Manage free trial messaging" section, enable the "Statement descriptor" option.
### API keys
Connect your Stripe account to your project.
1. Go to the [API keys](https://dashboard.stripe.com/apikeys) page. You can find this by clicking on the "Developers" link at the bottom of the left sidebar.
2. Create a "Secret key", and copy values for `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY`.
3. Add these values to your `.dev.vars` file in the following locations:
* `/apps/meter/.dev.vars`
* `/apps/web/.dev.vars`
```txt title=".dev.vars"
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
```
4. Go to the [Webhooks](https://dashboard.stripe.com/workbench/webhooks) page. You can find this by clicking on the "Developers" link at the bottom of the left sidebar.
5. Click on "Add destination" and set the "Endpoint URL" to point to a URL that you want to receive webhooks from Stripe.
* Note that if you are running the project locally, you will need to tunnel your selected Endpoint URL to your local machine. This is because Stripe is unable to access localhost origins.
6. Enable the following events:
* `checkout.session.completed`
* `customer.subscription.created`
* `customer.subscription.deleted`
* `customer.subscription.paused`
* `customer.subscription.pending_update_applied`
* `customer.subscription.pending_update_expired`
* `customer.subscription.resumed`
* `customer.subscription.trial_will_end`
* `customer.subscription.updated`
* `invoice.marked_uncollectible`
* `invoice.paid`
* `invoice.payment_failed`
* `invoice.payment_succeeded`
7. Click on "Create destination", and copy the "Signing secret" value.
8. Add this value to your `.dev.vars` file in `/apps/web/.dev.vars` as `STRIPE_WEBHOOK_SECRET`.
```txt title=".dev.vars"
STRIPE_WEBHOOK_SECRET="whsec_..."
```
### Product and prices
Create your product and prices to offer to your customers.
1. Go to the [Products catalog](https://dashboard.stripe.com/products) page.
2. Click on "Create product" and fill out the name, description and image.
3. In the pricing section of the product, select "More pricing options".
* Ensure "Recurring" is selected.
* Select the "Flat rate" pricing model, a "Monthly" billing period, and set the price to your desired price.
* This is your flat price before applying usage-based overage fees.
* Click "Next" to create the flat rate price.
4. Click on "Add another price"
* Ensure "Recurring" is selected.
* Select the "Usage-based" pricing model, with "per tier" and "Graduated" selected.
* Set the price of the first tier to $0 for both "Per unit" and "Flat fee", up to the amount of usage included in the flat rate price.
* e.g. If you have a flat rate price of $29/month for 10M tokens, set the first tier to $0 for 10M "Last units".
* Add another tier, and set the "Flat fee" price to $0, and the Per unit price to the usage-based fee.
* e.g. If you have a unit price of $3.5/1M tokens, set the Per unit price to $0.0000035.
5. Click on the "+" button next in the "Meter" section to create a new billing meter. This will track the usage of the product for billing purposes.
* Set the "Event name" to something like "token\_meter", and set the "Aggregation method" to "Sum".
* Save your "Event name" to the "STRIPE\_METER\_NAME" in your wrangler.jsonc file at `/apps/meter/.wrangler.jsonc` for both "staging" and "production" environments.
```json title="/apps/meter/.wrangler.jsonc"
{
// ...
"vars": {
"STRIPE_METER_NAME": "token_meter"
},
// ...
}
```
6. Copy the "Product ID" and "Price ID" (for both the flat rate and usage-based price) and add them to your `wrangler.jsonc` file at `/apps/web/.wrangler.jsonc` for both "staging" and "production" environments (note that staging should use IDs from your Stripe test account).
```json title="/apps/web/.wrangler.jsonc"
{
// ...
"vars": {
"STRIPE_FLAT_PRICE_ID": "price_...",
"STRIPE_METERED_PRICE_ID": "price_...",
"STRIPE_PRODUCT_ID": "prod_...",
},
// ...
}
```
### Set Product metadata
We assume that you may want to potentially use the same Stripe business account for multiple products, each with different Stripe webhooks and settings. Unfortunately, Stripe does not allow partitioning webhooks by product, so we use Stripe metadata to differentiate between products.
1. On your newly created product, click on the "Edit" button within the "Metadata" section.
* Create a key-value pair with the key `appId` and the value of your `appId` that you have chosen for your project in `/packages/configs/appConfig.js` under `stripe.appId`.
2. Click on your flat-rate price, and add the same `appId` key-value pair to the "Metadata" section.
3. Click on your usage-based price, and add the same `appId` key-value pair to the "Metadata" section.
# One-time
URL: /docs/features/payments/one-time
Learn how to setup and use one-time payments in your project.
One-time payments is not the same as e-commerce. It is a way to sell a specific product(s) for a one-time fee. e.g. [TailwindCSS Plus](https://tailwindcss.com/plus#pricing).
## Payment Structure
If you're using the [CLI Monorepo](/docs/picking-a-template/cli-monorepo) template, the `@workspace-apps/web` app defines one-time payments out of the box for a single product.
Note that if you need to change the pricing structure (e.g. add another price), you will need to make revisions to the `@workspace-apps/web` app in the following locations:
1. `/apps/web/src/server/router/payment/router.ts`.
2. `/apps/web/src/server/router/products/router.ts`.
## Setup
Create a new Stripe account and activate payments.
### Business settings
Set up your business details so that you can start accepting payments.
1. Go to the [Settings](https://dashboard.stripe.com/settings) of the Stripe dashboard
2. Click on the [Business](https://dashboard.stripe.com/settings/account) section.
3. Click on the [Account details](https://dashboard.stripe.com/settings/account) tab and fill out the form as needed.
4. Click on the [Business details](https://dashboard.stripe.com/settings/business-details) tab and fill out the form as needed.
* You can do this later when you need to start accepting payments (i.e. are ready to switch off Stripe's test mode).
5. Click on the [Branding](https://dashboard.stripe.com/settings/branding) tab, and add your icon, logo, and colors.
6. Click on the [Customer emails](https://dashboard.stripe.com/settings/emails) tab and enable emails "Successful payments" and "Refunds".
### Payment settings
Recommendations to avoid potential abuse and fraud.
1. Go back to the [Settings](https://dashboard.stripe.com/settings) page.
2. Click on the [Payments](https://dashboard.stripe.com/settings/payments) section.
3. Click on the [Payment methods](https://dashboard.stripe.com/settings/payment_methods) tab, and enable only the payment methods you want to offer.
* We recommend disabling "Cash App Pay" to mitigate abuse and fraud. Many customers using it tend to cancel transactions.
### Billing settings
Recommendations for avoiding failed payments.
1. Go back to the [Settings](https://dashboard.stripe.com/settings) page.
2. Click on the [Billing](https://dashboard.stripe.com/settings/billing/automatic) section.
3. Click on the [Subscriptions and emails](https://dashboard.stripe.com/settings/billing/automatic) tab.
4. Enable the following Customer emails:
* Send emails when card payments fail
* Send emails when bank debit payments fail
5. Within the "Payment method updates" section, select "Link to a Stripe-hosted page"
### API keys
Connect your Stripe account to your project.
1. Go to the [API keys](https://dashboard.stripe.com/apikeys) page. You can find this by clicking on the "Developers" link at the bottom of the left sidebar.
2. Create a "Secret key", and copy values for `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY`.
3. Add these values to your `.env` file in `/apps/web/.env`.
```txt title=".env"
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
```
4. Go to the [Webhooks](https://dashboard.stripe.com/workbench/webhooks) page. You can find this by clicking on the "Developers" link at the bottom of the left sidebar.
5. Click on "Add destination" and set the "Endpoint URL" to point to a URL that you want to receive webhooks from Stripe.
* Note that if you are running the project locally, you will need to tunnel your selected Endpoint URL to your local machine. This is because Stripe is unable to access localhost origins.
6. Enable the following events:
* `checkout.session.completed`
* `checkout.session.expired`
* `payment_intent.created`
* `payment_intent.canceled`
7. Click on "Create destination", and copy the "Signing secret" value.
8. Add this value to your `.env` file in `/apps/web/.env` as `STRIPE_WEBHOOK_SECRET`.
```txt title=".env"
STRIPE_WEBHOOK_SECRET="whsec_..."
```
### Product and prices
Create your product and prices to offer to your customers.
1. Go to the [Products catalog](https://dashboard.stripe.com/products) page.
2. Click on "Create product" and fill out the name, description and image.
3. In the pricing section of the product
* Ensure "One-off" is selected.
* Set the price to your desired price.
* Click "Add product" to finish creating the product.
4. Copy the "Product ID" and "Price ID" and add them to your `.env` file at `/apps/web/.env`
```txt title="/apps/web/.env"
STRIPE_PRICE_ID: "price_...",
STRIPE_PRODUCT_ID: "prod_...",
```
### Set Product metadata
We assume that you may want to potentially use the same Stripe business account for multiple products, each with different Stripe webhooks and settings. Unfortunately, Stripe does not allow partitioning webhooks by product, so we use Stripe metadata to differentiate between products.
1. On your newly created product, click on the "Edit" button within the "Metadata" section.
* Create a key-value pair with the key `appId` and the value of your `appId` that you have chosen for your project in `/packages/configs/appConfig.js` under `stripe.appId`.
2. Click on your flat-rate price, and add the same `appId` key-value pair to the "Metadata" section.
# Real-time
URL: /docs/features/real-time
Build real-time collaborative features with pluv.io.
## Real-time collaboration
[pluv.io](https://pluv.io) is a real-time library that allows you to build real-time collaborative features with ease. It is a type-safe API, and comes with a variety of features out-of-the-box, such as:
* Room authorization
* Real-time data syncs [powered-by CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)
* Presence
```tsx title="page.tsx"
"use client";
import { event, useBroadcast, useStorage } from "@/lib/react-pluv";
import { yjs } from "@pluv/crdt-yjs";
export default function Page() {
const broadcast = useBroadcast();
const [groceries, yGroceries] = useStorage("groceries");
event.receiveMessage.useEvent(({ data }) => {
console.log(data.message);
});
const sendMessage = (message: string) => {
broadcast.sendMessage({ message });
};
const addGrocery = (grocery: string) => {
yGroceries.push([grocery]);
};
return (
{/* ... */}
{groceries.map((grocery) => (
- {grocery}
))}
{/* ... */}
)
}
```
## Costs
When using the [Cloudflare Monorepo](/docs/picking-a-template/cloudflare-monorepo) template, pluv.io will run on your own [Durable Objects](https://www.cloudflare.com/developer-platform/products/durable-objects/) instances, which are free to use with extremely generous free tier limits.
When using the [Vercel Monorepo](/docs/picking-a-template/vercel-monorepo) template, pluv.io will run on [pluv.io's](https://pluv.io) servers, which have much more limited free tier limits, then cost $29/month.
## Setup
Setting up pluv.io is relatively simple, but depends on the template you are using.
### Cloudflare Monorepo
1. In the `realtime` app, add an environment variable for `PLUV_AUTH_SECRET` to your `.dev.vars` file.
* This secret will be used to generate securely-signed JWT tokens for your users.
```txt title="/apps/realtime/.dev.vars"
PLUV_AUTH_SECRET="your-secret-key"
```
2. In the `web` app, add an environment variable for `NEXT_PUBLIC_REALTIME_ORIGIN_URL` to your `.env` file.
* This should be the origin URL of your realtime app. For example, if you deployed your realtime app to `https://realtime.your-app.com`, you would add the following environment variable:
```txt title="/apps/web/.env"
NEXT_PUBLIC_REALTIME_ORIGIN_URL="https://realtime.your-app.com"
```
### Vercel Monorepo
1. Create an account on [pluv.io](https://pluv.io) and create a new project.
2. Go to the API Keys page and create a new API key. You can find this page by clicking on the "API Keys" link in the left sidebar.
3. Create an API secret key, and store both the "Publishable Key" and "Secret Key" values to your `.env` file.
```txt title="/apps/web/.env"
NEXT_PUBLIC_PLUV_PUBLISHABLE_KEY="your-publishable-key-here"
PLUV_SECRET_KEY="your-secret-key"
```
## Removal
You may not need to add real-time collaboration to your app. [ship.pluv.io](https://ship.pluv.io) has been built so that if you don't need it, removal of the real-time features is as simple as deleting some API routes and removing a React provider.
To remove pluv.io from the app, perform the following steps:
1. Disconnect the `rooms` route from your app's API routes.
```ts title="/apps/web/src/app/api/[[...route]]/route.ts"
const app = new Hono<{ Bindings: CloudflareEnv }>()
.basePath("/api")
.use(cors)
// ...
// Remove the `/rooms` route below
.route("/rooms", router.rooms);
```
2. Remove the `PluvRoomProvider` from your project page's layout, located in `/apps/web/src/app/(app)/[teamSlug]/[projectSlug]layout.tsx`.
```tsx title="/apps/web/src/app/(app)/[teamSlug]/[projectSlug]layout.tsx"
// ...
const Layout: FC = ({ children }) => {
return (
// Remove the `PluvRoomProvider` below
// ...
{children}
// ...
);
};
```
Then, you can optionally clean-up the `pluv.io` files in the codebase by deleting the following files:
1. `/apps/web/src/server/router/rooms/router.ts`
2. `/apps/web/src/lib/react-pluv.ts`
3. `/apps/realtime`
* If you are using the [Cloudflare Monorepo](/docs/picking-a-template/cloudflare-monorepo) template, you can delete this directory, as you will not be using it.
# RPC
URL: /docs/features/rpc
Learn how to make type-safe API calls from your client to your server with Hono RPC.
## Creating a new API endpoint
You can make a new API endpoint within the `/apps/web/src/server/router` directory.
```ts title="/apps/web/src/server/router/example/router.ts"
import { getFactory } from "@/server/router/factory";
export const exampleRouter = getFactory()
.createApp()
.get("/", async (c) => {
return c.json({ ok: true }, 200);
});
```
If you've added a new router, you will need to ensure that the route contains a path to the router you defined in `/apps/web/src/app/api/[[...route]]/route.ts`.
```ts title="/apps/web/src/app/api/[[...route]]/route.ts"
const app = new Hono<{ Bindings: CloudflareEnv }>()
.basePath("/api")
.use(cors)
.route("/example", router.example);
```
### Protecting an endpoint
You can protect an endpoint by adding the `isAuthenticated` middleware to the router.
```ts title="/apps/web/src/server/router/example/router.ts"
import { getFactory } from "@/server/router/factory";
import { isAuthenticated } from "@/server/router/middleware";
export const exampleRouter = getFactory()
.createApp()
.get("/", isAuthenticated({ id: "example" }), async (c) => {
// If `isAuthenticated` is provided, the user will be available
// in the context variable `c.var`
const { user } = c.var;
return c.json({ ok: true, user }, 200);
});
```
## Making an API call
```tsx title="hono.ts"
import { rpc } from "@/lib/rpc";
// This makes a GET request to the /api/teams endpoint
const res = await rpc.api.teams.$get();
if (res.status !== 200 || !res.ok) {
throw new Error("Failed to fetch teams");
}
const teams = await res.json().then((data) => data.teams);
```
### Querying in React
```tsx title="page.tsx"
"use client";
import { useQuery } from "@tanstack/react-query";
import { rpc } from "@/lib/rpc";
import type { FC } from "react";
const Page: FC = () => {
const teams = useQuery({
// This is a utility to create a query key, which is used to cache
// the result
queryKey: rpc.$key(rpc.api.teams),
queryFn: async () => {
// This makes a GET request to the /api/teams endpoint
const res = await rpc.api.teams.$get();
if (res.status !== 200 || !res.ok) {
throw new Error("Failed to fetch teams");
}
return res.json().then((data) => data.teams);
},
});
return (
{teams.isPending &&
Loading...
}
{teams.isError &&
Error: {teams.error.message}
}
{teams.isSuccess && (
{teams.data.map((team) => (
- {team.name}
))}
)}
);
};
export default Page;
```
### Mutating in React
```tsx title="page.tsx"
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { rpc } from "@/lib/rpc";
import type { InferRequestType } from "hono";
import type { FC } from "react";
interface PageProps {
params: Promise<{ teamSlug: string }>;
}
const Page: FC = async (props) => {
const { teamSlug } = await props.params;
const queryClient = useQueryClient();
// This makes a PATCH request to the /api/teams/:urlSlug endpoint
const updateFn = rpc.api.teams[":urlSlug"].$patch;
const updateTeam = useMutation({
mutationFn: async (input: InferRequestType) => {
const res = await updateFn(input);
if (res.status !== 200 || !res.ok) {
throw new Error("Failed to update team");
}
const updated = res.json().then((data) => data.team);
console.log("Updated team:", updated);
},
// This is used to invalidate the query cache for the teams endpoint
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: rpc.$key(rpc.api.teams),
});
},
});
const handleUpdate = async () => {
await updateTeam.mutateAsync({
// This specifies the :urlSlug parameter in the endpoint
// The type is inferred from the endpoint definition
// so you get type safety here
param: { urlSlug: teamSlug },
// This is the request body
// The type is also inferred from the endpoint definition
// so you get type safety here as well
json: { name: "My new team" },
});
};
return {/* ... */}
;
};
```
## TypeScript performance
TypeScript performance is a common concern when using heavily type-inferred libraries. Hono RPC is no exception. As you define more endpoints, you may notice a slowdown in your IDE's performance, particularly with intellisense and type checking.
When working within the `@workspace-apps/web` app, all of your types should be automatically inferred between the backend and frontend; however, you can run `pnpm hono:generate` to statically generate the types for your endpoints. Once you run this command, the generated RPC with flat types will be made available in `/apps/web/hono/rpc`.
You can continue using the rpc with fully inferred types, but if you find that your IDE is starting to struggle with performance, you can switch to rpc with flat types by importing from the generated rpc instead.
```bash title="/apps/web"
pnpm hono:generate
```
```tsx title="page.tsx"
// rpc with generated flat types
// this is extremely performant, but requires you to run `pnpm hono:generate`
import { rpc } from "@/hono/rpc";
// rpc with fully inferred types
// this may show down your Next.js dev server and IDE performance,
// but it does not require you to run `pnpm hono:generate`
import { rpc } from "@/lib/rpc";
```
## Why not tRPC?
[tRPC](https://trpc.io/) is a fantastic library for building type-safe APIs in TypeScript, and there are no reasons why you shouldn't use it if you enjoy its API. However, there are a few reasons why we chose to use Hono RPC instead.
### Built on Web Standards
Hono RPC is built on top of [Web Standards](https://hono.dev/docs/concepts/web-standard), notably the Fetch API, and includes basic HTTP primitives like Request, Response, URL and Headers.
Building on top of these standards makes it easier to follow tutorials and documentation that are based on these standards, and makes it more interoperable with other libraries and tools in the JavaScript and web ecosystems.
### Type Safety
Like tRPC, Hono RPC allows you to share types between your client and server, ensuring that your API calls are type-safe end-to-end.
```ts twoslash title="example.ts"
import { zValidator as zv } from "@hono/zod-validator";
import { Hono } from "hono";
import { hc } from "hono/client";
import { z } from "zod";
// server
const app = new Hono().basePath("/api").post(
"/teams",
zv("json", z.object({ name: z.string() })),
(c) => {
const { name } = c.req.valid("json");
const created = { id: "123", name };
return c.json({ team: created }, 200);
},
);
// client
const rpc = hc("http://localhost:3000");
const team = await rpc.api.teams.$post({
// This makes a POST request to the /api/teams endpoint
// json is the request body and is type-safe
json: { name: "My new team" },
})
// res is a type-safe standard Fetch Response
.then((res) => res.status === 200 ? res.json() : null)
.then((data) => data?.team ?? null);
```
Not only that, but each HTTP status code is also type-safe, so you can return different data for each status code, and the client will differentiate between them via [TypeScript discriminated unions](https://zod.dev/api?id=discriminated-unions).
### Type Performance
tRPC has been designed to always be type-inferred, which can lead to performance issues in larger applications as more procedures are added. For instance, [one such user](https://github.com/trpc/trpc/discussions/5508) cites that resolving types takes 4-8 seconds on a M1 Max and 32 GB of RAM, with [another reply](https://github.com/trpc/trpc/discussions/5508#discussioncomment-8878965) citing that casting the entire router to `any` resolves performance issues.
Meanwhile, when you create a new Hono RPC and provide it your Hono app type, the Hono client will strip all backend-specific types (e.g. your [server's context](https://trpc.io/docs/server/context)) and only retains the types that are relevant to the client. This means that you can start with fully inferred types, which is great for development; then you can statically generate flat types later on as your application scales to improve TypeScript performance in your IDE.
#### Trade-offs
* Because the Hono client strips all backend-specific types, you do lose the ability to cmd+click into the backend code from the client. While that is inconvenient, your routes should be easy to find in code with [good routing organization](https://hono.dev/docs/api/routing#grouping) (the [ship.pluv.io](https://ship.pluv.io) templates do this well).
* tRPC does have solutions like [xtrpc](https://github.com/algora-io/xtrpc), which users have created to solve tRPC performance issues. However, these solutions are not an official part of tRPC and can be unreliable as a result.
## tRPC vs Hono RPC
| | [tRPC](https://trpc.io/) | [Hono](https://hono.dev/) |
| ----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- |
| npm | [
](https://www.npmjs.com/package/@trpc/server) | [
](https://www.npmjs.com/package/hono) |
| GitHub | [https://github.com/trpc/trpc](https://github.com/trpc/trpc) | [https://github.com/honojs/hono](https://github.com/honojs/hono) |
| Built on Web Standards | 🪄 No. Abstracts away standard APIs | 🛠️ Yes. Builds around standard APIs |
| End-to-end type-safety | 🛡️ Yes | 🛡️ Yes |
| IDE-performant at scale | 🐌 No | ⚡ Yes, when using flat types. Else no |
| Can build flat types | 🛑 No. At least would be very difficult | ✅ Yes. Can simply use tsc |
| Can cmd+click to server | 🖱️ Yes. Goes straight to the procedure | 🔎 No. Must search for the endpoint in code |
| Works with React Query | ✅ Yes | ✅ Yes |
| Client/Server coupling | 🔗 Tightly coupled | 🌱 Uncoupled. Can be in different repos and different languages |
| Routing model | Procedure-based | HTTP verb and path-based |
# SEO
URL: /docs/features/seo
SEO utilities to make sure your page is represented correctly and ranks on Google
## Utility functions
[ship.pluv.io](https://ship.pluv.io) comes with several utility functions to ensure each of your pages have the relevant SEO tags and data to be represented correctly and ranked on search engines.
### getSeoMetadata
The `getSeoMetadata` function defines a central location where your site-wide defaults for SEO metadata can be configured. The default values for this function are defined within the following config file: `/packages/configs/defaultSeoMetadata.js`.
```tsx title="page.tsx"
import { getSeoMetadata } from "@workspace/utils";
import type { Metadata } from "next";
// As a value
export const metadata: Metadata = getSeoMetadata({
// You can provide properties to overwrite the defaults
title: "My custom title",
description: "My custom description",
});
// If you need to dynamically generate metadata, export `generateMetadata`
// instead, and provide a function parameter to getSeoMetadata
export const generateMetadata = getSeoMetadata(async () => {
const details = await getMyMetadataDetails(/* ... */);
return {
// You can provide properties to overwrite the defaults
title: details.title,
description: "My custom description",
};
});
```
#### Tips
* Next.js will [automatically merge metadata](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#merging) objects shallowly for a given route. So often times, you should only need to call `getSeoMetadata` in each Next.js app's root layout. Then for all other pages/layouts, you can just export the metadata partial object containing the fields you wish to overwrite.
### JsonLd
Type-safe SEO rich snippets via the `JsonLd` component. This component can represent either a single entity or a graph containing multiple entities.
Once the `@type` property is provided, TypeScript will infer the correct attributes for that entity so you can provide the other properties of those entities confidently.
Render this in your page component wherever you wish to provide this.
```tsx title="page.tsx"
import { JsonLd } from "@workspace/ui/custom/JsonLd";
// As a single entity
// As a graph
// example of rendering in your page
const Page = () => {
return (
<>
{/* ... */}
<>
);
}
```
#### Tips
* If you are building an application that supports user reviews for products, consider adding `JsonLd` to those product pages and include star ratings. These will appear on Google, and can make users more likely to visit your site.
### OG image generation
OG image generation is included via the [@vercel/og](https://vercel.com/docs/og-image-generation) package. OG images are generated dynamically via the `/api/og` route. Provide a custom `title` and `description` property to generate the image. The route can be called as a typesafe function via the [rpc](/features/rpc).
The included OG image template can be updated within the following component file: `/packages/og-images/src/templates/OgGlobalTemplate`.
```tsx title="page.tsx"
import { rpc } from "@/shared/rpc";
import type { Metadata } from "next";
const ogImage: URL = rpc.api.og.$url({
query: {
title: "How to Make Hard-Boiled Eggs",
description: "Delicious recipes",
},
});
// Next.js metadata export
export const metadata: Metadata = {
// ... other metadata
openGraph: {
// ... other openGraph data
images: [ogImage],
},
};
```
### Sitemap generation
Sitemaps and robots.txt files are automatically generated for each Next.js app zone via the [next-sitemap](https://www.npmjs.com/package/next-sitemap) package each time your app is built (i.e. during automated deployments with your hosting provider).
Each Next.js app zone's sitemaps will be correctly indexed and linked together, even when deployed separately. If you wish to configure your sitemap generation, edit the `next-sitemap.config.js` files at the base of each Next.js app directory.
## Blog
SEO metadata will be taken from the `SEO Title` and `Meta Description` columns automatically from your Notion database. All other properties will be taken from the `metadata` exported from the `marketing` root layout. See the [Blog](/doccs/features/pages/blog) documentation for more details.
## Docs
SEO metadata will be taken from your [page's frontmatter](https://fumadocs.dev/docs/ui/page-conventions#file) title and description properties. All other properties will be taken from the `metadata` exported from the `docs` root layout.
# UI
URL: /docs/features/ui
Learn about installing shadcn/ui compnents and how to use type-safe icons.
[ship.pluv.io](https://ship.pluv.io) templates are built with [shadcn/ui](https://ui.shadcn.com/), allowing you to quickly install components that you can customize to your liking.
## Installing shadcn/ui components
You can install shadcn/ui components by installing them with the [shadcn/ui CLI](https://ui.shadcn.com/docs/cli).
1. Navigate to the `/packages/ui` directory.
2. Find a component you want to install (e.g. [https://ui.shadcn.com/docs/components/checkbox](https://ui.shadcn.com/docs/components/checkbox))
3. Run the CLI installation command
```bash title="/packages/ui"
pnpm dlx shadcn@latest add checkbox
```
This package already has the shadcn/ui CLI installed, so you can choose to run the following command instead:
```bash title="/packages/ui"
pnpm shadcn add checkbox
```
## Using type-safe icons
There are 2 ways to use type-safe icons in your project:
### Using lucide-react
```tsx title="page.tsx"
import { RocketIcon } from "lucide-react";
```
### Using @workspace/icons
You can generate type-safe icons as React components without having to mess with Webpack loaders by generating them with [svgr](https://react-svgr.com/).
1. Go to the `/packages/icons` directory.
2. Add an svg file to the `/packages/icons/svgs` directory (e.g. "MyCustomIcon.svg").
The file name of the svg file will be used as the name of the icon component.
3. Run the following command to generate the icon component:
```bash title="/packages/icons"
pnpm build
```
4. Use the icon component in your project:
```tsx title="page.tsx"
import { MyCustomIcon } from "@workspace/icons";
```
# Webhook Testing
URL: /docs/features/webhook-testing
Learn how to test your webhooks.
## Testing on localhost
Services like Stripe provide webhooks to notify your app when certain events occur. However, when building your app on localhost, you will not be able to receive these webhooks because those same services will not be able to make requests to a localhost domain.
So in order to trigger your webhooks while developing locally, you will need to create a secure tunnel to localhost so that your local server can be made accessible via a public URL.
## Setup
This setup assumes that your domain is hosted on Cloudflare. If you are using a different DNS provider, you can consider using [ngrok](https://ngrok.com/) instead.
We recommend using [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) for tunneling to localhost. This is because it is free, the tunnels last forever, and you can lock your tunnel to a specific domain (allowing you to not have to keep updating your webhook's destination URL on Stripe or other services).
### Creating a permanent tunnel
1. Navigate to the [Cloudflare Dashboard](https://dash.cloudflare.com/) and click on the "Zero Trust" link on the left sidebar.
2. Once you are on the Zero Trust dashboard, click on the "Networks" -> "Tunnels" link on the left sidebar.
3. Click on the "Create a tunnel" button and select "Cloudflared".
4. Enter a name for your tunnel and click on the "Save tunnel" button.
5. Once your tunnel is created, you will be given multiple commands to run on your local machine.
* If you don't already have [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) installed on your machine, run the command that is provided to install it.
* Once you have cloudflared installed, run the command that is provided to install the tunnel.
* This will configure your tunnel to automatically start when your machine boots up.
6. Click on the "Public hostnames" tab at the top of the page.
* Click the "Add a public hostname" button.
* Under "Hostname", enter the public URL that you will use to test your webhooks.
* We recommend using a subdomain of your domain on Cloudflare.
* Under "Service", select "HTTP" for the type, and set the URL to `127.0.0.1:3000` (or whichever port you want to use for your local server).
7. Provide Stripe with the public URL you specified in step 6 as the destination URL for your webhook.
Once you have completed the above steps, whenever you run [http://localhost:3000](http://localhost:3000), you will be able to access your local server via the public URL you specified in step 6 above. And Stripe will be able to send webhooks to your local server.
# Future
URL: /docs/future
Considerations for updates to templates.
Each of the following items are considerations only. They are not currently on the roadmap, and may not be implemented.
## React-Notion-X alternative
[React-Notion-X](https://github.com/NotionX/react-notion-x) is a great to let you use [Notion](https://www.notion.so/) as a CMS for your blog content, but the API and developer experience are not great.
Consider repurposing [fumadocs](https://fumadocs.dev/) for handling blog content in addition to docs.
## Cloudflare auth alternative
Consider swapping the auth for the Cloudflare Monorepo template from [Lucia](https://lucia-auth.com/) to [OpenAuth](https://openauth.js.org/).
## Stripe alternative
Consider swapping [Stripe](https://stripe.com/) for [Polar](https://polar.sh/) for handling payments. Polar has higher fees, but provides a better developer experience and is easier to set up.
## Waitlist
Add a waitlist feature to the landing page.
# Get started
URL: /docs
Learn about ship.pluv.io, and how it can help you build your next product.
## Welcome to ship.pluv.io! 👋
[ship.pluv.io](https://ship.pluv.io) comes with several different boilerplates. Depending on the products you're building, one boilerplate may be a better starting point than another.
## Why ship.pluv.io?
### More than your typical boilerplate
If starting with [create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app) is like starting from zero, and traditional boilerplates are like starting from one, then [ship.pluv.io](https://ship.pluv.io) templates are like starting from two.
When it comes to building many types of products, there is an amount of boilerplating you will need to do before working on your product's actual core. These can include setting up:
1. The framework
* Next.js
* TailwindCSS
* ESLint
* TypeScript
**[create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app) ends here**
2. Your design system
3. Your database & storage
4. Billing & subscriptions
5. Authentication
6. Hosting & deployment
**Most boilerplates end here (e.g. [nextjs-starter-kit](https://github.com/michaelshimeles/nextjs-starter-kit))**
7. Analytics & tracking
8. Emails
9. AI integration
10. Team and members management
11. Cryptographically secure API key management
12. Usage tracking and dashboards for metered-billing
13. Landing page
14. Marketing blog
15. Product documentation
16. SEO metadata (with OpenGraph images)
**[ship.pluv.io](https://ship.pluv.io) ends here**
Your product might not need everything included in this list; but it may be easier to remove the parts you don't need, than to add the parts you do. That depends on what you are building.
In any case, these templates may be valuable as having additional options when starting your next product; or as educational resources when you can reference eventually.
### Why not ship.pluv.io?
The more batteries that are included in any tool you use, the more opinionated it will be. If your product deviates significantly from what these templates provide, it may make more sense to build from boilerplates like [nextjs-starter-kit](https://github.com/michaelshimeles/nextjs-starter-kit) that is geared more towards simply setting up your codebase than providing baseline features.
# CLI Monorepo
URL: /docs/picking-a-template/cli-monorepo
CLI boilerplate to be deployed to Vercel and npm. For developers building a CLI product.
## Project structure
Click through the file tree to see the full product structure.
## Tech Stack
* Framework:
* Web: [Next.js](https://nextjs.org) v15 with App Router
* CLI: [Pastel](https://github.com/vadimdemedes/pastel)
* Language: [TypeScript](https://www.typescriptlang.org/) with strict mode
* Build system: [Turborepo](https://turborepo.com)
* Styling: [TailwindCSS](https://tailwindcss.com/) v4 + [shadcn/ui](https://ui.shadcn.com/)
* Database: [NeonDB](https://neon.com/) (PostgreSQL) + [Drizzle ORM](https://orm.drizzle.team/)
* Auth: [Lucia](https://lucia-auth.com/) + GitHub OAuth
* Payments: [Stripe](https://stripe.com/)
* AI: [OpenRouter](https://openrouter.ai/) + [AI SDK](https://ai-sdk.dev/)
* Deployment:
* Web: [Vercel](https://vercel.com/)
* CLI: [npm](https://npmjs.com/) + [Changesets](https://github.com/changesets/changesets)
* DNS: [Cloudflare DNS](https://www.cloudflare.com/application-services/products/dns/)
## SEO
* Includes all important SEO tags
* Generate OG images via [@vercel/og](https://vercel.com/docs/og-image-generation)
* Typesafe JSON-LD rich snippet component
* Automatic sitemap generation
## Database
* Powered-by [NeonDB](https://neon.com/) (PostgreSQL) and [Drizzle ORM](https://orm.drizzle.team/)
* Commands to generate and apply migrations to local, staging or production databases
* [Docker](https://www.docker.com/) to set-up Postgres locally
* Guides on setting up NeonDB, local Postgres and migrations
## Styling
* Powered-by [TailwindCSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com)
* Swappable themes via shadcn themes
* Automatic light/dark mode
* Includes many custom components to support current features
## Auth
* Powered-by [Lucia](https://lucia-auth.com)
* GitHub OAuth included (for web and CLI)
* Authenticated CLI commands
## Payment
* Powered-by [Stripe](https://stripe.com)
* One-time payments
* e.g. [TailwindCSS Plus](https://tailwindcss.com/plus#pricing)'s components and templates
* Download past invoices
* Webhooks to handle stripe real-time events
* Guides on setting up Stripe and reducing fraud
* Guides on setting up [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks) for webhook testing
* Guides on paywalling CLI commands
## Email
* Guides on setting up [Cloudflare Email Routing](https://www.cloudflare.com/developer-platform/products/email-routing) to send & receive emails from custom domains via Gmail
## AI
* Powered-by [OpenRouter](https://openrouter.ai/) and [AI SDK](https://ai-sdk.dev/)
* Supports hundreds of models via OpenRouter
## Teams management
* Includes only user accounts (i.e. no teams and members management)
## Deployment
* Powered-by [Vercel](https://vercel.com/), [npm](https://npmjs.com/) and [Changesets](https://github.com/changesets/changesets)
* Guides on setting up your Cloudflare DNS
* Guides on setting up automated deployments for your CLI to [npm](https://npmjs.com/) via [Changesets](https://github.com/changesets/changesets)
## Misc.
* ChatGPT prompts for generating Terms of Service and Privacy Policies
# Cloudflare Monorepo
URL: /docs/picking-a-template/cloudflare-monorepo
Boilerplate to be deployed to Cloudflare. Recommended for more technical products using the Cloudflare Suite.
## Project structure
Click through the file tree to see the full product structure.
## Tech Stack
* Framework: [Next.js](https://nextjs.org) v15 with App Router
* Language: [TypeScript](https://www.typescriptlang.org/) with strict mode
* Build system: [Turborepo](https://turborepo.com)
* Styling: [TailwindCSS](https://tailwindcss.com/) v4 + [shadcn/ui](https://ui.shadcn.com/)
* Database: [Cloudflare D1](https://www.cloudflare.com/developer-platform/products/d1/) (SQLite) + [Drizzle ORM](https://orm.drizzle.team/)
* Auth: [Lucia](https://lucia-auth.com/) + GitHub OAuth
* Payments: [Stripe](https://stripe.com/)
* Email: [Resend](https://resend.com) + [React Email](https://react.email/)
* AI: [OpenRouter](https://openrouter.ai/) + [AI SDK](https://ai-sdk.dev/)
* Real-time: [pluv.io](https://pluv.io) + [Durable Objects](https://www.cloudflare.com/developer-platform/products/durable-objects/)
* Analytics: [PostHog](https://posthog.com), [Cloudflare Web Analytics](https://www.cloudflare.com/web-analytics) + [Google Analytics](https://developers.google.com/analytics)
* Blog: [Notion X](https://github.com/NotionX)
* Docs: [Fumadocs](https://fumadocs.dev/)
* Deployment: [OpenNext](https://opennext.js.org/) + [Cloudflare Workers](https://workers.cloudflare.com/)
* DNS: [Cloudflare DNS](https://www.cloudflare.com/application-services/products/dns/)
## SEO
* Includes all important SEO tags
* Generate OG images via [@vercel/og](https://vercel.com/docs/og-image-generation)
* Typesafe JSON-LD rich snippet component
* Automatic sitemap generation
## Database
* Powered-by [Cloudflare D1](https://developers.cloudflare.com/d1/) (SQLite) and [Drizzle ORM](https://orm.drizzle.team/)
* Commands to generate and apply migrations to local, staging or production databases
* Run drizzle studio on local Cloudflare D1
* Guides on setting up Cloudflare D1 and migrations
## Styling
* Powered-by [TailwindCSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com)
* Swappable themes via shadcn themes
* Automatic light/dark mode
* Includes many custom components to support current features
## Auth
* Powered-by [Lucia](https://lucia-auth.com)
* GitHub OAuth included (for web)
## Payment
* Powered-by [Stripe](https://stripe.com)
* Flat-rate subscriptions + usage-based overage fees
* e.g. [NeonDB](https://neon.com/pricing)'s "Launch" tier, and paying for more GBs that what's included.
* View the running billing estimate for next billing cycle
* Dashboard to track usage over time
* Download past invoices
* Webhooks to handle stripe real-time events
* Guides on setting up Stripe and reducing fraud
* Guides on setting up [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks) for webhook testing
## Email
* Powered-by [Resend](https://resend.com) and [React Email](https://react.email)
* Includes welcome email for newly created accounts
* Includes team invitation email, with limited ttl invite codes
* Guides on setting up Resend and avoiding the spam folder
* Guides on setting up [Cloudflare Email Routing](https://www.cloudflare.com/developer-platform/products/email-routing) to send & receive emails from custom domains via Gmail
## AI
* Powered-by [OpenRouter](https://openrouter.ai/) and [AI SDK](https://ai-sdk.dev/)
* Supports hundreds of models via OpenRouter
## Real-time collaboration
* Powered-by [pluv.io](https://pluv.io) and [Durable Objects](https://www.cloudflare.com/developer-platform/products/durable-objects/)
* Room authorization, real-time data and presence
## Analytics
* Powered-by [PostHog](https://posthog.com) [Cloudflare Web Analytics](https://www.cloudflare.com/web-analytics) and [Google Analytics](https://developers.google.com/analytics)
## Teams management
* Teams, members and team invitations included
* Create projects under personal accounts or teams
* Invite members by email with short TTL invite codes
## Blog
* Powered-by [Notion X](https://github.com/NotionX)
* Write blog content with [Notion](https://notion.com) and Notion AI
* Auto-generate SEO titles and descriptions with Notion AI
## Docs
* Powered-by [Fumadocs](https://fumadocs.dev/)
* Write product documentation in MDX
* Auto-generate OG images
* AI friendly with llms.txt generation and AI-powered search
## Deployment
* Powered-by [OpenNext](https://opennext.js.org/) and [Cloudflare Workers](https://workers.cloudflare.com/)
* Guides on setting up [Wrangler](https://developers.cloudflare.com/workers/wrangler/) configs
* Guides on setting up your Cloudflare DNS
* Guides on deployment and connecting multiple [Next.js zones](https://nextjs.org/docs/app/guides/multi-zones)
## Misc.
* ChatGPT prompts for generating Terms of Service and Privacy Policies
* Secure API key generation
# Picking a Template
URL: /docs/picking-a-template
undefined
[ship.pluv.io](https://ship.pluv.io) templates each provide different features that could make one a better starting point for building your app than another. Carefully review the features below to make sure that the template you choose is right for your product.
## Template differences
Below outlines the main differences between the boilerplates for your consideration:
| Feature | Cloudflare Monorepo | Vercel Monorepo | CLI Monorepo | Resume |
| -------------------- | :----------------------------------------------------: | :-------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------: |
| Codebase type | [Turborepo](https://turborepo.com/) monorepo | [Turborepo](https://turborepo.com/) monorepo | [Turborepo](https://turborepo.com/) monorepo | Monolith |
| Payment | Flat rate subscriptions + usage-based overage charges | Flat rate subscriptions | One-time payments | N/A |
| Auth | [Lucia](https://lucia-auth.com/) (with GitHub OAuth) | [Better Auth](https://www.better-auth.com/) (with GitHub OAuth) | [Lucia](https://lucia-auth.com/) (with GitHub OAuth for web and CLI) | N/A |
| Database | [Cloudflare D1](https://developers.cloudflare.com/d1/) | [NeonDB](https://neon.com/) | [NeonDB](https://neon.com/) | N/A |
| Key/value store | [Workers KV](https://developers.cloudflare.com/kv/) | [Upstash](https://upstash.com/) | [Upstash](https://upstash.com/) | N/A |
| Blog | [Notion X](https://github.com/NotionX) | [Notion X](https://github.com/NotionX) | N/A | N/A |
| Docs | [Fumadocs](https://fumadocs.dev/) | [Fumadocs](https://fumadocs.dev/) | N/A | N/A |
| Team management | Teams, members and team invitations | Teams, members and team invitations | No teams, just user accounts | N/A |
| Usage dashboard | Users can track usage over time for billing purposes | N/A | N/A | N/A |
| Deployment + Hosting | [Cloudflare Workers](https://workers.cloudflare.com/) | [Vercel](https://vercel.com/) | [Vercel](https://vercel.com/) + [npm](https://www.npmjs.com/) (via [Changesets](https://github.com/changesets)) | [GitHub Pages](https://pages.github.com/) |
| Misc. | N/A | N/A | [React](https://react.dev/) CLIs via [Pastel](https://github.com/vadimdemedes/pastel) | [React](https://react.dev/) resumes via [React-PDF](https://react-pdf.org/) |
This table covers only the main differences between each of these templates. Review the full features set below to see a more complete list.
### Recommendations
Below are some general recommendations for when to select one template over another. Note that these are simply recommendations as starting points, and any template can be extended to build any product that another template might be better for.
* Cloudflare Monorepo
* If users pay-by-usage (i.e. metered billing)
* Best for products where your operating costs scale closely by usage (e.g. LLM tokens or API calls)
* If your product is more technical to build and benefits from Cloudflare's [product suite](https://developers.cloudflare.com/products/)
* Vercel Monorepo
* If users purchase flat-rate (monthly) subscriptions
* Best for products where your operating costs aren't so different between users (i.e. more predictable)
* If your product is less technical to build (e.g. closer to a basic CRUD or LLM app)
* CLI Monorepo
* If you are building a CLI, especially if some commands are paywalled
* If users purchase individual items, not subscriptions
* If you do not need teams and seats
* Resume
* If you need to make a resume
## Full feature set
# Resume
URL: /docs/picking-a-template/resume
Boilerplate to be build a resume. Land your next job with a beautiful resume.
This template is currently under development, and will be available soon.
If you purchase this template, you will be able to access it once it is ready.
## Tech Stack
* Library: [React PDF](https://react-pdf.org/)
* Language: [TypeScript](https://www.typescriptlang.org/) with strict mode
* Styling: [TailwindCSS](https://tailwindcss.com/) (subset)
## Project structure
Click through the file tree to see the full product structure.
# Vercel Monorepo
URL: /docs/picking-a-template/vercel-monorepo
Boilerplate to be deployed to Vercel. Simpler to start-up for most SaaS applications.
## Project structure
Click through the file tree to see the full product structure.
## Tech Stack
* Framework: [Next.js](https://nextjs.org) v15 with App Router
* Language: [TypeScript](https://www.typescriptlang.org/) with strict mode
* Build system: [Turborepo](https://turborepo.com)
* Styling: [TailwindCSS](https://tailwindcss.com/) v4 + [shadcn/ui](https://ui.shadcn.com/)
* Database: [NeonDB](https://neon.com/) (PostgreSQL) + [Drizzle ORM](https://orm.drizzle.team/)
* Auth: [Better Auth](https://www.better-auth.com/) + GitHub OAuth
* Payments: [Stripe](https://stripe.com/)
* Email: [Resend](https://resend.com) + [React Email](https://react.email/)
* AI: [OpenRouter](https://openrouter.ai/) + [AI SDK](https://ai-sdk.dev/)
* Real-time: [pluv.io](https://pluv.io)
* Analytics: [PostHog](https://posthog.com) + [Google Analytics](https://developers.google.com/analytics)
* Blog: [Notion X](https://github.com/NotionX)
* Docs: [Fumadocs](https://fumadocs.dev/)
* Deployment: [Vercel](https://vercel.com/)
* DNS: [Cloudflare DNS](https://www.cloudflare.com/application-services/products/dns/)
## SEO
* Includes all important SEO tags
* Generate OG images via [@vercel/og](https://vercel.com/docs/og-image-generation)
* Typesafe JSON-LD rich snippet component
* Automatic sitemap generation
## Database
* Powered-by [NeonDB](https://neon.com/) (PostgreSQL) and [Drizzle ORM](https://orm.drizzle.team/)
* Commands to generate and apply migrations to local, staging or production databases
* [Docker](https://www.docker.com/) to set-up Postgres locally
* Guides on setting up NeonDB, local Postgres and migrations
## Styling
* Powered-by [TailwindCSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com)
* Swappable themes via shadcn themes
* Automatic light/dark mode
* Includes many custom components to support current features
## Auth
* Powered-by [Better Auth](https://www.better-auth.com/)
* GitHub OAuth included (for web)
## Payment
* Powered-by [Stripe](https://stripe.com)
* Flat-rate subscriptions
* e.g. [Netflix](https://help.netflix.com/en/node/24926)'s "Standard" tier
* Download past invoices
* Webhooks to handle stripe real-time events
* Guides on setting up Stripe and reducing fraud
* Guides on setting up [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks) for webhook testing
## Email
* Powered-by [Resend](https://resend.com) and [React Email](https://react.email)
* Includes welcome email for newly created accounts
* Includes team invitation email, with limited ttl invite codes
* Guides on setting up Resend and avoiding the spam folder
* Guides on setting up [Cloudflare Email Routing](https://www.cloudflare.com/developer-platform/products/email-routing) to send & receive emails from custom domains via Gmail
## AI
* Powered-by [OpenRouter](https://openrouter.ai/) and [AI SDK](https://ai-sdk.dev/)
* Supports hundreds of models via OpenRouter
## Real-time collaboration
* Powered-by [pluv.io](https://pluv.io)
* Room authorization, real-time data and presence
## Analytics
* Powered-by [PostHog](https://posthog.com) and [Google Analytics](https://developers.google.com/analytics)
## Teams management
* Teams, members and team invitations included
* Create projects under personal accounts or teams
* Invite members by email with short TTL invite codes
## Blog
* Powered-by [Notion X](https://github.com/NotionX)
* Write blog content with [Notion](https://notion.com) and Notion AI
* Auto-generate SEO titles and descriptions with Notion AI
## Docs
* Powered-by [Fumadocs](https://fumadocs.dev/)
* Write product documentation in MDX
* Auto-generate OG images
* AI friendly with llms.txt generation and AI-powered search
## Deployment
* Powered-by [Vercel](https://vercel.com/)
* Guides on setting up your Cloudflare DNS
* Guides on deployment and connecting multiple [Next.js zones](https://nextjs.org/docs/app/guides/multi-zones)
## Misc.
* ChatGPT prompts for generating Terms of Service and Privacy Policies
# pluv CLI
URL: /docs/pluv-cli
Learn how to use the pluv CLI to create new projects.
When you unlock templates on ship.pluv.io, you will be given an invitation to a GitHub repository for that template, allowing you to clone the repo, open issues and ask questions.
ship.pluv.io also provides a CLI with which you can also use to bootstrap your projects like you would with the [create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app) CLI.
## Usage
```package-install
pluv -g
```
After installing the CLI, you can create your new project by running the following commands:
```bash title="Bash terminal"
# Login to your ship.pluv.io account
pluv ship login
# Verify that you are logged in
pluv ship whoami
# List all of the templates you have access to (i.e. unlocked)
pluv ship list
# Create a new project from a template
# Options are:
# - vercel-monorepo
# - cloudflare-monorepo
# - cli-monorepo
# - resume
pluv ship create vercel-monorepo
```
When creating a new project with the CLI, you will be prompted to enter a name for your project. The project will be created in a new directory with the same name as your project (relative to the directory you are running these commands from).