Architecture and Technologies of Makerkit Next.js Drizzle
Understand the technologies, architecture, and patterns used in Makerkit. This foundation will help you navigate the codebase and build features effectively.
Before building TeamPulse, let's understand what Makerkit provides and how it's structured. This foundation will help you navigate the codebase and understand the patterns you'll use throughout the course.
What is Makerkit?
Makerkit is a SaaS starter kit that provides the foundational features every SaaS application needs:
- Authentication - Sign up, sign in, password reset, OAuth, MFA
- Multi-tenancy - Organizations, team members, invitations
- Billing - Subscriptions, checkout, customer portal
- Admin Dashboard - User management, metrics, impersonation
- Email - Transactional emails with templates
- Settings - User profile, organization settings, preferences
Instead of building these from scratch, you start with a working foundation and customize for your specific product.
B2B vs B2C Modes
Makerkit supports two account modes:
| Mode | Description | Use Case |
|---|---|---|
| B2B (organizations-only) | Users belong to organizations | Team collaboration tools, business software |
| B2C (personal-accounts-only) | Users have individual accounts | Consumer apps, personal tools |
| Hybrid | Users can have both | Platforms with personal and team features |
In this course, we use B2B mode for TeamPulse - a team feedback tool.
Core Technologies
The Stack
┌─────────────────────────────────────────────────────────┐│ Frontend ││ Next.js 16 + React 19 + TypeScript + Tailwind CSS 4 │├─────────────────────────────────────────────────────────┤│ UI Components ││ shadcn/ui + Base UI + Lucide Icons │├─────────────────────────────────────────────────────────┤│ Authentication + Subscriptions + Multi-tenancy ││ Better Auth │├─────────────────────────────────────────────────────────┤│ Database ││ Drizzle ORM + PostgreSQL │├─────────────────────────────────────────────────────────┤│ Payments ││ Stripe │└─────────────────────────────────────────────────────────┘Next.js 16 with App Router
Next.js is the React framework that powers the application:
- App Router - File-based routing with layouts and loading states
- Server Components - Fetch data on the server, reduce client JavaScript
- Server Actions - Call server functions directly from components
- Streaming - Progressive rendering with Suspense
React 19
The latest React version with:
- Server Components - Default for new components
- Server Actions - Form handling with automatic pending states
TypeScript
Full type safety from database to UI:
- Type-safe database queries with Drizzle
- Type-safe API with Zod validation
- Type-safe forms with react-hook-form
Tailwind CSS 4 + shadcn/ui
Styling approach:
- Tailwind CSS 4 - Utility-first CSS framework
- shadcn/ui - Pre-built accessible components
- Base UI - Headless component primitives
- Lucide Icons - Icon library
Better Auth
Authentication framework that handles:
- Email/password authentication
- OAuth providers (Google, GitHub, etc.)
- Multi-factor authentication (TOTP)
- Session management
- Organizations and teams
- Role-based access control
- Subscriptions and billing
Drizzle ORM
Type-safe database toolkit:
- Schema defined in TypeScript
- SQL-like query builder
- Automatic migrations
- Direct PostgreSQL access
PostgreSQL
As our relational database, we will be using PostgreSQL by default. Postgres is the most popular open-source relational database in the world and it's a great choice for most applications.
Stripe
As our payment processor, we will be using Stripe by default. Stripe is the most popular payment processor in the world and it's a great choice for most applications.
Turborepo & Monorepo Structure
Makerkit uses a monorepo - multiple packages in one repository, managed by Turborepo.
Why a Monorepo?
- Code Sharing - Reuse packages across apps
- Consistent Tooling - Same linting, formatting, TypeScript config
- Atomic Changes - Update related code together
- Simplified Dependencies - One
pnpm installfor everything
How Turborepo Works
Turborepo orchestrates tasks across packages:
pnpm dev # Starts all apps in developmentpnpm typecheck # Type-checks all packagespnpm lint:fix # Lints all packagespnpm format:fix # Formats all codeKey features:
- Task Caching - Skips unchanged packages
- Parallel Execution - Runs independent tasks concurrently
- Dependency Graph - Builds packages in correct order
Package Structure
makerkit/├── apps/│ ├── web/ # Main Next.js application│ └── e2e/ # Playwright end-to-end tests│├── packages/│ ├── ui/ # shadcn/ui components│ ├── database/ # Drizzle schema & migrations│ ├── better-auth/ # Auth configuration & plugins│ ├── auth/ # Auth utilities & middleware│ ├── billing/ # Payment provider abstraction│ │ ├── api/ # Billing API endpoints│ │ ├── core/ # Core billing logic│ │ ├── hooks/ # Billing React hooks│ │ ├── stripe/ # Stripe integration│ │ └── ui/ # Billing UI components│ ├── email-templates/ # React Email templates│ ├── mailers/ # Email sending (Resend, Nodemailer)│ ├── rbac/ # Role-based access control│ ├── accounts/ # Account utilities│ ├── account-hooks/ # Account lifecycle hooks│ ├── account-settings/ # User settings components│ ├── organizations/ # Organization utilities│ ├── organization-hooks/ # Organization lifecycle hooks│ ├── organization-settings/ # Org settings components│ ├── admin/ # Admin dashboard│ ├── i18n/ # Internationalization│ ├── monitoring/ # Error tracking (Sentry)│ ├── analytics/ # Analytics integration│ ├── storage/ # File storage│ ├── otp/ # One-time passwords│ ├── policies/ # Access policies│ ├── cms/ # Content management│ └── shared/ # Shared utilities│├── turbo.json # Turborepo configuration├── pnpm-workspace.yaml # Workspace definition└── package.json # Root dependenciesPackage Dependencies
The diagram below is simplified to show key relationships. The actual dependency graph includes all 24+ packages.
┌─────────────┐ │ apps/web │ └──────┬──────┘ │ ┌─────────────────┼─────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌──────────┐ ┌──────────┐ │ ui │ │ admin │ │ billing │ └─────────┘ └────┬─────┘ └────┬─────┘ │ │ ▼ ▼ ┌──────────┐ ┌──────────┐ │ auth │ │ database │ └────┬─────┘ └──────────┘ │ ▼ ┌────────────┐ │better-auth │ └────────────┘Application Architecture
Request Flow
┌──────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────┐│ Browser │────▶│ Next.js │────▶│ Better Auth │────▶│ Database ││ │◀────│ App Router │◀────│ Session │◀────│PostgreSQL│└──────────┘ └──────────────┘ └─────────────┘ └──────────┘- Browser makes request to Next.js route
- Next.js handles routing, renders Server Components
- Better Auth validates session, provides user context
- Database stores and retrieves data via Drizzle
Multi-Tenancy Model
┌─────────────────────────────────────────────────────────┐│ User ││ (authentication) │└─────────────────────────┬───────────────────────────────┘ │ │ belongs to (via membership) ▼┌─────────────────────────────────────────────────────────┐│ Organization ││ (tenant boundary) │├─────────────────────────────────────────────────────────┤│ Members │ Invitations │ Subscription │├─────────────────────────────────────────────────────────┤│ Application Data ││ (scoped to organization) │└─────────────────────────────────────────────────────────┘- Users can belong to multiple organizations
- Organizations are the tenant boundary
- All data is scoped to organizations
- Members have roles (owner, admin, member)
Billing Flow
The billing flow is as follows:
┌──────────────┐ ┌─────────────┐ ┌──────────┐│ Organization │────▶│ Stripe │────▶│ Checkout ││ Billing │ │ Checkout │ │ Page │└──────────────┘ └─────────────┘ └────┬─────┘ │ │ payment ▼┌──────────────┐ ┌─────────────┐ ┌──────────┐│ Database │◀────│ Webhook │◀────│ Stripe ││ Subscription │ │ Handler │ │ Events │└──────────────┘ └─────────────┘ └──────────┘- User initiates checkout from billing page
- Stripe Checkout handles payment
- Stripe sends webhook events
- Webhook handler updates subscription in database
Webhooks are managed by Better Auth - so you don't need to worry about them.
Key Patterns
Server Components vs Client Components
// Server Component (default) - fetches data on server// renders only on the server sideexport default async function DashboardPage() { const data = await fetchDashboardData(); // Direct DB access return <Dashboard data={data} />;}// Client Component - interactive UI// renders both on the server and the client side (hydrated)'use client';export function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>;}Use Server Components for:
- Data fetching
- Accessing backend resources
- Keeping sensitive logic server-side
Use Client Components for:
- Interactivity (onClick, onChange)
- Browser APIs
- React hooks (useState, useEffect)
Server Actions with next-safe-action
// Define action with validationexport const updateProfileAction = authenticatedActionClient .inputSchema(UpdateProfileSchema) .action(async ({ parsedInput, ctx }) => { const { user } = ctx; await db.update(users) .set({ name: parsedInput.name }) .where(eq(users.id, user.id)); revalidatePath('/settings'); return { success: true }; });// Use in componentconst { execute, status } = useAction(updateProfileAction);const isPending = status === 'executing';Data Loading Pattern
For data loading, we will be using the cache function from the next/cache module. This function is used to cache the data on the server side on a per-request basis. This means that if you fetch the same data across multiple components, the data will be fetched only once and then cached for the duration of the request.
// Loader function (cached, server-side)export const loadDashboardData = cache(async () => { const orgId = await requireActiveOrganizationId(); return db.query.board.findMany({ where: eq(board.organizationId, orgId), });});// Page componentexport default async function DashboardPage() { const data = await loadDashboardData(); return <DashboardView data={data} />;}Feature Overview
Authentication
| Feature | Implementation |
|---|---|
| Email/Password | Better Auth credential provider |
| OAuth | Google, GitHub, etc. via Better Auth |
| Magic Links | Passwordless sign-in |
| MFA/2FA | TOTP with authenticator apps |
| Password Reset | Email-based recovery |
| Email Verification | Required before access |
Organizations & Teams
| Feature | Implementation |
|---|---|
| Create Organization | Auto on signup or manual |
| Invite Members | Email invitations with role |
| Role Hierarchy | Owner > Admin > Member |
| Remove Members | With permission checks |
| Transfer Ownership | Owner can transfer |
Billing & Subscriptions
| Feature | Implementation |
|---|---|
| Checkout | Stripe Checkout sessions |
| Subscriptions | Monthly/yearly plans |
| Plan Limits | Seat-based or feature-based |
| Customer Portal | Manage payment methods |
| Webhooks | Automatic status updates |
Admin Dashboard
| Feature | Implementation |
|---|---|
| User Management | List, search, filter users |
| Ban/Unban | Block user access |
| Impersonation | Act as user temporarily |
| Metrics | User counts, session stats |
| Organization View | Browse all organizations |
Project Files Overview
Key Directories
| Path | Purpose |
|---|---|
apps/web/app/ | Next.js routes and pages |
apps/web/config/ | Application configuration |
apps/web/lib/ | App-specific utilities |
packages/ui/src/ | Shared UI components |
packages/database/src/ | Database schema |
Important Files
| File | Purpose |
|---|---|
apps/web/.env.local | Environment variables (secrets) |
apps/web/config/app.config.ts | App settings validation |
apps/web/config/auth.config.ts | OAuth providers config |
apps/web/config/account-mode.config.ts | Account mode (B2B/B2C) settings |
apps/web/config/feature-flags.config.ts | Feature toggles |
apps/web/app/[locale]/(internal)/_config/navigation.config.tsx | Sidebar navigation |
packages/database/src/schema/ | Database tables |
packages/better-auth/src/billing/config.ts | Pricing plans |
Route Structure
Note: This shows the key route groups. Each group contains additional sub-routes.
apps/web/app/[locale]/├── (public)/ # Public pages│ ├── page.tsx # Home page│ ├── blog/ # Blog pages│ ├── docs/ # Documentation│ ├── faq/ # FAQ page│ ├── changelog/ # Changelog pages│ └── (legal)/ # Legal pages (privacy, terms)├── auth/ # Auth pages (sign-in, sign-up, verify, password-reset)├── (internal)/ # Protected app pages│ ├── dashboard/ # User dashboard│ ├── settings/ # Account settings│ │ ├── billing/ # Subscription management│ │ ├── members/ # Team members│ │ ├── organization/ # Organization settings│ │ ├── preferences/ # User preferences│ │ └── security/ # Security settings (MFA, sessions)│ ├── admin/ # Admin pages (users, organizations)│ └── password-reset/ # Password reset flow└── (invitation)/ # Invitation acceptanceapps/web/app/api/ # API routes (NOT under [locale])├── auth/[...all]/ # Better Auth endpoints└── version/ # Version endpointEnvironment Variables
Environment variables control application behavior. See .env.local.example for the complete list.
Public Variables (exposed to browser)
All variables that are prefixed with NEXT_PUBLIC_ are exposed to the browser. This makes it a good practice to use them for public information like the site URL, the account mode, and the Stripe publishable key - but never use them for sensitive information.
NEXT_PUBLIC_SITE_URL=http://localhost:3000NEXT_PUBLIC_ACCOUNT_MODE=organizations-only # or: personal-only, hybridNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...The NEXT_PUBLIC_ACCOUNT_MODE values control the app mode:
organizations-only- B2B mode, users must belong to an organizationpersonal-only- B2C mode, personal accounts onlyhybrid- Mixed mode, supports both
Private Variables (server-only)
All variables that are not prefixed with NEXT_PUBLIC_ are exposed to the server only and won't be available on the client side.
DATABASE_URL=postgresql://...BETTER_AUTH_SECRET=random-32-char-secretSTRIPE_SECRET_KEY=sk_test_...STRIPE_WEBHOOK_SECRET=whsec_...Common Commands
Here are the commands you'll use most frequently during development.
Development
# Start all services (database, mail, app)pnpm dev# Start only the web app (if services already running)pnpm --filter web devCode Quality
# Type-check all packagespnpm typecheck# Type-check specific packagepnpm --filter web typecheckpnpm --filter @kit/database typecheck# Lint and auto-fix issuespnpm lint:fix# Format code with Prettierpnpm format:fixDatabase
# Generate migration filespnpm --filter @kit/database drizzle:generate# Push schema changes to databasepnpm --filter @kit/database drizzle:push# Run migrationspnpm --filter @kit/database drizzle:migrate# Open Drizzle Studio (database UI)pnpm --filter @kit/database drizzle:studio# Seed the database with mock datapnpm seedTesting
# Run E2E testspnpm --filter e2e test# Run E2E tests with UIpnpm --filter e2e test:uiInstalling Dependencies
# Install all dependenciespnpm install# Add dependency to specific packagepnpm --filter web add package-namepnpm --filter @kit/ui add package-nameUseful Shortcuts
| Command | What it does |
|---|---|
pnpm dev | Start everything for development |
pnpm typecheck | Verify TypeScript is happy |
pnpm lint:fix | Fix linting issues automatically |
pnpm format:fix | Format all code consistently |
pnpm build | Build for production |
Development Workflow
A typical workflow after making changes:
# 1. Make your code changes# 2. Check for TypeScript errorspnpm typecheck# 3. Fix linting issuespnpm lint:fix# 4. Format codepnpm format:fix# 5. Test locallypnpm devReady to Build
You now understand:
- What Makerkit provides out of the box
- The technology stack and why each piece was chosen
- How the monorepo is structured with Turborepo
- The application architecture and data flow
- Key patterns for building features
- Where to find things in the codebase
Next: In Module 1, you'll set up your development environment and configure TeamPulse as a B2B application.
Next: Setup & Configuration