@withmata/blueprints 0.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,9 @@ Complete.
10
10
 
11
11
  ## Problem
12
12
 
13
- Environment variable management is tedious and error-prone across projects. Developers forget to add new variables to `.env.example`, miss the split between client and server vars in Next.js, deploy with missing variables that only crash at runtime, and have no type safety when accessing `process.env`. This blueprint creates a validated, type-safe environment variable system using `@t3-oss/env` + Zod that catches missing or malformed variables at build time.
13
+ Environment variable management is tedious and error-prone across projects. Developers miss the split between client and server vars in Next.js, deploy with missing variables that only crash at runtime, and have no type safety when accessing `process.env`. This blueprint creates a validated, type-safe environment variable system using `@t3-oss/env` + Zod that catches missing or malformed variables at build time.
14
+
15
+ The Zod schema files are the **registry** of what environment variables an app needs — there is no separate `.env.example`. Values are delivered by Infisical (see the `secrets-infisical` blueprint) or the deployment platform; for local work, a developer uses `.env.local` or `infisical run`.
14
16
 
15
17
  This blueprint covers both Next.js apps (client/server split with `@t3-oss/env-nextjs`) and backend apps (server-only with `@t3-oss/env-core`).
16
18
 
@@ -56,9 +58,9 @@ Both files are imported in `next.config.ts` to trigger build-time validation.
56
58
 
57
59
  - **`z.preprocess` for PORT** (recommended) — Environment variables are always strings. PORT needs to be a number. `z.preprocess` handles the string-to-number conversion cleanly.
58
60
 
59
- - **`.env.example` with documentation** (required) — Every app gets a documented `.env.example` with section headers, comments explaining each variable, and local/production value hints. This is the single source of truth for what environment variables an app needs.
61
+ - **Schema files are the registry** (required) — The Zod schema files (`client.ts`, `server.ts`, `env.ts`) are the single source of truth for what environment variables an app needs. Keep them well-commented (purpose, local/production hints) they replace the old `.env.example`. Values come from Infisical or the platform.
60
62
 
61
- - **Empty `.env.local` created** (required) — An empty `.env.local` file is created during scaffolding so developers have the file ready to fill in. This file is gitignored.
63
+ - **Empty `.env.local` created** (required) — An empty `.env.local` file is created during scaffolding so developers have the file ready to fill in for local work. This file is gitignored.
62
64
 
63
65
  ---
64
66
 
@@ -70,7 +72,7 @@ Both files are imported in `next.config.ts` to trigger build-time validation.
70
72
  - Zod for schema validation
71
73
  - Client/server split in Next.js (two files)
72
74
  - Build-time validation via `next.config.ts` import
73
- - `.env.example` for every app
75
+ - Well-commented schema files as the variable registry (no `.env.example`)
74
76
 
75
77
  **Recommended defaults — ask during scaffolding:**
76
78
 
@@ -123,7 +125,6 @@ apps/web/
123
125
  │ ├── client.ts # Client env schema + validation
124
126
  │ └── server.ts # Server env schema + validation
125
127
  ├── next.config.ts # Imports both env files for build-time validation
126
- ├── .env.example # Documented env template
127
128
  └── .env.local # Local values (gitignored)
128
129
  ```
129
130
 
@@ -152,7 +153,6 @@ apis/server/
152
153
  ├── src/
153
154
  │ ├── env.ts # Server env schema + validation
154
155
  │ └── index.ts # Imports env.ts at top
155
- ├── .env.example # Documented env template
156
156
  └── .env.local # Local values (gitignored)
157
157
  ```
158
158
 
@@ -175,7 +175,6 @@ For standalone applications (Next.js or backend) without a monorepo, the pattern
175
175
  |--------|----------|-------------|
176
176
  | Next.js env location | `apps/web/src/env/` | `src/env/` |
177
177
  | Backend env location | `apis/server/src/env.ts` | `src/env.ts` |
178
- | `.env.example` location | Per-app (e.g., `apps/web/.env.example`) | Project root `.env.example` |
179
178
  | `.env.local` location | Per-app (e.g., `apps/web/.env.local`) | Project root `.env.local` |
180
179
  | Import path | `~/env/server` or `~/env/client` | `~/env/server` or `@/env/server` |
181
180
 
@@ -186,7 +185,7 @@ All core patterns are project-type-agnostic:
186
185
  - Zod schemas for validation
187
186
  - Client/server split (two files) for Next.js
