How to do common tasks in the monorepo. All commands run from the repo root unless noted.
pnpm dev # both apps in parallel (Turborepo TUI)
pnpm dev:web # public app only
pnpm dev:admin # admin dashboard onlypnpm build # build everything (used in CI)
pnpm turbo run build --filter=@repo/web # just one app + its depspnpm lint # oxlint across the workspace (NOT eslint)
pnpm format # oxfmt (NOT prettier); pnpm format:fix to write
pnpm typecheck # tsc --noEmit across the workspace
pnpm test # Vitest unit testsUnit tests live next to the code they cover under packages/*/src/**/*.test.ts
(see vitest.config.ts / vitest.workspace.ts). End-to-end tests live in
e2e/ and run on Playwright (playwright.config.ts).
pnpm db:generate # prisma generate (refresh client types)
pnpm db:migrate # prisma migrate deploy (apply pending)
pnpm db:migrate:dev # prisma migrate dev (author + apply a new migration)
pnpm db:seed # idempotent seed of example data
pnpm db:studio # open Prisma Studio
pnpm db:push # bypass migrations, push schema directly (DEV ONLY)pnpm auth:gen-schema # regenerate auth schema after plugin changeslefthook runs on commit. Pre-commit lints + formats staged source files
(oxlint --fix, oxfmt) and re-stages the fixes. commit-msg validates the
message against Conventional Commits via commitlint. Hooks install on
pnpm install.
mkdir -p packages/new-pkg/srcpackages/new-pkg/package.json:
{
"name": "@repo/new-pkg",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": { ".": "./src/index.ts" },
"scripts": { "typecheck": "tsc --noEmit" },
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"@types/node": "catalog:",
"typescript": "catalog:"
}
}packages/new-pkg/tsconfig.json:
{ "extends": "@repo/tsconfig/base.json", "include": ["src/**/*.ts"] }Then in the consuming app's package.json add "@repo/new-pkg": "workspace:*",
add it to the app's transpilePackages array in next.config.ts, and run
pnpm install.
cd apps/web # or apps/admin
pnpm dlx shadcn@latest add dialogRewrite literal colors to design tokens (bg-primary, not bg-amber-500)
before merging. If a component is used in both apps, move it to
packages/ui/src/components/dialog.tsx and add it to the exports map in
packages/ui/package.json.
- Add it to the relevant
.env.examplefiles (root + the app(s) that need it), with a comment. - Add it to
turbo.json'sbuild.envarray so Turborepo invalidates cache on change. - Declare it in the right
@repo/envmodule underpackages/env/src/:db.ts,auth.ts,client.ts(forNEXT_PUBLIC_*),storage.ts, orrevalidate.ts— and add it to that module'sruntimeEnvmap. The Zod schema there is the source of truth. - Read it via
import { env } from "@repo/env/<module>"— neverprocess.env.Xdirectly.
-
Add a model to
packages/db/prisma/schema.prisma:model widget { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) @@map("widgets") }
-
Author + apply the migration in dev, then refresh the client:
pnpm db:migrate:dev pnpm db:generate
-
Add typed query helpers in
packages/db/src/queries/(export them fromqueries/index.ts). -
Add matching Zod schemas/types to
@repo/typesif the shape crosses the server/client boundary. -
Use from either app via
import { listWidgets } from "@repo/db"(orimport { prisma } from "@repo/db"for raw access).
Mirror the Item example end to end:
- Model in
schema.prisma+ migration (above). - Queries in
packages/db/src/queries/. - Types in
@repo/types(*InputSchema,*FilterSchema). - Route under
apps/admin/app/(dashboard)/<resource>/page.tsx, callingrequireAdmin(). - Server Actions in
<resource>/_actions.ts— guard withrequireAdmin(), validate with the Zod schema, mutate via@repo/db, thenupdateTag(...). - Nav — append an entry to the
NAVarray inapps/admin/components/sidebar-nav.tsx.
// apps/web/app/<route>/page.tsx
import { fetchItems } from "@/lib/db";
export default async function Page() {
const items = await fetchItems({ status: "active" });
return <List items={items} />;
}fetchItems / fetchItemById in apps/web/lib/db.ts add caching and the
DB-less fallback. Filters/sort/selection belong in URL search params, not local
state.
When an admin mutation changes data the public app caches, call
revalidateWeb([...]) (apps/admin/lib/revalidate-web.ts). It POSTs the tags to
the web app's /api/revalidate route after the response (after()), only when
NEXT_PUBLIC_WEB_URL and REVALIDATE_SECRET are both set. The known web tags
live in apps/web/lib/db.ts (PUBLIC_ITEMS_TAG and the per-item tag).
Check whether you're in a Server Component (no "use client" at the top).
Server Components only re-fetch on URL change or an explicit router.refresh().
pnpm turbo run dev --ui=streamIf a single package rebuilds repeatedly, check for a circular dependency.
Turn on query logging by adding "query" to the log array on the
PrismaClient in packages/db/src/client.ts while debugging. For anything the
typed builder can't express, drop to prisma.$queryRaw<Row[]> with a tagged
template literal — it parameterizes values safely. Never reach for any.
- Avoid abbreviations in public APIs (
organization, notorg). - Server-side data fetching by default — use Server Components; drop to client only for interactivity.
- No
any— type$queryRawrows with the generic parameter. - No barrel files inside packages — re-exports are fine, but big
re-export
index.tsfiles hurt tree-shaking. - No hard-coded colors/fonts — use
@repo/uitokens.
feat: new feature
fix: bug fix
refactor: behavior-preserving rewrite
chore: config, dependency bumps
docs: documentation only
style: formatting only
test: test changes only
Branch names: feat/short-description, fix/issue-summary. Squash on merge to
keep the master branch linear. commitlint enforces the format on commit-msg.
pnpm --filter @repo/db studio # run a script in one workspace
pnpm -r typecheck # run in all workspaces
pnpm --filter @repo/web add some-package
pnpm --filter @repo/web add @repo/logger --workspace # internal dep