Architecture

Folder Structure

The Turborepo layout: four apps, shared packages, and the rule that packages never depend on apps

Init is a Turborepo monorepo with clear separation between applications and shared packages. Apps consume packages; packages never depend on apps.

Overview

init/
├── apps/
│   ├── web/               # Next.js 16 web app
│   ├── mobile/            # Expo mobile app
│   ├── extension/         # Chrome extension (WXT)
│   └── desktop/           # Tauri desktop app
├── packages/
│   ├── permissions/       # Client-safe auth helpers: role permissions, slugify
│   ├── contract/          # oRPC contract: procedure inputs, outputs, errors
│   ├── service/           # oRPC implementation of the contract + better-auth
│   ├── db/                # Drizzle schema + local Supabase config
│   └── ui/                # Shared React components
├── scripts/               # bootstrap.ts (provision + trim), seed via packages/service
├── package.json           # Root workspace scripts
├── turbo.json             # Turborepo pipeline + env
└── pnpm-workspace.yaml    # Workspaces + version catalog

Applications (apps/)

Web App (apps/web/)

Next.js 16 with the App Router:

apps/web/
├── content/
│   └── docs/              # These docs (fumadocs MDX)
├── src/
│   ├── app/
│   │   ├── (auth)/        # Login, register, password reset
│   │   ├── (dashboard)/   # Protected dashboard
│   │   │   └── dashboard/
│   │   │       ├── [slug]/    # Org pages: todos, members, settings, billing
│   │   │       └── account/   # User account settings
│   │   ├── (docs)/        # Docs routes (fumadocs)
│   │   ├── (marketing)/   # Public landing page
│   │   └── api/
│   │       ├── auth/[...all]/  # better-auth handler
│   │       ├── orpc/[[...rest]]/ # oRPC endpoint
│   │       ├── health/         # Liveness check
│   │       └── search/         # Docs search
│   ├── components/        # App-level components (header, nav, theme)
│   ├── lib/               # auth-client, site-config, fumadocs source
│   ├── orpc/              # oRPC client setup (react, server, query-client)
│   └── proxy.ts           # Markdown content negotiation; signed-out /dashboard → login
├── next.config.ts
└── package.json

Route Conventions

  • (auth)/, (dashboard)/, (marketing)/, (docs)/ - route groups
  • [slug]/ - dynamic org routes
  • _components/ - colocated components, not routes

Mobile App (apps/mobile/)

Expo + React Native with NativeWind:

apps/mobile/
├── src/
│   ├── app/               # Expo Router (_layout.tsx, index.tsx)
│   ├── utils/             # auth (SecureStore), api (oRPC), base-url
│   └── styles.css
├── app.config.ts
└── package.json

Chrome Extension (apps/extension/)

Built with WXT:

apps/extension/
├── src/
│   ├── entrypoints/       # background, popup, options
│   ├── assets/
│   └── lib/
└── wxt.config.ts

Desktop App (apps/desktop/)

A Tauri 2 shell around the web app, not a second frontend: a Rust binary that opens the system webview (WebKit on macOS) on devUrl under pnpm dev:desktop and on frontendDist in a build. The web app talks to it through apps/web/src/lib/desktop-bridge.ts, and only through what its capability grants:

apps/desktop/
├── src-tauri/
│   ├── src/
│   │   ├── lib.rs           # the builder: plugins, commands, menu, window
│   │   ├── window.rs        # the window tauri.conf.json declares, with its navigation pin
│   │   ├── navigation.rs    # which origins load in the window and which open in the browser
│   │   ├── menu.rs          # Tauri's default macOS menu, plus Settings… and Check for Updates…
│   │   └── updater.rs       # the update flow behind three commands and one event
│   ├── capabilities/        # what the web app may invoke
│   ├── build.rs             # the app manifest: every command is a permission
│   ├── Cargo.toml
│   └── tauri.conf.json      # identifier, URLs, the window, bundle settings
└── rust-toolchain.toml      # the Rust version local checks and CI share

