Files
Alejandro Gutiérrez d3163a5bff feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:19:32 +01:00

32 KiB

TurboStarter Framework Patterns

TurboStarter framework patterns and commands.

Note: This file is subordinate to .context/CLAUDE.md. Project-specific decisions take precedence.

For more docs: Check index.md for keyword search across 222 documentation pages.

Purpose

This document contains TurboStarter monorepo patterns, commands, and architecture guidelines:

  • Framework-specific commands (pnpm, database, services)
  • Monorepo structure and package organization
  • Code conventions and patterns established by TurboStarter

When to consult .context/project.md instead:

  • Project-specific architecture decisions
  • Project package structure
  • Business logic and feature requirements

Documentation usage: Examples here are illustrative. Adapt them to match existing repository patterns rather than copying verbatim.

Project Overview

This is a TurboStarter monorepo - a fullstack SaaS starter kit built with Turborepo. It contains:

  • Web app: Next.js 16 (App Router) with React 19
  • Mobile app: React Native + Expo
  • Shared packages: API (Hono), auth (Better Auth), database (Drizzle + PostgreSQL), billing, email, i18n, UI components

Essential Commands

Development

# Install dependencies (uses pnpm 10.25.0)
pnpm install

# Start Docker services (PostgreSQL)
pnpm services:start

# First-time database setup (migrate + seed)
pnpm with-env -F @turbostarter/db db:setup

# Start all apps in dev mode
pnpm dev

# Start specific app only
pnpm --filter web dev
pnpm --filter mobile dev

# Mobile-specific dev commands
pnpm --filter mobile ios           # Run on iOS simulator
pnpm --filter mobile android       # Run on Android emulator

Database Operations

All database commands must use pnpm with-env to load environment variables from .env at repo root.

# Generate migration after schema changes
pnpm with-env -F @turbostarter/db db:generate

# Apply migrations
pnpm with-env -F @turbostarter/db db:migrate

# Push schema directly (dev only - skips migrations)
pnpm with-env -F @turbostarter/db db:push

# Check schema drift
pnpm with-env -F @turbostarter/db db:check

# Open Drizzle Studio (database GUI)
pnpm with-env -F @turbostarter/db db:studio

# Check migration status
pnpm with-env -F @turbostarter/db db:status

# Seed database (dev only)
pnpm with-env -F @turbostarter/db db:seed

# Reset database (dev only)
pnpm with-env -F @turbostarter/db db:reset

Quality & Testing

# Type check entire monorepo
pnpm typecheck

# Lint (check)
pnpm lint

# Lint (fix)
pnpm lint:fix

# Format (check)
pnpm format

# Format (fix)
pnpm format:fix

# Run tests (Vitest)
pnpm test

# Run tests in watch mode
pnpm test:projects:watch

# Build all packages/apps
pnpm build

# Build specific app
pnpm --filter web build

Services

# Start Docker services
pnpm services:start

# Stop Docker services
pnpm services:stop

# View service logs
pnpm services:logs

# Check service status
pnpm services:status

Architecture

Monorepo Structure

  • apps/web/ - Next.js web application
  • apps/mobile/ - React Native (Expo) mobile app
  • packages/api/ - Hono API server with modular routers
  • packages/auth/ - Better Auth configuration and helpers
  • packages/billing/ - Billing integrations (Stripe, LemonSqueezy)
  • packages/db/ - Drizzle ORM schema, migrations, and database utilities
  • packages/email/ - Email templates and providers
  • packages/i18n/ - Internationalization setup and translations
  • packages/shared/ - Common utilities, hooks, constants
  • packages/storage/ - File storage providers and types
  • packages/ui/ - Shared UI components (web/mobile variants)

API Architecture (Hono)

The API is built with Hono and follows a modular router pattern:

Main router (packages/api/src/index.ts):

  • Base path: /api
  • Applies middleware: CSRF (web only), CORS, logger, localization
  • Routes to sub-routers: /admin, /ai, /auth, /billing, /organizations, /storage

Module pattern: Each feature module (e.g., packages/api/src/modules/admin/users/) contains:

  • router.ts - Hono router with route definitions
  • queries.ts - Database query functions
  • mutations.ts - Database mutation functions
  • Schema validation via Zod

Type safety:

  • API types are exported from packages/api/src/index.ts as AppRouter
  • Consumed in web/mobile apps via Hono RPC client for end-to-end type safety

