Monorepo layout
The npm-workspace layout — apps/web, apps/realtime, apps/mobile, and packages/core — what each workspace is, how they relate, and the rules that keep the shared package portable.
Valcraven is an npm-workspace monorepo. The root package.json (named valcraven-monorepo) declares the workspace globs and delegates almost every script to the web app, so you run the familiar commands from the repo root:
"workspaces": ["apps/*", "packages/*"]The four workspaces
| Directory | What it is |
|---|---|
apps/web | The Next.js app — UI, API routes, server logic, the database, the Better Auth backend, Stripe, email, admin. This is where most feature work happens. |
apps/realtime | The realtime Worker — a small standalone Cloudflare Worker (built on the Agents SDK) that hosts the AI agent (AppAgent) and the generic RealtimeChannel, both as Durable Objects. See The realtime Worker. |
apps/mobile | An Expo (SDK 54) React Native app. It has no server of its own — it authenticates against the web app's Better Auth backend over the network and consumes @valcraven/core. See Mobile. |
packages/core | @valcraven/core — framework-agnostic shared logic: Zod schemas, Better Auth $Infer-derived types, shared chat logic, and a typed fetch API client. Consumed by both apps/web and apps/mobile. |
Package names are per-project — the
/valcraven-createscaffolder rewrites them. The directory is the stable identifier; the root scripts reference workspaces by path (-w apps/web,-w apps/realtime).
valcraven/
├── apps/
│ ├── web/ # Next.js app (see apps/web/lib, apps/web/app)
│ ├── realtime/ # agent + realtime-channel Worker
│ │ └── src/ # index.ts (gate), channel/, tools.ts, chat.ts, ...
│ └── mobile/ # Expo / React Native app
│ └── src/
│ app/ # expo-router screens
│ lib/auth-client.ts # @better-auth/expo + expo-secure-store
│ lib/api.ts # authenticated calls via @valcraven/core
└── packages/
└── core/ # @valcraven/core
└── src/
index.ts # public barrel export
http.ts # fetch helpers / HTTP utilities
auth-types.ts # Better Auth $Infer-derived session/user types
api-client.ts # typed fetch API client (createApiClient)
chat/ # shared chat logic (model catalog, message building)
schemas/ # Zod schemas shared by web + mobileThe @valcraven/core import contract
@valcraven/core is the seam between the web app (server / Node / Workers) and mobile (React Native). It must import cleanly into both runtimes, so it is forbidden from depending on anything that only exists on one side. Its own src/index.ts states the rule, and its tsconfig.json enforces it at compile time (no DOM lib, no @types/node):
next/*drizzle/better-sqlite3react-dom- Node built-ins (
fs,path,crypto, …) - DOM globals (
window,document,localStorage, …)
If a piece of shared logic reaches for any of those, it does not belong in core — keep it in apps/web (server) or apps/mobile (native). Both apps import the package the same way:
import { createApiClient, ApiError, type Session, type User } from "@valcraven/core";Root scripts delegate to the workspaces
The root package.json forwards each script to the right workspace, so you run these from the repo root:
| From repo root | What it does |
|---|---|
npm install | Installs all workspaces (deps are hoisted). |
npm run dev | next dev in apps/web. |
npm run dev:realtime | wrangler dev for the realtime Worker. |
npm run build | next build in apps/web. |
npm run build:cf / npm run deploy:cf | OpenNext build / deploy of the web Worker. |
npm run deploy:realtime | Deploy the realtime Worker. |
npm run deploy:cf:all | Build → deploy web → deploy realtime. |
npm run db:migrate / npm run db:generate | Apply / generate database migrations. |
npm run test / npm run test:e2e | Vitest / Playwright for apps/web. |
npm run typecheck | Typecheck every workspace (--workspaces --if-present). |
How mobile talks to the web backend
apps/mobile authenticates against the web app's Better Auth backend over the network — it reads EXPO_PUBLIC_API_URL to find the API, uses @better-auth/expo + expo-secure-store for the session, and injects the session cookie into @valcraven/core's API client. The only server-side change mobile requires is the expo() plugin and the valcraven:// trusted origin in apps/web/lib/auth.ts (both already wired). See Mobile for the full setup.
React version is unified at the root
React is pinned to a single version across the whole monorepo via the root package.json overrides (npm only honors overrides from the root). React Native requires react to match its renderer version exactly, so do not bump React in one workspace without the others — change it once at the root override.
Where does new code go?
| I want to add… | …it goes in |
|---|---|
A web page or /app feature, an API route, server logic | apps/web (inside the /app shell — see Conventions) |
| A Zod schema, request/response type, or API-client method shared by web + mobile | packages/core (@valcraven/core) |
| A native screen or mobile-only behavior | apps/mobile |
| A DB table, Drizzle query, or migration | apps/web (never packages/core — Drizzle is forbidden there) |
Anything touching next/*, the DOM, or Node built-ins | apps/web (never packages/core) |
| An AI agent tool, scheduling, or realtime-channel behavior | apps/realtime |
| Auth backend config (providers, trusted origins, plugins) | apps/web/lib/auth.ts |
Related reading: the request lifecycle shows how a request flows through apps/web, and the data layer covers getDb() and migrations.
Architecture
How Valcraven is put together — the monorepo, the request lifecycle, and how Next.js, Cloudflare Workers, D1, R2, and the realtime Worker fit together.
Request & auth lifecycle
How a request travels from the edge through the cache, middleware, the OpenNext worker, and into an API route — and where the session is checked at each step.