- 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>
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.mdfor 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 applicationapps/mobile/- React Native (Expo) mobile apppackages/api/- Hono API server with modular routerspackages/auth/- Better Auth configuration and helperspackages/billing/- Billing integrations (Stripe, LemonSqueezy)packages/db/- Drizzle ORM schema, migrations, and database utilitiespackages/email/- Email templates and providerspackages/i18n/- Internationalization setup and translationspackages/shared/- Common utilities, hooks, constantspackages/storage/- File storage providers and typespackages/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 definitionsqueries.ts- Database query functionsmutations.ts- Database mutation functions- Schema validation via Zod
Type safety:
- API types are exported from
packages/api/src/index.tsasAppRouter - 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
apifrom~/lib/api/server.ts - Client components: Use
apifrom~/lib/api/client.tsxwith 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:generate→db: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:pushfor local dev) - ✅ Always use
pnpm with-envfor 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.tsinstead) - 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 stringPRODUCT_NAME- Application nameURL- Base URL for web appDEFAULT_LOCALE- Default language (e.g., "en")
Setup:
- Create
.envat repo root - Copy from
.env.examplefiles - 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 containerDashboardHeaderTitle- Main page title (h1)DashboardHeaderDescription- Subtitle/description textDashboardInset- Main content wrapper with proper spacingDashboardSidebar- Collapsible sidebar with menuSidebarLink- Navigation link with active state
Data Tables
For admin/list pages, use the data table pattern:
DataTableSkeleton- Loading skeleton during SuspensecreateSearchParamsCachefromnuqs/server- Type-safe URL paramshandle()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)
nuqsfor 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:
-
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
-
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
-
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
-
Existing database queries (
packages/db/src/):- Check for similar queries in other modules
- Reuse Drizzle query patterns
- Don't duplicate relationship definitions
-
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
- Dashboard pages: check
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:
- Search the codebase for similar implementations
- Read existing code in the same domain (admin, dashboard, auth)
- Ask the user if you're unsure whether something exists
- Reuse existing components, utilities, and patterns
- 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 constinstead - 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 clientscoped to specific components; avoid at layout level - Minimize client-side state (
useState,useEffect) - Wrap client components in
Suspensewith 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
nuqsfor URL search param state management
React Native (Mobile)
- Use safe area primitives:
SafeAreaProvider,SafeAreaView, scroll variants - Minimize
useStateanduseEffect; prefer memoization - Use
React.memo,useMemo,useCallbackfor performance - Expo Router for file-based navigation
- Use Expo SplashScreen for loading states
- Optimize images:
expo-imagepackage, WebP format - Shared UI components from
@turbostarter/ui-mobile
File Organization
- Exported component/function first
- Sub-components below
- Helper functions
- Static content
- 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 tosrc/ - Packages:
@turbostarter/<package-name>
- Apps:
- Group imports: external → internal → types
- Add imports and types explicitly; avoid
anyand 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
- Create module in
packages/api/src/modules/<feature>/ - Define router with Hono, add queries/mutations
- Export router from module
- Mount in
packages/api/src/index.ts - Client auto-gets types via Hono RPC
Database schema changes
- Edit schema in
packages/db/src/schema/ - Generate migration:
pnpm with-env -F @turbostarter/db db:generate - Review generated SQL in
packages/db/migrations/ - Apply:
pnpm with-env -F @turbostarter/db db:migrate - 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.jsonoradmin.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:
- Generate package:
turbo gen package
# Enter package name (e.g., "example" → @turbostarter/example)
- Enable fast refresh in
apps/web/next.config.ts:
const INTERNAL_PACKAGES = [
// ...existing packages
"@turbostarter/example",
];
- 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/serverexport)
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:
- Create subtree from
apps/web(one-time setup):
git subtree split --prefix=apps/web --branch web-branch
- 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
- 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
.envat repo root (not.env.local) - Use
pnpm with-envprefix for all DB commands - Check
turbo.jsonfor requiredglobalEnvvariables
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-envwhenever a command depends on environment variables - Leverage Turbo caching: Commands like
build,lint,testare 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
.envfiles - use.env.exampleas templates - Always use
pnpm with-envfor 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
packageManagerfield) - Conventional Commits enforced via commitlint (husky hook)
- Workspace validation runs on
postinstallvia sherif