Skip to content

Latest commit

 

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Monorepo Template

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

Overview (Simple)

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.

Steps

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 via featureToggles.ts
  1. Add your intro paragraph to the root AGENTS.md and CLAUDE.md files
  2. Have Claude-Code setup everything else based on this readme, ai files and project structure
  3. Ensure you have setup providers and .env variables
  4. Ensure you have setup github secrets if you are using the .github workflows

Production Deployments

  • You will need to manually setup your deployments to get this on the internet.

Recommendations

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.

Advanced

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.

Overview (Further)

Part 1 Services/Repositories Pattern

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.

Part 2 Server/Client relationship

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.

Server/Client Rules

  • Always do data fetching on the server usually async page.tsx or layout.tsx files.
  • 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)
  • secureFetch requires a user session to exist
  • publicFetch does not require a user session
  • pnpm lint enforces the route → service → repository data flow as errors. Route handlers, actions, and services cannot reach into Drizzle, database schemas, or db.
  • 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 under app/(routes)/api/core/.

API layer wrapper function

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.

Public API routes

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} & req work 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 API routes

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

Summary of API Arcitecture

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.

Summary of API Arcitecture (Technical)

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

Putting it together

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) {}
}

Tech Stack

  • 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

Quick Start (Docker Compose)

# From repo root
docker compose up --build -d

pnpm db:migrate

pnpm run dev

The web app runs on the host. Configure .env.local using .env.example:

  • DATABASE_URL=postgres://postgres:postgres@localhost:5432/better-stack?sslmode=disable (use POSTGRES_PORT if overridden)
  • NEXT_PUBLIC_URL is the browser-facing origin (localhost, LAN, or Tailscale as appropriate).

To stop:

docker compose down

Development commands (Monorepo)

# 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 lint

Useful 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:dev

Database tools (via workspace scripts):

pnpm db:generate
pnpm db:migrate
pnpm db:studio

Configuration

Create .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=disable

Docker 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.

Project Structure

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

Key Features

  • 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

Deployment

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

License

MIT License - see LICENSE file for details.

Template maintenance and security defaults

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_URL explicitly to the browser-facing origin, including for production builds. INTERNAL_URL optionally selects the server-to-server fetch origin. Development still binds 0.0.0.0:3000; this machine's LAN and Tailscale hosts are discovered automatically. Add custom hosts through DEV_ALLOWED_ORIGINS, rather than broad domain wildcards.
  • Docker Postgres and pgweb bind to loopback only, using POSTGRES_PORT and PGWEB_PORT when configured. They use development credentials and are not public services. Mobile devices use the web API, not Postgres.
  • FROM_EMAIL is a complete sender address, such as noreply@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 name and image; image: null clears 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:migrate against 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.

About

No description, website, or topics provided.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages