Scripts & Env
Where scripts live, naming conventions, and environment file loading.
Where scripts live
Scripts are split across four locations depending on their scope.
Package scripts (package.json)
Each app and package defines its own lifecycle scripts — build, dev, test, check-types. These are what Turbo orchestrates.
{
"scripts": {
"dev": "vite-node --watch src/index.ts",
"build": "tsup",
"test": "vitest run",
"check-types": "tsc --noEmit -p tsconfig.package.json"
}
}Package script files (apps/X/scripts/)
When a script is too complex for a one-liner in JSON (codegen, multi-step tooling), it goes in a scripts/ directory next to the package:
apps/api/scripts/generate-openapi.ts
apps/docs/scripts/generate-api-docs.ts
apps/web/scripts/generate-theme-indexes.tsReferenced from package.json as "generate:openapi": "tsx scripts/generate-openapi.ts".
Root scripts (package.json)
The root package.json provides shortcuts that delegate to Turbo or Docker — never business logic:
{
"dev:app": "turbo run dev -F app",
"build:api": "turbo run build -F api",
"docker:build:api": "docker build -f apps/api/Dockerfile -t bridge-training-api:latest .",
"test": "turbo run test",
"release:app": "./scripts/release-app.sh"
}Root script files (scripts/)
Multi-step release and CI scripts that span multiple packages:
scripts/release-app.sh
scripts/release-docs.sh
scripts/release-web.shNaming conventions
Scripts are prefixed by domain so related commands group together in package.json:
db:* — Schema and migrations
Code-level database operations (generate, migrate, seed). These run drizzle-kit or psql under the hood, using with:env for env loading:
{
"db:generate": "pnpm with:env drizzle-kit generate",
"db:migrate": "pnpm with:env drizzle-kit migrate",
"db:migrate:prod": "pnpm with:env:prod drizzle-kit migrate",
"db:studio": "pnpm with:env drizzle-kit studio"
}local:* — Local infra
Start, stop, and reset the local Supabase instance. These manage Docker containers, not code.
{
"local:start": "pnpm local:supabase start",
"local:stop": "pnpm local:supabase stop",
"local:reset": "pnpm local:supabase db reset && pnpm db:migrate && pnpm local:seed"
}test:* — Testing variants
The bare test script runs the default suite. Prefixed variants add scope:
{
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:integration": "./scripts/test-integration.sh"
}generate:* — Codegen
Scripts that produce generated files (OpenAPI specs, route trees, type definitions):
{
"generate:openapi": "tsx scripts/generate-openapi.ts",
"generate:routes": "tsr generate"
}Environment files
File hierarchy
| File | Committed | Purpose |
|---|---|---|
.env | Yes | Shared base defaults |
.env.development | Yes | Development-specific values |
.env.production | Yes | Production-specific values |
.env.tests | Yes | Test-specific values |
Any file can be suffixed with .local (e.g. .env.local, .env.development.local) to add personal overrides. .local files are gitignored and always take precedence over their non-local counterpart.
How env files are loaded
Vite-based tools (vite, vite-node, vitest) load .env files automatically — no extra setup needed.
Non-Vite CLIs (drizzle-kit, supabase, psql) don't know about .env files. Instead of repeating dotenv -e .env -e .env.local -- in every script, packages define centralized with:env scripts:
{
"with:env": "dotenv -e .env -e .env.local --",
"with:env:prod": "dotenv -e .env.production -e .env.production.local --"
}Other scripts compose on top of these:
{
"db:generate": "pnpm with:env drizzle-kit generate",
"db:migrate:prod": "pnpm with:env:prod drizzle-kit migrate",
"local:supabase": "pnpm with:env npx supabase"
}This keeps the env loading logic in one place — if the file list changes, only with:env needs updating.