Web App Structure (Next.js App Router)

apps/web/src/app/
├── [locale]/                    # Internationalized routes
│   ├── (marketing)/             # Public routes (landing, blog, pricing)
│   ├── auth/                    # Auth pages (login, register, password reset)
│   ├── dashboard/               # Protected routes
│   │   ├── (user)/              # Personal dashboard
│   │   └── [organization]/      # Organization-scoped routes
│   └── admin/                   # Super admin dashboard
└── api/[...route]/route.ts      # Catch-all API route (proxies to Hono)

Route groups:

  • (marketing) - Public pages, shared marketing layout
  • (user) - Personal user dashboard
  • [organization] - Multi-tenant org routes, slug-based

API integration:

  • Server components: Use api from ~/lib/api/server.ts
  • Client components: Use api from ~/lib/api/client.tsx with React Query

Database (Drizzle + PostgreSQL)

Schema location: packages/db/src/schema/

  • Multiple schema files organized by domain
  • Exported via packages/db/src/schema/index.ts

Migrations:

  • Generated in packages/db/migrations/ as SQL files
  • Workflow: Edit schema → db:generatedb:migrate

Database client:

  • Server-side only via @turbostarter/db/server
  • Uses Drizzle ORM with PostgreSQL driver
  • Connection pooling configured for serverless

Critical invariants:

  • Never access database directly from web/mobile apps
  • Never use raw SQL outside migrations (use Drizzle queries)
  • Never skip migrations in production (only db:push for local dev)
  • Always use pnpm with-env for all database commands
  • Always go through API layer for data access

Authentication (Better Auth)

  • Server config: packages/auth/src/server.ts
  • Client helpers: packages/auth/src/client.tsx
  • Supports: email/password, magic links, OAuth providers, 2FA, passkeys
  • Session management via cookies
  • Organizations plugin enabled for multi-tenancy

Multi-tenancy / Organizations

  • Organization-scoped routes: /dashboard/[organization]/...
  • Active organization stored in session (activeOrganizationId)
  • RBAC: owner, admin, member roles
  • Invitation system with email-based invites

Business Logic Placement

Core principle: Business logic lives in the API layer, not in UI components.

Where logic belongs:

  • API layer (packages/api/src/modules/): Business rules, validation, authorization, data transformations
  • Database layer (packages/db): Schema definitions, relations, type-safe queries via Drizzle
  • Web/Mobile apps: Orchestration, presentation, user interaction, calling API endpoints
  • UI packages: Pure presentation components, no business rules

Where logic must NOT live:

  • React components (web or mobile)
  • UI packages (@turbostarter/ui-web, @turbostarter/ui-mobile)
  • Directly in API route files (use queries.ts/mutations.ts instead)
  • Client-side validation as source of truth (use for UX only; validate server-side)

Example:

// ❌ BAD - business logic in component
export function UserProfile({ userId }) {
  const canEdit = user.role === 'admin' || user.id === userId;
  // Complex business rules in component
}

// ✅ GOOD - business logic in API
export function UserProfile({ userId }) {
  const { data } = useQuery(api.users.canEdit.$get({ query: { userId } }));
  // API returns authorization decision
}

Layout & Sidebar Patterns

Dashboard Layout Hierarchy:

layout.tsx (root)
└── [locale]/layout.tsx (i18n wrapper)
    └── dashboard/layout.tsx (main dashboard - no sidebar)
        ├── (user)/layout.tsx (user sidebar + auth check)
        │   ├── page.tsx (home)
        │   ├── ai/page.tsx
        │   └── settings/layout.tsx (sub-nav)
        │       ├── page.tsx (general)
        │       ├── security/page.tsx
        │       └── billing/page.tsx
        ├── [organization]/layout.tsx (org sidebar + auth check + org fetch)
        │   ├── page.tsx (org home)
        │   ├── members/page.tsx
        │   └── settings/layout.tsx (sub-nav)
        └── admin/layout.tsx (admin sidebar + permission check)
            ├── page.tsx (admin home)
            ├── users/page.tsx
            └── organizations/page.tsx

Sidebar Structure: Each layout defines its own sidebar menu with groups:

  • User sidebar: Personal features (home, AI) + account (settings)
  • Organization sidebar: Platform features (home) + organization (settings, members)
  • Admin sidebar: Admin resources (users, organizations, customers)

