Skip to main content
The FE Monorepo is a Bun workspace that houses three production applications and shared packages. This architecture enables code sharing, consistent tooling, and efficient development across web and mobile platforms.

Monorepo structure

The repository is organized into two main directories:
fe-monorepo/
├── apps/              # Production applications
│   ├── spa/          # React 19 SPA with TanStack Router
│   ├── web/          # Next.js 16 full-stack app
│   └── expo/         # React Native with Expo 53
├── packages/          # Shared code
│   ├── core/         # Business logic, types, hooks
│   └── typescript-config/  # Shared TypeScript config
└── package.json       # Workspace configuration
All apps in apps/* and packages in packages/* are defined as workspaces in the root package.json.

Workspace configuration

The monorepo uses Bun workspaces for dependency management and script execution. The root package.json defines the workspace:
package.json
{
  "name": "fe-monorepo",
  "version": "1.0.0",
  "private": true,
  "packageManager": "[email protected]",
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

Running workspace commands

You can run commands for specific apps from the monorepo root using the --filter flag:
bun --filter @workspace/spa dev
bun --filter @workspace/spa build
bun --filter @workspace/spa typecheck
The root package.json provides convenient aliases:
# These are equivalent
bun --filter @workspace/spa dev
bun spa dev

bun --filter @workspace/web typecheck
bun web:typecheck

Applications

Each app is a complete, deployable application with its own build configuration and dependencies.

@workspace/spa

Framework

React 19

Routing

TanStack Router

Build Tool

Vite 7

Port

3001
A bulletproof Single Page Application with:
  • React 19 with the new React Compiler
  • TanStack Router for type-safe routing
  • TanStack Query for data fetching and caching
  • TanStack Form for form management
  • Vite for fast development and optimized builds
  • Tailwind CSS 4 for styling
  • React Aria Components for accessible UI
  • PWA support with Vite PWA plugin
  • OpenTelemetry for observability (metrics and traces)
  • Playwright for E2E testing
apps/spa/package.json
{
  "name": "@workspace/spa",
  "version": "1.1.0",
  "dependencies": {
    "@workspace/core": "workspace:*",
    "react": "19.2.4",
    "@tanstack/react-router": "1.159.10",
    "@tanstack/react-query": "5.90.21",
    "@tanstack/react-form": "1.28.2"
  }
}

@workspace/web

Framework

Next.js 16

Database

PostgreSQL + Drizzle ORM

Auth

Better Auth

Port

3002
A bulletproof full-stack Next.js application with:
  • Next.js 16 with App Router and React Server Components
  • Better Auth for authentication
  • Drizzle ORM for type-safe database queries
  • PostgreSQL database
  • TanStack Query for client-side data fetching
  • React Hook Form with Zod validation
  • Next Safe Action for type-safe server actions
  • Tailwind CSS 4 for styling
  • React Aria Components for accessible UI
  • OpenTelemetry for observability (metrics, traces, and logs)
  • Playwright for E2E testing
  • Internationalization with next-intl
apps/web/package.json
{
  "name": "@workspace/web",
  "version": "1.1.0",
  "dependencies": {
    "@workspace/core": "workspace:*",
    "next": "16.1.6",
    "react": "19.2.4",
    "drizzle-orm": "0.45.1",
    "better-auth": "1.4.18",
    "@tanstack/react-query": "5.90.21"
  }
}

@workspace/expo

Framework

React Native + Expo 53

Routing

Expo Router

UI

Tamagui

Testing

Maestro
A bulletproof React Native mobile application with:
  • Expo 53 for managed React Native development
  • Expo Router for file-based routing
  • React Navigation for navigation primitives
  • Tamagui for cross-platform UI components
  • TanStack Query for data fetching
  • React Hook Form with Zod validation
  • MMKV for fast key-value storage
  • Internationalization with i18next
  • Maestro for E2E testing
  • EAS Build for cloud builds
  • EAS Update for over-the-air updates
apps/expo/package.json
{
  "name": "@workspace/expo",
  "version": "1.1.0",
  "dependencies": {
    "@workspace/core": "workspace:*",
    "expo": "53.0.11",
    "react": "19.2.4",
    "react-native": "0.79.3",
    "expo-router": "5.1.0",
    "tamagui": "1.126.16",
    "@tanstack/react-query": "5.90.21"
  }
}

Shared packages

Packages provide reusable code that all apps can consume.

@workspace/core

The core package contains shared business logic, types, hooks, and utilities that work across both browser and mobile environments.
packages/core/
├── src/
│   ├── apis/        # API schemas and types
│   ├── assets/      # Shared images, fonts
│   ├── constants/   # App-wide constants
│   ├── hooks/       # React hooks
│   ├── libs/        # Utility libraries
│   ├── services/    # Business logic
│   ├── types/       # TypeScript types
│   └── utils/       # Helper functions
└── package.json
The package uses granular exports for tree-shaking:
packages/core/package.json
{
  "name": "@workspace/core",
  "version": "1.1.0",
  "exports": {
    "./apis/*": "./src/apis/*.ts",
    "./assets/*": "./src/assets/*",
    "./constants/*": "./src/constants/*.ts",
    "./hooks/*": "./src/hooks/*.ts",
    "./libs/*": "./src/libs/*.ts",
    "./services/*": "./src/services/*.ts",
    "./types/*": "./src/types/*.ts",
    "./utils/*": "./src/utils/*.ts"
  },
  "peerDependencies": {
    "ky": "1.14.3",
    "radashi": "12.7.1",
    "react": ">=19.0.0",
    "type-fest": "5.4.4",
    "zod": "4.3.6"
  }
}
Example usage:
// Import API schemas
import { errorResponseSchema } from '@workspace/core/apis/core'

// Import types
import type { ErrorResponseSchema } from '@workspace/core/types/api'

// Import hooks
import { useDebounce } from '@workspace/core/hooks/use-debounce'

// Import utilities
import { formatDate } from '@workspace/core/utils/date'
The core package uses peer dependencies to avoid duplication. Apps must install ky, radashi, react, type-fest, and zod.

@workspace/typescript-config

Shared TypeScript configuration that ensures consistent type checking across all apps and packages.
apps/spa/tsconfig.json
{
  "extends": "@workspace/typescript-config/base.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
All apps reference this package to maintain consistent TypeScript settings.

Dependency management

The monorepo follows strict dependency management practices:

Exact versions

All dependencies use exact versions (no ^ or ~ prefixes) to ensure reproducible builds.
{
  "dependencies": {
    "react": "19.2.4",          // ✅ Exact version
    "zod": "^4.3.6"            // ❌ Avoid semver ranges
  }
}

Workspace dependencies

Internal packages use the workspace:* protocol:
{
  "dependencies": {
    "@workspace/core": "workspace:*",
    "@workspace/typescript-config": "workspace:*"
  }
}
This ensures apps always use the local version of packages.

Shared dependencies

Common dependencies are installed at the workspace root when possible:
Root dependencies
{
  "devDependencies": {
    "@antfu/eslint-config": "7.4.3",
    "eslint": "9.39.2",
    "typescript": "5.9.3",
    "@workspace/typescript-config": "workspace:*"
  }
}
App-specific dependencies are installed in each app’s package.json.

Code organization

Each app follows a consistent directory structure:
apps/spa/
├── public/              # Static assets
├── src/
│   ├── auth/           # Authentication logic
│   ├── components/     # React components
│   ├── hooks/          # Custom hooks
│   ├── routes/         # TanStack Router routes
│   ├── styles/         # Global styles
│   └── main.tsx        # Entry point
├── .env.dev            # Dev environment vars
├── .env.prod           # Prod environment vars
├── package.json
├── tsconfig.json
└── vite.config.ts

Build and deployment

Each app has its own build and deployment strategy:

SPA deployment

The SPA builds to static files that can be deployed to any static hosting:
# Build for production
bun spa build:prod

# Output: apps/spa/dist/
Deploy to Vercel, Netlify, Cloudflare Pages, or any CDN.

Web deployment

The Next.js app can be deployed to Vercel or any Node.js hosting:
# Build for production
bun web build:prod

# Start production server
bun web start

Expo deployment

Expo uses EAS Build for cloud builds and EAS Update for OTA updates:
# Build for Android
bun expo build:android:prod

# Build for iOS
bun expo build:ios:prod

# Send OTA update
bun expo update:prod

Development tools

The monorepo includes shared tooling:

Linting

ESLint with @antfu/eslint-config for consistent code style:
bun lint
bun lint:fix

Type checking

TypeScript checking across all apps:
# Check all apps in parallel
bun typecheck

# Check specific app
bun spa:typecheck

Testing

  • SPA & Web: Playwright for E2E tests
  • Expo: Maestro for mobile E2E tests

Changesets

For versioning and changelog generation:
# Create a changeset
bun cs

# Version packages
bun cs:v

Environment variables

Local environment files are the source of truth. Update deployment/CI environments when you change local files.

SPA & Web

Both apps use .env.dev and .env.prod files:
# Development
cp .env.dev .env.local

# Production
cp .env.prod .env.local
For CI/CD, set GitHub environment secrets:
  • Environment: dev → Secret: SPA_ENV_FILE with .env.dev content
  • Environment: prod → Secret: SPA_ENV_FILE with .env.prod content

Expo

Expo uses EAS environment variables:
# Pull from EAS
bun expo env:pull:dev
bun expo env:pull:preview
bun expo env:pull:prod

Observability

The monorepo includes OpenTelemetry integration for production observability:

Local development

Run the Grafana LGTM stack for local observability:
bun compose:up
This starts:
  • Prometheus for metrics
  • Tempo for traces
  • Loki for logs
  • Pyroscope for profiling
  • Grafana dashboard at http://localhost:3111
Login credentials:
  • Username: admin
  • Password: admin

Production

Both SPA and Web apps include OpenTelemetry instrumentation:
  • SPA: Browser metrics and traces
  • Web: Server-side metrics, traces, and logs

Best practices

Shared code

Extract shared logic to @workspace/core when used by 2+ apps

Type safety

Use TypeScript strictly with strict: true and exactOptionalPropertyTypes: true

Exact versions

Always use exact dependency versions for reproducible builds

Environment vars

Keep .env files in sync between local and deployment

Next steps

SPA Guide

Explore the React SPA architecture

Web Guide

Learn about the Next.js app

Expo Guide

Build mobile apps with Expo

Build docs developers (and LLMs) love