A production-ready monorepo template for building modern web applications with:
- Next.js App Router with TypeScript
- Better-auth for authentication
- Drizzle ORM with PostgreSQL
- Tailwind CSS for styling
- Workspace management with pnpm
- I18n Translations OPTIONAL
Perfect starting point for SaaS applications, dashboards, and full-stack projects. This stack is optimised for AI agent workflows. Generally speaking when these patterns are followed then code review becomes much simpler and refactoring later becomes trivial. Most template repos contain loads of crap that most people wont use and will just slow them down. The core concept here has 2 parts.
Setting up a database provider and Email provider is required for all projects. This template runs a database locally via docker alternatively you can use a remote database but I recommend using a local database since it is free.
PreSteps:
- Neon or Supabase are great postgres providers
- Resend is a great email provider (Tip: If you have multiple projects you can get free accounts with a single email if you use gmail
with
email_name+project_name@provider_name.com) Email verification is disabled locally viafeatureToggles.ts
- Add your intro paragraph to the root
AGENTS.mdandCLAUDE.mdfiles - Have Claude-Code setup everything else based on this readme, ai files and project structure
- Ensure you have setup providers and
.envvariables - Ensure you have setup github secrets if you are using the .github workflows
- You will need to manually setup your deployments to get this on the internet.
Railway: Railway is a great tool for website deployments and advanced features in the future. Neon: Neon is a cheap serverless database provider. Resend: Resend is your email SMTP provider.
Other good options: Vercel: A great tool for weekend projects but can get very expensive for production uses with many uses. [Supabase]: Another great database provider with other optional features, but more complex and often introduces breaking changes.
All of the above platforms integrate with Github 1 click deployments if you choose, some I have setup workflows to ensure a deployment is consistent.
This does not cover many of the other optional things you could use because not everyone will want them. Somethings you might want to look into later are as follows.
- I18n to allow you to offer your product in other languages.
- Mixpanel for advanced analytics once you have users.
- OAuth and BetterAuth plugins.
Service/repository is a popular pattern both in AI coding and traditioanl coding. It shines as a core business layer for the following reasons
- DDD or Onion Architecture.
- Consistently testable.
- Easy to refactor and extend.
- Easy to for example completely swap from Drizzle to Supabase ORM at any point, just replace the repository layer for a single service at a time.
- Simpler code reviews
- Easier for AI to find references via services rather then complex API route structurues reducing context side and repetitive code.
Next.JS server actions were actually designed as a sort of proxy to access API layers. People have just started using them incorrectly for direct backend executions. Exposing API configuration opens not only the current route but your entire platform to direct to public API access. There are several situations where I have collected hundreds of thousands of emails or user information because of this exact thing.
A simple example if I can see that /users/1234 exists why cant I run a script that hits every single api call from 0 to 1,000,000? or some people even have routes like /users/list which gets me all of this data in a single API request.
Protecting our API is arguably the most important part of any project.
- Always do data fetching on the server usually async
page.tsxorlayout.tsxfiles. - Never call an API route directly from the client (If you need to trigger an API route from the client use the Server Actions as they are intended)(use the
secureFetch()&publicFetch()methods I have created. They cannot be imported on the client and will fail) secureFetchrequires a user session to existpublicFetchdoes not require a user sessionpnpm lintenforces the route → service → repository data flow as errors. Route handlers, actions, and services cannot reach into Drizzle, database schemas, ordb.- Server actions cannot bypass core routes through repositories, services, auth internals, or third-party SDKs.
- Client modules cannot import modules marked with
server-only, and core route handlers belong underapp/(routes)/api/core/.
This part can be a bit hard to understand its castings so I would rather just understand how to use it rather then how it works.
The following code is a simple example of a public called api route
- PATCH works exactly as next.js does by default
- createRouteHander is a route specific authentication layer. This can be expanded in the future to cover things like tenant based authentication ect.
{params}&reqwork exactly the same as normal Next.js for public routes.
export const PATCH = createRouteHandler(
{ isPublic: true },
async (req, { params }: { params: Promise<{ param: string }> }) => {
const { param } = await params;
return NextResponse.json(
{
data: {
param,
},
error: null,
},
{ status: 200 }
);
}
);see /(routes)/api/core/v1/users/[params] for the actual API route
Secure routes work in tandem with secureFetch() here we dont have any params but we have an attatched user and isAuthenticated: true requires a user to be able to access this route.
export const GET = createRouteHandler(
{ isAuthenticated: true },
async (req) => {
// The authenticated user sent via secureFetch() method in any application
const user = req.user;
const session = req.session;
return NextResponse.json({
data: {
user: user,
},
error: null,
});
}
);see /(routes)/api/core/v1/users for the actual API route to fetch user info
These function exactly as normal API routes in Next.js you can do all the same things with them as normal API routes. This method of flow just adds a route based authentication layer that can be easily extended in the future.
I want to emphisis the importance of layers here. Layers and patterns are repeatable and maintainable. AI is a pattern machine, the goal here is to always be moving forward quickly, these patterns allow me and my tools to move as quickly as possible.
These are called higher order functions that take a function as an arguement. Feel free to look at how createRouteHander works if you want to learn more. I personally use this with tenant based architecure and pro service plans to account for things like the following example
See /(routes)/api/core/v1/users for the full backend usage example with these patterns icluded but an API route should only be around 4-5 lines of actual code always.
export const PATCH = createRouteHandler({ isAuthenticated: true }, async (req, { params }: { params: Promise<{ userId: string }> }) => {
try {
// 1. Route segments
const { userId } = await params
const body = await req.json()
// 2. Form valiadtions
const validatedData = updateUserSettingsSchema.parse(body)
// 3. Business Logic
const userService = createUserService(req)
await userService.updateUserSettings(userId, validatedData)
// 4. Results
return NextResponse.json({
data: { success: true },
error: null,
})
} catch (error: any) {}
}- Frontend: Next.js 16+ with App Router and TypeScript
- Database: PostgreSQL with Drizzle ORM
- Authentication: Better-auth (email/password + OAuth)
- Styling: Tailwind CSS with custom components
- Package Manager: pnpm with workspaces
- Development: Docker Compose for local development
# From repo root
docker compose up --build -d
pnpm db:migrate
pnpm run dev- Web app: http://localhost:3000 (also reachable on your Tailscale hostname/IP; the dev server binds
0.0.0.0) - Postgres: localhost:5432
- PGWeb (DB UI): http://localhost:5050
The web app runs on the host. Configure .env.local using .env.example:
DATABASE_URL=postgres://postgres:postgres@localhost:5432/better-stack?sslmode=disable(usePOSTGRES_PORTif overridden)NEXT_PUBLIC_URLis the browser-facing origin (localhost, LAN, or Tailscale as appropriate).
To stop:
docker compose down# Install deps
pnpm install
# Run all in dev (web + packages)
pnpm dev
# Build all
pnpm build
# Tests
pnpm test
# Architecture and code-quality checks
pnpm lintUseful filters:
pnpm --filter web-app dev
pnpm --filter web-app build
# Expo mobile app (start the web app first — it's the API)
pnpm mobile:devDatabase tools (via workspace scripts):
pnpm db:generate
pnpm db:migrate
pnpm db:studioCreate .env.local at the repo root for local dev (Next.js reads via apps/web-app/lib/env.ts). Example:
NEXT_PUBLIC_URL=http://localhost:3000
AUTH_SECRET=replace-with-a-strong-secret-of-32-chars-min
RESEND_API_KEY=replace-with-your-resend-key
FROM_EMAIL=noreply@example.com
DATABASE_URL=postgres://postgres:postgres@localhost:5432/better-stack?sslmode=disableDocker Compose starts only Postgres and pgweb. Configure the web app with .env.local; Compose does not set the web app’s DATABASE_URL or NEXT_PUBLIC_URL.
better-stack-monorepo/
├── apps/
│ ├── web-app/ # Next.js application
│ └── mobile-app/ # Expo (React Native) app — authenticates against web-app's API
├── packages/
│ ├── common/ # Shared utilities and types
│ └── database/ # Database schemas and repositories
└── docker/ # Docker configurations
- Modern Stack - Latest Next.js, TypeScript, and Tailwind CSS
- Type Safety - End-to-end type safety with TypeScript and Drizzle
- Authentication - Ready-to-use auth with Better-auth
- Developer Experience - Hot reloading, linting, and formatting
- Production Ready - Docker setup (Local DB) and deployment configurations are simple
My web hosting platform of choice is Railway for all deployments. Neon for our Postgres database We use github actions for database migrations and running out tests before any deployment I use Cloudflare for domain hosting
just add your env variables to Github secrets and Railway env configuration
MIT License - see LICENSE file for details.
Use Node.js 22.13+ and the pinned pnpm version (corepack enable, then pnpm install --frozen-lockfile). Expo's React and native package versions follow its SDK compatibility check; they intentionally need not match the web app. Update Better Auth and its Expo plugin together in both apps.
- Set
NEXT_PUBLIC_URLexplicitly to the browser-facing origin, including for production builds.INTERNAL_URLoptionally selects the server-to-server fetch origin. Development still binds0.0.0.0:3000; this machine's LAN and Tailscale hosts are discovered automatically. Add custom hosts throughDEV_ALLOWED_ORIGINS, rather than broad domain wildcards. - Docker Postgres and pgweb bind to loopback only, using
POSTGRES_PORTandPGWEB_PORTwhen configured. They use development credentials and are not public services. Mobile devices use the web API, not Postgres. FROM_EMAILis a complete sender address, such asnoreply@example.com, on a verified Resend domain. Delivery errors fail the auth email operation instead of reporting success.- The example profile PATCH endpoints accept only
nameandimage;image: nullclears the image. Email and verification status are auth-owned fields. To add email changes, implement Better Auth's verified change-email flow, not a direct profile update. - Session cookie caching is disabled so server-side revocation takes effect on the next request. Authenticated core responses use
Cache-Control: private, no-store. - Run
pnpm db:migrateagainst your local sandbox to install the auth lookup indexes. Apply migrations to a hosted database only as part of your deployment process.
See the security and performance audit for verified fixes, validation, and remaining upstream advisories. pnpm audit may report tooling vulnerabilities even when the app's checks pass; review the affected call path before forcing a dependency override.