Shared Packages

One of the main benefits of a monorepo is extracting shared logic into internal packages. Instead of duplicating code across apps, you create a package once and import it everywhere. In the Chirp monorepo, shared packages live in the packages/ directory and are scoped under @chirp/.

tRPC

tRPC (TypeScript Remote Procedure Call) enables you to build fully type-safe APIs without a separate schema language or code generation. It ensures server and client types stay in sync, catching type errors at compile time rather than runtime.

Key things to know about tRPC:

  • It is built on top of TanStack Query, so everything you learned about useQuery and useMutation applies here too. When tRPC's docs aren't clear, TanStack Query's documentation often fills the gap.
  • It uses a procedure-based API instead of REST endpoints. You define queries and mutations as typed functions, and call them from the client with full autocompletion.
  • There is a great 5-minute introduction by Matt Pocock that covers the basics.

Monorepo pattern

The tRPC router and all procedures live in packages/trpc. Both the web and mobile apps import the AppRouter type from @chirp/trpc to create fully typed clients — no code duplication.

packages/trpc — defines the router:

import { router, publicProcedure } from './trpc'

export const appRouter = router({
  tweet: tweetRouter,
  user: userRouter,
  // ...
})

export type AppRouter = typeof appRouter

apps/api — mounts the router via @hono/trpc-server:

import { trpcServer } from '@hono/trpc-server'
import { appRouter } from '@chirp/trpc'

app.use('/trpc/*', trpcServer({ router: appRouter }))

apps/web — creates a typed React client:

import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '@chirp/trpc'

export const trpc = createTRPCReact<AppRouter>()

apps/mobile — same pattern, different transport if needed:

import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '@chirp/trpc'

export const trpc = createTRPCReact<AppRouter>()

Resources

Better Auth

Better Auth is a TypeScript-first authentication framework. It is framework-agnostic but has first-class integrations for both Next.js and Expo.

Key features:

  • Email/password auth with built-in session management.
  • Social sign-on — GitHub, Google, Discord, and more via OAuth providers.
  • Database-backed sessions — works directly with your existing Prisma schema and PostgreSQL database, no separate auth database needed.
  • Plugin system — extend with two-factor auth, organizations/teams with roles, and more.

Monorepo pattern

The auth configuration lives in packages/auth. Each app imports it and uses the appropriate framework integration.

packages/auth — shared auth configuration:

import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { prisma } from '@chirp/db'

export const auth = betterAuth({
  database: prismaAdapter(prisma, { provider: 'postgresql' }),
  emailAndPassword: { enabled: true },
  // social providers, plugins, etc.
})

apps/web — uses the Next.js integration to handle cookies and sessions in Server Components and Server Actions.

apps/mobile — uses the Expo integration for native auth flows and secure token storage.

Resources

Email

For transactional emails (welcome emails, notifications, password resets), use React Email to build templates as React components and Resend to send them.

This package is intentionally light — research the libraries, explore the docs, and decide how to structure it for your project.

Resources

Other Packages

You are encouraged to extract additional shared concerns into packages as the project grows. Some ideas:

  • @chirp/db — Prisma client and schema, shared across the API and any package that needs database access.
  • @chirp/config — shared ESLint, TypeScript, and Prettier configurations.

The decision of what to extract is yours. If you find yourself duplicating code across apps, that is a signal to create a package.

Adding the Packages to the Monorepo

Create packages/trpc, packages/auth, and packages/email directories. Each package gets its own package.json with the @chirp/ scope:

{
  "name": "@chirp/trpc",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts"
}

Then in each app's package.json, add the shared packages as dependencies:

{
  "dependencies": {
    "@chirp/trpc": "workspace:*",
    "@chirp/auth": "workspace:*",
    "@chirp/email": "workspace:*"
  }
}

Run bun install at the root to link everything.

Now continue to the Mobile with Expo section to add the mobile app.