Common sidebar footer (all sidebars):

  • Support link
  • Feedback link
  • User navigation (profile, logout)

Layout Authentication Patterns:

// User dashboard - basic auth
const { user } = await getSession();
if (!user) return redirect(pathsConfig.auth.login);

// Organization dashboard - auth + org fetch + hydration
const { user } = await getSession();
if (!user) return redirect(pathsConfig.auth.login);
const org = await getOrganization({ slug });
if (!org) return redirect(pathsConfig.dashboard.user.index);
// Pre-fetch and hydrate organization data via queryClient

// Admin dashboard - auth + permission check
const { user } = await getSession();
if (!user) return redirect(pathsConfig.auth.login);
if (!hasAdminPermission(user)) return redirect(pathsConfig.dashboard.user.index);

Environment Variables

Required globals (defined in turbo.json):

  • DATABASE_URL - PostgreSQL connection string
  • PRODUCT_NAME - Application name
  • URL - Base URL for web app
  • DEFAULT_LOCALE - Default language (e.g., "en")

Setup:

  1. Create .env at repo root
  2. Copy from .env.example files
  3. Commands automatically load via pnpm with-env

App-specific variables:

  • Web: apps/web/.env.local
  • Mobile: apps/mobile/.env.local

Common UI Patterns

Dashboard Components

Standard dashboard components from ~/modules/common/layout/dashboard/:

  • DashboardHeader - Page header container
  • DashboardHeaderTitle - Main page title (h1)
  • DashboardHeaderDescription - Subtitle/description text
  • DashboardInset - Main content wrapper with proper spacing
  • DashboardSidebar - Collapsible sidebar with menu
  • SidebarLink - Navigation link with active state

Data Tables

For admin/list pages, use the data table pattern:

  • DataTableSkeleton - Loading skeleton during Suspense
  • createSearchParamsCache from nuqs/server - Type-safe URL params
  • handle() from @turbostarter/api/utils - Unwraps API responses
  • React Query for client-side data fetching
  • Built-in sorting, filtering, pagination via URL params

Icons

Import from @turbostarter/ui-web/icons:

import { Icons } from "@turbostarter/ui-web/icons";

<Icons.Home />
<Icons.Settings />
<Icons.UsersRound />

Common icons:

  • Home, Settings, UsersRound, Building (sidebar)
  • Brain (AI), HandCoins (billing), LifeBuoy (support)
  • MessageCircle (feedback)

Internationalization (i18n)

  • Translation keys: namespace:key.nested.path
  • Server: const { t } = await getTranslation({ ns: "dashboard" })
  • Client: const { t } = useTranslation({ ns: "dashboard" })
  • Sidebar labels auto-translate if key exists in common.json
  • Metadata: getMetadata({ title: "common:myFeature" })

UI Components (shadcn/ui)

TurboStarter uses shadcn/ui for atomic, accessible, customizable components built with Tailwind CSS and Radix UI.

Two UI packages:

  • @turbostarter/ui - Shared styles, themes, assets (icons)
  • @turbostarter/ui-web - Pre-built web components (Button, Card, Dialog, etc.)

Adding new components:

# From repo root - launches interactive CLI
pnpm --filter @turbostarter/ui-web ui:add

# Or copy-paste from shadcn/ui website into packages/ui/web/src/

Using components (each has standalone export):

// Import from specific component path
import { Card, CardContent, CardHeader } from "@turbostarter/ui-web/card";
import { Button } from "@turbostarter/ui-web/button";
import { Dialog, DialogContent } from "@turbostarter/ui-web/dialog";

// Build app-specific components by composition
export function MyComponent() {
  return (
    <Card>
      <CardHeader>...</CardHeader>
      <CardContent>...</CardContent>
    </Card>
  );
}

Component organization principle:

  • Shared package (@turbostarter/ui-web): Atomic components (Button, Input, Card, Dialog)
  • App directory (apps/web/src/): Specific composed components (LoginForm, UserProfile)
  • Keep shared components atomic for reusability and tree-shaking

Security Boundaries

Critical principle: Authentication and authorization are server-side only. Client-side checks are for UX, never security.

Security rules:

  • Auth/authz enforcement: API layer only (via Better Auth + middleware)
  • Secrets/env vars: Server-side packages only (packages/api, packages/db)
  • Role checks: API layer, never client components
  • Organization permissions: Validated in API, pre-fetched for UX