188
187
  - Build-time validation via `next.config.ts` import
189
- - `.env.example` documentation pattern
188
+ - Well-commented schema files as the variable registry
190
189
  - `emptyStringAsUndefined: true`
191
190
 
192
191
  ---
@@ -197,10 +196,8 @@ All core patterns are project-type-agnostic:
197
196
  |---|---|---|---|
198
197
  | `files/nextjs/env/client.ts` | Next.js client env schema | `apps/web/src/env/client.ts` | `src/env/client.ts` |
199
198
  | `files/nextjs/env/server.ts` | Next.js server env schema | `apps/web/src/env/server.ts` | `src/env/server.ts` |
200
- | `files/nextjs/.env.example` | Documented env template (Next.js) | `apps/web/.env.example` | `.env.example` |
201
199
  | `files/nextjs/.env.local` | Empty local env file (Next.js) | `apps/web/.env.local` | `.env.local` |
202
200
  | `files/server/env.ts` | Backend env schema (`@t3-oss/env-core`) | `apis/server/src/env.ts` | `src/env.ts` |
203
- | `files/server/.env.example` | Documented env template (backend) | `apis/server/.env.example` | `.env.example` |
204
201
  | `files/server/.env.local` | Empty local env file (backend) | `apis/server/.env.local` | `.env.local` |
205
202
 
206
203
  ---
@@ -222,29 +219,27 @@ All core patterns are project-type-agnostic:
222
219
  import "./src/env/client";
223
220
  ```
224
221
  If existing `import "./env"` is present, replace it with the two new imports.
225
- 6. Copy `.env.example` — replace `{{APP_NAME}}`
226
- 7. Create `.env.local` (empty)
227
- 8. If other blueprints are already installed, pre-populate their env vars into the appropriate schema files
222
+ 6. Create `.env.local` (empty)
223
+ 7. If other blueprints are already installed, pre-populate their env vars into the appropriate schema files
228
224
 
229
225
  ### Phase 2: Backend App Setup (if present)
230
226
 
231
- 9. Copy `env.ts` to `src/env.ts` in the backend app — replace `{{APP_NAME}}`
232
- 10. Update the app's entry point (e.g., `src/index.ts`) to import `env.ts`:
233
- ```typescript
234
- import { env } from "./env.js";
235
- ```
236
- 11. Copy `.env.example` — replace `{{APP_NAME}}`
237
- 12. Create `.env.local` (empty)
227
+ 8. Copy `env.ts` to `src/env.ts` in the backend app — replace `{{APP_NAME}}`
228
+ 9. Update the app's entry point (e.g., `src/index.ts`) to import `env.ts`:
229
+ ```typescript
230
+ import { env } from "./env.js";
231
+ ```
232
+ 10. Create `.env.local` (empty)
238
233
 
239
234
  ### Phase 3: Dependency Installation
240
235
 
241
- 13. For each Next.js app: ensure `@t3-oss/env-nextjs` and `zod` are in `dependencies`
242
- 14. For each backend app: ensure `@t3-oss/env-core`, `zod`, and `dotenv` are in `dependencies`
236
+ 11. For each Next.js app: ensure `@t3-oss/env-nextjs` and `zod` are in `dependencies`
237
+ 12. For each backend app: ensure `@t3-oss/env-core`, `zod`, and `dotenv` are in `dependencies`
243
238
 
244
239
  ### Phase 4: Verification
245
240
 
246
- 15. Run `bun dev` (or equivalent) — env validation should run without errors
247
- 16. Try removing a required variable from `.env.local` — build should fail with a clear error message
241
+ 13. Run `bun dev` (or equivalent) — env validation should run without errors
242
+ 14. Try removing a required variable from `.env.local` — build should fail with a clear error message
248
243
 
249
244
  ---
250
245
 
@@ -254,22 +249,22 @@ All core patterns are project-type-agnostic:
254
249
 
255
250
  | When you... | Then do... | Why |
256
251
  |---|---|---|
257
- | Add a new env var in code | Add to `env/client.ts` or `env/server.ts` schema AND `.env.example` AND `.env.local` | Missing from schema = build-time crash; missing from example = teammates can't set up |
252
+ | Add a new env var in code | Add to the `env/client.ts` or `env/server.ts` schema (the registry) and, if used locally, `.env.local` | Missing from schema = build-time crash; the schema documents what's needed |
258
253
  | Add a `NEXT_PUBLIC_*` var | Put it in `env/client.ts` with both `client` schema and `runtimeEnv` entries | Client vars must be explicitly mapped in Next.js |
259
254
  | Add a server-only var to Next.js | Put it in `env/server.ts` (uses `experimental__runtimeEnv: process.env`) | Avoids listing every server var twice |
260
- | Remove an env var | Remove from schema file, `.env.example`, and `.env.local` | Stale vars cause confusion |
255
+ | Remove an env var | Remove from the schema file and `.env.local` | Stale vars cause confusion |
261
256
  | Add a new app to the monorepo | Create its own `env/` files — don't share env schemas between apps | Each app has different variable requirements |
262
- | Change a var from optional to required | Update the Zod schema (remove `.optional()`) and ensure `.env.example` has a value | Will break builds for teammates with empty values |
257
+ | Change a var from optional to required | Update the Zod schema (remove `.optional()`) and ensure the value is provided (Infisical/platform/`.env.local`) | Will break builds where the value is empty |
263
258
 
264
259
  ### Condensed Rules for project rules file
265
260
 
266
261
  ```markdown
