Config snippets
Adjust names and filters while preserving the script and workspace contracts.
Root package.json
{
"name": "my-project",
"private": true,
"packageManager": "pnpm@latest",
"scripts": {
"dev": "turbo run dev --filter=web",
"dev:all": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint && howells-workspace-check",
"lint:fix": "turbo run lint:fix && howells-workspace-fix",
"format": "howells-fix .",
"typecheck": "turbo run typecheck",
"test": "turbo run test",
"check": "pnpm lint && pnpm typecheck && pnpm test",
"check:affected": "turbo run build lint typecheck test --affected",
"clean": "turbo run clean --continue=always && rm -rf .turbo",
"prepare": "howells-husky"
},
"devDependencies": {
"@howells/lint": "latest",
"@howells/husky": "latest",
"@howells/typescript-config": "latest",
"lint-staged": "latest",
"tsx": "latest",
"turbo": "latest",
"typescript": "latest",
"vitest": "latest"
},
"lint-staged": {
"*.{js,ts,jsx,tsx,json,jsonc,css,md}": "howells-fix"
},
"engines": {
"node": ">=24 <25"
}
}Notes:
- replace
webwith the primary app package when needed - if
testis expensive, keepchecklight and create a heavier CI-only job pnpmis the current house baseline- for published packages that can support Node 22, use
"node": ">=22"in the package itself while keeping repo tooling on Node 24
.node-version
24Keep local development, CI, and deployment runtimes on Node 24 LTS. Do not use Node 26 for the house baseline until it reaches LTS.
Default workspace shape
For a full-stack product repo, start with the core shape:
apps/
web/
packages/
db/
trpc/ # optional: same-workspace typed API
ui/
typescript-config/
tailwind-config/
env/ # when typed env is centralized
motion/ # when motion tokens/presets are sharedAdd capability packages only when the repo needs them:
apps/
storybook/ # when shared UI exists
packages/
auth/ # when auth is shared
ai/ # only for repo-specific logic above @howells/ai
mastra/ # when Mastra owns agent/workflow runtime behavior
agents/ # when non-Mastra agent behavior is shared
mcp/ # when the repo exposes MCP tools or resources
assets/ # when assets are shared
upload/ # only if the repo has real upload/media behaviorThis is a starting shape, not a checklist. Do not create empty packages just to satisfy either diagram.
pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- "@howells/*"
allowBuilds:
esbuild: true
sharp: true
catalog:
typescript: "^6.0.0"Add extra workspaces such as scripts/* explicitly. Keep private-package cooldown exclusions exact, review each lifecycle build entry, and pin catalog versions in the consuming repo.
Root turbo.json
{
"$schema": "https://turborepo.dev/schema.json",
"ui": "stream",
"globalDependencies": ["**/.env", "**/.env.local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**", "build/**"],
"cache": false
},
"dev": {
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"cache": false,
"persistent": true
},
"start": {
"dependsOn": ["build"],
"cache": false,
"persistent": true
},
"lint": {
"cache": false
},
"lint:fix": {
"cache": false
},
"typecheck": {
"cache": false
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**", "playwright-report/**", "test-results/**"],
"cache": false
},
"clean": {
"cache": false
}
}
}Add task-level env only when the task reads it.
Root oxlint.config.ts
For a Next.js monorepo:
import next from "@howells/lint/oxlint/next";
export default {
extends: [next],
};For a non-UI or mixed repo, start with @howells/lint/oxlint/core or add targeted overrides.
Root oxfmt.config.ts
import howells from "@howells/lint/oxfmt";
export default howells;Root tsconfig.json
For a UI-oriented monorepo root:
{
"extends": "@howells/typescript-config/bundler-dom-app",
"compilerOptions": {
"baseUrl": "."
},
"exclude": [
"node_modules",
"**/node_modules",
"**/.next",
"**/dist",
"**/storybook-static"
]
}For a Next.js app leaf:
{
"extends": "@howells/typescript-config/nextjs",
"compilerOptions": {
"baseUrl": "."
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]
}For a React library leaf:
{
"extends": "@howells/typescript-config/react-library",
"include": ["src/**/*.ts", "src/**/*.tsx"]
}For a non-DOM package:
{
"extends": "@howells/typescript-config/bundler-no-dom-library-monorepo",
"include": ["src/**/*.ts"]
}components.json for UI repos
Use this when the repo owns a local shared UI package seeded from the bundled UI baseline:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "packages/tailwind-config/shared-styles.css",
"baseColor": "neutral",
"cssVariables": true
},
"iconLibrary": "lucide",
"aliases": {
"components": "packages/ui/src/components",
"utils": "packages/ui/src/lib",
"ui": "packages/ui/src/components",
"lib": "packages/ui/src/lib"
}
}If the repo has its own local UI package, keep aliases aligned to that package rather than scattering local component paths across apps.
Git hooks
@howells/husky writes the immutable .husky/pre-commit and .husky/pre-push files during prepare. Don't hand-edit the generated hooks in a consumer repository. Pre-commit runs lint-staged; pre-push runs typecheck and lint when the pushed ref is the checked-out HEAD.
Envy env boundary
Use this shape for repos with runtime env:
// packages/env/src/schema.ts
import { defineEnv, v } from "@howells/envy";
import { z } from "zod";
export const envSchema = defineEnv({
server: {
DATABASE_URL: v(z.url()),
},
public: {
NEXT_PUBLIC_APP_URL: v(z.url()),
},
});{
"scripts": {
"env:check": "envy check local --schema packages/env/src/schema.ts",
"check": "pnpm lint && pnpm typecheck && pnpm test && pnpm env:check"
}
}For provider checks, prefer Envy's Vercel or Railway adapters over hand-written shell scripts.
Drizzle + Neon db client
Use @howells/neon — it carries the fleet's hardening (write-safe retries, IPv4-first DNS, cold-start timeouts, HMR-safe caching, endpoint guards) so repos never hand-roll clients. The schema lives in packages/db. Full rationale: Neon.
// packages/db/src/client.ts
import { createHttpDb } from "@howells/neon/http";
import { getDatabaseUrl } from "@your-scope/env"; // pooled DATABASE_URL
import * as schema from "./schema";
export const db = createHttpDb({ schema, url: getDatabaseUrl() });
export type Db = typeof db;Need interactive/session transactions (db.transaction(async (tx) => ...)), LISTEN/NOTIFY, or a long-running worker? Swap the subpath — createPooledDb from @howells/neon/pool (hardened pg, same call shape). Never drizzle-orm/neon-serverless; enforce with createOxlintConfig() from @howells/neon/lint.
// drizzle.config.ts — asserts the DIRECT (non-pooler) endpoint
import { neonKitConfig } from "@howells/neon/kit";
export default neonKitConfig({
directUrl: process.env.DIRECT_DATABASE_URL ?? "",
schema: "./packages/db/src/schema.ts",
});Local or disposable database workflow:
{
"scripts": {
"db:push": "envy run local --schema packages/env/src/schema.ts --from .env.local -- drizzle-kit push",
"db:studio": "envy run local --schema packages/env/src/schema.ts --from .env.local -- drizzle-kit studio"
}
}For production or valuable data, replace db:push with an owned migration command and runbook: explicit target, checked-in reviewed migration, backup or repair path, pre/post schema verification, and a smoke test. Keep db:push out of production deployment scripts.
Minimal AGENTS.md
# Project instructions
- Continually explain what you are doing, especially with long and complex tasks.
- Prefer `rg` for search.
- Use `apply_patch` for file edits.
- Never add generic starter code when project-local patterns already exist.Keep it short and operational.