Never do this:

  • Client-side role/permission checks as source of truth
  • Secrets in web/mobile apps or UI packages
  • Authorization logic in React components
  • Direct API calls bypassing authentication

Pattern:

// ❌ BAD - client-side auth check
export function AdminPanel() {
  if (user.role !== 'admin') return null; // Security by obscurity!
  return <SensitiveData />;
}

// ✅ GOOD - server-side enforcement
export async function AdminPanel() {
  const { user } = await getSession();
  if (!hasAdminPermission(user)) redirect(pathsConfig.dashboard.user.index);
  const data = await api.admin.getSensitiveData(); // API enforces auth
  return <SensitiveData data={data} />;
}

Architectural Constraints

Allowed patterns (use these freely):

  • Hono for API routing
  • Drizzle ORM for database queries
  • Zod for validation
  • Better Auth for authentication
  • React Server Components (default)
  • nuqs for URL state
  • shadcn/ui for UI components

Forbidden patterns (do not introduce):

  • New state management libraries (Redux, Zustand, MobX)
  • Database access outside @turbostarter/db/server
  • Bypassing API layer from apps (direct DB access)
  • Business logic in React components
  • Client-side auth checks as security boundaries
  • Ad-hoc environment variable loading (use existing patterns)
  • New packages without justification (see "Adding a new package")
  • Runtime schema mutations or direct SQL queries

When in doubt: Ask the user before introducing new dependencies, patterns, or architectural changes.

Reuse-First Principle

Critical rule: Always search for and reuse existing implementations before creating new ones.

Before implementing ANY feature, check:

  1. Existing UI components (packages/ui/web/src/, packages/ui/mobile/src/):

    • Check for similar components (forms, buttons, modals, tables)
    • Use shadcn/ui components via pnpm --filter @turbostarter/ui-web ui:add
    • Don't recreate what already exists in the UI packages
  2. Existing utilities (packages/shared/src/):

    • Common functions, hooks, constants
    • Check packages/shared/src/utils/ before writing helpers
    • Check packages/shared/src/hooks/ before creating custom hooks
  3. Existing API patterns (packages/api/src/modules/):

    • Look at similar endpoints (users, organizations, admin)
    • Reuse query/mutation patterns from existing modules
    • Use established error handling and validation patterns
  4. Existing database queries (packages/db/src/):

    • Check for similar queries in other modules
    • Reuse Drizzle query patterns
    • Don't duplicate relationship definitions
  5. Existing patterns in similar pages:

    • Dashboard pages: check apps/web/src/app/[locale]/dashboard/
    • Admin pages: check apps/web/src/app/[locale]/admin/
    • Auth pages: check apps/web/src/app/[locale]/auth/
    • Settings pages: check sub-navigation patterns

Search workflow before implementing:

# Search for similar functionality
grep -r "keyword" packages/
grep -r "ComponentName" apps/web/src/

# Check UI components
ls packages/ui/web/src/
ls packages/ui/mobile/src/

# Check utilities
ls packages/shared/src/utils/
ls packages/shared/src/hooks/

Examples of reuse over reimplementation:

Bad - Reimplementing:

// Creating a new button variant when one exists
export function MyCustomButton() {
  return <button className="custom-styles">Click</button>;
}

Good - Reusing:

// Using existing button with variant
import { Button } from "@turbostarter/ui-web/button";
export function MyFeature() {
  return <Button variant="outline">Click</Button>;
}

Bad - Duplicating logic:

// Writing custom date formatter
function formatDate(date: Date) {
  return date.toLocaleDateString();
}

Good - Using existing utility:

// Check if packages/shared has formatDate first
import { formatDate } from "@turbostarter/shared/utils";

Bad - Creating new API pattern:

// Inventing new error handling
if (!user) throw new Error("Not found");

Good - Following existing patterns:

// Copy pattern from packages/api/src/modules/admin/users/
if (!user) {
  return c.json({ error: "User not found" }, 404);
}

Decision tree for new implementations:

Does similar functionality exist?
├─ YES → Reuse or extend it
│   └─ Can you extend the existing component/utility?
│       ├─ YES → Add props/options to existing code
│       └─ NO → Compose with existing primitives
└─ NO → Implement new, but:
    ├─ Follow established patterns from similar code
    ├─ Use existing primitives (UI components, utilities)
    └─ Make it reusable for future needs