267
262
  ### env-t3 maintenance
268
- - After adding a new env var: add it to the env schema file (client.ts or server.ts), `.env.example`, and `.env.local`
263
+ - The Zod schema files (client.ts/server.ts/env.ts) are the registry of env vars keep them well-commented. There is no .env.example; values come from Infisical or the platform.
264
+ - After adding a new env var: add it to the env schema file (client.ts or server.ts) and, if used locally, .env.local
269
265
  - NEXT_PUBLIC_* vars go in env/client.ts with explicit runtimeEnv mapping; server vars go in env/server.ts
270
- - After removing an env var: remove from schema, .env.example, and .env.local
266
+ - After removing an env var: remove from the schema and .env.local
271
267
  - Never access process.env directly — always use the typed env objects (clientEnv, serverEnv, or env)
272
- - Keep .env.example documented with section headers and local/production hints
273
268
  ```
274
269
 
275
270
  ---
@@ -288,7 +283,7 @@ All core patterns are project-type-agnostic:
288
283
  - **Client/server split** — Next.js apps always get two files, not one combined file
289
284
  - **Build-time validation** — env files are always imported in `next.config.ts`
290
285
  - **`emptyStringAsUndefined`** — always enabled (prevents empty string foot-guns)
291
- - **`.env.example` documentation** always includes section headers and comments
286
+ - **Schema as registry** — the Zod schema files are the documented source of truth for env vars; no `.env.example` (values come from Infisical or the platform)
292
287
 
293
288
  ## Project Context Output
294
289
 
@@ -307,13 +302,11 @@ apps_configured:
307
302
  files:
308
303
  - src/env/client.ts
309
304
  - src/env/server.ts
310
- - .env.example
311
305
  - .env.local
312
306
  - name: server
313
307
  type: backend
314
308
  files:
315
309
  - src/env.ts
316
- - .env.example
317
310
  - .env.local
318
311
  packages_added:
319
312
  - "@t3-oss/env-nextjs"
@@ -0,0 +1,295 @@
1
+ # Secrets Management Blueprint — Infisical /shared + Env-Aware References
2
+
3
+ ## Tier
4
+
5
+ Feature
6
+
7
+ ## Status
8
+
9
+ Complete.
10
+
11
+ ## Problem
12
+
13
+ Multi-app monorepos duplicate the same secret (DB URLs, API keys) across every app's env, so a rotation means editing N places and inevitably drifting. Local `.env` files also have to be copied into every fresh clone / workspace, and there's no single place that says "this is the canonical value." This blueprint centralizes every shared secret **once** in Infisical under a domain-grouped `/shared` folder, and has each app reference those values, so a single change propagates everywhere and a fresh checkout needs only `infisical login` + the committed `.infisical.json`.
14
+
15
+ The agent does all the plumbing (folders, placeholders, references, `infisical run` wiring); the human's only remaining job is to paste real secret values over the placeholders in the Infisical dashboard. The agent never invents or guesses secret values.
16
+
17
+ ## Blueprint Dependencies
18
+
19
+ - **Required:** `env-t3` — the T3 Zod schemas (`env/client.ts`, `env/server.ts`, backend `env.ts`) are the source of truth for which variables exist; this blueprint reads them to build the Infisical structure. If `env-t3` is not installed, run `/scaffold-env` first.
20
+ - **Recommended:** `db-drizzle-postgres`, `auth-better-auth` — they define the natural domains (databases, auth) and their variables.
21
+
22
+ ### Interaction with Other Blueprints
23
+
24
+ - `env-t3` defines *what* vars exist + validation. This blueprint defines *where the values live and how they're delivered*. They are complementary layers — `env-t3` stays usable without Infisical (some projects deliver secrets via Vercel/Railway directly), and Infisical cleanly **consumes** the T3 schemas instead of bloating them.
25
+ - When a feature blueprint adds an env var to a T3 schema, the maintenance rules here sync it into Infisical as a placeholder. The T3 schema change and the Infisical change are two halves of the same edit.
26
+
27
+ ---
28
+
29
+ ## Architecture
30
+
31
+ ### Core Pattern
32
+
33
+ - Canonical **values** live exactly once in `/shared/<domain>/<KEY>` (placeholders until a human fills them).
34
+ - Apps/packages hold **environment-aware references**: `${<ENV_SLUG>.shared.<domain>.<KEY>}` — never raw values for shared vars.
35
+ - **App-native** vars (`PORT`, `NEXT_PUBLIC_*`, langfuse keys, etc.) stay as real values in the app folder, never referenced.
36
+ - The CLI delivers secrets at launch via `infisical run --path=<app> --env=<env>`.
37
+
38
+ Change a shared value once → every app that references it sees the new value. No copy-paste drift across `apps/server`, `apps/web`, `packages/db`.
39
+
40
+ ### Reference Syntax (CRITICAL)
41
+
42
+ References MUST be environment-prefixed, and the slug MUST match the environment the reference lives in:
43
+
44
+ ```
45
+ ${dev.shared.pipeline.PIPELINE_VERSION} # in the dev environment
46
+ ${staging.shared.pipeline.PIPELINE_VERSION} # in the staging environment
47
+ ${prod.shared.pipeline.PIPELINE_VERSION} # in the prod environment
48
+ ```
49
+
50
+ The **same key** therefore has a **different literal value in each environment** — each points at its own environment's shared subtree.
51
+
52
+ The bare form `${shared.<domain>.<KEY>}` **DOES NOT RESOLVE** (this is the #1 past failure). Other wrong forms:
53
+
54
+ ```
55
+ ${shared.pipeline.PIPELINE_VERSION} ← MISSING env slug — does not resolve
56
+ ${env.shared.pipeline.PIPELINE_VERSION} ← literal "env" — wrong
57
+ ${/shared/pipeline/PIPELINE_VERSION} ← slash path form — wrong for this case
58
+ ```
59
+
60
+ Verify with the §Verification check below — the reference must print its value, not the literal `${...}`.
61
+
62
+ ### Domains
63
+
64
+ `/shared` is subdivided by **concern** (not by app), derived from installed blueprints + the T3 schemas: e.g. `core`, `auth`, `pipeline`, `knowledge-base`, `image-storage`, `ai`, `search`, `infra`. Domains are a soft taxonomy; the user confirms the map before anything is applied.
65
+
66
+ | Source signal | Domain(s) created |
67
+ |---|---|
68
+ | `db-drizzle-postgres` installed, schema groups | one domain per DB group (`core`, `auth`, `pipeline`, …), each with a `*_DATABASE_URL` |
69
+ | `auth-better-auth` installed | `auth` (`BETTER_AUTH_*`, `GOOGLE_*`, `RESEND_EMAIL_API_KEY`) |
70
+ | Object storage / S3-style vars present | `image-storage`, `knowledge-base` (bucket keys) |
71
+ | LLM / model API keys present | `ai` |
72
+ | Web search / scraping / media API keys present | `search` |
73
+ | Redis / queue / cache | `infra` |
74
+ | Anything not classifiable | `core` (catch-all) or ask |
75
+
76
+ ### Placeholders
77
+
78
+ Every shared secret is created with the `REPLACE_ME` sentinel (caps-with-underscore, trivially greppable in the UI). Sensible **non-secret** defaults are allowed for clearly non-sensitive config (`KB_QUEUE_NAMESPACE=pgboss_<env>`, `KB_ENABLE_RERANK=false`, `*_PUBLIC_BASE_URL=https://replace-me.example.com`). Anything that is an actual secret (keys, passwords, connection strings) → `REPLACE_ME`. The sync script only stubs a value if it is currently empty — it never overwrites a filled value.
79
+
80
+ ### Verbose, Meaningful Names
81
+
82
+ The blueprint encourages self-describing variable names with the domain/purpose embedded: `STORAGE_ENDPOINT` → `IMAGE_BUCKET_ENDPOINT`, `RESEND_API_KEY` → `RESEND_EMAIL_API_KEY`, `SYNTHETIC_API_KEY` → `SYNTHETIC_AI_API_KEY`, `PARALLEL_API_KEY` → `PARALLEL_RESEARCH_API_KEY`. The skill offers an optional rename/cleanup step.
83
+
84
+ ---
85
+
86
+ ## Hard Requirements vs Recommended Defaults
87
+
88
+ **Hard requirements:**
89
+ - Infisical CLI + a linked project (`.infisical.json` committed per app/package — holds no secrets).
90
+ - `/shared/<domain>` canonical values; app folders hold env-aware references.
91
+ - Environment-prefixed reference syntax (the slug matches the environment the reference lives in).
92
+ - An idempotent, dry-run-by-default sync script committed to the repo.
93
+ - `infisical run --path --env` wrappers in dev/run scripts.
94
+
95
+ **Recommended defaults — ask during scaffolding:**
96
+
97
+ | Choice | Recommended | Alternatives |
98
+ |---|---|---|
99
+ | Environments | `dev`, `staging`, `prod` | project-specific set (use the real Infisical slugs) |
100
+ | Placeholder sentinel | `REPLACE_ME` | per-key sentinel `REPLACE_ME_<KEY>`; key-name-as-value |
101
+ | Sync script language | bash (uses Infisical CLI, zero deps) | TypeScript |
102
+ | Domain map | derived from blueprints/schemas, user confirms | fully manual |
103
+
104
+ ---
105
+
106
+ ## Dependencies
107
+
108
+ ### Tooling
109
+
110
+ | Tool | Install | Purpose |
111
+ |---|---|---|
112
+ | Infisical CLI | `brew install infisical/get-cli/infisical` | Create folders/secrets, deliver them via `infisical run` |
113
+
114
+ ### npm packages
115
+
116
+ None required at runtime — delivery is via the CLI wrapper. `env-t3` already provides the validation layer.
117
+
118
+ ### Environment Variables
119
+
120
+ This blueprint creates no new variables of its own. It mirrors the variables already declared in the project's T3 env schemas into Infisical.
121
+
122
+ ---
123
+
124
+ ## Monorepo Wiring
125
+
126
+ - `.infisical.json` committed in each of `apps/*`, `packages/*` that need secrets (workspaceId only — no secrets).
127
+ - `package.json` dev scripts wrapped in `infisical run --path=/apps/<app> --env=dev`:
128
+ ```jsonc
129
+ // apps/server
130
+ "dev": "infisical run --path=/apps/server --env=dev -- bun --watch src/index.ts"
131
+ // apps/web
132
+ "dev": "infisical run --path=/apps/web --env=dev -- bun run --bun next dev"
133
+ // packages/db — a :dev variant per script that needs DB envs
134
+ "core:migrate:dev": "infisical run --env=dev --path=/packages/db -- bun run core:migrate"
135
+ ```
136
+ - Production scripts stay clean — Railway/the host injects Infisical at the platform level.
137
+
138
+ ## Single-Repo Adaptation
139
+
140
+ One app folder (e.g. `/app`) plus `/shared`. Same env-aware references; same placeholder + sync-script approach. `infisical run --path=/app --env=dev`. The `APP_PATHS` and `REF_MAP` in the data map collapse to the single app.
141
+
142
+ ---
143
+
144
+ ## File Manifest
145
+
146
+ | File | Purpose | Target |
147
+ |---|---|---|
148
+ | `files/scripts/infisical-sync.sh` | Idempotent, dry-run-by-default structure sync; loops all environments | `scripts/infisical-sync.sh` |
149
+ | `files/scripts/infisical-map.sh` | Data map: `ENVIRONMENTS`, `DOMAINS`, `APP_PATHS`, `SHARED_MAP`, `REF_MAP` (generated per project) | `scripts/infisical-map.sh` |
150
+ | `files/docs/INFISICAL.md` | Runbook: setup, reference syntax, verification, maintenance | `docs/INFISICAL.md` |
151
+
152
+ ---
153
+
154
+ ## Integration Steps
155
+
156
+ ### Phase 0: Preconditions
157
+
158
+ 1. Confirm Infisical CLI installed (`infisical --version`); else `brew install infisical/get-cli/infisical`.
159
+ 2. Confirm `infisical login` done.
160
+ 3. Confirm an Infisical project exists and `infisical init` produced a committed `.infisical.json` in each app/package dir.
161
+ 4. Confirm `env-t3` is installed (read `.project-context.md`). If not, run `/scaffold-env` first.
162
+ 5. Capture the real environment **slugs** (default `dev`, `staging`, `prod`) — these go into the reference prefix and MUST match Infisical exactly.
163
+
164
+ ### Phase 1: Read Sources of Truth
165
+
166
+ 6. Read the T3 env schemas in every app/package → full variable list per app.
167
+ 7. Read `.project-context.md` → installed blueprints, app layout, npm scope.
168
+
169
+ ### Phase 2: Derive and Confirm the Plan (do not apply yet)
170
+
171
+ 8. Classify each var: shared-reference vs app-native.
172
+ 9. Group shared vars into domains (table above).
173
+ 10. Produce a dry-run plan: folders, shared placeholders, app references, app-native vars left in place.
174
+ 11. **Present the domain map + plan and ask the user to confirm/edit.**
175
+
176
+ ### Phase 3: Generate the Sync Script + Data Map
177
+
178
+ 12. Write `scripts/infisical-sync.sh` and `scripts/infisical-map.sh` from the templates, filling the data map from the confirmed plan.
179
+
180
+ ### Phase 4: Apply
181
+
182
+ 13. Run dry-run, show output, then (on user OK) re-run with `APPLY=1`. The script loops every environment.
183
+
184
+ ### Phase 5: Wire package.json
185
+
186
+ 14. Add/adjust `infisical run --path=... --env=...` wrappers in dev/run scripts; add `:dev` variants for `packages/db` scripts. Leave production scripts clean.
187
+
188
+ ### Phase 6: Verify (do not skip)
189
+
190
+ 15. Resolve a reference end to end for at least `dev`:
191
+ ```bash
192
+ infisical run --path=/apps/server --env=dev -- printenv CORE_DATABASE_URL
193
+ ```
194
+ Prints a value → good. Prints the literal `${dev.shared...}` → the reference is malformed or the shared key/folder is missing. Fix before continuing.
195
+
196
+ ### Phase 7: Record + Summarize
197
+
198
+ 16. Update `.project-context.md` and the project-maintenance skill.
199
+ 17. Print files created, the full domain map, and the **`REPLACE_ME` checklist** the user must fill in Infisical.
200
+
201
+ ---
202
+
203
+ ## Maintenance Hooks
204
+
205
+ ### Secrets Hooks
206
+
207
+ | When you... | Then do... | Why |
208
+ |---|---|---|
209
+ | Add a **shared** env var to a T3 schema | Add the key to `/shared/<domain>` (placeholder) in every env, add an env-aware reference in each consuming app folder, update `infisical-map.sh`, re-run the sync script | Keeps the single source of truth and every consumer in sync |
210
+ | Add an **app-native** var | Set it directly in that app's Infisical folder as a real value — no shared key, no reference | App-native vars don't belong in `/shared` |
211
+ | Add a new environment | Add its slug to `ENVIRONMENTS` and re-run the sync script | Folders + references are per-environment |
212
+ | Rotate a secret | Edit the single `/shared/<domain>/<KEY>` value; all apps inherit it | Never edit a value in an app folder — it should be a reference |
213
+ | Remove a var | Delete the shared key AND every app reference; remove from `infisical-map.sh` | Stale references print the literal `${...}` |
214
+
215
+ ### Condensed Rules for project rules file
216
+
217
+ ```markdown
218
+ ### secrets-infisical maintenance
219
+ - Adding a SHARED env var (used by >1 app, or a domain's canonical value):
220
+ 1. Add the key to its `/shared/<domain>` folder in EVERY environment, set to
221
+ `REPLACE_ME` (or a non-secret default).
222
+ 2. Add an environment-aware reference `${<env>.shared.<domain>.<KEY>}` in each
223
+ consuming app/package folder, for every environment.
224
+ 3. Add the key to scripts/infisical-map.sh (SHARED_MAP + REF_MAP) and re-run
225
+ `APPLY=1 ./scripts/infisical-sync.sh`.
226
+ 4. Tell the human which `/shared/<domain>/<KEY>` placeholders to fill.
227
+ - Adding an APP-NATIVE var (PORT, NEXT_PUBLIC_*, single-app config): set it
228
+ directly in that app's Infisical folder as a real value — do NOT create a
229
+ shared key or reference.
230
+ - Reference syntax is ALWAYS `${<env-slug>.shared.<domain>.<KEY>}` where the slug
231
+ matches the environment the reference lives in. The bare `${shared...}` form
232
+ DOES NOT resolve. Verify with:
233
+ `infisical run --path=/apps/<app> --env=dev -- printenv <KEY>`.
234
+ - Rotating a secret: edit the single `/shared/<domain>/<KEY>` value; all
235
+ referencing apps inherit it. Never edit the value in an app folder.
236
+ - Removing a var: delete the shared key AND every app reference to it; remove from
237
+ infisical-map.sh.
238
+ - Never commit real secret values. `.infisical.json` is safe to commit; `.env*`
239
+ stays gitignored.
240
+ ```
241
+
242
+ ---
243
+
244
+ ## What's Configurable
245
+
246
+ - Environments list (slugs)
247
+ - Domain map (the soft taxonomy)
248
+ - Placeholder sentinel
249
+ - Which apps/packages are wired
250
+ - Sync script language (bash vs TS)
251
+
252
+ ## What's Opinionated
253
+
254
+ - **Infisical** as the secret store.
255
+ - **`/shared` + env-aware references** as the single-source-of-truth mechanism.
256
+ - **Environment-prefixed reference syntax** — non-negotiable; it's the only form that resolves.
257
+ - **Placeholder-first, human-fills-values** workflow — the agent never writes real secret values.
258
+
259
+ ## Project Context Output
260
+
261
+ Appends to `.project-context.md` under `## Installed Blueprints`:
262
+
263
+ ```yaml
264
+ ### secrets-infisical
265
+ blueprint: secrets-infisical
266
+ installed_at: <date>
267
+ choices:
268
+ placeholder_sentinel: REPLACE_ME
269
+ script_language: bash
270
+ environments:
271
+ - dev
272
+ - staging
273
+ - prod
274
+ domain_map:
275
+ core: [CORE_DATABASE_URL]
276
+ auth: [AUTH_DATABASE_URL, BETTER_AUTH_URL, BETTER_AUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, RESEND_EMAIL_API_KEY]
277
+ # ... one entry per domain
278
+ apps_wired:
279
+ - path: /apps/server
280
+ references: [core, auth, ...]
281
+ - path: /apps/web
282
+ references: [ai]
283
+ - path: /packages/db
284
+ references: [core, auth, pipeline, knowledge-base]
285
+ script_path: scripts/infisical-sync.sh
286
+ ```
287
+
288
+ ---
289
+
290
+ ## References
291
+
292
+ - **Infisical secret references:** https://infisical.com/docs/documentation/platform/secret-reference
293
+ - **Infisical CLI:** https://infisical.com/docs/cli/overview
294
+ - **`infisical run`:** https://infisical.com/docs/cli/commands/run
295
+ - **`infisical secrets`:** https://infisical.com/docs/cli/commands/secrets
@@ -0,0 +1,119 @@
1
+ # Infisical Secrets Runbook
2
+
3
+ How secrets work in this project, and how to keep them in sync.
4
+
5
+ ## The model
6
+
7
+ - **Canonical values live once** in Infisical under `/shared/<domain>/<KEY>`.
8
+ - **Apps and packages hold references**, never raw values, for anything shared.
9
+ - **App-native values** (`PORT`, `NEXT_PUBLIC_*`, etc.) live directly in the app's
10
+ Infisical folder.
11
+ - Secrets are delivered at launch by the Infisical CLI:
12
+ `infisical run --path=<app> --env=<env> -- <command>`.
13
+
14
+ Change a shared value once and every referencing app inherits it — no drift.
15
+
16
+ ## Reference syntax (read this before editing anything)
17
+
18
+ References are **environment-prefixed**. The slug MUST match the environment the
19
+ reference lives in:
20
+
21
+ ```
22
+ ${dev.shared.pipeline.PIPELINE_VERSION} # stored in the dev environment
23
+ ${staging.shared.pipeline.PIPELINE_VERSION} # stored in the staging environment
24
+ ${prod.shared.pipeline.PIPELINE_VERSION} # stored in the prod environment
25
+ ```
26
+
27
+ The same key has a different literal value per environment — each points at its
28
+ own environment's `/shared` subtree.
29
+
30
+ **Wrong forms (do not use — they silently fail to resolve):**
31
+
32
+ ```
33
+ ${shared.pipeline.PIPELINE_VERSION} ← missing env slug (the classic bug)
34
+ ${env.shared.pipeline.PIPELINE_VERSION} ← literal "env"
35
+ ${/shared/pipeline/PIPELINE_VERSION} ← slash path form
36
+ ```
37
+
38
+ ## First-time setup
39
+
40
+ 1. Install the CLI: `brew install infisical/get-cli/infisical`
41
+ 2. `infisical login`
42
+ 3. Ensure each app/package has a committed `.infisical.json` (from `infisical init`).
43
+ It holds the workspace id only — no secrets — and is safe to commit.
44
+ 4. Run the sync script to create the structure (see below).
45
+ 5. Open the Infisical dashboard and paste real values over every `REPLACE_ME`
46
+ placeholder in `/shared/<domain>`.
47
+
48
+ ## The sync script
49
+
50
+ `scripts/infisical-sync.sh` creates and maintains the `/shared` structure and all
51
+ app references. It is **dry-run by default** and **idempotent** (never overwrites a
52
+ filled value):
53
+
54
+ ```bash
55
+ # from a dir with .infisical.json (e.g. apps/server)
56
+ APPLY=0 ./scripts/infisical-sync.sh # dry run — prints the plan, changes nothing
57
+ APPLY=1 ./scripts/infisical-sync.sh # apply to every environment in ENVIRONMENTS
58
+ ```
59
+
60
+ The plan lives in `scripts/infisical-map.sh` (`ENVIRONMENTS`, `DOMAINS`,
61
+ `APP_PATHS`, `SHARED_MAP`, `REF_MAP`). Edit that file to add/remove vars.
62
+
63
+ ## Verification
64
+
65
+ After applying, resolve a reference end to end:
66
+
67
+ ```bash
68
+ infisical run --path=/apps/server --env=dev -- printenv CORE_DATABASE_URL
69
+ ```
70
+
71
+ - ✅ prints the value (or `REPLACE_ME`) → the reference resolved correctly.
72
+ - ❌ prints the literal `${dev.shared.core.CORE_DATABASE_URL}` → the reference is
73
+ malformed or the shared key/folder is missing. Fix before proceeding (almost
74
+ always a missing shared key or a bare `${shared...}` reference).
75
+
76
+ ## Running apps with secrets
77
+
78
+ ```jsonc
79
+ // apps/server
80
+ "dev": "infisical run --path=/apps/server --env=dev -- bun --watch src/index.ts"
81
+ // apps/web
82
+ "dev": "infisical run --path=/apps/web --env=dev -- bun run --bun next dev"
83
+ // packages/db — a :dev variant per script that needs DB envs
84
+ "core:migrate:dev": "infisical run --env=dev --path=/packages/db -- bun run core:migrate"
85
+ ```
86
+
87
+ Production scripts stay clean — the host (e.g. Railway) injects Infisical at the
88
+ platform level.
89
+
90
+ ## Maintenance
91
+
92
+ **Add a shared var** (used by >1 app, or a domain's canonical value):
93
+ 1. Add `"domain KEY"` to `SHARED_MAP` and `"appPath domain KEY"` to `REF_MAP` in
94
+ `scripts/infisical-map.sh`.
95
+ 2. Run `APPLY=1 ./scripts/infisical-sync.sh`.
96
+ 3. Paste the real value over the new `/shared/<domain>/<KEY>` placeholder.
97
+
98
+ **Add an app-native var:** set it directly in that app's Infisical folder as a
99
+ real value. Do not add it to the map.
100
+
101
+ **Add an environment:** add its slug to `ENVIRONMENTS` and re-run the sync.
102
+
103
+ **Rotate a secret:** edit the single `/shared/<domain>/<KEY>` value. Every
104
+ referencing app inherits it. Never edit the value in an app folder.
105
+
106
+ **Remove a var:** delete the shared key and every app reference, and remove it
107
+ from `infisical-map.sh`.
108
+
109
+ ## Gotchas
110
+
111
+ - **Env slugs must match Infisical exactly.** If your envs are
112
+ Development/Staging/Production with slugs `dev`/`staging`/`prod`, the references
113
+ use `dev`/`staging`/`prod`.
114
+ - **Folders are per-environment.** The tree exists in every environment; the sync
115
+ loops them all.
116
+ - **Local override foot-gun:** a local `.env.local` applied with `override: true`
117
+ beats Infisical. Watch for stale local values masking the real ones.
118
+ - **Never commit real secret values.** `.infisical.json` is safe to commit;
119
+ `.env*` stays gitignored.