To initialize a new project with turbo-kit, simply run the following command:
pnpm create turbo-kit@latestCurrently turbo-kit works with only pnpm. There is an issue with the template repo that breaks the conversion to other package managers that is typically handled by create-turbo.
turbo-kit is Evan Yu's opinionated starter monorepo template for Turborepo + Next.js with tRPC, Drizzle, Shadcn/ui, Better Auth, and more.
It is based heavily on create-t3-turbo with some modifications to help you get started quickly. (notably, removing Tanstack Start and Expo)
It uses Turborepo and contains:
.github
└─ workflows
└─ CI with pnpm cache setup
.vscode
└─ Recommended extensions and settings for VSCode users
apps
├─ nextjs
│ ├─ Next.js 15
│ ├─ React 19
│ ├─ Tailwind CSS v4
│ └─ E2E Typesafe API Server & Client
packages
├─ api
│ └─ tRPC v11 router definition
├─ auth
│ └─ Authentication using better-auth.
├─ db
│ └─ Typesafe db calls using Drizzle & Supabase
├─ email
│ └─ React Email templates
├─ redis
│ └─ Upstash Redis client
├─ types
│ └─ The types used in the application
└─ ui
└─ Start of a UI package for the webapp using shadcn-ui
tooling
├─ eslint
│ └─ shared, fine-grained, eslint presets
├─ prettier
│ └─ shared prettier configuration
├─ tailwind
│ └─ shared tailwind theme and configuration
└─ typescript
└─ shared tsconfig you can extend from
To get it running, follow the steps below:
# Install dependencies
pnpm i
# Configure environment variables
# There is an `.env.example` in the root directory you can use for reference
vim .envNavigate to Settings > Developer Settings > Oauth Apps > New, and fill out the details.
For the Authorization callback URL, fill in http://localhost:3000/api/auth/callback/github.
If you are deploying the auth proxy, change the domain to your production domain
# Push the Drizzle schema to the database
pnpm db:push
# Spin up docker container
# First run will take a while to download the images, and set things up
# Subsequent runs will be faster
docker compose up -d
# Run next dev server
# This command actually also runs the above docker command
pnpm run devThis project uses Drizzle ORM for type-safe database queries. The database schema is defined in packages/db/src/schema/index.ts.
Here's an example of how to define a table using Drizzle:
import { pgTable } from "drizzle-orm/pg-core";
export const posts = pgTable("posts", (t) => ({
id: t.uuid().notNull().primaryKey().defaultRandom(),
title: t.varchar({ length: 256 }).notNull(),
content: t.text().notNull(),
createdAt: t.timestamp().defaultNow().notNull(),
updatedAt: t
.timestamp({ mode: "date", withTimezone: true })
.$onUpdate(() => new Date()),
}));The database client is exported from @silo-storage/db/client. Here are some example queries:
import { db } from "@silo-storage/db/client";
import { posts } from "@silo-storage/db/schema";
import { eq, desc } from "@silo-storage/db";
// Find all posts
const allPosts = await db.query.posts.findMany({
orderBy: desc(posts.createdAt),
limit: 10,
});
// Find a post by ID
const post = await db.query.posts.findFirst({
where: eq(posts.id, postId),
});
// Insert a new post
await db.insert(posts).values({
title: "My Post",
content: "Post content here",
});
// Update a post
await db.update(posts)
.set({ title: "Updated Title" })
.where(eq(posts.id, postId));
// Delete a post
await db.delete(posts).where(eq(posts.id, postId));For input validation and type inference, check out packages/types/src/index.ts. This package uses drizzle-zod to generate Zod schemas from your Drizzle tables, which can be used for both runtime validation and type inference.
For more in-depth information about Drizzle ORM, see the official Drizzle documentation.
This project uses tRPC for end-to-end type-safe APIs. Routers are defined in packages/api/src/router/ and combined in packages/api/src/root.ts.
tRPC uses procedures to define your API endpoints. This template provides two types of procedures:
-
publicProcedure: A base procedure that doesn't require authentication. Anyone can call these endpoints, but you can still access session data if the user is logged in (viactx.session). -
protectedProcedure: A procedure that requires authentication. It verifies the session is valid and guaranteesctx.session.useris not null. If a user is not authenticated, it throws anUNAUTHORIZEDerror.
Here's an example of a simple tRPC router:
import type { TRPCRouterRecord } from "@trpc/server";
import { z } from "zod/v4";
import { desc, eq } from "@silo-storage/db";
import { posts } from "@silo-storage/db/schema";
import { CreatePostSchema } from "@silo-storage/types";
import { protectedProcedure, publicProcedure } from "../trpc";
export const postRouter = {
// Public query - fetch all posts
all: publicProcedure.query(async ({ ctx }) => {
return ctx.db.query.posts.findMany({
orderBy: desc(posts.createdAt),
limit: 10,
});
}),
// Public query with input - fetch post by ID
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.db.query.posts.findFirst({
where: eq(posts.id, input.id),
});
}),
// Protected mutation - create a post (requires authentication)
create: protectedProcedure
.input(CreatePostSchema)
.mutation(async ({ ctx, input }) => {
return ctx.db.insert(posts).values(input);
}),
// Protected mutation - delete a post
delete: protectedProcedure
.input(z.string())
.mutation(async ({ ctx, input }) => {
return ctx.db.delete(posts).where(eq(posts.id, input));
}),
} satisfies TRPCRouterRecord;Routers are combined in packages/api/src/root.ts:
import { postRouter } from "./router/post";
import { createTRPCRouter } from "./trpc";
export const appRouter = createTRPCRouter({
post: postRouter,
// Add more routers here
});For more examples, see the existing routers in packages/api/src/router/. For comprehensive tRPC documentation, visit https://trpc.io/docs/.
This project uses Better Auth for authentication. The template comes with a pre-generated auth schema, so you can start using authentication features right away.
However, if you make any changes to the auth configuration (e.g., adding new providers, plugins, or modifying the auth setup), you'll need to regenerate the auth schema:
# Generate the Better Auth schema
pnpm --filter @silo-storage/auth generateThis command runs the Better Auth CLI with the following configuration:
- Config file:
packages/auth/script/auth-cli.ts- A CLI-only configuration file (isolated from src to prevent imports) - Output:
packages/db/src/schema/auth.ts- Generated Drizzle schema for authentication tables
The generation process:
- Reads the Better Auth configuration from
packages/auth/script/auth-cli.ts - Generates the appropriate database schema based on your auth setup
- Outputs a Drizzle-compatible schema file to the
@silo-storage/dbpackage
Note: The
auth-cli.tsfile is placed in thescript/directory (instead ofsrc/) to prevent accidental imports from other parts of the codebase. This file is exclusively for CLI schema generation and should not be used directly in your application. For runtime authentication, use the configuration frompackages/auth/src/index.ts.
For more information about the Better Auth CLI, see the official documentation.
Better-auth comes with an auth proxy plugin. By deploying the Next.js app, you can get OAuth working in preview deployments and development.
By using the proxy plugin, the Next.js apps will forward any auth requests to the proxy server, which will handle the OAuth flow and then redirect back to the Next.js app. This makes it easy to get OAuth working since you'll have a stable URL that is publicly accessible and doesn't change for every deployment and doesn't rely on what port the app is running on. So if port 3000 is taken and your Next.js app starts at port 3001 instead, your auth should still work without having to reconfigure the OAuth provider.
Run the ui-add script to add a new UI component using the interactive shadcn/ui CLI:
pnpm ui-addWhen the component(s) has been installed, you should be good to go and start using it in your app.
To add a new package, simply run pnpm turbo gen init in the monorepo root. This will prompt you for a package name as well as if you want to install any dependencies to the new package (of course you can also do this yourself later).
The generator sets up the package.json, tsconfig.json and a index.ts, as well as configures all the necessary configurations for tooling around your package such as formatting, linting and typechecking. When the package is created, you're ready to go build out the package.
No, it does not. The api package should only be a production dependency in the Next.js application where it's served. This lets you have full typesafety in your client applications, while keeping your backend code safe.
If you need to share runtime code between the client and server, such as input validation schemas, you can create a separate shared package for this and import it on both sides.
See Next.js docs - Local Development
Docker is used in this project to run only the development databases and services.
Let's deploy the Next.js application to Vercel. If you've never deployed a Turborepo app there, don't worry, the steps are quite straightforward. You can also read the official Turborepo guide on deploying to Vercel.
-
Create a new project on Vercel, select the
apps/nextjsfolder as the root directory. Vercel's zero-config system should handle all configurations for you. -
Add your
POSTGRES_URLenvironment variable. -
Done! Your app should now be successfully deployed.
This template is pre-configured to use Upstash, a fast and cheap redis provider. You can either create a free-tier account, or self-host redis + SRH
The auth proxy comes as a better-auth plugin. This is required for the Next.js app to be able to authenticate users in preview deployments. The auth proxy is not used for OAuth request in production deployments. The easiest way to get it running is to deploy the Next.js app to vercel.
The stack originates from create-t3-turbo and create-t3-app.