What this prevents:

  • Duplicate button/input/modal components
  • Multiple implementations of the same utility function
  • Inconsistent API response formats
  • Different auth/validation patterns across features
  • Reimplementing data table patterns
  • Creating custom hooks that already exist

Required agent behavior:

Before implementing ANY feature:

  1. Search the codebase for similar implementations
  2. Read existing code in the same domain (admin, dashboard, auth)
  3. Ask the user if you're unsure whether something exists
  4. Reuse existing components, utilities, and patterns
  5. Only create new when genuinely needed and nothing similar exists

Optimization target: Minimize code duplication and maximize consistency through reuse.

Code Conventions

TypeScript

  • Write concise, technical TypeScript code
  • Prefer functional and declarative patterns over classes
  • Use interfaces over type aliases
  • Avoid enums; use const objects with as const instead
  • Descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)
  • Prefer iteration and modularization over code duplication

React (Web)

  • Favor React Server Components (default in Next.js App Router)
  • Minimize "use client" directive - only when necessary for:
    • Browser APIs (localStorage, window)
    • Event handlers and interactivity
    • React hooks (useState, useEffect)
  • Keep use client scoped to specific components; avoid at layout level
  • Minimize client-side state (useState, useEffect)
  • Wrap client components in Suspense with fallbacks
  • Use dynamic imports for non-critical client components
  • Use Tailwind CSS for styling; mobile-first responsive design
  • Use Shadcn/Radix UI components from @turbostarter/ui-web
  • Image optimization: WebP format, responsive sizes, lazy loading
  • Focus on Web Vitals: LCP, CLS, FID
  • Use nuqs for URL search param state management

React Native (Mobile)

  • Use safe area primitives: SafeAreaProvider, SafeAreaView, scroll variants
  • Minimize useState and useEffect; prefer memoization
  • Use React.memo, useMemo, useCallback for performance
  • Expo Router for file-based navigation
  • Use Expo SplashScreen for loading states
  • Optimize images: expo-image package, WebP format
  • Shared UI components from @turbostarter/ui-mobile

File Organization

  1. Exported component/function first
  2. Sub-components below
  3. Helper functions
  4. Static content
  5. Types/interfaces at bottom

Error Handling

  • Use guard clauses and early returns
  • Expected errors: model as return values in Server Actions
  • Unexpected errors: let error boundaries catch
  • API: Use Zod for input validation
  • Handle edge cases early in function logic

Imports

  • Use path aliases:
    • Apps: ~/ maps to src/
    • Packages: @turbostarter/<package-name>
  • Group imports: external → internal → types
  • Add imports and types explicitly; avoid any and unsafe casts

Code Quality

  • Adhere to existing formatting; do not reformat unrelated code
  • Match existing code style and patterns
  • Keep components small and modular
  • When modifying multiple areas, prefer creating shared helpers in packages/ to avoid duplication

AI Agent Guidelines

When working autonomously, prioritize:

What to do:

  • Prefer modifying existing patterns over creating new ones
  • Keep changes minimal and scoped to the specific task
  • Preserve existing functionality unless explicitly asked to change it
  • Follow the established patterns in similar files
  • Ask for clarification when requirements conflict with this document
  • Use TypeScript's type system to catch errors early

What to avoid:

  • Large refactors without explicit instruction
  • Introducing breaking changes to APIs or schemas silently
  • "Improving" code that isn't part of the task
  • Making assumptions when requirements are ambiguous
  • Adding dependencies without justification

Optimize for: Maintainability, type safety, and consistency with existing architecture over "clever" solutions.

Key Workflows

Adding a new API endpoint

  1. Create module in packages/api/src/modules/<feature>/
  2. Define router with Hono, add queries/mutations
  3. Export router from module
  4. Mount in packages/api/src/index.ts
  5. Client auto-gets types via Hono RPC

Database schema changes

  1. Edit schema in packages/db/src/schema/
  2. Generate migration: pnpm with-env -F @turbostarter/db db:generate
  3. Review generated SQL in packages/db/migrations/
  4. Apply: pnpm with-env -F @turbostarter/db db:migrate
  5. Verify in Studio: pnpm with-env -F @turbostarter/db db:studio

Adding a new dashboard page

1. Define path in apps/web/src/config/paths.ts:

// For user dashboard
dashboard: {
  user: {
    myFeature: `${DASHBOARD_PREFIX}/my-feature`,
  }
}

