Core Concepts
Master the fundamental concepts that make OneApp a powerful, scalable monorepo for building Next.js applications.
Ready to dive in?
Why understand these concepts?
Working in a monorepo without understanding its architecture leads to problems:
- Wrong dependencies — Adding packages to the wrong locations breaks builds
- Type errors — Not using branded types causes ID mix-ups
- Phantom dependencies — Dependencies leak between packages causing CI failures
- Inefficient workflows — Don't know how to leverage workspace features
OneApp's architecture is built on pnpm workspaces (strict dependency management), shared packages (reusable code), team isolation (autonomous development), strict TypeScript (type safety), and modern tooling (ESLint 9, Vitest 4) — ensuring your applications are maintainable, testable, and scalable.
Production-ready with 40+ packages, TypeScript strict mode, branded types for ID safety, content-addressable storage (3x faster installs), and comprehensive testing infrastructure.
Core principles
OneApp is built on these key architectural decisions:
1. Code Sharing via Internal Packages
Share code across applications using scoped packages:
import { Button } from "@repo/ui"; // Shared UI components
import { formatDate } from "@repo/utils"; // Common utilities
import type { UserId } from "@repo/types"; // TypeScript utilitiesBenefits:
- ✅ Single source of truth for shared code
- ✅ Changes propagate automatically to all consumers
- ✅ TypeScript provides type safety across packages
2. Workspace Isolation
Teams and individuals have dedicated spaces:
teams/ai/apps/ # AI team's applications
teams/ai/packages/ # AI team's shared packages
personal/andy/apps/ # Andy's personal projectsBenefits:
- ✅ Teams control their own dependencies and tooling
- ✅ No conflicts between team projects
- ✅ Safe space for experimentation
3. Consistent Configuration
Shared configs ensure consistency across all packages:
import reactConfig from "@repo/config/eslint/react";
export default [...reactConfig];{
"extends": "@repo/config/typescript/nextjs.json"
}Benefits:
- ✅ Consistent code quality across teams
- ✅ No configuration drift
- ✅ Easy to update tooling for all packages
4. Type Safety with Branded Types
Prevent common type errors using branded types:
import { Brand } from "@repo/types";
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function getUser(id: UserId) {
/* ... */
}
function getOrder(id: OrderId) {
/* ... */
}
const userId = "user_123" as UserId;
const orderId = "order_456" as OrderId;
getUser(orderId); // ❌ Compiler error: Type 'OrderId' is not assignable to 'UserId'Benefits:
- ✅ Prevents ID type mix-ups at compile time
- ✅ Self-documenting code
- ✅ Catches errors before runtime
5. Strict Dependency Management
pnpm prevents phantom dependencies:
{
"dependencies": {
"@repo/ui": "workspace:*", // Internal package
"react": "^19.0.0" // External package
}
}Benefits:
- ✅ No accidental access to transitive dependencies
- ✅ Explicit dependency declarations
- ✅ Reliable CI/CD builds
What you'll learn
Workspace Architecture
How pnpm organizes packages into logical groups:
- Workspace patterns — Configure
pnpm-workspace.yamlfor your needs - Dependency resolution — How
workspace:*protocol works - Best practices — Organizing packages for scalability
Dependency Management
Master pnpm's powerful dependency management:
- Adding dependencies — To specific workspaces or the root
- Internal dependencies — Using
workspace:*protocol - Updating dependencies — Interactive updates and automation
- Troubleshooting — Fix common dependency issues
Type Safety
TypeScript patterns for maximum safety:
- Branded types — Prevent ID type confusion
- AsyncResult pattern — Type-safe async operations
- API response types — Standardized response structures
- Type guards — Runtime type checking
Quick examples
Using shared packages
import { Button, Card } from "@repo/ui";
import { formatDate, formatCurrency } from "@repo/utils";
import type { UserId, AsyncResult } from "@repo/types";
export default async function DashboardPage() {
const userId = "user_123" as UserId;
const result: AsyncResult<User> = await fetchUser(userId);
if (!result.success) {
return <div>Error: {result.error.message}</div>;
}
return (
<Card>
<h1>Welcome, {result.data.name}</h1>
<p>Member since {formatDate(result.data.createdAt)}</p>
<p>Balance: {formatCurrency(result.data.balance)}</p>
<Button>View Details</Button>
</Card>
);
}Workspace filtering
# Build a package and all its dependencies
pnpm --filter=web... build
# Run tests for all packages in a directory
pnpm --filter="./packages/*" test
# Lint packages changed since main branch
pnpm --filter="[origin/main]" lintType-safe environment variables
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod/v4";
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1)
},
client: {
NEXT_PUBLIC_SITE_URL: z.string().url()
},
experimental__runtimeEnv: {
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL
},
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
emptyStringAsUndefined: true
});
// Usage
import { env } from "./env";
console.log(env.DATABASE_URL); // ✅ Works in server components
console.log(env.NEXT_PUBLIC_SITE_URL); // ✅ Works anywhere
console.log(env.API_KEY); // ❌ Error in client componentsKey technologies
| Technology | Version | Purpose |
|---|---|---|
| pnpm | 10+ | Package management |
| Turborepo | Latest | Task orchestration |
| TypeScript | 5 | Type safety |
| ESLint | 9 | Linting (flat config) |
| Vitest | 4 | Testing |
| Husky | 9 | Git hooks |
| Prettier | 3 | Code formatting |
| Changesets | Latest | Version management |
Next steps
Ready to dive deeper?
- Workspace Architecture → — Understand pnpm workspaces
- Dependency Management → — Master pnpm commands
- Type Safety → — Learn TypeScript patterns