Skip to main content
vibestrap is opinionated about dev tooling because indie hackers don’t have time to babysit a build. Everything is either fast (Biome over ESLint+Prettier) or load-bearing (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 engines in package.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 in biome.json.
pnpm lint        # biome check .
pnpm lint:fix    # biome check --write .
pnpm format      # biome format --write .
Style is single quotes, ES5 trailing commas, 100-column width. Toggle rules in biome.json under linter.rules.

Knip 6 — unused-export detection

Finds dead exports, unused dependencies, and orphaned files. Run before shipping a refactor.
pnpm knip
Config lives in knip.config.ts. Add false-positives to its ignore / ignoreDependencies lists.

Drizzle ORM 0.45 — type-safe SQL

Schema split across src/db/{auth,app,affiliate,ai,license}.schema.ts. IDs are text with nanoid prefixes (e.g. lic_xxx), never serial.
pnpm db:push        # dev — sync schema to DB without a migration file
pnpm db:generate    # prod — generate a SQL migration from schema diff
pnpm db:migrate     # prod — apply pending migrations
pnpm db:studio      # visual DB explorer at localhost:4983

Vitest 4 — unit tests

Tests live in tests/unit/** and src/**/*.{test,spec}.ts. Node environment, globals enabled, @/ alias resolved.
pnpm test           # one-shot run
pnpm test:watch     # watch mode

next-safe-action 3-tier — typed server actions

Every mutation goes through one of three clients in src/lib/safe-action.ts:
actionClient        // public, no auth
userActionClient    // requires logged-in user (provides ctx.user)
adminActionClient   // requires user.role === 'admin'
Each action declares an input schema with Zod, returns a typed result.

server-only — runtime guard against client leaks

Modules that touch the DB or secrets import '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:
pnpm typecheck && pnpm lint && pnpm build
If any of the three fails, the error message tells you what to fix. Don’t push code that fails any of them — the production CI runs the same chain.

Common pitfalls

  1. Forgetting import 'server-only' in a new module that touches secrets. Add it as the first line of any file that reads env.STRIPE_SECRET_KEY, queries the DB, or hits an API key. The build will catch the leak.
  2. process.env.X instead of env.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 for process\.env\..
  3. Calling a userActionClient action from an unauthenticated route. It throws UNAUTHORIZED and returns 401. Wrap the call in a session check or use actionClient if the action is genuinely public.
  4. pnpm db:push against production. It diffs the schema and applies destructive changes (drops, renames) without a migration file. Use db:generate + db:migrate for prod, and pass --strict if you want a confirmation prompt before destructive ops.
  5. Knip false positives. Skill registries and dynamically-imported providers look unused to static analysis. Add them to knip.config.ts’s ignore list — don’t delete the file.

Official docs