create-tigra 2.8.0 → 3.0.2

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.
Files changed (53) hide show
  1. package/README.md +10 -3
  2. package/bin/create-tigra.js +77 -37
  3. package/package.json +5 -5
  4. package/template/_claude/commands/create-server.md +8 -2
  5. package/template/_claude/rules/client/01-project-structure.md +12 -0
  6. package/template/_claude/rules/client/03-data-and-state.md +1 -1
  7. package/template/_claude/rules/client/04-design-system.md +23 -0
  8. package/template/_claude/rules/client/07-deployment.md +99 -0
  9. package/template/_claude/rules/client/08-lockfile-cross-platform.md +79 -0
  10. package/template/_claude/rules/client/core.md +1 -0
  11. package/template/_claude/rules/global/core.md +20 -1
  12. package/template/_claude/rules/global/investigation-before-conclusions.md +57 -0
  13. package/template/_claude/rules/server/core.md +2 -0
  14. package/template/_claude/rules/server/deployment.md +78 -0
  15. package/template/client/next.config.ts +12 -2
  16. package/template/client/package-lock.json +12345 -0
  17. package/template/client/package.json +3 -2
  18. package/template/client/src/components/common/SafeImage.tsx +2 -1
  19. package/template/client/src/lib/api/axios.config.ts +19 -4
  20. package/template/client/src/middleware.ts +7 -0
  21. package/template/gitignore +1 -0
  22. package/template/server/.env.example +248 -194
  23. package/template/server/.env.example.production +221 -168
  24. package/template/server/Dockerfile +29 -5
  25. package/template/server/docker-compose.yml +32 -4
  26. package/template/server/package-lock.json +6544 -6823
  27. package/template/server/package.json +76 -75
  28. package/template/server/prisma/seed.ts +20 -4
  29. package/template/server/src/app.ts +316 -271
  30. package/template/server/src/config/env.ts +150 -99
  31. package/template/server/src/config/rate-limit.config.ts +16 -0
  32. package/template/server/src/libs/__tests__/auth-path.test.ts +24 -0
  33. package/template/server/src/libs/__tests__/client-ip.test.ts +121 -0
  34. package/template/server/src/libs/__tests__/http.test.ts +23 -9
  35. package/template/server/src/libs/__tests__/ip-block.test.ts +62 -0
  36. package/template/server/src/libs/__tests__/origin-check.test.ts +53 -0
  37. package/template/server/src/libs/__tests__/url-safety.test.ts +80 -0
  38. package/template/server/src/libs/auth-path.ts +14 -0
  39. package/template/server/src/libs/auth.ts +6 -16
  40. package/template/server/src/libs/client-ip.ts +77 -0
  41. package/template/server/src/libs/cookies.ts +1 -1
  42. package/template/server/src/libs/duration.ts +30 -0
  43. package/template/server/src/libs/ip-block.ts +220 -206
  44. package/template/server/src/libs/origin-check.ts +38 -0
  45. package/template/server/src/libs/query-counter.ts +59 -0
  46. package/template/server/src/libs/redis.ts +1 -1
  47. package/template/server/src/libs/url-safety.ts +121 -0
  48. package/template/server/src/modules/auth/__tests__/auth.service.test.ts +274 -44
  49. package/template/server/src/modules/auth/auth.controller.ts +128 -127
  50. package/template/server/src/modules/auth/auth.repo.ts +2 -0
  51. package/template/server/src/modules/auth/auth.service.ts +103 -12
  52. package/template/server/src/test/setup.ts +22 -2
  53. package/template/server/vitest.config.ts +43 -43
package/README.md CHANGED
@@ -44,7 +44,7 @@ Includes `.claude/` rules for Claude Code with project-specific conventions, arc
44
44
 
45
45
  ## Prerequisites
46
46
 
47
- - **Node.js** 18+
47
+ - **Node.js** 22.12+
48
48
  - **Docker** (for MySQL and Redis)
49
49
 
50
50
  ## After Scaffolding
@@ -72,8 +72,15 @@ npm run dev
72
72
 
73
73
  - Server: http://localhost:8000
74
74
  - Client: http://localhost:3000
75
- - phpMyAdmin: http://localhost:8080
76
- - Redis Commander: http://localhost:8081
75
+ - phpMyAdmin: http://localhost:8080 (optional — see below)
76
+ - Redis Commander: http://localhost:8081 (optional — see below)
77
+
78
+ The admin UIs (phpMyAdmin, Redis Commander) are behind the docker-compose `tools` profile and do **not** start with plain `docker compose up -d`. Start them with:
79
+
80
+ ```bash
81
+ cd my-app/server
82
+ docker compose --profile tools up -d
83
+ ```
77
84
 
78
85
  ## License
79
86
 
@@ -17,6 +17,10 @@ const VERSION = packageJson.version;
17
17
 
18
18
  const TEMPLATE_DIR = path.join(__dirname, '..', 'template');
19
19
 
