Bridge Training
Contributing

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.

apps/api/package.json
{
  "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.ts

Referenced 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.sh

Naming 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

FileCommittedPurpose
.envYesShared base defaults
.env.developmentYesDevelopment-specific values
.env.productionYesProduction-specific values
.env.testsYesTest-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.

On this page