server-only keeps secrets out of the client bundle). This
page covers what’s installed, what each tool does, and the mistakes that bite.
Prerequisites
- Node.js 20 or newer (see
enginesinpackage.json). - pnpm 10 (
corepack enable && corepack prepare [email protected] --activate). - A Postgres database for
db:push/db:migrate.
The stack
Biome 2 — linter + formatter in one binary
Replaces ESLint and Prettier with a single Rust binary that’s roughly 10x faster. Config lives inbiome.json.
biome.json under linter.rules.
Knip 6 — unused-export detection
Finds dead exports, unused dependencies, and orphaned files. Run before shipping a refactor.knip.config.ts. Add false-positives to its ignore /
ignoreDependencies lists.
Drizzle ORM 0.45 — type-safe SQL
Schema split acrosssrc/db/{auth,app,affiliate,ai,license}.schema.ts. IDs
are text with nanoid prefixes (e.g. lic_xxx), never serial.
Vitest 4 — unit tests
Tests live intests/unit/** and src/**/*.{test,spec}.ts. Node environment,
globals enabled, @/ alias resolved.
next-safe-action 3-tier — typed server actions
Every mutation goes through one of three clients insrc/lib/safe-action.ts:
server-only — runtime guard against client leaks
Modules that touch the DB or secretsimport 'server-only' at the top. If
they’re accidentally imported into a client component, the build fails with
a clear error instead of shipping your DATABASE_URL to the browser.
Already protected: src/db/index.ts, src/lib/auth.ts,
src/payment/provider/*, src/credits/server.ts, src/license/index.ts.
@t3-oss/env-nextjs — typed env vars
src/env.ts declares every required and optional env var with Zod. The app
refuses to boot when a required var is missing, so misconfiguration fails at
build time instead of in production at 3 AM.
Always import from @/env (not process.env.X) so you get types and
validation.
Verify it works
Before every commit:Common pitfalls
- Forgetting
import 'server-only'in a new module that touches secrets. Add it as the first line of any file that readsenv.STRIPE_SECRET_KEY, queries the DB, or hits an API key. The build will catch the leak. process.env.Xinstead ofenv.X. You skip Zod validation, lose types, and break in production when the var is misspelled. There’s no ESLint rule against it — use code review or grep forprocess\.env\..- Calling a
userActionClientaction from an unauthenticated route. It throwsUNAUTHORIZEDand returns 401. Wrap the call in a session check or useactionClientif the action is genuinely public. pnpm db:pushagainst production. It diffs the schema and applies destructive changes (drops, renames) without a migration file. Usedb:generate+db:migratefor prod, and pass--strictif you want a confirmation prompt before destructive ops.- Knip false positives. Skill registries and dynamically-imported
providers look unused to static analysis. Add them to
knip.config.ts’signorelist — don’t delete the file.
Official docs
- Biome: biomejs.dev
- Knip: knip.dev
- Drizzle ORM: orm.drizzle.team
- Vitest: vitest.dev
- next-safe-action: next-safe-action.dev
- t3-env: env.t3.gg