A full-stack TypeScript monorepo built on Cloudflare's infrastructure, featuring a React Router server application, Expo mobile app, and Nextra documentation site.
server- React Router v7 app running on Cloudflare Workers with:- D1 (SQLite) database
- R2 (Object Storage) for file uploads
- Workflows for background jobs
- AI bindings for ML models
- Better Auth for authentication
mobile- Expo/React Native mobile app with tRPC integrationdocs- Nextra documentation site
@{project}/auth- Better Auth configuration and utilities@{project}/db- Drizzle ORM schema and database client@{project}/trpc- tRPC API routers and type-safe client@{project}/repositories- Data access layer and business logic@{project}/schemas- Zod validation schemas@{project}/ui- Shared React components (shadcn/ui)
- Turborepo - Build system and monorepo orchestration
- Bun - Fast package manager and runtime
- TypeScript - Type safety across the entire stack
- Drizzle ORM - Type-safe SQL query builder
- tRPC - End-to-end type-safe APIs
- Better Auth - Modern authentication library
- Bun (v1.3.0 or higher)
- Cloudflare Account
- Wrangler CLI (installed globally)
Make sure you're logged in to Wrangler:
wrangler loginThe first thing you should do after cloning this repository is run the interactive setup script:
bun run setupThis script will:
-
Configure Your Project
- Prompt for a project name
- Update all package references from
@repo/*to@{your-project}/* - Configure resource names (database, buckets, etc.)
-
Create Cloudflare Resources
- D1 database for SQLite storage
- R2 bucket for object storage
- KV namespace (optional)
-
Set Up Authentication
- Generate secure
BETTER_AUTH_SECRET - Create
.envfiles in root andapps/server
- Generate secure
-
Configure Wrangler
- Create
apps/server/wrangler.jsoncwith your resource bindings - Remove
wrangler.jsoncfrom.gitignore
- Create
-
Run Database Migrations
- Generate initial migrations
- Apply migrations to local and remote D1 databases
-
Optional Deployment
- Upload secrets to Cloudflare Workers
- Build and deploy your application
After setup completes, all configuration files will be customized for your project and ready for development.
bun run devThis command runs all apps in parallel using Turborepo's task orchestration. Here's what happens:
cf-typegen- Generates TypeScript types fromwrangler.jsoncbindingsdb:copy-migrations- Copies database migrations frompackages/db/drizzle/toapps/server/migrations/- This ensures your server has the latest schema changes
- Migrations must be in the server app directory for Wrangler to apply them
db:migrate:local- Applies pending migrations to your local D1 database- Start dev servers:
serveron http://localhost:8787mobileMetro bundlerdocson http://localhost:3000
The migration copy step is crucial because:
- Database schemas are defined in
packages/db/schema/ - Drizzle generates migrations in
packages/db/drizzle/ - Wrangler needs migrations in
apps/server/migrations/to apply them to D1 - The copy step keeps them in sync automatically
# Server only
bun run dev:server
# Mobile only
cd apps/mobile && bun run dev
# Docs only
cd apps/docs && bun run dev# Generate new migration after schema changes
bun run db:generate
# Open Drizzle Studio to browse your database
bun run db:studio
# Apply migrations to local D1 database
bun run db:migrate:local
# Apply migrations to remote D1 database
bun run db:migrate:remote
# Manually copy migrations (usually automatic during dev)
bun run db:copy-migrations- Define your schema in
packages/db/schema/:
// packages/db/schema/users.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});- Generate a migration:
bun run db:generate- Apply migrations (happens automatically during
bun run dev):
bun run db:migrate:local- Use in repositories (
packages/repositories/src/):
import { db } from "@{project}/db";
import { users } from "@{project}/db/schema";
export async function createUser(email: string) {
return db.insert(users).values({
id: crypto.randomUUID(),
email,
createdAt: new Date(),
});
}This monorepo uses shadcn/ui components in the @{project}/ui package:
bun run ui-addThis will prompt you to select components to add. They'll be added to packages/ui/src/ and can be imported from @{project}/ui.
Mobile App (Expo)
β tRPC
Server (React Router + Workers)
β Repositories
Database (D1) + Storage (R2)
- Client (mobile/web) initiates auth with Better Auth
- Better Auth handles OAuth providers or email/password
- Session stored in D1 database
- Subsequent requests authenticated via session token
- Protected tRPC routes verify session
- Client requests signed upload URL from tRPC endpoint
- Server generates presigned R2 URL
- Client uploads directly to R2
- Client notifies server of upload completion
- Server stores metadata in D1
Use Cloudflare Workflows for long-running tasks:
// apps/server/workflows/example.ts
export default class ExampleWorkflow extends Workflow {
async run(event: any, step: Step) {
await step.do("process", async () => {
// Your background job logic
});
}
}Trigger from your application:
// In a tRPC route or request handler
await env.EXAMPLE_WORKFLOW.create({
params: { userId: "123" },
});bun run deployThis will:
- Copy migrations to the server app
- Apply migrations to remote D1 database
- Build the server application
- Deploy to Cloudflare Workers
Secrets are not stored in wrangler.jsonc and must be set via Wrangler:
cd apps/server
echo "your-secret-value" | wrangler secret put BETTER_AUTH_SECRET- Local development: Use
.envfiles (not committed to git) - Production: Use
wrangler secret putfor sensitive values - Public variables: Add to
wrangler.jsoncundervars
cd apps/mobile
bun run ioscd apps/mobile
bun run androidThe mobile app connects to your server via the EXPO_PUBLIC_API_URL environment variable:
# .env (root)
EXPO_PUBLIC_API_URL=http://localhost:8787 # Local development
# or
EXPO_PUBLIC_API_URL=https://your-worker.workers.dev # ProductionThe docs app uses Nextra for a beautiful documentation site. Add MDX files to apps/docs/app/ and they'll automatically be added to the navigation.
# Run tests across all packages
bun test
# Type checking
bun run check-types
# Linting (all packages/apps use unified configs from /tooling)
bun run lint
# Format code
bun run formatThis monorepo uses centralized ESLint and TypeScript configurations from the /tooling directory:
-
tooling/eslint-config/- Unified ESLint configurations:base.js- Base config for all packagesnext-js.js- Next.js applications (docs)react-internal.js- React component libraries (ui)react-native.js- React Native/Expo apps (mobile, server)
-
tooling/typescript-config/- Unified TypeScript configurations:base.json- Base TypeScript confignextjs.json- Next.js applicationsreact.json- React applicationsreact-library.json- React component libraries
All apps and packages automatically inherit these configurations and follow consistent linting and type-checking standards.
bun add <package> -D # Development dependencycd packages/db
bun add drizzle-ormPackages can depend on each other via workspace protocol:
{
"dependencies": {
"@{project}/db": "workspace:*"
}
}If migrations aren't applying during bun run dev:
- Check that migrations exist in
packages/db/drizzle/ - Manually run
bun run db:copy-migrations - Run
bun run db:migrate:local
Turborepo caches build outputs. Clear the cache:
rm -rf node_modules/.cache
bun run buildRe-authenticate with Cloudflare:
wrangler logout
wrangler loginDelete the local database and re-run migrations:
cd apps/server
rm -rf .wrangler
bun run db:migrate:local- Create a new branch for your feature
- Make your changes
- Run
bun run check-typesandbun run lint - Submit a pull request
[Your License Here]