Shared Packages (packages/)

Contract Package (packages/contract/)

The API's single source of truth - what each procedure accepts, returns, and can fail with. Clients type against it without importing server code:

packages/contract/src/
├── organization/          # organization-contract.ts, organization-schema.ts
├── todo/                  # Example CRUD contract
├── waitlist/              # Waitlist contract
├── base.ts                # publicBase, protectedBase, organizationBase
└── index.ts               # Exports: contract, ContractClient, RouterInputs/Outputs

Convention per feature: *-contract.ts (procedures), *-schema.ts (Zod inputs).

Service Package (packages/service/)

oRPC implementation of the contract, plus better-auth config:

packages/service/src/
├── auth/                  # auth.ts (better-auth), auth-schema.ts, utils
├── organization/          # organization-router.ts, -test
├── todo/                  # Example CRUD router
├── waitlist/              # Waitlist router
├── root-router.ts         # Implements the whole contract
├── orpc.ts                # Context, implementer, middleware
├── test-utils.ts          # node:test helpers
└── index.ts               # Exports: appRouter, AppRouter, createORPCContext

Convention per feature: *-router.ts (handlers), *.test.ts (tests).

Permissions Package (packages/permissions/)

Client-safe auth helpers, imported by the server and by web and mobile clients alike:

packages/permissions/src/
├── permissions.ts         # better-auth access control: ac, roles, hasPermission, ROLES, roleSchema
└── slug.ts                # slugify, FALLBACK_ORGANIZATION_SLUG

No server code (database, better-auth instance, secrets) belongs here.

Database Package (packages/db/)

packages/db/
├── src/
│   ├── drizzle-schema.ts       # App tables (waitlist, todo)
│   ├── drizzle-schema-auth.ts  # better-auth generated tables
│   ├── drizzle-relations.ts    # defineRelations graph over both schema files
│   └── drizzle-client.ts       # postgres.js + Drizzle client
├── supabase/config.toml        # Local Supabase: Postgres 17 alone, native runtime
├── drizzle.config.ts
└── package.json                # db scripts (start, push, reset, studio)

UI Package (packages/ui/)

shadcn-style components on Base UI (base-vega registry):

packages/ui/
├── src/
│   ├── components/        # Flat primitives
│   ├── hooks/
│   └── styles/globals.css
├── components.json        # shadcn config (style: base-vega)
└── postcss.config.mjs

Apps import explicit paths:

  • @repo/ui/components/<name>
  • @repo/ui/globals.css

Add components from the registry:

cd packages/ui && pnpm dlx shadcn@latest add <component>

Documentation

No separate docs package - these pages are MDX files in apps/web/content/docs/, rendered by fumadocs (apps/web/src/lib/source.ts).

Configuration Files

  • package.json - Workspace scripts (dev:web, db:push, ...)
  • turbo.json - Pipeline + globalEnv allow-list for env vars
  • pnpm-workspace.yaml - Workspaces and the shared version catalog
  • .oxlintrc.json - Linting (oxlint); formatting is oxfmt

Key Design Principles

  1. Separation of concerns - Apps own UX; packages own logic, data, and primitives
  2. Code sharing - One oRPC router, one auth config, one schema for every platform
  3. Type safety - Types flow from schema through API to UI, no codegen
  4. Explicit imports - @repo/ui/components/button, not barrel files

Adding New Features

New API Routes

  1. Add packages/contract/src/<feature>/ with contract + schema, and register it in packages/contract/src/index.ts
  2. Add packages/service/src/<feature>/<feature>-router.ts implementing it
  3. Register in packages/service/src/root-router.ts
  4. Consume with full types via the orpc utils

New UI Components

  1. Add to packages/ui/src/components/ (or pnpm dlx shadcn@latest add)
  2. Import as @repo/ui/components/<name>

New Database Tables

  1. Define in packages/db/src/drizzle-schema.ts with pgTable.withRLS
  2. Run pnpm db:push
  3. Types flow everywhere automatically

On this page