Valcraven Docs
Architecture

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

DirectoryWhat it is
apps/webThe 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/realtimeThe 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/mobileAn 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-create scaffolder 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 + mobile

The @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-sqlite3
  • react-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 rootWhat it does
npm installInstalls all workspaces (deps are hoisted).
npm run devnext dev in apps/web.
npm run dev:realtimewrangler dev for the realtime Worker.
npm run buildnext build in apps/web.
npm run build:cf / npm run deploy:cfOpenNext build / deploy of the web Worker.
npm run deploy:realtimeDeploy the realtime Worker.
npm run deploy:cf:allBuild → deploy web → deploy realtime.
npm run db:migrate / npm run db:generateApply / generate database migrations.
npm run test / npm run test:e2eVitest / Playwright for apps/web.
npm run typecheckTypecheck 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 logicapps/web (inside the /app shell — see Conventions)
A Zod schema, request/response type, or API-client method shared by web + mobilepackages/core (@valcraven/core)
A native screen or mobile-only behaviorapps/mobile
A DB table, Drizzle query, or migrationapps/web (never packages/core — Drizzle is forbidden there)
Anything touching next/*, the DOM, or Node built-insapps/web (never packages/core)
An AI agent tool, scheduling, or realtime-channel behaviorapps/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.

On this page