From f37b0a526de70c1caa737783ed2f5d68fd608a48 Mon Sep 17 00:00:00 2001 From: Casey Hoover Date: Fri, 24 Apr 2026 00:57:59 +0000 Subject: [PATCH] feat: onboard to dotenvx for env loading Replace `tsx --env-file` and raw Prisma invocations with `dotenvx run`, so every workspace task pulls from the single root `.env.local`. This unlocks encrypted env files (`dotenvx set` / `DOTENV_PRIVATE_KEY_*`) and removes the need for per-package `.env.local` duplicates. Closes #58 Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 14 ++++++++++++++ .gitignore | 7 ++++++- CLAUDE.md | 2 ++ README.md | 18 +++++++++++++++++- apps/api/package.json | 2 +- package.json | 9 +++++---- packages/db/package.json | 6 +++--- pnpm-lock.yaml | 20 ++++++++++++++++++++ 8 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4e69177 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# BetterAuth session signing key — `openssl rand -base64 32` +BETTER_AUTH_SECRET= +BETTER_AUTH_URL=http://localhost:3000 + +# GitHub OAuth app — https://github.com/settings/developers +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# Postgres (matches docker-compose.yaml) +DATABASE_URL=postgresql://skeleton:skeleton@localhost:5432/skeleton + +# Sentry Spotlight for local error/trace inspection +SENTRY_SPOTLIGHT=1 +NEXT_PUBLIC_SENTRY_SPOTLIGHT=1 diff --git a/.gitignore b/.gitignore index ccf7c17..f4fd22a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,8 +35,13 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files +# env files — dotenvx: commit .env.example and encrypted .env files; never commit decryption keys or local overrides .env* +!.env.example +!.env.vault +.env.keys +.env.local +.env.*.local # vercel .vercel diff --git a/CLAUDE.md b/CLAUDE.md index 8017866..0e2fc8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,8 @@ pnpm workspaces + Turborepo. Key commands: - `pnpm test` — run tests with coverage - `pnpm codegen:openapi` — generate OpenAPI spec from API routes +Env vars are loaded via [dotenvx](https://dotenvx.com). `.env.local` at the repo root is the single source of truth — `pnpm dev` injects it into every task, so per-package `.env.local` files are not required. Use `pnpm exec dotenvx set KEY value -f .env.` to commit encrypted secrets; decryption keys live in `.env.keys` (gitignored). + ## Structure | Path | Role | diff --git a/README.md b/README.md index fdd9787..192b1b6 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Requires [VS Code](https://code.visualstudio.com/) with the [Dev Containers exte 1. [Create a GitHub OAuth app](https://github.com/settings/developers) with callback URL `http://localhost:3000/api/auth/callback/github` 2. Open VS Code and run **Dev Containers: Clone Repository in Container Volume** from the command palette 3. Paste the repo URL and let the container build -4. The container generates `.env.local` with sensible defaults. Fill in your GitHub OAuth credentials: +4. The container generates `.env.local` with sensible defaults (template lives at `.env.example`). Fill in your GitHub OAuth credentials: ```bash GITHUB_CLIENT_ID= @@ -65,6 +65,22 @@ GITHUB_CLIENT_SECRET= 5. Run `pnpm dev` and you're up +### Environment variables + +Secrets are loaded through [dotenvx](https://dotenvx.com) — `pnpm dev` injects `.env.local` into every workspace task, so a single file at the repo root feeds the API, web, and Prisma. Use `.env.example` as the checked-in template. + +To commit secrets for a non-local environment, encrypt them first: + +```bash +# set an encrypted value (creates .env.production if needed; keys go to .env.keys) +pnpm exec dotenvx set GITHUB_CLIENT_SECRET "" -f .env.production + +# run with a specific encrypted file +pnpm exec dotenvx run -f .env.production -- pnpm start +``` + +`.env.local` and `.env.keys` are always gitignored. Encrypted files like `.env.production` are safe to commit — the decryption key stays in `.env.keys` (or a `DOTENV_PRIVATE_KEY_*` env var in production). + ### What `pnpm dev` starts Open http://localhost:3000 to access the app. The other services are available from the admin sidebar after signing in: diff --git a/apps/api/package.json b/apps/api/package.json index 934161f..5158b34 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "NODE_ENV=development tsx watch --env-file=.env.local src/index.ts", + "dev": "NODE_ENV=development dotenvx run --quiet -f ../../.env.local -- tsx watch src/index.ts", "build": "tsup src/index.ts --format esm", "start": "node dist/index.js", "lint": "tsc --noEmit", diff --git a/package.json b/package.json index 0b89e9e..9f7b037 100644 --- a/package.json +++ b/package.json @@ -3,24 +3,25 @@ "private": true, "license": "MIT", "scripts": { - "dev": "bash .devcontainer/check-env.sh && (spotlight server & turbo dev test:ui)", + "dev": "bash .devcontainer/check-env.sh && dotenvx run --quiet --convention=nextjs -- bash -c 'spotlight server & turbo dev test:ui'", "build": "turbo build", "lint": "turbo lint", "format": "prettier --write .", "format:check": "prettier --check .", "codegen:openapi": "turbo codegen:openapi", "db:generate": "turbo db:generate", - "db:push": "turbo db:push", + "db:push": "dotenvx run --quiet --convention=nextjs -- turbo db:push", "test": "vitest run --coverage", "test:e2e": "pnpm --filter @skeleton/web test:e2e", - "test:ui": "vitest --ui --open false --changed", - "db:studio": "pnpm --filter @skeleton/db studio", + "test:ui": "dotenvx run --quiet --convention=nextjs -- vitest --ui --open false --changed", + "db:studio": "dotenvx run --quiet --convention=nextjs -- pnpm --filter @skeleton/db studio", "docker:up": "docker compose up -d", "docker:down": "docker compose down", "clean": "find . -name node_modules -o -name .turbo -o -name .next -o -name .pnpm-store | xargs rm -rf", "reset": "pnpm clean && pnpm install && pnpm build && pnpm dev" }, "devDependencies": { + "@dotenvx/dotenvx": "^1.62.0", "@spotlightjs/spotlight": "^4.11.3", "@types/node": "^25.6.0", "@vitest/coverage-v8": "^4.1.5", diff --git a/packages/db/package.json b/packages/db/package.json index 4833722..5c952b6 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -9,10 +9,10 @@ ".": "./src/index.ts" }, "scripts": { - "dev": "prisma studio --port 5555 --browser none", + "dev": "dotenvx run --quiet -f ../../.env.local -- prisma studio --port 5555 --browser none", "db:generate": "prisma generate", - "db:push": "prisma db push", - "studio": "prisma studio" + "db:push": "dotenvx run --quiet -f ../../.env.local -- prisma db push", + "studio": "dotenvx run --quiet -f ../../.env.local -- prisma studio" }, "dependencies": { "@prisma/adapter-pg": "^7.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b8c4cc..a52955c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@dotenvx/dotenvx': + specifier: ^1.62.0 + version: 1.62.0 '@spotlightjs/spotlight': specifier: ^4.11.3 version: 4.11.3(hono-rate-limiter@0.4.2(hono@4.12.14)) @@ -589,6 +592,10 @@ packages: resolution: {integrity: sha512-g6QvAdXmSKMxmF1oFeCcDwklB5/fmkRXzApL3q2n20Z7YXUzDvFZg1ItTsXdX9g5hTyEKjmcOPJON37O5TiDew==} hasBin: true + '@dotenvx/dotenvx@1.62.0': + resolution: {integrity: sha512-dHMoiNqIyLnDxbsy16Zr55qN6a52dyocvOiVV4+ptjRIWNrBItbCNjazcv+hwKZGa7+WSKDHLTlyxzpK5yhxaQ==} + hasBin: true + '@ecies/ciphers@0.2.6': resolution: {integrity: sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==} engines: {bun: '>=1', deno: '>=2.7.10', node: '>=16'} @@ -6374,6 +6381,19 @@ snapshots: which: 4.0.0 yocto-spinner: 1.1.0 + '@dotenvx/dotenvx@1.62.0': + dependencies: + commander: 11.1.0 + dotenv: 17.4.2 + eciesjs: 0.4.18 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.4) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.4 + which: 4.0.0 + yocto-spinner: 1.1.0 + '@ecies/ciphers@0.2.6(@noble/ciphers@1.3.0)': dependencies: '@noble/ciphers': 1.3.0