// For organization dashboard
organization: (slug: string) => ({
  myFeature: `${DASHBOARD_PREFIX}/${slug}/my-feature`,
})

// For admin dashboard
admin: {
  myResource: {
    index: `${ADMIN_PREFIX}/my-resource`,
    detail: (id: string) => `${ADMIN_PREFIX}/my-resource/${id}`,
  }
}

2. Add sidebar menu item in layout:

  • User dashboard: apps/web/src/app/[locale]/dashboard/(user)/layout.tsx
  • Organization: apps/web/src/app/[locale]/dashboard/[organization]/layout.tsx
  • Admin: apps/web/src/app/[locale]/admin/layout.tsx
const menu = [
  {
    label: "platform", // or "account", "organization", "admin"
    items: [
      {
        title: "myFeature",        // i18n key from common.json
        href: pathsConfig.dashboard.user.myFeature,
        icon: Icons.YourIcon,      // from @turbostarter/ui-web/icons
      },
    ],
  },
];

3. Create page file with standard structure:

Basic page (page.tsx):

import { getTranslation } from "@turbostarter/i18n/server";
import { getMetadata } from "~/lib/metadata";
import {
  DashboardHeader,
  DashboardHeaderTitle,
  DashboardHeaderDescription,
} from "~/modules/common/layout/dashboard/header";

export const generateMetadata = getMetadata({
  title: "common:myFeature",
  description: "dashboard:myFeature.description",
});

export default async function MyFeaturePage() {
  const { t } = await getTranslation({ ns: "dashboard" });

  return (
    <>
      <DashboardHeader>
        <div>
          <DashboardHeaderTitle>{t("myFeature.title")}</DashboardHeaderTitle>
          <DashboardHeaderDescription>
            {t("myFeature.description")}
          </DashboardHeaderDescription>
        </div>
      </DashboardHeader>

      {/* Page content */}
    </>
  );
}

Data table page (admin/list pages):

import { createSearchParamsCache, parseAsInteger } from "nuqs/server";
import { Suspense } from "react";
import { handle } from "@turbostarter/api/utils";
import { DataTableSkeleton } from "@turbostarter/ui-web/data-table/data-table-skeleton";
import { api } from "~/lib/api/server";

const searchParamsCache = createSearchParamsCache({
  page: parseAsInteger.withDefault(1),
  perPage: parseAsInteger.withDefault(10),
  sort: getSortingStateParser().withDefault([{ id: "name", desc: false }]),
  q: parseAsString,
});

export default async function MyResourcesPage(props: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const searchParams = await props.searchParams;
  const { page, perPage, sort, ...filters } = searchParamsCache.parse(searchParams);

  const promise = handle(api.admin.myResources.$get)({
    query: { page: page.toString(), perPage: perPage.toString(), sort: JSON.stringify(sort) },
  });

  return (
    <>
      <DashboardHeader>
        <DashboardHeaderTitle>My Resources</DashboardHeaderTitle>
      </DashboardHeader>
      <Suspense fallback={<DataTableSkeleton columnCount={5} />}>
        <MyResourcesDataTable promise={promise} perPage={perPage} />
      </Suspense>
    </>
  );
}

4. Add sub-navigation (for settings-like pages):

Create layout.tsx with SettingsNav pattern:

const LINKS = [
  { label: "general", href: pathsConfig.myFeature.general },
  { label: "advanced", href: pathsConfig.myFeature.advanced },
] as const;

export default async function MyFeatureLayout({ children }) {
  const { t } = await getTranslation();

  return (
    <>
      <DashboardHeader>
        <DashboardHeaderTitle>{t("myFeature.title")}</DashboardHeaderTitle>
        <div className="lg:hidden">
          <SettingsNav links={LINKS.map(link => ({ ...link, label: t(link.label) }))} />
        </div>
      </DashboardHeader>
      <div className="flex w-full gap-3">
        <div className="hidden w-96 lg:block">
          <div className="sticky top-[calc(var(--banner-height)+theme(spacing.6))]">
            <SettingsNav links={LINKS.map(link => ({ ...link, label: t(link.label) }))} />
          </div>
        </div>
        <div className="flex w-full flex-col gap-6">{children}</div>
      </div>
    </>
  );
}

