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
@@ -7,6 +7,7 @@
7
7
  "build": "next build",
8
8
  "start": "next start",
9
9
  "lint": "eslint src/",
10
+ "typecheck": "tsc --noEmit",
10
11
  "generate:env": "node -e \"import('fs').then(f=>{if(f.existsSync('.env'))console.log('.env already exists, skipping');else{f.copyFileSync('.env.example','.env');console.log('.env created from .env.example')}})\""
11
12
  },
12
13
  "dependencies": {
@@ -17,7 +18,7 @@
17
18
  "class-variance-authority": "^0.7.1",
18
19
  "clsx": "^2.1.1",
19
20
  "lucide-react": "^0.563.0",
20
- "next": "16.1.6",
21
+ "next": "16.2.9",
21
22
  "next-themes": "^0.4.6",
22
23
  "radix-ui": "^1.4.3",
23
24
  "react": "19.2.3",
@@ -38,7 +39,7 @@
38
39
  "@types/react": "^19",
39
40
  "@types/react-dom": "^19",
40
41
  "eslint": "^9",
41
- "eslint-config-next": "16.1.6",
42
+ "eslint-config-next": "16.2.9",
42
43
  "shadcn": "^3.8.4",
43
44
  "tailwindcss": "^4",
44
45
  "tw-animate-css": "^1.4.0",
@@ -36,12 +36,13 @@ function isLoopbackUrl(src: ImageProps['src']): boolean {
36
36
  }
37
37
  }
38
38
 