20
+ // Non-secret placeholder written into the COMMITTED server/.env.example.
21
+ // The real secret replaces it only in the git-ignored server/.env.
22
+ const JWT_SECRET_PLACEHOLDER = 'CHANGE_ME_generate_with_openssl_rand_hex_48';
23
+
20
24
  // Files that contain template variables and need replacement
21
25
  const FILES_TO_REPLACE = [
22
26
  'server/package.json',
@@ -24,6 +28,7 @@ const FILES_TO_REPLACE = [
24
28
  'server/docker-compose.yml',
25
29
  'client/package.json',
26
30
  'client/.env.example',
31
+ 'client/.env.example.production',
27
32
  'server/postman/collection.json',
28
33
  'server/postman/environment.json',
29
34
  ];
@@ -123,6 +128,43 @@ function replaceVariables(content, variables) {
123
128
  return result;
124
129
  }
125
130
 
131
+ // When stdin is closed/non-interactive, `prompts` never settles its promise:
132
+ // the event loop drains and Node exits 0 mid-await — no scaffold, no error.
133
+ // That silently "succeeds" in CI. Track the in-flight prompt and fail loudly.
134
+ let activePromptMessage = null;
135
+
136
+ process.on('exit', (code) => {
137
+ if (activePromptMessage !== null && code === 0) {
138
+ // writeSync: stderr writes via console.error can be lost in an 'exit'
139
+ // handler when stderr is a pipe (async on Windows).
140
+ fs.writeSync(
141
+ 2,
142
+ `\n Aborted: the prompt "${activePromptMessage}" was not answered ` +
143
+ `(stdin is closed or non-interactive).\n` +
144
+ ` Run in an interactive terminal, or pipe answers via stdin.\n\n`,
145
+ );
146
+ process.exitCode = 1;
147
+ }
148
+ });
149
+
150
+ async function ask(question) {
151
+ activePromptMessage = question.message;
152
+ const response = await prompts(question, {
153
+ onCancel: () => {
154
+ console.error(chalk.red('\n Cancelled.\n'));
155
+ process.exit(1);
156
+ },
157
+ });
158
+ activePromptMessage = null;
159
+ if (response[question.name] === undefined) {
160
+ console.error(
161
+ chalk.red(`\n Aborted: no answer received for "${question.message}".\n`),
162
+ );
163
+ process.exit(1);
164
+ }
165
+ return response;
166
+ }
167
+
126
168
  function registerAddCommand(program) {
127
169
  program
128
170
  .command('add <module>')
@@ -248,20 +290,12 @@ async function main() {
248
290
  let projectName = projectNameArg;
249
291
 
250
292
  if (!projectName) {
251
- const response = await prompts(
252
- {
253
- type: 'text',
254
- name: 'projectName',
255
- message: 'What is your project name?',
256
- validate: validateProjectName,
257
- },
258
- {
259
- onCancel: () => {
260
- console.log(chalk.red('\n Cancelled.\n'));
261
- process.exit(1);
262
- },
263
- }
264
- );
293
+ const response = await ask({
294
+ type: 'text',
295
+ name: 'projectName',
296
+ message: 'What is your project name?',
297
+ validate: validateProjectName,
298
+ });
265
299
  projectName = response.projectName;
266
300
  }
267
301
 
@@ -286,34 +320,30 @@ async function main() {
286
320
  }
287
321
 
288
322
  // Ask about email verification
289
- const { enableVerification } = await prompts(
290
- {
291
- type: 'toggle',
292
- name: 'enableVerification',
293
- message: 'Enable email verification for new users?',
294
- initial: false,
295
- active: 'Yes',
296
- inactive: 'No',
297
- hint: 'Users must verify email before accessing the app',
298
- },
299
- {
300
- onCancel: () => {
301
- console.log(chalk.red('\n Cancelled.\n'));
302
- process.exit(1);
303
- },
304
- }
305
- );
323
+ const { enableVerification } = await ask({
324
+ type: 'toggle',
325
+ name: 'enableVerification',
326
+ message: 'Enable email verification for new users?',
327
+ initial: false,
328
+ active: 'Yes',
329
+ inactive: 'No',
330
+ hint: 'Users must verify email before accessing the app',
331
+ });
306
332
 
307
333
  // Generate random port offset (1-200) so multiple projects don't conflict
308
334
  const portOffset = crypto.randomInt(1, 201);
309
335
 
310
- // Derive all variables
336
+ // Derive all variables.
337
+ // SECURITY: everything in this map is written into COMMITTED files
338
+ // (.env.example, docker-compose.yml, ...) — never put a real secret here.
339
+ // JWT_SECRET gets an instructional placeholder; the real secret is
340
+ // generated below and written ONLY to the git-ignored .env.
311
341
  const variables = {
312
342
  PROJECT_NAME: projectName,
313
343
  PROJECT_NAME_SNAKE: toSnakeCase(projectName),
314
344
  PROJECT_DISPLAY_NAME: toTitleCase(projectName),
315
345
  DATABASE_NAME: `${toSnakeCase(projectName)}_db`,
316
- JWT_SECRET: crypto.randomBytes(48).toString('hex'),
346
+ JWT_SECRET: JWT_SECRET_PLACEHOLDER,
317
347
  MYSQL_PORT: String(3306 + portOffset),
318
348
  PHPMYADMIN_PORT: String(8080 + portOffset),
319
349
  REDIS_PORT: String(6379 + portOffset),
@@ -337,12 +367,21 @@ async function main() {
337
367
  }
338
368
  }
339
369
 
340
- // Generate .env from .env.example (so users don't have to copy manually)
370
+ // Generate .env from .env.example (so users don't have to copy manually).
371
+ // The real JWT secret is injected HERE and only here — .env is
372
+ // git-ignored, while .env.example is committed and must keep the
373
+ // CHANGE_ME placeholder.
374
+ const jwtSecret = crypto.randomBytes(48).toString('hex');
341
375
  for (const envExample of ['server/.env.example', 'client/.env.example']) {
342
376
  const examplePath = path.join(targetDir, envExample);
343
377
  const envPath = path.join(targetDir, envExample.replace('.env.example', '.env'));
344
378
  if (await fs.pathExists(examplePath)) {
345
- await fs.copy(examplePath, envPath);
379
+ const content = await fs.readFile(examplePath, 'utf-8');
380
+ await fs.writeFile(
381
+ envPath,
382
+ content.replaceAll(JWT_SECRET_PLACEHOLDER, jwtSecret),
383
+ 'utf-8',
384
+ );
346
385
  }
347
386
  }
348
387
 
@@ -423,8 +462,9 @@ async function main() {
423
462
  console.log();
424
463
  console.log(dim(' App ') + cyan('http://localhost:3000'));
425
464
  console.log(dim(' API ') + cyan('http://localhost:8000'));
426
- console.log(dim(' phpMyAdmin ') + cyan(`http://localhost:${variables.PHPMYADMIN_PORT}`));
427
- console.log(dim(' Redis CMD ') + cyan(`http://localhost:${variables.REDIS_COMMANDER_PORT}`));
465
+ console.log(dim(' DB/Redis UIs ') + 'optional — start with ' + cyan('npm run docker:tools'));
466
+ console.log(dim(' phpMyAdmin ') + cyan(`http://localhost:${variables.PHPMYADMIN_PORT}`));
467
+ console.log(dim(' Redis CMD ') + cyan(`http://localhost:${variables.REDIS_COMMANDER_PORT}`));
428
468
  console.log();
429
469
  console.log(line);
430
470
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-tigra",
3
- "version": "2.8.0",
3
+ "version": "3.0.2",
4
4
  "type": "module",
5
5
  "description": "Create a production-ready full-stack app with Next.js 16 + Fastify 5 + Prisma + Redis",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "template"
14
14
  ],
15
15
  "engines": {
16
- "node": ">=18.0.0"
16
+ "node": ">=22.12.0"
17
17
  },
18
18
  "keywords": [
19
19
  "create",
@@ -46,10 +46,10 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "chalk": "^5.4.1",
49
- "commander": "^13.1.0",
49
+ "commander": "^15.0.0",
50
50
  "fs-extra": "^11.3.0",
51
- "ora": "^8.2.0",
51
+ "ora": "^9.0.0",
52
52
  "prompts": "^2.4.2",
53
- "ts-morph": "^27.0.2"
53
+ "ts-morph": "^28.0.0"
54
54
  }
55
55
  }
@@ -134,12 +134,18 @@ Include: node_modules, dist, .env, .env.local, .env*.local, *.log, .prisma
134
134
  #### `docker-compose.yml`
135
135
  Create a Docker Compose file with these services:
136
136
  1. **mysql** - MySQL 8.0, port 3306, database name = `$ARGUMENTS`, root password = `rootpassword`, volume for data persistence, healthcheck
137
- 2. **phpmyadmin** - Latest, port 8080, linked to mysql
137
+ 2. **phpmyadmin** - Latest, port 8080, linked to mysql, behind the `tools` profile
138
138
  3. **redis** - Redis 7 Alpine, port 6379, volume for data persistence, healthcheck
139
- 4. **redis-commander** - Redis Commander UI, port 8081, linked to redis
139
+ 4. **redis-commander** - Redis Commander UI, port 8081, linked to redis, behind the `tools` profile
140
140
 
141
141
  Use a named network for all services. Add restart policies.
142
142
 
143
+ **Security requirements (dev attack surface):**
144
+ - Bind ALL published ports to localhost (`127.0.0.1:<port>:<container-port>`), never `0.0.0.0` — these are dev credentials (root password, unpassworded Redis).
145
+ - Put the admin UIs (**phpmyadmin**, **redis-commander**) behind `profiles: ["tools"]` so they do NOT start by default:
146
+ - `docker compose up -d` → MySQL + Redis only
147
+ - `docker compose --profile tools up -d` → also starts phpMyAdmin + Redis Commander
148
+
143
149
  ---
144
150
 
145
151
  ### Step 2: Source Code Structure
@@ -133,3 +133,15 @@ export const CURRENCIES = { USD: 'USD', EUR: 'EUR' } as const;
133
133
 
134
134
  Protected paths: `/dashboard`, `/profile`, `/admin` — redirect to `/login` if no token.
135
135
  Auth paths: `/login`, `/register` — redirect to `/dashboard` if already authenticated.
136
+
137
+ When creating a new page, always add it to `protectedPaths` and `config.matcher` in `middleware.ts` if it requires authentication. If you are unsure whether a page should be protected, **ask the user** — do not guess.
138
+
139
+ ### Deleting a Route
140
+
141
+ When removing a page/route, you MUST update **all three** locations:
142
+
143
+ 1. **Delete the page file**: `app/<route>/page.tsx` (and the folder if empty)
144
+ 2. **Remove from `routes.ts`**: Delete the entry in `lib/constants/routes.ts`
145
+ 3. **Remove from `middleware.ts`**: Delete from `protectedPaths` array AND `config.matcher` array
146
+
147
+ Missing any of these leaves dead references or broken middleware matches.
@@ -75,7 +75,7 @@ const mutation = useMutation({
75
75
 
76
76
  ### Next.js Router Cache
77
77
 
78
- `next.config.ts` sets `experimental.staleTimes: { dynamic: 0, static: 0 }` to disable client-side Router Cache reuse. **Never raise these values** — doing so reintroduces the back-navigation stale-data bug across every page that uses Server Components for data fetching.
78
+ `next.config.ts` sets `experimental.staleTimes: { dynamic: 0, static: 30 }` to minimize client-side Router Cache reuse. `static: 30` is the **Next 16.2+ minimum** — the config schema rejects values below 30, and an invalid value is silently ignored (re-enabling the default Router Cache). `dynamic: 0` is what protects data pages. **Never raise these values above these minimums** — doing so reintroduces the back-navigation stale-data bug across every page that uses Server Components for data fetching.
79
79
 
80
80
  ---
81
81
 
@@ -339,6 +339,28 @@ Link: transition-colors duration-150 active:opacity-70 md:hover:text-primary
339
339
  - **Sticky action bars**: Form submit buttons, checkout CTAs — `sticky bottom-0` on mobile.
340
340
  - **No hamburger menus** for ≤5 items. Use bottom tab bar instead.
341
341
 
342
+ ### BottomNav Overlap — Fixed Bottom Elements
343
+
344
+ When a page has its own `fixed bottom-0` element (e.g., sticky order bar, floating CTA), it **will be hidden behind BottomNav** on mobile. The BottomNav is `fixed bottom-0 z-50` with a height of `4rem` (64px).
345
+
346
+ **Define the CSS variable** in the BottomNav component:
347
+ ```css
348
+ :root {
349
+ --bottom-nav-height: 4rem;
350
+ }
351
+ ```
352
+
353
+ **Every fixed bottom element on a page** must offset itself above BottomNav on mobile:
354
+ ```
355
+ bottom-[var(--bottom-nav-height)] md:bottom-0
356
+ ```
357
+ Or the shorthand equivalent:
358
+ ```
359
+ bottom-16 md:bottom-0
360
+ ```
361
+
362
+ This is not optional — without it, BottomNav covers the element and users cannot tap it on mobile.
363
+
342
364
  ---
343
365
 
344
366
  ## Images
@@ -383,3 +405,4 @@ Link: transition-colors duration-150 active:opacity-70 md:hover:text-primary
383
405
  - Reduce shadow visibility in dark mode (use subtle light borders instead).
384
406
  - Consider `brightness-90` on images in dark mode.
385
407
  - Add `suppressHydrationWarning` to `<html>` tag.
408
+ - **Hydration guard for conditional theme renders**: `useTheme()` returns `undefined` on the server. Any component that renders *different JSX* based on `theme` (e.g. showing a Sun icon OR a Moon icon — not both) will throw a hydration mismatch. Either render both icons and style the active one (see `components/common/ThemeToggle.tsx`), or add a `mounted` guard: `const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []);` and return `null`/a placeholder until `mounted` is true.
@@ -0,0 +1,99 @@
1
+ > **SCOPE**: These rules apply specifically to the **client** directory (Next.js App Router).
2
+
3
+ # Deployment & Docker
4
+
5
+ This project is deployed via **Docker** on **Coolify** (or any Docker-based platform). The `Dockerfile` is the production deployment contract. Every code change must remain compatible with it.
6
+
7
+ ---
8
+
9
+ ## Dockerfile Architecture
10
+
11
+ The client uses a **3-stage multi-stage build** with Next.js `standalone` output:
12
+
13
+ ```
14
+ Stage 1 (dependencies) → Installs all node_modules (cached layer)
15
+ Stage 2 (builder) → Copies deps, builds Next.js with standalone output
16
+ Stage 3 (production) → Alpine + dumb-init, non-root user, copies standalone + static + public
17
+ ```
18
+
19
+ **Entry point**: `node server.js` (generated by Next.js standalone output in `.next/standalone/`)
20
+ **Health check**: `GET /` — returns 200 if the Next.js server is running.
21
+
22
+ ### Critical Config Requirement
23
+
24
+ `next.config.ts` **must** have `output: "standalone"`. Without it, the Dockerfile will fail because `.next/standalone/` won't be generated. Never remove this setting.
25
+
26
+ ---
27
+
28
+ ## Build Arguments (NEXT_PUBLIC_* Variables)
29
+
30
+ Next.js **inlines** all `NEXT_PUBLIC_*` values into the JavaScript bundle at build time. They are NOT read from the environment at runtime. This means:
31
+
32
+ - Every `NEXT_PUBLIC_*` variable must be declared as `ARG` + `ENV` in the **builder stage** of the Dockerfile.
33
+ - They must be passed via `--build-arg` during `docker build` (or via Coolify's Build Arguments UI).
34
+ - **If you add a new `NEXT_PUBLIC_*` env var, you MUST add it to the Dockerfile builder stage.**
35
+
36
+ ```dockerfile
37
+ # In the builder stage:
38
+ ARG NEXT_PUBLIC_NEW_VAR
39
+ ENV NEXT_PUBLIC_NEW_VAR=$NEXT_PUBLIC_NEW_VAR
40
+ ```
41
+
42
+ Current build arguments:
43
+ - `NEXT_PUBLIC_API_BASE_URL` — Backend API URL (e.g., `https://api.yourdomain.com/api/v1`)
44
+ - `NEXT_PUBLIC_APP_NAME` — App display name
45
+
46
+ ---
47
+
48
+ ## When to Update the Dockerfile
49
+
50
+ | You did this... | Update Dockerfile? | What to change |
51
+ |---|---|---|
52
+ | Added a new npm dependency | No | Automatic — installed during build |
53
+ | Added a native/system dependency (e.g., `sharp` for image optimization) | **Yes** | Add `apk add` in the production stage |
54
+ | Added a new `NEXT_PUBLIC_*` env var | **Yes** | Add `ARG` + `ENV` in the builder stage |
55
+ | Changed the public directory structure | No | Automatic — `COPY /app/public ./public` |
56
+ | Added server-only env vars (no `NEXT_PUBLIC_` prefix) | No | Injected at runtime via Coolify |
57
+ | Changed the default port | **Yes** | Update `ENV PORT`, `EXPOSE`, and `HEALTHCHECK` |
58
+ | Added custom `next.config.ts` rewrites/redirects | No | Baked into the build automatically |
59
+ | Added API routes (`app/api/`) | No | Included in standalone output |
60
+ | Removed `output: "standalone"` from next.config.ts | **BREAKING** | Entire Dockerfile depends on standalone output |
61
+
62
+ ---
63
+
64
+ ## Critical Rules
65
+
66
+ 1. **Never remove `output: "standalone"`** from `next.config.ts`. The entire Docker build depends on it. Without it, the standalone server won't be generated and the Dockerfile will fail.
67
+
68
+ 2. **Every `NEXT_PUBLIC_*` var needs a Dockerfile `ARG`.** Next.js inlines these at build time. If you add one to `.env.example` but forget the Dockerfile, the value will be `undefined` in production.
69
+
70
+ 3. **Static assets and public files are separate.** The standalone output does NOT include `.next/static/` or `public/`. The Dockerfile copies them explicitly. If you add a new top-level directory that must be available at runtime (unlikely), add a `COPY` line.
71
+
72
+ 4. **Non-root user.** The app runs as `nextjs:nodejs` (UID 1001). Next.js standalone doesn't write to disk at runtime, so this is straightforward.
73
+
74
+ 5. **No secrets in the image.** Server-only env vars (without `NEXT_PUBLIC_` prefix) are injected at runtime. Never hardcode secrets or use `ENV` for sensitive values in the Dockerfile.
75
+
76
+ 6. **Keep `.dockerignore` in sync.** When adding directories that should NOT be in the build context (test fixtures, docs, Storybook), add them to `.dockerignore`. When adding files needed at build time, ensure they're not ignored.
77
+
78
+ 7. **`HOSTNAME="0.0.0.0"`** is required. Without it, Next.js standalone only listens on `127.0.0.1` inside the container, making it unreachable from outside.
79
+
80
+ ---
81
+
82
+ ## Coolify-Specific Notes
83
+
84
+ - **Build Arguments**: Set `NEXT_PUBLIC_API_BASE_URL` and `NEXT_PUBLIC_APP_NAME` in Coolify's "Build Arguments" section (not environment variables — those are runtime only).
85
+ - **Environment Variables**: Server-only vars (database URLs, API keys used in Server Components/Route Handlers) go in Coolify's "Environment Variables" section — available at runtime.
86
+ - **Port**: Default `3000`. Override via `PORT` environment variable in Coolify.
87
+ - **No volume mounts needed**: Next.js client apps are stateless — no uploads, no local file writes.
88
+
89
+ ---
90
+
91
+ ## Files That Matter for Deployment
92
+
93
+ | File | Purpose | Must exist? |
94
+ |---|---|---|
95
+ | `Dockerfile` | Production build instructions | Yes |
96
+ | `.dockerignore` | Excludes files from Docker build context | Yes |
97
+ | `next.config.ts` | Must have `output: "standalone"` | Yes |
98
+ | `package.json` | Dependencies and build script | Yes |
99
+ | `public/` | Static assets (favicon, images) | Yes |
@@ -0,0 +1,79 @@
1
+ > **SCOPE**: These rules apply specifically to the **client** directory (Next.js App Router).
2
+
3
+ # Lockfile — Cross-Platform Regeneration
4
+
5
+ `client/package-lock.json` has burned us — and bots — multiple times in projects scaffolded from this template. Read this before touching it.
6
+
7
+ ## The trap
8
+
9
+ `client/package-lock.json` is consumed by **three different environments**:
10
+
11
+ | Environment | OS / libc | npm |
12
+ |---|---|---|
13
+ | Local dev (most contributors) | Windows / macOS | npm 11.x |
14
+ | GitHub Actions CI (`server-ci.yml`, `client-ci.yml`) | `ubuntu-latest` (Debian glibc) | npm 10.x (ships with Node 20) |
15
+ | Coolify / production deploy (`client/Dockerfile`) | `node:20-alpine` (musl) | npm 10.x |
16
+
17
+ `npm ci` is **strict** — it refuses to install if the lockfile is even slightly out of sync with `package.json`, AND it only installs the platform-specific `optionalDependencies` whose top-level `node_modules/<pkg>` entries exist in the lockfile. If you regenerate on Windows with `npm install`, you get a Windows-leaning lockfile that:
18
+
19
+ 1. May be missing entries the newer npm 10 (CI) considers required (`Missing: @swc/helpers@0.5.21 from lock file` — common failure mode).
20
+ 2. Lacks `*-linux-x64-gnu` / `*-linuxmusl-x64` entries → CI's `next build` fails with `Cannot find module '../lightningcss.linux-x64-gnu.node'` or `No prebuild or local build of @parcel/watcher found.`
21
+
22
+ Affected native packages (Next 16 + Tailwind v4 dep tree): `@parcel/watcher`, `lightningcss`, `@img/sharp`, `@next/swc`, `@swc/core`, `@tailwindcss/oxide`, `@unrs/resolver-binding`, `@rolldown/binding`.
23
+
24
+ ## Canonical fix (use this exactly)
25
+
26
+ Whenever you regenerate `client/package-lock.json` — even just because `package.json` changed by one dep — run **all three** steps. They are additive (`--package-lock-only` doesn't touch `node_modules`).
27
+
28
+ ```bash
29
+ cd client
30
+
31
+ # 0. (Optional) Start fresh if the existing lockfile is already broken:
32
+ rm -rf node_modules package-lock.json
33
+
34
+ # 1. Sync the lockfile against package.json (fixes the "Missing: foo from lock file" class).
35
+ npx -y npm@10 install --package-lock-only
36
+
37
+ # 2. Add Linux/glibc native variants (GitHub Actions Ubuntu).
38
+ npx -y npm@10 install --os=linux --cpu=x64 --libc=glibc --package-lock-only
39
+
40
+ # 3. Add Linux/musl native variants (Coolify Alpine deploy).
41
+ npx -y npm@10 install --os=linux --cpu=x64 --libc=musl --package-lock-only
42
+ ```
43
+
44
+ ### Required choices
45
+
46
+ - **`npx -y npm@10` explicitly.** Local default npm 11.x resolves a tree npm 10 strict-checks reject. Match CI's npm version or you'll push a lockfile that fails immediately.
47
+ - **`--package-lock-only`** keeps each command at ~1 sec (no install, no audit).
48
+ - **Three platforms.** Even if you only intend to fix CI, do the musl pass too — Coolify deploy will fail otherwise.
49
+ - **Skip darwin** unless a team member dev's on macOS and reports `npm ci` failing. Not worth the lockfile churn pre-emptively.
50
+
51
+ ## Verify before committing
52
+
53
+ ```bash
54
+ cd client
55
+
56
+ # A. Strict install passes with CI's npm version (this is the exact check CI runs):
57
+ npx -y npm@10 ci --include=optional --no-audit --no-fund 2>&1 | tail -3
58
+ # Expect: "added N packages in Xs". Any "EUSAGE" / "Missing:" means step 1 didn't take.
59
+
60
+ # B. All native deps have glibc + musl entries:
61
+ for pkg in '@next/swc' '@swc/core' '@parcel/watcher' '@rolldown/binding' \
62
+ '@tailwindcss/oxide' '@unrs/resolver-binding' 'lightningcss'; do
63
+ g=$(grep -cE "node_modules/${pkg}.*-linux-x64-(gnu|glibc)\"" package-lock.json)
64
+ m=$(grep -cE "node_modules/${pkg}-linux-x64-musl\"" package-lock.json)
65
+ echo "$pkg glibc=$g musl=$m"
66
+ done
67
+ # Expect every line: glibc=1 musl=1. (sharp uses different naming — check separately:
68
+ # grep -oE 'node_modules/@img/sharp[-a-z0-9]*' package-lock.json | sort -u)
69
+ ```
70
+
71
+ If either check fails, the lockfile is not ready — do not commit.
72
+
73
+ ## Anti-patterns (don't do these — every one has burned us)
74
+
75
+ 1. **Plain `npm install` on Windows/macOS.** Uses local npm 11; produces lockfiles CI rejects.
76
+ 2. **Whack-a-mole pinning** in `package.json` `optionalDependencies` (e.g., adding only `@parcel/watcher-linux-x64-glibc` and hoping it fixes everything). It doesn't — there are 7+ such native deps and you'll bounce through them one CI failure at a time.
77
+ 3. **Disabling `npm ci` in CI** by switching to `npm install` to "fix" the symptom. That makes builds non-reproducible.
78
+ 4. **Bypassing the husky pre-commit hook** with `--no-verify` to push a broken lockfile fast.
79
+ 5. **Generating inside Docker on a hung Docker daemon** without verifying the daemon is healthy first — the run silently buffers and you wait 10+ minutes. (The non-Docker `npx npm@10 install --package-lock-only` chain above is faster and equally correct.)
@@ -12,6 +12,7 @@
12
12
  | Choosing colors, styling, typography, spacing, motion, **theme colors**, **font presets** | `04-design-system.md` |
13
13
  | Auth tokens, env vars, security headers | `05-security.md` |
14
14
  | UX psychology, cognitive load, a11y, performance | `06-ux-checklist.md` |
15
+ | Regenerating `client/package-lock.json`, fixing CI `npm ci` failures, cross-platform native binaries (`@parcel/watcher`, `lightningcss`, `@img/sharp`, etc.) | `08-lockfile-cross-platform.md` |
15
16
 
16
17
  ---
17
18
 
@@ -51,7 +51,7 @@ When the user asks to create, scaffold, or start a new server or client project,
51
51
  - **`.env` files are never committed.** Use `.env.example` as the template with placeholder values.
52
52
  - **Adding a new env var**: Add it to `.env.example` with a comment, and document where it's used.
53
53
  - **Keep `.env` and `.env.example` in sync**: Any change to `.env` (adding, removing, or renaming a variable) must be reflected in `.env.example`, and vice versa. They must always have the same set of variables.
54
- - **Keep `.env.example.production` in sync**: Every time `.env` or `.env.example` changes (variable added, removed, or renamed), also update `.env.example.production` in the same directory. If the file does not exist, create it. This file uses the same variable names but with **production-appropriate placeholder values** and comments (e.g., secure passwords, real domain URLs, SSL-enabled connection strings, stricter timeouts). This applies to both `server/` and `client/` directories.
54
+ - **Keep `.env.example.production` in sync**: Every time `.env` or `.env.example` changes (variable added, removed, or renamed), also update `.env.example.production` in the same directory. If the file does not exist, create it. This file uses the same variable names but with **production-appropriate placeholder values** and comments (e.g., secure passwords, real domain URLs, SSL-enabled connection strings, stricter timeouts). See the server's `.env.example.production` for the expected style. This applies to both `server/` and `client/` directories.
55
55
  - **Secrets** (DB URLs, JWT secrets, API keys) must never be prefixed with `NEXT_PUBLIC_` and must never appear in client-side code.
56
56
  - **Public values only** (API base URL, app name) get the `NEXT_PUBLIC_` prefix.
57
57
 
@@ -63,6 +63,23 @@ When the user asks to create, scaffold, or start a new server or client project,
63
63
  - **Test naming**: `describe('<ModuleName>')` → `it('should <expected behavior>')`.
64
64
  - **Tests must be deterministic**: No reliance on real time, network, or random values. Mock external dependencies.
65
65
  - **Don't test implementation details** — test behavior and outputs.
66
+ - **No "mock-echo" tests**: Don't mock a dependency (Prisma/ORM, HTTP client) and then only assert it was called with the same arguments the code passes straight through — that restates the implementation and verifies no behavior. A "was-called-with" assertion is valid only when the layer *transformed* the input first (filtering, pagination math, conditional queries) or when you ALSO assert on the returned value / observable result.
67
+ - **Don't test logic-free layers**: If a unit only forwards to a dependency with no logic of its own (a thin repository/passthrough), don't test it — put the test on the service/behavior layer, where the branches and bugs are.
68
+
69
+ ---
70
+
71
+ ## Deployment Awareness
72
+
73
+ Both server and client are deployed via **Docker on Coolify**. The `Dockerfile` in each directory is the production deployment contract. Code changes must never silently break the Docker build.
74
+
75
+ **Always check deployment impact when you:**
76
+ - Add a native/system npm dependency (needs `apk add` in Dockerfile)
77
+ - Add, remove, or rename a `NEXT_PUBLIC_*` env var (needs `ARG` + `ENV` in client Dockerfile)
78
+ - Change the build output directory, entry point file, or default port
79
+ - Add runtime file dependencies (templates, static assets not in `public/`)
80
+ - Change or remove a health check endpoint
81
+
82
+ **See the deployment rules** in `server/deployment.md` and `client/07-deployment.md` for full details.
66
83
 
67
84
  ---
68
85
 
@@ -83,3 +100,5 @@ Before considering work done, verify:
83
100
  - [ ] Inputs validated with Zod.
84
101
  - [ ] Error cases handled (not just the happy path).
85
102
  - [ ] Existing tests still pass.
103
+ - [ ] No "mock-echo" tests (asserting only that a mock was called, without asserting behavior/output or testing a real transform).
104
+ - [ ] Dockerfile still compatible with changes (see Deployment Awareness above).
@@ -0,0 +1,57 @@
1
+ > **SCOPE**: These rules apply to the **entire workspace** (server + client). Always active.
2
+
3
+ # Investigation Before Conclusions
4
+
5
+ This project (create-tigra) generates starter templates for developers who may not have deep coding experience. They trust AI output without questioning it. **A wrong suggestion that "fixes" a non-existent problem can introduce real problems.** Every recommendation must be grounded in verified understanding of how the code actually works.
6
+
7
+ ---
8
+
9
+ ## The Rule
10
+
11
+ **Never suggest fixes, changes, or improvements until you have fully traced how the relevant system works in this codebase.** Seeing a file, a config, or a pattern is not enough. You must follow the code path end-to-end before making any claim.
12
+
13
+ ---
14
+
15
+ ## Before Answering "Is This a Bug?" or "Does This Need Fixing?"
16
+
17
+ 1. **Trace the actual workflow.** If the question is about uploads, go read the upload service, follow where files are saved, how they're served, how they're deleted. If it's about auth, trace the full auth flow. Don't stop at the first file you find.
18
+
19
+ 2. **Understand the runtime environment.** Ask yourself: does this code run in Docker or locally? Is this a dev tool or a production path? Does docker-compose run the server or just infrastructure services (MySQL, Redis)? Don't assume — verify.
20
+
21
+ 3. **Verify the problem exists in the real workflow.** Not in theory, not in a hypothetical scenario — in the actual way developers use this project. If no one would ever hit the issue in normal usage, it's not a problem worth fixing.
22
+
23
+ 4. **Don't pattern-match to conclusions.** "Uploads + Docker + no volume = must fix" is pattern-matching, not investigation. The server runs locally with `npm run dev`, not inside Docker. The `docker-compose.yml` is for MySQL and Redis only. A volume for uploads in docker-compose would solve nothing.
24
+
25
+ 5. **If you're unsure, say so.** "I need to check how this works before I can answer" is always better than a confident wrong answer.
26
+
27
+ ---
28
+
29
+ ## What NOT to Do
30
+
31
+ | Bad behavior | Why it's dangerous |
32
+ |---|---|
33
+ | See a Dockerfile, immediately suggest `VOLUME` | The server may not run in Docker locally |
34
+ | See docker-compose.yml, suggest adding volumes | docker-compose may only run infrastructure, not the app |
35
+ | Find a missing config and assume it's a bug | It may be intentionally absent because it's not needed |
36
+ | Suggest "best practice" improvements unprompted | Unnecessary changes confuse beginners and can introduce real bugs |
37
+ | Stop researching after finding the first related file | The first file is not the full picture — trace the complete flow |
38
+ | Offer a fix before confirming the problem is real | Fixing non-existent problems wastes time and creates new problems |
39
+
40
+ ---
41
+
42
+ ## The Standard
43
+
44
+ Before every suggestion, ask yourself:
45
+
46
+ 1. **Did I trace the full code path?** Not just one file — the whole flow.
47
+ 2. **Do I understand the runtime environment?** Where does this code actually run?
48
+ 3. **Would a real developer actually hit this problem?** In normal usage, not edge cases.
49
+ 4. **Am I solving a real problem or a theoretical one?**
50
+
51
+ If the answer to any of these is "no" — keep investigating before responding.
52
+
53
+ ---
54
+
55
+ ## Why This Matters
56
+
57
+ create-tigra exists to help developers who are learning to code with AI assistance. These developers will trust your suggestions without pushback. A confident but wrong suggestion — like adding a Docker volume for a service that doesn't run in Docker — will send them down a rabbit hole debugging something that was never broken. **Your job is to understand what actually works and why, not to guess based on patterns.**
@@ -17,6 +17,7 @@
17
17
  | Writing service or business logic | `project-conventions.md` (Layer Responsibilities) |
18
18
  | Changing DB schema, migrations, indexes | `database.md` |
19
19
  | Adding or changing API endpoints | `project-conventions.md` (Postman Collection) |
20
+ | Adding dependencies, changing ports, entry points, or env vars | `deployment.md` |
20
21
  | Unsure where to start | This file |
21
22
 
22
23
  ---
@@ -48,3 +49,4 @@ Request
48
49
  5. **Logging**: Use `logger` from `src/libs/logger`. Never `console.log`.
49
50
  6. **Routes**: All prefixed with `/api/v1`. Registered as Fastify plugins.
50
51
  7. **Postman**: Every new or modified endpoint must be reflected in `postman/collection.json`. Create the collection if it doesn't exist.
52
+ 8. **Deployment**: Every change must remain compatible with the Dockerfile. Read `deployment.md` when adding system dependencies, changing ports, renaming entry points, or adding env vars.