5. Add translations:

  • Common labels: packages/i18n/translations/en/common.json
  • Page content: packages/i18n/translations/en/dashboard.json or admin.json
{
  "myFeature": "My Feature",
  "myFeature.title": "Feature Title",
  "myFeature.description": "Feature description"
}

Adding a new package to monorepo

When to add a new package (advanced):

  • Only when functionality needs to be shared across multiple apps
  • NOT for adding pages/components to a single app (use apps/web/src/ instead)
  • NOT for modifying existing packages

Steps:

  1. Generate package:
turbo gen package
# Enter package name (e.g., "example" → @turbostarter/example)
  1. Enable fast refresh in apps/web/next.config.ts:
const INTERNAL_PACKAGES = [
  // ...existing packages
  "@turbostarter/example",
];
  1. Define exports in package.json:
{
  "exports": {
    ".": "./src/index.ts",           // Default export
    "./client": "./src/client.ts",   // Client-only code
    "./server": "./src/server.ts"    // Server-only code
  }
}

Why separate exports (client/server pattern):

  • Better tree-shaking (avoid bundling server code in client)
  • Clear separation of concerns
  • Used in existing packages like @turbostarter/db (has /server export)

Usage:

// Default export
import { example } from "@turbostarter/example";

// Named exports (better tree-shaking)
import { clientFn } from "@turbostarter/example/client";
import { serverFn } from "@turbostarter/example/server";

Adding a new app to monorepo

When to add a new app (very advanced):

  • Only when you need multiple web apps sharing the same infrastructure
  • Want to keep pulling updates from TurboStarter for the base apps/web
  • Alternative: Create a separate repository (often simpler)

Use git subtree workflow:

  1. Create subtree from apps/web (one-time setup):
git subtree split --prefix=apps/web --branch web-branch
  1. Add new app using web as template:
# Example: create apps/ai-chat from apps/web template
git subtree add --prefix=apps/ai-chat origin web-branch --squash
  1. Update new app when pulling TurboStarter updates:
# Pull latest from TurboStarter
git pull upstream main

# Update web-branch with latest apps/web
git subtree split --prefix=apps/web --branch web-branch
git push origin web-branch

# Pull updates into your new app
git subtree pull --prefix=apps/ai-chat origin web-branch --squash

Why this approach:

  • Keeps new apps in sync with base web app structure
  • Allows selective updates (can modify ai-chat independently)
  • Maintains ability to pull upstream TurboStarter updates

Multi-platform development

  • Share logic in packages/ to avoid duplication
  • UI components: separate web (ui-web) and mobile (ui-mobile) packages
  • API client works across web and mobile with same types

Testing

  • Test framework: Vitest
  • Unit tests: co-located with source (*.test.ts)
  • Run tests: pnpm test (uses Turbo caching)
  • Watch mode: pnpm test:projects:watch

Troubleshooting

Common Issues

Node/pnpm version mismatch:

  • Ensure Node >= 22.17.0: node -v
  • Ensure pnpm 10.25.0: pnpm -v

Services not available / connection refused:

  • Ensure Docker is running
  • Start services: pnpm services:start
  • Check logs: pnpm services:logs
  • Verify status: pnpm services:status

DATABASE_URL or env not loaded:

  • Create .env at repo root (not .env.local)
  • Use pnpm with-env prefix for all DB commands
  • Check turbo.json for required globalEnv variables

Turbo or module resolution issues after refactors:

  • Clear caches: pnpm clean
  • Reinstall: pnpm install

Migration drift or conflicts:

  • Check status: pnpm with-env -F @turbostarter/db db:check
  • Re-generate migration: db:generate
  • Apply: db:migrate

Performance Tips

  • Prefer targeted commands: Use pnpm --filter <app-or-package> <cmd> to minimize work
  • Use pnpm with-env whenever a command depends on environment variables
  • Leverage Turbo caching: Commands like build, lint, test are cached
  • Web app: Prefer React Server Components to reduce client bundle
  • Mobile app: Memoize components and callbacks to prevent unnecessary re-renders

Important Notes

  • Never commit .env files - use .env.example as templates
  • Always use pnpm with-env for database commands
  • Docker must be running for local development (PostgreSQL)
  • Node.js >= 22.17.0 required
  • pnpm 10.25.0 is the package manager (enforced via packageManager field)
  • Conventional Commits enforced via commitlint (husky hook)
  • Workspace validation runs on postinstall via sherif