39
- export const SafeImage = (props: ImageProps): React.ReactElement => {
39
+ export const SafeImage = ({ alt, ...props }: ImageProps): React.ReactElement => {
40
40
  const shouldSkipOptimization = isLoopbackUrl(props.src);
41
41
 
42
42
  return (
43
43
  <Image
44
44
  {...props}
45
+ alt={alt}
45
46
  unoptimized={props.unoptimized || shouldSkipOptimization}
46
47
  />
47
48
  );
@@ -108,15 +108,30 @@ apiClient.interceptors.response.use(
108
108
  refreshError.response.status < 500;
109
109
 
110
110
  if (isAuthFailure) {
111
- // Trip the circuit breaker FIRST prevents any subsequent 401s
112
- // from re-entering this flow while the redirect is pending.
113
- isSessionDead = true;
111
+ // The bootstrap probe (AuthInitializer's getMe) runs on EVERY page,
112
+ // including public ones. For an anonymous visitor, getMe 401 →
113
+ // refresh 401 is the NORMAL "not logged in" outcome — not a dead
114
+ // session. The probe must fail silently to "anonymous": clear any
115
+ // stale Redux state, but never trip the circuit breaker, clear the
116
+ // session cookie, or hard-redirect a visitor off a public page.
117
+ // (On protected pages, AuthInitializer handles the redirect itself.)
118
+ // Note: a genuine request queued behind a probe-initiated refresh is
119
+ // rejected via processQueue without tripping the breaker or
120
+ // redirecting — AuthInitializer covers the redirect on protected pages.
121
+ const isBootstrapProbe = originalRequest.url === API_ENDPOINTS.AUTH.ME;
122
+
123
+ if (!isBootstrapProbe) {
124
+ // A genuine authenticated action failed mid-session — the session
125
+ // is dead. Trip the circuit breaker FIRST — prevents any subsequent
126
+ // 401s from re-entering this flow while the redirect is pending.
127
+ isSessionDead = true;
128
+ }
114
129
 
115
130
  const { logout } = await import('@/features/auth/store/authSlice');
116
131
  const { store } = await import('@/store');
117
132
  store.dispatch(logout());
118
133
 
119
- if (typeof window !== 'undefined') {
134
+ if (!isBootstrapProbe && typeof window !== 'undefined') {
120
135
  // Clear session indicator so middleware won't let user through
121
136
  // to protected pages — prevents redirect loops
122
137
  document.cookie = 'auth_session=; Max-Age=0; path=/; SameSite=Strict';
@@ -16,6 +16,13 @@ function isTokenExpired(token: string): boolean {
16
16
 
17
17
  export function middleware(request: NextRequest): NextResponse {
18
18
  const { pathname } = request.nextUrl;
19
+
20
+ // NOTE: This middleware only checks COOKIE PRESENCE (and a best-effort,
21
+ // unverified exp claim) for UX-level routing — redirecting users who are
22
+ // obviously logged out away from protected pages. It does NOT verify the
23
+ // JWT signature and is NOT a security boundary. Real authorization happens
24
+ // server-side on every API call; a forged cookie gets past this middleware
25
+ // but every API request it makes will be rejected with 401/403.
19
26
  const accessToken = request.cookies.get('access_token')?.value;
20
27
  const authSession = request.cookies.get('auth_session')?.value;
21
28
 
@@ -5,6 +5,7 @@ node_modules/
5
5
  dist/
6
6
  .next/
7
7
  out/
8
+ *.tsbuildinfo
8
9
 
9
10
  # Environment files
10
11
  .env
@@ -1,194 +1,248 @@
1
- # ===================================================================
2
- # APPLICATION CONFIGURATION
3
- # ===================================================================
4
- #
5
- # COOLIFY DEPLOYMENT NOTE:
6
- # When adding environment variables in Coolify, do NOT check
7
- # "Available at Buildtime" for any variable unless explicitly noted.
8
- # The server Dockerfile handles build-time config internally.
9
- # All variables below are RUNTIME-ONLY unless marked otherwise.
10
- #
11
- # ===================================================================
12
-
13
- # Environment: development | production | test
14
- # COOLIFY: Do NOT check "Available at Buildtime" — the Dockerfile
15
- # sets NODE_ENV=development during build to ensure devDependencies install.
16
- NODE_ENV=development
17
-
18
- # Server port (default: 8000)
19
- PORT=8000
20
-
21
- # Server host (0.0.0.0 = listen on all interfaces)
22
- HOST=0.0.0.0
23
-
24
- # ===================================================================
25
- # DATABASE CONFIGURATION (MySQL 8.0+)
26
- # ===================================================================
27
-
28
- # Database connection string
29
- # Format: mysql://username:password@host:port/database
30
- # COOLIFY: Runtime only. Do NOT check "Available at Buildtime".
31
- DATABASE_URL="mysql://root:rootpassword@localhost:{{MYSQL_PORT}}/{{DATABASE_NAME}}"
32
-
33
- # Connection pool settings (for high-traffic production)
34
- # Min connections: 2-5 for low traffic, 5-10 for medium, 10-20 for high
35
- # Max connections: 10 for dev, 20-50 for production (10K-100K users/day)
36
- DATABASE_POOL_MIN=2
37
- DATABASE_POOL_MAX=10
38
-
39
- # ===================================================================
40
- # REDIS CONFIGURATION
41
- # ===================================================================
42
-
43
- # Redis connection URL
44
- # Format: redis://[:password@]host:port[/database]
45
- # COOLIFY: Runtime only. Do NOT check "Available at Buildtime".
46
- REDIS_URL="redis://localhost:{{REDIS_PORT}}"
47
-
48
- # Max retry attempts for failed Redis operations
49
- REDIS_MAX_RETRIES=3
50
-
51
- # Connection timeout in milliseconds
52
- REDIS_CONNECT_TIMEOUT=10000
53
-
54
- # ===================================================================
55
- # RATE LIMITING
56
- # ===================================================================
57
-
58
- # Master switch to enable/disable rate limiting (default: true)
59
- # Set to false in development to disable all rate limits
60
- RATE_LIMIT_ENABLED=true
61
-
62
- # Multiply all rate limit max values by this factor (default: 1)
63
- # Set to 10 in development for 10x headroom, or 0.5 for tighter limits
64
- RATE_LIMIT_MULTIPLIER=1
65
-
66
- # Optional: Override specific critical route limits (uses defaults if not set)
67
- # RATE_LIMIT_AUTH_LOGIN_MAX=10
68
- # RATE_LIMIT_AUTH_REGISTER_MAX=5
69
-
70
- # ===================================================================
71
- # ACCOUNT ACTIVATION
72
- # ===================================================================
73
-
74
- # When true (default), new users are created as inactive and must
75
- # verify their account before they can log in.
76
- # When false, users are active immediately after registration.
77
- # NOTE: When this variable is not provided, users are NOT activated
78
- # by default — you must explicitly set it to false to skip verification.
79
- REQUIRE_USER_VERIFICATION=true
80
-
81
- # ===================================================================
82
- # FILE UPLOAD
83
- # ===================================================================
84
-
85
- # Maximum file upload size in MB (default: 10)
86
- MAX_FILE_SIZE_MB=10
87
-
88
- # COOLIFY PERSISTENT STORAGE (required for uploads to survive redeployments):
89
- # Go to your service in Coolify Storages Add Volume Mount:
90
- # Name: uploads (or <project-name>-uploads)
91
- # Source Path: (leave empty Coolify manages the Docker volume)
92
- # Destination Path: /app/uploads
93
- # Without this, all uploaded files are lost on every redeployment.
94
-
95
- # ===================================================================
96
- # DOCKER PORTS — LOCAL DEVELOPMENT ONLY
97
- # ===================================================================
98
- #
99
- # These ports are used by docker-compose to expose MySQL, Redis, and
100
- # their admin UIs on your local machine. They are NOT needed in
101
- # productionproduction connects via DATABASE_URL and REDIS_URL
102
- # (typically over a private network), not through exposed ports.
103
- #
104
- # Change these if they conflict with other services on your machine.
105
-
106
- MYSQL_PORT={{MYSQL_PORT}}
107
- PHPMYADMIN_PORT={{PHPMYADMIN_PORT}}
108
- REDIS_PORT={{REDIS_PORT}}
109
- REDIS_COMMANDER_PORT={{REDIS_COMMANDER_PORT}}
110
-
111
- # ===================================================================
112
- # JWT AUTHENTICATION
113
- # ===================================================================
114
-
115
- # JWT secret key (MUST be at least 32 characters)
116
- # CRITICAL: Generate a strong random secret for production!
117
- # Example: openssl rand -base64 48
118
- # COOLIFY: Runtime only. Do NOT check "Available at Buildtime" this is a secret!
119
- JWT_SECRET="{{JWT_SECRET}}"
120
-
121
- # Access token expiry (short-lived)
122
- # Format: 1s, 1m, 1h, 1d (default: 15m)
123
- JWT_ACCESS_EXPIRY="15m"
124
-
125
- # Refresh token expiry (long-lived)
126
- # Format: 1s, 1m, 1h, 1d (default: 7d)
127
- JWT_REFRESH_EXPIRY="7d"
128
-
129
- # Cookie signing secret (separate from JWT for defense-in-depth)
130
- # Optional: defaults to JWT_SECRET if not set
131
- # For production: generate a separate secret: openssl rand -base64 48
132
- # COOLIFY: Runtime only. Do NOT check "Available at Buildtime" — this is a secret!
133
- # COOKIE_SECRET="change-this-to-a-different-secret-at-least-32-chars"
134
-
135
- # Cookie domain for cross-origin deployments
136
- # REQUIRED when client and API are on different subdomains:
137
- # Client: https://app.example.com | API: https://api.example.com
138
- # → Set COOKIE_DOMAIN=".example.com" (note the leading dot)
139
- # NOT needed when client and API share the same hostname (local dev, same-origin prod)
140
- # Without this, cookies are scoped to the API hostname only and the browser
141
- # will silently reject them on cross-origin requests (login appears to do nothing).
142
- # COOKIE_DOMAIN=".example.com"
143
-
144
- # ===================================================================
145
- # CORS (Cross-Origin Resource Sharing)
146
- # ===================================================================
147
-
148
- # Allowed origins for CORS
149
- # Development: Optional (allows all origins for easier local dev)
150
- # Production: REQUIRED (must be your frontend URL for security)
151
- # Multiple origins: Separate with commas
152
- # Examples:
153
- # Single origin: CORS_ORIGIN="https://myapp.com"
154
- # Multiple origins: CORS_ORIGIN="https://myapp.com,https://app.myapp.com"
155
- # Local dev: CORS_ORIGIN="http://localhost:3001"
156
- # CORS_ORIGIN="http://localhost:3001"
157
-
158
- # ===================================================================
159
- # EMAIL (Resend)
160
- # ===================================================================
161
-
162
- # Resend API key for transactional emails (password reset, verification, etc.)
163
- # Get your API key from: https://resend.com/api-keys
164
- # COOLIFY: Runtime only. Do NOT check "Available at Buildtime" — this is a secret!
165
- RESEND_API_KEY="re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
166
-
167
- # Sender email address
168
- # Development: Use Resend's test address (onboarding@resend.dev) — delivers to your Resend dashboard
169
- # Production: Use a verified domain email (e.g., noreply@yourdomain.com)
170
- RESEND_FROM_EMAIL="onboarding@resend.dev"
171
-
172
- # Frontend URL used to build links in emails (e.g., password reset links)
173
- # Must match the URL where your Next.js client is running
174
- CLIENT_URL="http://localhost:3000"
175
-
176
- # ===================================================================
177
- # LOGGING
178
- # ===================================================================
179
-
180
- # Log level: fatal | error | warn | info | debug | trace
181
- # Production: info or warn (reduces noise)
182
- # Development: debug (verbose logging)
183
- # Staging: info
184
- LOG_LEVEL=info
185
-
186
- # ===================================================================
187
- # ERROR TRACKING (Optional)
188
- # ===================================================================
189
-
190
- # Sentry DSN for error tracking and monitoring
191
- # Get your DSN from: https://sentry.io/settings/projects/
192
- # Leave empty to disable Sentry
193
- # Example: SENTRY_DSN="https://examplePublicKey@o0.ingest.sentry.io/0"
194
- # SENTRY_DSN=""
1
+ # ===================================================================
2
+ # APPLICATION CONFIGURATION
3
+ # ===================================================================
4
+ #
5
+ # COOLIFY DEPLOYMENT NOTE:
6
+ # When adding environment variables in Coolify, do NOT check
7
+ # "Available at Buildtime" for any variable unless explicitly noted.
8
+ # The server Dockerfile handles build-time config internally.
9
+ # All variables below are RUNTIME-ONLY unless marked otherwise.
10
+ #
11
+ # ===================================================================
12
+
13
+ # Environment: development | production | test
14
+ # COOLIFY: Do NOT check "Available at Buildtime" — the Dockerfile
15
+ # sets NODE_ENV=development during build to ensure devDependencies install.
16
+ NODE_ENV=development
17
+
18
+ # Server port (default: 8000)
19
+ PORT=8000
20
+
21
+ # Server host (0.0.0.0 = listen on all interfaces)
22
+ HOST=0.0.0.0
23
+
24
+ # ===================================================================
25
+ # SERVER TIMEOUTS
26
+ # ===================================================================
27
+
28
+ # Fastify request timeout in milliseconds (default: 30000 = 30s)
29
+ # Long-running routes (LLM calls, big exports) may need 180000+ (180s).
30
+ # IMPORTANT: the reverse proxy (Nginx/Coolify) timeout must be raised to
31
+ # match, or the proxy cuts the connection before the server does.
32
+ REQUEST_TIMEOUT_MS=30000
33
+
34
+ # Fastify connection timeout in milliseconds (default: 60000 = 60s)
35
+ CONNECTION_TIMEOUT_MS=60000
36
+
37
+ # ===================================================================
38
+ # DATABASE CONFIGURATION (MySQL 8.0+)
39
+ # ===================================================================
40
+
41
+ # Database connection string
42
+ # Format: mysql://username:password@host:port/database
43
+ # COOLIFY: Runtime only. Do NOT check "Available at Buildtime".
44
+ DATABASE_URL="mysql://root:rootpassword@localhost:{{MYSQL_PORT}}/{{DATABASE_NAME}}"
45
+
46
+ # Connection pool settings (for high-traffic production)
47
+ # Min connections: 2-5 for low traffic, 5-10 for medium, 10-20 for high
48
+ # Max connections: 10 for dev, 20-50 for production (10K-100K users/day)
49
+ DATABASE_POOL_MIN=2
50
+ DATABASE_POOL_MAX=10
51
+
52
+ # ===================================================================
53
+ # REDIS CONFIGURATION
54
+ # ===================================================================
55
+
56
+ # Redis connection URL
57
+ # Format: redis://[:password@]host:port[/database]
58
+ # COOLIFY: Runtime only. Do NOT check "Available at Buildtime".
59
+ REDIS_URL="redis://localhost:{{REDIS_PORT}}"
60
+
61
+ # Max retry attempts for failed Redis operations
62
+ REDIS_MAX_RETRIES=3
63
+
64
+ # Connection timeout in milliseconds
65
+ REDIS_CONNECT_TIMEOUT=10000
66
+
67
+ # ===================================================================
68
+ # RATE LIMITING
69
+ # ===================================================================
70
+
71
+ # Master switch to enable/disable rate limiting (default: true)
72
+ # Set to false in development to disable all rate limits
73
+ RATE_LIMIT_ENABLED=true
74
+
75
+ # Multiply all rate limit max values by this factor (default: 1)
76
+ # Set to 10 in development for 10x headroom, or 0.5 for tighter limits
77
+ RATE_LIMIT_MULTIPLIER=1
78
+
79
+ # Optional: Override specific critical route limits (uses defaults if not set)
80
+ # RATE_LIMIT_AUTH_LOGIN_MAX=10
81
+ # RATE_LIMIT_AUTH_REGISTER_MAX=5
82
+
83
+ # Trust Cloudflare's CF-Connecting-IP header for the real client IP (default: false)
84
+ # Used ONLY for rate-limiting and IP auto-block decisions. Behind Cloudflare,
85
+ # request.ip is a CF edge IP without this, all users behind one edge collapse
86
+ # onto a single IP and can rate-limit or auto-ban each other.
87
+ # COOLIFY: Runtime only. Do NOT check "Available at Buildtime".
88
+ # SECURITY: the header is client-spoofable. Set true ONLY when the origin
89
+ # accepts traffic exclusively via Cloudflare (direct origin access is blocked).
90
+ # Note: the left-most X-Forwarded-For entry is now trusted as the client IP
91
+ # regardless of this flag (covers grey-cloud / DNS-only), so the origin must be
92
+ # proxy-locked (firewall direct access) in production either way.
93
+ TRUST_CLOUDFLARE=false
94
+
95
+ # ===================================================================
96
+ # IP AUTO-BLOCK
97
+ # ===================================================================
98
+ #
99
+ # An IP that exceeds rate limits IP_AUTO_BLOCK_THRESHOLD times within
100
+ # IP_AUTO_BLOCK_WINDOW_SECONDS is blocked for IP_AUTO_BLOCK_DURATION_SECONDS.
101
+ # The threshold targets SUSTAINED abuse keep it high enough that a
102
+ # retry-looping legitimate client or a NAT'd office sharing one IP cannot
103
+ # self-ban. See src/config/rate-limit.config.ts for the interaction notes.
104
+
105
+ # Rate-limit violations before an IP is auto-blocked (default: 20)
106
+ IP_AUTO_BLOCK_THRESHOLD=20
107
+
108
+ # Sliding window for counting violations, in seconds (default: 300 = 5 min)
109
+ IP_AUTO_BLOCK_WINDOW_SECONDS=300
110
+
111
+ # How long an auto-blocked IP stays blocked, in seconds (default: 3600 = 1 hour)
112
+ IP_AUTO_BLOCK_DURATION_SECONDS=3600
113
+
114
+ # ===================================================================
115
+ # ACCOUNT ACTIVATION
116
+ # ===================================================================
117
+
118
+ # When true (default), new users are created as inactive and must
119
+ # verify their account before they can log in.
120
+ # When false, users are active immediately after registration.
121
+ # NOTE: When this variable is not provided, users are NOT activated
122
+ # by default you must explicitly set it to false to skip verification.
123
+ REQUIRE_USER_VERIFICATION=true
124
+
125
+ # ===================================================================
126
+ # FILE UPLOAD
127
+ # ===================================================================
128
+
129
+ # Maximum file upload size in MB (default: 10)
130
+ MAX_FILE_SIZE_MB=10
131
+
132
+ # COOLIFY PERSISTENT STORAGE (required for uploads to survive redeployments):
133
+ # Go to your service in Coolify → Storages → Add Volume Mount:
134
+ # Name: uploads (or <project-name>-uploads)
135
+ # Source Path: (leave empty Coolify manages the Docker volume)
136
+ # Destination Path: /app/uploads
137
+ # Without this, all uploaded files are lost on every redeployment.
138
+
139
+ # ===================================================================
140
+ # DOCKER PORTS LOCAL DEVELOPMENT ONLY
141
+ # ===================================================================
142
+ #
143
+ # These ports are used by docker-compose to expose MySQL, Redis, and
144
+ # their admin UIs on your local machine. They are NOT needed in
145
+ # production production connects via DATABASE_URL and REDIS_URL
146
+ # (typically over a private network), not through exposed ports.
147
+ #
148
+ # Change these if they conflict with other services on your machine.
149
+
150
+ MYSQL_PORT={{MYSQL_PORT}}
151
+ PHPMYADMIN_PORT={{PHPMYADMIN_PORT}}
152
+ REDIS_PORT={{REDIS_PORT}}
153
+ REDIS_COMMANDER_PORT={{REDIS_COMMANDER_PORT}}
154
+
155
+ # ===================================================================
156
+ # JWT AUTHENTICATION
157
+ # ===================================================================
158
+
159
+ # JWT secret key (MUST be at least 32 characters)
160
+ # CRITICAL: Generate a strong random secret for production!
161
+ # Example: openssl rand -base64 48
162
+ # COOLIFY: Runtime only. Do NOT check "Available at Buildtime" — this is a secret!
163
+ JWT_SECRET="{{JWT_SECRET}}"
164
+
165
+ # Access token expiry (short-lived)
166
+ # Format: 1s, 1m, 1h, 1d (default: 15m)
167
+ JWT_ACCESS_EXPIRY="15m"
168
+
169
+ # Refresh token expiry (long-lived)
170
+ # Format: 1s, 1m, 1h, 1d (default: 7d)
171
+ JWT_REFRESH_EXPIRY="7d"
172
+
173
+ # Cookie signing secret (separate from JWT for defense-in-depth)
174
+ # Optional: defaults to JWT_SECRET if not set
175
+ # For production: generate a separate secret: openssl rand -base64 48
176
+ # COOLIFY: Runtime only. Do NOT check "Available at Buildtime" — this is a secret!
177
+ # COOKIE_SECRET="change-this-to-a-different-secret-at-least-32-chars"
178
+
179
+ # Cookie domain for cross-origin deployments
180
+ # REQUIRED when client and API are on different subdomains:
181
+ # Client: https://app.example.com | API: https://api.example.com
182
+ # Set COOKIE_DOMAIN=".example.com" (note the leading dot)
183
+ # NOT needed when client and API share the same hostname (local dev, same-origin prod)
184
+ # Without this, cookies are scoped to the API hostname only and the browser
185
+ # will silently reject them on cross-origin requests (login appears to do nothing).
186
+ # COOKIE_DOMAIN=".example.com"
187
+
188
+ # ===================================================================
189
+ # CORS (Cross-Origin Resource Sharing)
190
+ # ===================================================================
191
+
192
+ # Allowed origins for CORS
193
+ # Development: Optional (allows all origins for easier local dev)
194
+ # Production: REQUIRED (must be your frontend URL for security)
195
+ # Multiple origins: Separate with commas
196
+ # Examples:
197
+ # Single origin: CORS_ORIGIN="https://myapp.com"
198
+ # Multiple origins: CORS_ORIGIN="https://myapp.com,https://app.myapp.com"
199
+ # Local dev: CORS_ORIGIN="http://localhost:3001"
200
+ # CORS_ORIGIN="http://localhost:3001"
201
+
202
+ # ===================================================================
203
+ # EMAIL (Resend)
204
+ # ===================================================================
205
+
206
+ # Resend API key for transactional emails (password reset, verification, etc.)
207
+ # Get your API key from: https://resend.com/api-keys
208
+ # COOLIFY: Runtime only. Do NOT check "Available at Buildtime" — this is a secret!
209
+ RESEND_API_KEY="re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
210
+
211
+ # Sender email address
212
+ # Development: Use Resend's test address (onboarding@resend.dev) — delivers to your Resend dashboard
213
+ # Production: Use a verified domain email (e.g., noreply@yourdomain.com)
214
+ RESEND_FROM_EMAIL="onboarding@resend.dev"
215
+
216
+ # Frontend URL used to build links in emails (e.g., password reset links)
217
+ # Must match the URL where your Next.js client is running
218
+ CLIENT_URL="http://localhost:3000"
219
+
220
+ # ===================================================================
221
+ # LOGGING
222
+ # ===================================================================
223
+
224
+ # Log level: fatal | error | warn | info | debug | trace
225
+ # Production: info or warn (reduces noise)
226
+ # Development: debug (verbose logging)
227
+ # Staging: info
228
+ LOG_LEVEL=info
229
+
230
+ # ===================================================================
231
+ # DATABASE SEEDING (npm run prisma:seed — dev/test only)
232
+ # ===================================================================
233
+
234
+ # Passwords for the seeded demo accounts (admin@example.com / user@example.com).
235
+ # Optional in development — well-known dev defaults (Admin123! / User123!) are
236
+ # used when unset. The seed script REFUSES to run when NODE_ENV=production.
237
+ # SEED_ADMIN_PASSWORD="choose-a-dev-admin-password"
238
+ # SEED_USER_PASSWORD="choose-a-dev-user-password"
239
+
240
+ # ===================================================================
241
+ # ERROR TRACKING (Optional)
242
+ # ===================================================================
243
+
244
+ # Sentry DSN for error tracking and monitoring
245
+ # Get your DSN from: https://sentry.io/settings/projects/
246
+ # Leave empty to disable Sentry
247
+ # Example: SENTRY_DSN="https://examplePublicKey@o0.ingest.sentry.io/0"
248
+ # SENTRY_DSN=""