@prajwolkc/stk 0.5.1 → 0.7.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.
- package/dist/commands/brain.js +36 -3
- package/dist/mcp/server.js +397 -1
- package/dist/services/brain.d.ts +32 -0
- package/dist/services/brain.js +163 -0
- package/dist/services/metrics.d.ts +35 -0
- package/dist/services/metrics.js +78 -0
- package/dist/services/security.d.ts +19 -0
- package/dist/services/security.js +194 -0
- package/package.json +2 -1
- package/src/data/seed-patterns.json +1802 -0
|
@@ -0,0 +1,1802 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000001",
|
|
4
|
+
"title": "Express error handler must be last",
|
|
5
|
+
"content": "Express error-handling middleware must be defined after all other app.use() and route calls. If you place it before your routes, it won't catch errors from those routes. The 4-param signature (err, req, res, next) is what tells Express it's an error handler, and order determines when it runs.",
|
|
6
|
+
"category": "api",
|
|
7
|
+
"source": "seed:stack",
|
|
8
|
+
"tags": ["express", "middleware"],
|
|
9
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000002",
|
|
13
|
+
"title": "Async route errors need explicit catching",
|
|
14
|
+
"content": "Express doesn't catch promise rejections in route handlers. An unhandled async error will hang the request or crash the process. Wrap handlers in try/catch or use express-async-errors which patches Router to forward rejected promises to your error middleware automatically.",
|
|
15
|
+
"category": "api",
|
|
16
|
+
"source": "seed:stack",
|
|
17
|
+
"tags": ["express", "async"],
|
|
18
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000003",
|
|
22
|
+
"title": "Headers already sent crash",
|
|
23
|
+
"content": "Calling res.json() or res.send() after a response is already sent throws 'Cannot set headers after they are sent to the client'. This often happens when you forget to return after sending an error response. Always use return res.status(400).json({error}) to prevent code from continuing.",
|
|
24
|
+
"category": "api",
|
|
25
|
+
"source": "seed:stack",
|
|
26
|
+
"tags": ["express", "http"],
|
|
27
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000004",
|
|
31
|
+
"title": "CORS credentials need explicit origin",
|
|
32
|
+
"content": "When using credentials: true in CORS config, the origin cannot be '*'. The browser blocks the response silently. You must specify the exact origin like 'https://myapp.com' or use a function that dynamically returns the allowed origin from a whitelist.",
|
|
33
|
+
"category": "security",
|
|
34
|
+
"source": "seed:stack",
|
|
35
|
+
"tags": ["express", "cors"],
|
|
36
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000005",
|
|
40
|
+
"title": "body-parser has 100kb default limit",
|
|
41
|
+
"content": "Express's built-in JSON parser (body-parser) rejects payloads over 100kb with a 413 status. If your API accepts file uploads or large JSON, increase the limit: app.use(express.json({ limit: '10mb' })). This catches many devs off guard with image base64 payloads.",
|
|
42
|
+
"category": "api",
|
|
43
|
+
"source": "seed:stack",
|
|
44
|
+
"tags": ["express", "configuration"],
|
|
45
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000006",
|
|
49
|
+
"title": "trust proxy needed behind reverse proxy",
|
|
50
|
+
"content": "Behind Nginx, AWS ALB, or Railway, req.ip returns the proxy IP not the client IP. Set app.set('trust proxy', 1) so Express reads X-Forwarded-For. Without this, rate limiters like express-rate-limit key all requests to the same IP, making them useless.",
|
|
51
|
+
"category": "deployment",
|
|
52
|
+
"source": "seed:stack",
|
|
53
|
+
"tags": ["express", "proxy"],
|
|
54
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000007",
|
|
58
|
+
"title": "Static middleware placement matters",
|
|
59
|
+
"content": "express.static() should come before your API routes but after security middleware like helmet. If placed after a catch-all route, it never serves files. If placed before auth middleware, static files bypass authentication. Order: helmet > static > auth > api routes > 404 > error handler.",
|
|
60
|
+
"category": "api",
|
|
61
|
+
"source": "seed:stack",
|
|
62
|
+
"tags": ["express", "middleware"],
|
|
63
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000008",
|
|
67
|
+
"title": "express.json() must come before routes",
|
|
68
|
+
"content": "If you define routes before app.use(express.json()), req.body will be undefined on POST/PUT requests. This is a common mistake when reorganizing middleware. The JSON parser must execute before any route handler that reads the request body.",
|
|
69
|
+
"category": "api",
|
|
70
|
+
"source": "seed:stack",
|
|
71
|
+
"tags": ["express", "middleware"],
|
|
72
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000009",
|
|
76
|
+
"title": "Error middleware needs exactly 4 params",
|
|
77
|
+
"content": "Express identifies error middleware by its 4-parameter signature: (err, req, res, next). If you omit any parameter, Express treats it as regular middleware and skips it during error handling. Even if you don't use next, include it: (err, req, res, _next) => {}.",
|
|
78
|
+
"category": "api",
|
|
79
|
+
"source": "seed:stack",
|
|
80
|
+
"tags": ["express", "middleware"],
|
|
81
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000010",
|
|
85
|
+
"title": "Route params are always strings",
|
|
86
|
+
"content": "req.params.id is always a string, even if the URL looks numeric like /users/42. Passing it directly to a database query expecting an integer causes type errors or wrong results. Always parse: const id = parseInt(req.params.id, 10) or Number(req.params.id), and validate it's not NaN.",
|
|
87
|
+
"category": "api",
|
|
88
|
+
"source": "seed:stack",
|
|
89
|
+
"tags": ["express", "types"],
|
|
90
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000011",
|
|
94
|
+
"title": "next('route') skips remaining middleware",
|
|
95
|
+
"content": "Calling next('route') in Express skips any remaining middleware attached to the current route and jumps to the next matching route. This is different from next() which runs the next middleware in the stack. Useful for conditional middleware chains but confusing if hit accidentally.",
|
|
96
|
+
"category": "api",
|
|
97
|
+
"source": "seed:stack",
|
|
98
|
+
"tags": ["express", "middleware"],
|
|
99
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000012",
|
|
103
|
+
"title": "res.locals scoped to single request",
|
|
104
|
+
"content": "res.locals is an object that persists only within a single request-response cycle. It's perfect for passing data between middleware (e.g., auth middleware sets res.locals.user), but it's gone after the response is sent. Don't confuse it with app.locals which persists across requests.",
|
|
105
|
+
"category": "api",
|
|
106
|
+
"source": "seed:stack",
|
|
107
|
+
"tags": ["express", "middleware"],
|
|
108
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000013",
|
|
112
|
+
"title": "Helmet doesn't set CSP by default",
|
|
113
|
+
"content": "Helmet sets many security headers but Content-Security-Policy is not enabled by default since it breaks most apps. You need to explicitly configure it: helmet({ contentSecurityPolicy: { directives: { defaultSrc: [\"'self'\"], scriptSrc: [\"'self'\"] } } }). Without CSP, XSS attacks remain possible.",
|
|
114
|
+
"category": "security",
|
|
115
|
+
"source": "seed:stack",
|
|
116
|
+
"tags": ["express", "security"],
|
|
117
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000014",
|
|
121
|
+
"title": "Rate limiter keys by IP—needs trustProxy",
|
|
122
|
+
"content": "express-rate-limit uses req.ip for keying. Behind a proxy, all requests appear from the same IP unless you set app.set('trust proxy', 1). Without it, one abusive client rate-limits everyone. Set trustProxy and consider adding a custom keyGenerator for authenticated routes.",
|
|
123
|
+
"category": "security",
|
|
124
|
+
"source": "seed:stack",
|
|
125
|
+
"tags": ["express", "rate-limiting"],
|
|
126
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000015",
|
|
130
|
+
"title": "Cookie parser before cookie-reading routes",
|
|
131
|
+
"content": "app.use(cookieParser()) must be registered before any route that accesses req.cookies. Otherwise req.cookies is undefined. This also applies to signed cookies—pass your secret to cookieParser(secret) and access them via req.signedCookies.",
|
|
132
|
+
"category": "api",
|
|
133
|
+
"source": "seed:stack",
|
|
134
|
+
"tags": ["express", "cookies"],
|
|
135
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000016",
|
|
139
|
+
"title": "Multiple res.send() calls crash server",
|
|
140
|
+
"content": "Calling res.send(), res.json(), or res.end() more than once per request throws ERR_HTTP_HEADERS_SENT and can crash your server. This typically happens in if/else branches without return statements, or in async code that continues after sending a response.",
|
|
141
|
+
"category": "api",
|
|
142
|
+
"source": "seed:stack",
|
|
143
|
+
"tags": ["express", "http"],
|
|
144
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000017",
|
|
148
|
+
"title": "404 handler must go after all routes",
|
|
149
|
+
"content": "A catch-all 404 handler like app.use((req, res) => res.status(404).json({error: 'Not Found'})) must be placed after all your route definitions. If placed before, it intercepts every request. This is the most common cause of 'all my routes return 404'.",
|
|
150
|
+
"category": "api",
|
|
151
|
+
"source": "seed:stack",
|
|
152
|
+
"tags": ["express", "routing"],
|
|
153
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000018",
|
|
157
|
+
"title": "req.body undefined without body parser",
|
|
158
|
+
"content": "Express does not parse request bodies by default. req.body is undefined until you add app.use(express.json()) for JSON or app.use(express.urlencoded({ extended: true })) for form data. This is the #1 Express beginner gotcha—always set up body parsing before routes.",
|
|
159
|
+
"category": "api",
|
|
160
|
+
"source": "seed:stack",
|
|
161
|
+
"tags": ["express", "middleware"],
|
|
162
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000019",
|
|
166
|
+
"title": "Multer field names must match exactly",
|
|
167
|
+
"content": "Multer's upload.single('avatar') requires the form field name to be exactly 'avatar'. A mismatch means req.file is undefined with no error. For multiple fields, use upload.fields([{name: 'photo', maxCount: 1}]) and access via req.files['photo'].",
|
|
168
|
+
"category": "api",
|
|
169
|
+
"source": "seed:stack",
|
|
170
|
+
"tags": ["express", "file-upload"],
|
|
171
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000020",
|
|
175
|
+
"title": "Morgan logger should be first middleware",
|
|
176
|
+
"content": "Place morgan (HTTP request logger) as one of the first middleware to capture all requests including those rejected by auth or other middleware. If placed after auth middleware, failed auth requests won't be logged, making debugging harder.",
|
|
177
|
+
"category": "api",
|
|
178
|
+
"source": "seed:stack",
|
|
179
|
+
"tags": ["express", "logging"],
|
|
180
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000021",
|
|
184
|
+
"title": "CORS preflight needs OPTIONS handling",
|
|
185
|
+
"content": "Browsers send an OPTIONS preflight request before cross-origin requests with custom headers or non-simple methods. If your server doesn't handle OPTIONS, the actual request never fires. The cors() middleware handles this automatically, but custom CORS setups often forget it.",
|
|
186
|
+
"category": "api",
|
|
187
|
+
"source": "seed:stack",
|
|
188
|
+
"tags": ["express", "cors"],
|
|
189
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000022",
|
|
193
|
+
"title": "Express misses promise rejections by default",
|
|
194
|
+
"content": "Express 4 doesn't catch rejected promises from async handlers. An unhandled rejection hangs the request until timeout. Either wrap every async handler in try/catch, use a wrapper function, or install express-async-errors at the top of your entry file before any routes.",
|
|
195
|
+
"category": "api",
|
|
196
|
+
"source": "seed:stack",
|
|
197
|
+
"tags": ["express", "async"],
|
|
198
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000023",
|
|
202
|
+
"title": "res.status().json() not res.json().status()",
|
|
203
|
+
"content": "The correct chain is res.status(400).json({error: 'Bad request'}). Calling res.json().status() sends the response with status 200 first, then tries to set status on an already-sent response, which either fails silently or throws. Status must be set before sending the body.",
|
|
204
|
+
"category": "api",
|
|
205
|
+
"source": "seed:stack",
|
|
206
|
+
"tags": ["express", "http"],
|
|
207
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000024",
|
|
211
|
+
"title": "Express route order—first match wins",
|
|
212
|
+
"content": "Express matches routes top-to-bottom and stops at the first match. If you define GET /users/:id before GET /users/me, the request to /users/me matches :id='me'. Fix: define specific routes before parameterized ones, or use regex constraints on params.",
|
|
213
|
+
"category": "api",
|
|
214
|
+
"source": "seed:stack",
|
|
215
|
+
"tags": ["express", "routing"],
|
|
216
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000025",
|
|
220
|
+
"title": "app.use without path matches everything",
|
|
221
|
+
"content": "app.use(myMiddleware) with no path argument applies to every route and HTTP method. This is intentional for global middleware like cors() and helmet(), but accidentally omitting the path on route-specific middleware applies it globally. Use app.use('/api', myMiddleware) to scope it.",
|
|
222
|
+
"category": "api",
|
|
223
|
+
"source": "seed:stack",
|
|
224
|
+
"tags": ["express", "middleware"],
|
|
225
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000026",
|
|
229
|
+
"title": "Prisma N+1 with include",
|
|
230
|
+
"content": "Using include: { posts: true } on a list query generates a separate query per parent record—classic N+1. Use select to limit fields, or restructure to fetch relations in a separate batch query. Prisma's query engine batches some includes, but deeply nested includes still cause N+1.",
|
|
231
|
+
"category": "performance",
|
|
232
|
+
"source": "seed:stack",
|
|
233
|
+
"tags": ["prisma", "database"],
|
|
234
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000027",
|
|
238
|
+
"title": "prisma db push drops data on conflicts",
|
|
239
|
+
"content": "prisma db push syncs schema to DB but may drop columns or tables when resolving conflicts. It shows a warning and asks for confirmation, but in CI/scripts with --accept-data-loss it's silent. Use prisma migrate for production databases where data preservation matters.",
|
|
240
|
+
"category": "database",
|
|
241
|
+
"source": "seed:stack",
|
|
242
|
+
"tags": ["prisma", "migration"],
|
|
243
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000028",
|
|
247
|
+
"title": "Prisma connection pool exhaustion",
|
|
248
|
+
"content": "Prisma's default connection pool is 10 connections (num_physical_cpus * 2 + 1). Under high load or with long transactions, the pool exhausts and requests queue up, eventually timing out. Set connection_limit in your DATABASE_URL and increase pool_timeout. Use pgbouncer for serverless.",
|
|
249
|
+
"category": "database",
|
|
250
|
+
"source": "seed:stack",
|
|
251
|
+
"tags": ["prisma", "performance"],
|
|
252
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000029",
|
|
256
|
+
"title": "findUnique returns null not error",
|
|
257
|
+
"content": "prisma.user.findUnique({ where: { id: 999 } }) returns null when no record exists—it doesn't throw. If you assume it returns an object, you get 'Cannot read properties of null'. Use findUniqueOrThrow() if you want an error, or add a null check before accessing properties.",
|
|
258
|
+
"category": "database",
|
|
259
|
+
"source": "seed:stack",
|
|
260
|
+
"tags": ["prisma", "errors"],
|
|
261
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000030",
|
|
265
|
+
"title": "Soft delete filter forgotten in queries",
|
|
266
|
+
"content": "After adding soft deletes (deletedAt column), every query needs where: { deletedAt: null }. It's easy to forget in count(), aggregate(), or new queries. Use Prisma middleware or a wrapper to automatically add the filter, or you'll leak deleted records to users.",
|
|
267
|
+
"category": "database",
|
|
268
|
+
"source": "seed:stack",
|
|
269
|
+
"tags": ["prisma", "soft-delete"],
|
|
270
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000031",
|
|
274
|
+
"title": "Prisma unique constraint error is P2002",
|
|
275
|
+
"content": "When a unique constraint is violated, Prisma throws a PrismaClientKnownRequestError with code 'P2002'. Catch it specifically: if (error.code === 'P2002') { return res.status(409).json({ error: 'Already exists' }); }. The error.meta.target field tells you which field(s) caused the conflict.",
|
|
276
|
+
"category": "database",
|
|
277
|
+
"source": "seed:stack",
|
|
278
|
+
"tags": ["prisma", "errors"],
|
|
279
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000032",
|
|
283
|
+
"title": "Prisma transaction rollback on any error",
|
|
284
|
+
"content": "Inside prisma.$transaction(), if any operation throws, ALL operations roll back. This includes validation errors and unique constraint violations. Don't mix side effects (emails, API calls) inside transactions—they can't be rolled back. Do side effects after the transaction succeeds.",
|
|
285
|
+
"category": "database",
|
|
286
|
+
"source": "seed:stack",
|
|
287
|
+
"tags": ["prisma", "transactions"],
|
|
288
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000033",
|
|
292
|
+
"title": "Prisma DateTime stored as UTC",
|
|
293
|
+
"content": "Prisma stores all DateTime fields as UTC in the database and returns JavaScript Date objects in UTC. If you display them directly, users see UTC times. Convert to local timezone on the frontend: new Date(record.createdAt).toLocaleString(). Never store local times in the DB.",
|
|
294
|
+
"category": "database",
|
|
295
|
+
"source": "seed:stack",
|
|
296
|
+
"tags": ["prisma", "datetime"],
|
|
297
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000034",
|
|
301
|
+
"title": "Cascade delete needs explicit onDelete",
|
|
302
|
+
"content": "Prisma doesn't cascade deletes by default. Deleting a parent record with child references throws a foreign key violation. Add onDelete: Cascade to your relation: @relation(fields: [userId], references: [id], onDelete: Cascade). Or use onDelete: SetNull to nullify the reference instead.",
|
|
303
|
+
"category": "database",
|
|
304
|
+
"source": "seed:stack",
|
|
305
|
+
"tags": ["prisma", "relations"],
|
|
306
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000035",
|
|
310
|
+
"title": "Prisma raw query SQL injection risk",
|
|
311
|
+
"content": "prisma.$queryRawUnsafe() is vulnerable to SQL injection. Use prisma.$queryRaw with template literals instead—it parameterizes automatically: prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`. The tagged template syntax looks like string interpolation but is actually safe.",
|
|
312
|
+
"category": "security",
|
|
313
|
+
"source": "seed:stack",
|
|
314
|
+
"tags": ["prisma", "security"],
|
|
315
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000036",
|
|
319
|
+
"title": "prisma generate needed after schema changes",
|
|
320
|
+
"content": "After modifying schema.prisma, you must run npx prisma generate to regenerate the TypeScript client. Without this, your code uses stale types—new fields won't appear in autocomplete and queries will fail at runtime. Add prisma generate to your build script.",
|
|
321
|
+
"category": "database",
|
|
322
|
+
"source": "seed:stack",
|
|
323
|
+
"tags": ["prisma", "workflow"],
|
|
324
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000037",
|
|
328
|
+
"title": "connectOrCreate race condition",
|
|
329
|
+
"content": "Prisma's connectOrCreate can fail with a unique constraint error under concurrent requests. Two requests check simultaneously, both find nothing, both try to create. Wrap in a retry loop or use an upsert instead. This is a real problem in high-concurrency environments.",
|
|
330
|
+
"category": "database",
|
|
331
|
+
"source": "seed:stack",
|
|
332
|
+
"tags": ["prisma", "concurrency"],
|
|
333
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000038",
|
|
337
|
+
"title": "BigInt serialization to JSON fails",
|
|
338
|
+
"content": "JSON.stringify() throws 'Do not know how to serialize a BigInt' for Prisma BigInt fields. Add a custom serializer: BigInt.prototype.toJSON = function() { return this.toString() }; or convert BigInt fields to strings/numbers before sending responses.",
|
|
339
|
+
"category": "api",
|
|
340
|
+
"source": "seed:stack",
|
|
341
|
+
"tags": ["prisma", "serialization"],
|
|
342
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000039",
|
|
346
|
+
"title": "Prisma include is eager loading",
|
|
347
|
+
"content": "Prisma's include always executes a JOIN or separate query immediately—there's no lazy loading. Including deeply nested relations (user.posts.comments.author) generates many queries. Use select to pick only needed fields, or split into multiple targeted queries for better performance.",
|
|
348
|
+
"category": "performance",
|
|
349
|
+
"source": "seed:stack",
|
|
350
|
+
"tags": ["prisma", "performance"],
|
|
351
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000040",
|
|
355
|
+
"title": "Count doesn't respect soft delete filter",
|
|
356
|
+
"content": "prisma.user.count() returns ALL records including soft-deleted ones unless you add where: { deletedAt: null }. This causes mismatched counts on paginated lists—the total says 100 but the list shows 95. Always apply the same where clause to both findMany and count.",
|
|
357
|
+
"category": "database",
|
|
358
|
+
"source": "seed:stack",
|
|
359
|
+
"tags": ["prisma", "soft-delete"],
|
|
360
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000041",
|
|
364
|
+
"title": "updateMany doesn't trigger middleware",
|
|
365
|
+
"content": "Prisma middleware (used for soft deletes, audit logs) only runs on single-record operations like update() and delete(). updateMany() and deleteMany() bypass middleware entirely. If you rely on middleware for business logic, avoid bulk operations or implement the logic at the database level.",
|
|
366
|
+
"category": "database",
|
|
367
|
+
"source": "seed:stack",
|
|
368
|
+
"tags": ["prisma", "middleware"],
|
|
369
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000042",
|
|
373
|
+
"title": "Decimal fields need string in JSON",
|
|
374
|
+
"content": "Prisma Decimal fields serialize to Prisma.Decimal objects, not numbers. JSON.stringify converts them but may lose precision. When sending to clients, convert to string: price.toString(). When receiving from clients, accept strings to avoid floating-point issues: new Prisma.Decimal('19.99').",
|
|
375
|
+
"category": "database",
|
|
376
|
+
"source": "seed:stack",
|
|
377
|
+
"tags": ["prisma", "types"],
|
|
378
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000043",
|
|
382
|
+
"title": "prisma migrate vs db push in production",
|
|
383
|
+
"content": "prisma db push is for prototyping—it doesn't create migration files and can drop data. For production, use prisma migrate deploy which applies versioned migration files. However, if your migration chain is broken, db push with a backup is sometimes the pragmatic choice.",
|
|
384
|
+
"category": "deployment",
|
|
385
|
+
"source": "seed:stack",
|
|
386
|
+
"tags": ["prisma", "migration"],
|
|
387
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000044",
|
|
391
|
+
"title": "Connection string needs connection_limit",
|
|
392
|
+
"content": "Without connection_limit in your DATABASE_URL, Prisma uses the default pool size which may exceed your database's max connections, especially on managed databases. Add ?connection_limit=25&pool_timeout=15 to your connection string. Serverless apps need even lower limits.",
|
|
393
|
+
"category": "database",
|
|
394
|
+
"source": "seed:stack",
|
|
395
|
+
"tags": ["prisma", "configuration"],
|
|
396
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000045",
|
|
400
|
+
"title": "findFirst without orderBy is nondeterministic",
|
|
401
|
+
"content": "prisma.user.findFirst({ where: { email: 'x' } }) returns an arbitrary matching record when multiple exist—the order depends on the database engine. Always add orderBy to get consistent results: findFirst({ where: {...}, orderBy: { createdAt: 'desc' } }).",
|
|
402
|
+
"category": "database",
|
|
403
|
+
"source": "seed:stack",
|
|
404
|
+
"tags": ["prisma", "queries"],
|
|
405
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000046",
|
|
409
|
+
"title": "Nested create doesn't validate unique",
|
|
410
|
+
"content": "When using nested create inside a Prisma query, unique constraints are only checked at the database level, not in Prisma's type system. You'll get a runtime P2002 error. Validate uniqueness before the nested create, or handle the constraint error gracefully.",
|
|
411
|
+
"category": "database",
|
|
412
|
+
"source": "seed:stack",
|
|
413
|
+
"tags": ["prisma", "validation"],
|
|
414
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000047",
|
|
418
|
+
"title": "$transaction is sequential by default",
|
|
419
|
+
"content": "prisma.$transaction([query1, query2]) runs queries sequentially, not in parallel. For independent operations that need atomicity but not ordering, this is slower than necessary. Use the interactive transaction API ($transaction(async (tx) => {})) for more control over execution order.",
|
|
420
|
+
"category": "performance",
|
|
421
|
+
"source": "seed:stack",
|
|
422
|
+
"tags": ["prisma", "transactions"],
|
|
423
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000048",
|
|
427
|
+
"title": "Prisma client is not tree-shakeable",
|
|
428
|
+
"content": "The generated Prisma client includes code for all models and operations, even ones you don't use. This bloats serverless function bundles. Use prisma generate --no-engine for edge runtimes, or consider splitting your schema into smaller clients for different services.",
|
|
429
|
+
"category": "performance",
|
|
430
|
+
"source": "seed:stack",
|
|
431
|
+
"tags": ["prisma", "bundle-size"],
|
|
432
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000049",
|
|
436
|
+
"title": "Prisma enum changes need migration",
|
|
437
|
+
"content": "Adding or renaming enum values in schema.prisma requires a database migration. db push may fail or drop the enum. Create a migration: prisma migrate dev --name add_enum_value. Renaming enums is especially tricky—you may need raw SQL to ALTER TYPE.",
|
|
438
|
+
"category": "database",
|
|
439
|
+
"source": "seed:stack",
|
|
440
|
+
"tags": ["prisma", "migration"],
|
|
441
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000050",
|
|
445
|
+
"title": "Relation filter where syntax differs",
|
|
446
|
+
"content": "Prisma's relation filters use a different syntax: where: { posts: { some: { published: true } } } not where: { posts: { published: true } }. The some/every/none operators are required for to-many relations. This trips up developers coming from other ORMs like Sequelize.",
|
|
447
|
+
"category": "database",
|
|
448
|
+
"source": "seed:stack",
|
|
449
|
+
"tags": ["prisma", "queries"],
|
|
450
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000051",
|
|
454
|
+
"title": "useEffect stale closure from missing deps",
|
|
455
|
+
"content": "Omitting a variable from useEffect's dependency array creates a stale closure—the effect captures the old value. React's exhaustive-deps lint rule catches this. Either add the dependency, use a ref for values you don't want to trigger re-runs, or use useReducer for complex state.",
|
|
456
|
+
"category": "architecture",
|
|
457
|
+
"source": "seed:stack",
|
|
458
|
+
"tags": ["react", "hooks"],
|
|
459
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000052",
|
|
463
|
+
"title": "setState on unmounted component",
|
|
464
|
+
"content": "Calling setState after a component unmounts causes a React warning and potential memory leak. This happens with async operations that complete after navigation. Use an AbortController in useEffect cleanup, or check a ref: if (mountedRef.current) setState(data). React 18+ suppresses the warning but the leak remains.",
|
|
465
|
+
"category": "architecture",
|
|
466
|
+
"source": "seed:stack",
|
|
467
|
+
"tags": ["react", "hooks"],
|
|
468
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000053",
|
|
472
|
+
"title": "Use stable ID for key prop, not index",
|
|
473
|
+
"content": "Using array index as key causes bugs when items are reordered, inserted, or deleted—React reuses the wrong DOM node and component state gets mixed up. Use a stable unique identifier like a database ID: items.map(item => <Item key={item.id} />). Index is only safe for static lists.",
|
|
474
|
+
"category": "architecture",
|
|
475
|
+
"source": "seed:stack",
|
|
476
|
+
"tags": ["react", "rendering"],
|
|
477
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000054",
|
|
481
|
+
"title": "useMemo doesn't prevent children re-render",
|
|
482
|
+
"content": "useMemo memoizes a computed value but doesn't prevent child components from re-rendering. If the parent re-renders, children re-render even if the memoized value didn't change. Use React.memo() on child components to prevent unnecessary re-renders based on prop comparison.",
|
|
483
|
+
"category": "performance",
|
|
484
|
+
"source": "seed:stack",
|
|
485
|
+
"tags": ["react", "performance"],
|
|
486
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000055",
|
|
490
|
+
"title": "Context change re-renders all consumers",
|
|
491
|
+
"content": "When a Context value changes, every component that calls useContext(MyContext) re-renders—even if it only uses a portion of the context. Split large contexts into smaller ones (AuthContext, ThemeContext) or use libraries like zustand/jotai for fine-grained subscriptions.",
|
|
492
|
+
"category": "performance",
|
|
493
|
+
"source": "seed:stack",
|
|
494
|
+
"tags": ["react", "context"],
|
|
495
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000056",
|
|
499
|
+
"title": "useCallback identity changes with new deps",
|
|
500
|
+
"content": "useCallback returns a new function reference when any dependency changes. If the dependency is an object created in render, useCallback is useless—it gets a new identity every render. Ensure deps are primitives or stable references. Memoize the deps themselves if needed.",
|
|
501
|
+
"category": "performance",
|
|
502
|
+
"source": "seed:stack",
|
|
503
|
+
"tags": ["react", "hooks"],
|
|
504
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000057",
|
|
508
|
+
"title": "Ref updates don't trigger re-render",
|
|
509
|
+
"content": "Changing ref.current does not cause a re-render. If you need the UI to update when a ref changes, you need state instead. Refs are for values that don't affect rendering: DOM elements, timer IDs, previous values, or mutable values shared across renders without re-rendering.",
|
|
510
|
+
"category": "architecture",
|
|
511
|
+
"source": "seed:stack",
|
|
512
|
+
"tags": ["react", "hooks"],
|
|
513
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000058",
|
|
517
|
+
"title": "useState initial value only on mount",
|
|
518
|
+
"content": "The initial value passed to useState(initialValue) is only used during the first render. Passing a new value on re-render does nothing: const [name, setName] = useState(props.name) won't update when props.name changes. Use useEffect to sync props to state, or derive state from props directly.",
|
|
519
|
+
"category": "architecture",
|
|
520
|
+
"source": "seed:stack",
|
|
521
|
+
"tags": ["react", "hooks"],
|
|
522
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000059",
|
|
526
|
+
"title": "useEffect cleanup runs before next effect",
|
|
527
|
+
"content": "useEffect's cleanup function runs before the next effect execution AND on unmount. This is intentional for cleaning up stale subscriptions. If your effect fetches data, the cleanup can abort the previous fetch: return () => controller.abort(). Not understanding this order causes race conditions.",
|
|
528
|
+
"category": "architecture",
|
|
529
|
+
"source": "seed:stack",
|
|
530
|
+
"tags": ["react", "hooks"],
|
|
531
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000060",
|
|
535
|
+
"title": "Strict mode double-invokes effects in dev",
|
|
536
|
+
"content": "React.StrictMode intentionally runs effects twice in development to catch bugs. Your useEffect fires, runs cleanup, then fires again. This exposes missing cleanup functions and side effects that aren't idempotent. It only happens in dev—production runs effects once.",
|
|
537
|
+
"category": "testing",
|
|
538
|
+
"source": "seed:stack",
|
|
539
|
+
"tags": ["react", "debugging"],
|
|
540
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000061",
|
|
544
|
+
"title": "Event handlers capture stale state",
|
|
545
|
+
"content": "Event handlers in React closures capture the state value at the time the handler was created. If state updates between the handler creation and execution, it reads the old value. Use a ref to always read the latest value, or use the functional setState form: setCount(prev => prev + 1).",
|
|
546
|
+
"category": "architecture",
|
|
547
|
+
"source": "seed:stack",
|
|
548
|
+
"tags": ["react", "closures"],
|
|
549
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000062",
|
|
553
|
+
"title": "forwardRef needed for ref on custom components",
|
|
554
|
+
"content": "Passing ref to a custom component does nothing unless the component uses React.forwardRef. Without it, ref is undefined inside the child. Wrap your component: const Input = forwardRef((props, ref) => <input ref={ref} {...props} />). React 19 passes ref as a regular prop instead.",
|
|
555
|
+
"category": "architecture",
|
|
556
|
+
"source": "seed:stack",
|
|
557
|
+
"tags": ["react", "refs"],
|
|
558
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000063",
|
|
562
|
+
"title": "Suspense needs error boundary for errors",
|
|
563
|
+
"content": "React Suspense handles loading states but NOT errors. If a suspended component throws during rendering, Suspense won't catch it—the error propagates up. You need an error boundary wrapping or surrounding the Suspense component to gracefully handle fetch or render errors.",
|
|
564
|
+
"category": "architecture",
|
|
565
|
+
"source": "seed:stack",
|
|
566
|
+
"tags": ["react", "error-handling"],
|
|
567
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000064",
|
|
571
|
+
"title": "React.lazy needs Suspense wrapper",
|
|
572
|
+
"content": "Components loaded with React.lazy() must be rendered inside a <Suspense fallback={<Loading/>}> component. Without Suspense, React throws an error when the lazy component is loading. Place Suspense at a meaningful boundary—wrapping individual lazy components or a group of them.",
|
|
573
|
+
"category": "architecture",
|
|
574
|
+
"source": "seed:stack",
|
|
575
|
+
"tags": ["react", "code-splitting"],
|
|
576
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000065",
|
|
580
|
+
"title": "setState is batched in event handlers",
|
|
581
|
+
"content": "React batches multiple setState calls in event handlers into a single re-render. Calling setA(1); setB(2); setC(3) causes one re-render, not three. In React 18+, batching also applies to setTimeout, promises, and native event handlers. Use flushSync() to force immediate updates if needed.",
|
|
582
|
+
"category": "performance",
|
|
583
|
+
"source": "seed:stack",
|
|
584
|
+
"tags": ["react", "state"],
|
|
585
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000066",
|
|
589
|
+
"title": "Object/array state needs spread to update",
|
|
590
|
+
"content": "React uses Object.is() to detect state changes. Mutating an object/array and calling setState with the same reference doesn't trigger re-render. Always create a new reference: setItems([...items, newItem]) or setUser({...user, name: 'new'}). This is React's immutability contract.",
|
|
591
|
+
"category": "architecture",
|
|
592
|
+
"source": "seed:stack",
|
|
593
|
+
"tags": ["react", "state"],
|
|
594
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000067",
|
|
598
|
+
"title": "useLayoutEffect blocks browser paint",
|
|
599
|
+
"content": "useLayoutEffect fires synchronously after DOM mutations but before the browser paints. It blocks visual updates, causing jank. Only use it for DOM measurements (getBoundingClientRect) or to prevent visual flicker. For everything else, use useEffect which fires asynchronously after paint.",
|
|
600
|
+
"category": "performance",
|
|
601
|
+
"source": "seed:stack",
|
|
602
|
+
"tags": ["react", "hooks"],
|
|
603
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000068",
|
|
607
|
+
"title": "Custom hooks must start with 'use'",
|
|
608
|
+
"content": "React's linting rules and hook call validation only work if custom hooks start with 'use'. A function called 'getData' that uses hooks internally won't get lint warnings for rule violations. Rename it to 'useGetData'. This is a convention React tooling depends on, not just a style choice.",
|
|
609
|
+
"category": "architecture",
|
|
610
|
+
"source": "seed:stack",
|
|
611
|
+
"tags": ["react", "hooks"],
|
|
612
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000069",
|
|
616
|
+
"title": "Portal events bubble through React tree",
|
|
617
|
+
"content": "Events from React portals bubble through the React component tree, NOT the DOM tree. A click inside a portal child bubbles to the portal's React parent, even though the portal renders in a different DOM location. This surprises developers who expect DOM-based event propagation.",
|
|
618
|
+
"category": "architecture",
|
|
619
|
+
"source": "seed:stack",
|
|
620
|
+
"tags": ["react", "events"],
|
|
621
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000070",
|
|
625
|
+
"title": "Conditional hooks violate rules of hooks",
|
|
626
|
+
"content": "Hooks must be called in the same order every render. Putting a hook inside an if/else, loop, or early return breaks this rule and causes unpredictable bugs. React tracks hooks by call order, not name. Move the condition inside the hook instead: useEffect(() => { if (condition) {...} }).",
|
|
627
|
+
"category": "architecture",
|
|
628
|
+
"source": "seed:stack",
|
|
629
|
+
"tags": ["react", "hooks"],
|
|
630
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000071",
|
|
634
|
+
"title": "Derived state is usually an anti-pattern",
|
|
635
|
+
"content": "Storing state that can be computed from other state or props is an anti-pattern. const [fullName, setFullName] = useState(first + last) will go stale. Instead, compute during render: const fullName = first + ' ' + last. Use useMemo if the computation is expensive.",
|
|
636
|
+
"category": "architecture",
|
|
637
|
+
"source": "seed:stack",
|
|
638
|
+
"tags": ["react", "state"],
|
|
639
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000072",
|
|
643
|
+
"title": "React.memo does shallow comparison only",
|
|
644
|
+
"content": "React.memo skips re-render only if all props are shallowly equal (===). Objects, arrays, and functions created in the parent are new references each render, defeating memo. Memoize these with useMemo/useCallback, or pass a custom comparison: React.memo(Comp, (prev, next) => ...).",
|
|
645
|
+
"category": "performance",
|
|
646
|
+
"source": "seed:stack",
|
|
647
|
+
"tags": ["react", "performance"],
|
|
648
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000073",
|
|
652
|
+
"title": "Fragment key prop needs explicit Fragment",
|
|
653
|
+
"content": "The shorthand <></> syntax doesn't support the key prop. If you need a key on a Fragment (e.g., in a list), use the full syntax: <Fragment key={item.id}>...</Fragment>. Import Fragment from React. This is a common error when rendering lists without wrapper elements.",
|
|
654
|
+
"category": "architecture",
|
|
655
|
+
"source": "seed:stack",
|
|
656
|
+
"tags": ["react", "jsx"],
|
|
657
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000074",
|
|
661
|
+
"title": "Hydration mismatch with dynamic values",
|
|
662
|
+
"content": "Using Date.now(), Math.random(), or locale-dependent formatting in SSR causes hydration mismatches—server HTML differs from client render. Suppress with suppressHydrationWarning, or defer dynamic content with useEffect. For dates, format on the client only inside a useEffect.",
|
|
663
|
+
"category": "architecture",
|
|
664
|
+
"source": "seed:stack",
|
|
665
|
+
"tags": ["react", "ssr"],
|
|
666
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000075",
|
|
670
|
+
"title": "defaultValue only for uncontrolled inputs",
|
|
671
|
+
"content": "The defaultValue prop sets the initial value of an uncontrolled input—it's ignored on subsequent renders. If you also use value and onChange (controlled), defaultValue does nothing. Pick one pattern: controlled (value + onChange) or uncontrolled (defaultValue + ref). Mixing them causes confusion.",
|
|
672
|
+
"category": "architecture",
|
|
673
|
+
"source": "seed:stack",
|
|
674
|
+
"tags": ["react", "forms"],
|
|
675
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000076",
|
|
679
|
+
"title": "Server components can't use hooks",
|
|
680
|
+
"content": "Next.js Server Components run on the server and cannot use useState, useEffect, or any React hook. They also can't use browser APIs like window or document. Add 'use client' at the top of files that need interactivity. Keep server components for data fetching and static rendering.",
|
|
681
|
+
"category": "architecture",
|
|
682
|
+
"source": "seed:stack",
|
|
683
|
+
"tags": ["nextjs", "server-components"],
|
|
684
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000077",
|
|
688
|
+
"title": "ISR revalidation path must match exactly",
|
|
689
|
+
"content": "revalidatePath('/blog/post-1') must match the exact route path including dynamic segments. A trailing slash mismatch or wrong casing means the page isn't revalidated and stale content persists. Use revalidateTag() for more flexible cache invalidation across multiple pages.",
|
|
690
|
+
"category": "deployment",
|
|
691
|
+
"source": "seed:stack",
|
|
692
|
+
"tags": ["nextjs", "caching"],
|
|
693
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000078",
|
|
697
|
+
"title": "Next.js middleware runs on Edge runtime",
|
|
698
|
+
"content": "Next.js middleware.ts runs on the Edge Runtime which lacks Node.js APIs like fs, path, crypto.randomBytes, and most npm packages. You can't use Prisma, bcrypt, or Node streams. Only use Web APIs. For Node-dependent logic, move it to API routes or server actions instead.",
|
|
699
|
+
"category": "architecture",
|
|
700
|
+
"source": "seed:stack",
|
|
701
|
+
"tags": ["nextjs", "edge"],
|
|
702
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000079",
|
|
706
|
+
"title": "Next.js dynamic route params are strings",
|
|
707
|
+
"content": "In Next.js, params from dynamic routes like [id] are always strings. params.id is '42' not 42. This causes subtle bugs when comparing with === against numbers. Always parse: const id = Number(params.id) and validate: if (isNaN(id)) return notFound().",
|
|
708
|
+
"category": "api",
|
|
709
|
+
"source": "seed:stack",
|
|
710
|
+
"tags": ["nextjs", "routing"],
|
|
711
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000080",
|
|
715
|
+
"title": "NEXT_PUBLIC prefix for client env vars",
|
|
716
|
+
"content": "Environment variables in Next.js are server-only by default. To expose them to the browser, prefix with NEXT_PUBLIC_: NEXT_PUBLIC_API_URL. Without the prefix, process.env.API_URL is undefined on the client. This is a security feature—don't prefix secrets like database URLs.",
|
|
717
|
+
"category": "deployment",
|
|
718
|
+
"source": "seed:stack",
|
|
719
|
+
"tags": ["nextjs", "configuration"],
|
|
720
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000081",
|
|
724
|
+
"title": "getServerSideProps blocks page rendering",
|
|
725
|
+
"content": "getServerSideProps runs on every request and blocks the page from rendering until it completes. A slow database query or API call delays the entire page. For non-critical data, consider client-side fetching with SWR/TanStack Query, or use ISR with revalidation instead.",
|
|
726
|
+
"category": "performance",
|
|
727
|
+
"source": "seed:stack",
|
|
728
|
+
"tags": ["nextjs", "performance"],
|
|
729
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000082",
|
|
733
|
+
"title": "Next.js Image needs width/height or fill",
|
|
734
|
+
"content": "The Next.js Image component requires explicit width and height props for layout calculation, or use fill prop for responsive images. Without them, you get a build error. For dynamic images, use fill with a sized parent container: <div style={{position:'relative',width:300,height:200}}><Image fill .../></div>.",
|
|
735
|
+
"category": "architecture",
|
|
736
|
+
"source": "seed:stack",
|
|
737
|
+
"tags": ["nextjs", "images"],
|
|
738
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000083",
|
|
742
|
+
"title": "Next.js API routes lack CORS by default",
|
|
743
|
+
"content": "Next.js API routes don't include CORS headers. Cross-origin requests from other domains fail silently. Add CORS headers manually or use the next-cors package. For App Router route handlers, set headers in the Response: return new Response(body, { headers: { 'Access-Control-Allow-Origin': '*' } }).",
|
|
744
|
+
"category": "api",
|
|
745
|
+
"source": "seed:stack",
|
|
746
|
+
"tags": ["nextjs", "cors"],
|
|
747
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000084",
|
|
751
|
+
"title": "generateStaticParams for dynamic ISR",
|
|
752
|
+
"content": "In Next.js App Router, dynamic routes need generateStaticParams() to tell Next.js which paths to pre-render at build time. Without it, dynamic pages default to on-demand rendering. Return an array of param objects: return posts.map(post => ({ slug: post.slug })).",
|
|
753
|
+
"category": "deployment",
|
|
754
|
+
"source": "seed:stack",
|
|
755
|
+
"tags": ["nextjs", "isr"],
|
|
756
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000085",
|
|
760
|
+
"title": "'use client' needed for interactive components",
|
|
761
|
+
"content": "In Next.js App Router, all components are Server Components by default. onClick, onChange, useState, and all interactivity require 'use client' at the top of the file. The boundary is the file level—you can't mix server and client code in one file. Keep client components small and leaf-level.",
|
|
762
|
+
"category": "architecture",
|
|
763
|
+
"source": "seed:stack",
|
|
764
|
+
"tags": ["nextjs", "server-components"],
|
|
765
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000086",
|
|
769
|
+
"title": "cookies() makes Next.js route dynamic",
|
|
770
|
+
"content": "Calling cookies() or headers() in a Server Component or route handler opts the entire route out of static generation. It becomes dynamically rendered on every request. If you only need cookies for auth, consider middleware instead to keep the page statically cacheable.",
|
|
771
|
+
"category": "performance",
|
|
772
|
+
"source": "seed:stack",
|
|
773
|
+
"tags": ["nextjs", "caching"],
|
|
774
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000087",
|
|
778
|
+
"title": "revalidatePath doesn't work cross-layout",
|
|
779
|
+
"content": "revalidatePath() only revalidates the specific path segment and its child segments. It doesn't revalidate parent layouts or sibling routes that might show the same data. Use revalidateTag() with fetch cache tags for broader invalidation across layouts and pages.",
|
|
780
|
+
"category": "architecture",
|
|
781
|
+
"source": "seed:stack",
|
|
782
|
+
"tags": ["nextjs", "caching"],
|
|
783
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000088",
|
|
787
|
+
"title": "fetch defaults to force-cache in RSC",
|
|
788
|
+
"content": "In Next.js Server Components, fetch() defaults to { cache: 'force-cache' }—results are cached indefinitely. Your page shows stale data until revalidated. Add { cache: 'no-store' } for always-fresh data, or { next: { revalidate: 60 } } for time-based revalidation.",
|
|
789
|
+
"category": "architecture",
|
|
790
|
+
"source": "seed:stack",
|
|
791
|
+
"tags": ["nextjs", "caching"],
|
|
792
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000089",
|
|
796
|
+
"title": "loading.tsx only works with Suspense",
|
|
797
|
+
"content": "Next.js loading.tsx creates an automatic Suspense boundary for its route segment. But it only shows during the initial load of Server Components. For client-side navigation, it may flash briefly or not show at all. For granular loading states, use manual <Suspense> boundaries within your page.",
|
|
798
|
+
"category": "architecture",
|
|
799
|
+
"source": "seed:stack",
|
|
800
|
+
"tags": ["nextjs", "loading"],
|
|
801
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000090",
|
|
805
|
+
"title": "Parallel routes need default.tsx",
|
|
806
|
+
"content": "Next.js parallel routes (using @folder convention) require a default.tsx file as a fallback when the slot doesn't have a matching route. Without default.tsx, navigating to a route where one parallel slot doesn't match renders a 404 for the entire page.",
|
|
807
|
+
"category": "architecture",
|
|
808
|
+
"source": "seed:stack",
|
|
809
|
+
"tags": ["nextjs", "routing"],
|
|
810
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000091",
|
|
814
|
+
"title": "Stripe webhook needs raw body",
|
|
815
|
+
"content": "Stripe webhook signature verification requires the raw request body, not parsed JSON. If express.json() parses it first, verification fails with 'No signatures found matching the expected signature'. Use express.raw({type:'application/json'}) on the webhook route before any JSON parser.",
|
|
816
|
+
"category": "payments",
|
|
817
|
+
"source": "seed:stack",
|
|
818
|
+
"tags": ["stripe", "webhooks"],
|
|
819
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000092",
|
|
823
|
+
"title": "Stripe idempotency key prevents duplicates",
|
|
824
|
+
"content": "Without an idempotency key, retrying a failed Stripe API call can create duplicate charges. Pass idempotencyKey to every mutation: stripe.charges.create({...}, {idempotencyKey: orderId}). Stripe remembers the response for 24 hours and returns the cached result on retry.",
|
|
825
|
+
"category": "payments",
|
|
826
|
+
"source": "seed:stack",
|
|
827
|
+
"tags": ["stripe", "reliability"],
|
|
828
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000093",
|
|
832
|
+
"title": "Stripe test vs live webhook differences",
|
|
833
|
+
"content": "Stripe test mode webhooks have subtle differences from live: test events include 'livemode: false', some event types behave differently, and webhook endpoints are separate. Always use Stripe CLI (stripe listen --forward-to) for local development and test the exact webhook handler that runs in production.",
|
|
834
|
+
"category": "payments",
|
|
835
|
+
"source": "seed:stack",
|
|
836
|
+
"tags": ["stripe", "testing"],
|
|
837
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000094",
|
|
841
|
+
"title": "Subscription needs default payment method",
|
|
842
|
+
"content": "Creating a Stripe subscription without a default payment method on the customer or subscription fails silently—the first invoice stays in 'draft' or 'open' state. Attach a payment method first: stripe.paymentMethods.attach(pm, {customer}) then set it as default on the customer or subscription.",
|
|
843
|
+
"category": "payments",
|
|
844
|
+
"source": "seed:stack",
|
|
845
|
+
"tags": ["stripe", "subscriptions"],
|
|
846
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000095",
|
|
850
|
+
"title": "Stripe metadata values must be strings",
|
|
851
|
+
"content": "Stripe metadata only accepts string key-value pairs. Passing numbers, booleans, or objects silently converts or drops them. Always stringify: metadata: { orderId: String(orderId), isPremium: 'true' }. Metadata is limited to 50 keys with 500-char values—use it for IDs, not large data.",
|
|
852
|
+
"category": "payments",
|
|
853
|
+
"source": "seed:stack",
|
|
854
|
+
"tags": ["stripe", "api"],
|
|
855
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000096",
|
|
859
|
+
"title": "Webhook signature verification is time-sensitive",
|
|
860
|
+
"content": "Stripe's webhook signature includes a timestamp, and verification rejects events older than 300 seconds by default. If your server clock is skewed or webhook processing is delayed, verification fails. Ensure NTP is synced, or adjust tolerance: stripe.webhooks.constructEvent(body, sig, secret, 600).",
|
|
861
|
+
"category": "payments",
|
|
862
|
+
"source": "seed:stack",
|
|
863
|
+
"tags": ["stripe", "security"],
|
|
864
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000097",
|
|
868
|
+
"title": "Stripe checkout session expires in 24h",
|
|
869
|
+
"content": "A Stripe Checkout session expires 24 hours after creation. If a user abandons checkout and returns later, the session is invalid. Create a new session on each checkout attempt, and use the expires_at parameter to set shorter expirations. Store the session ID to handle the expired_checkout_session webhook.",
|
|
870
|
+
"category": "payments",
|
|
871
|
+
"source": "seed:stack",
|
|
872
|
+
"tags": ["stripe", "checkout"],
|
|
873
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000098",
|
|
877
|
+
"title": "Stripe Price is immutable",
|
|
878
|
+
"content": "Stripe Price objects cannot be updated once created—they're immutable by design. To change a price, create a new Price and archive the old one: stripe.prices.update(oldId, {active: false}). Update subscriptions to the new price: stripe.subscriptions.update(sub, {items: [{id, price: newPriceId}]}).",
|
|
879
|
+
"category": "payments",
|
|
880
|
+
"source": "seed:stack",
|
|
881
|
+
"tags": ["stripe", "pricing"],
|
|
882
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000099",
|
|
886
|
+
"title": "Stripe Customer Portal needs config",
|
|
887
|
+
"content": "The Stripe Customer Portal doesn't work out of the box. You must configure it in the Stripe Dashboard (allowed features, cancellation reasons, proration behavior) before creating portal sessions. Without configuration, portal.sessions.create() returns an error about missing portal configuration.",
|
|
888
|
+
"category": "payments",
|
|
889
|
+
"source": "seed:stack",
|
|
890
|
+
"tags": ["stripe", "billing"],
|
|
891
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000100",
|
|
895
|
+
"title": "Stripe retries webhooks for 72 hours",
|
|
896
|
+
"content": "Stripe retries failed webhook deliveries (non-2xx response) for up to 72 hours with exponential backoff. If your handler isn't idempotent, duplicate processing occurs on retries. Always check if you've already processed the event: store event IDs and skip duplicates.",
|
|
897
|
+
"category": "payments",
|
|
898
|
+
"source": "seed:stack",
|
|
899
|
+
"tags": ["stripe", "webhooks"],
|
|
900
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000101",
|
|
904
|
+
"title": "Stripe test clock for subscription testing",
|
|
905
|
+
"content": "Testing subscription lifecycle (trials, renewals, cancellations) in Stripe requires test clocks. Without them, you'd wait real-time for billing cycles. Create a test clock: stripe.testHelpers.testClocks.create({frozen_time}), attach a customer, then advance time to simulate billing events.",
|
|
906
|
+
"category": "testing",
|
|
907
|
+
"source": "seed:stack",
|
|
908
|
+
"tags": ["stripe", "testing"],
|
|
909
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000102",
|
|
913
|
+
"title": "Stripe invoice finalization triggers payment",
|
|
914
|
+
"content": "Stripe invoices must be finalized before payment is attempted. Auto-finalization happens on the due date, but manually created invoices stay in 'draft'. Call stripe.invoices.finalizeInvoice(id) then stripe.invoices.pay(id). Sending an unfinalised invoice link results in a 'not ready' error.",
|
|
915
|
+
"category": "payments",
|
|
916
|
+
"source": "seed:stack",
|
|
917
|
+
"tags": ["stripe", "invoices"],
|
|
918
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000103",
|
|
922
|
+
"title": "Stripe refund processing time",
|
|
923
|
+
"content": "Stripe processes refunds immediately on their end, but the customer's bank takes 5-10 business days to show the credit. Customers often contact support thinking the refund failed. Display the expected timeline in your UI and store the refund ID for tracking: stripe.refunds.create({payment_intent}).",
|
|
924
|
+
"category": "payments",
|
|
925
|
+
"source": "seed:stack",
|
|
926
|
+
"tags": ["stripe", "refunds"],
|
|
927
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000104",
|
|
931
|
+
"title": "Connect accounts need separate webhooks",
|
|
932
|
+
"content": "Stripe Connect account events are delivered to a different webhook endpoint than your platform events. You need to register a separate webhook URL for Connect events and handle them with stripe.webhooks.constructEvent() using the Connect webhook secret, not your platform secret.",
|
|
933
|
+
"category": "payments",
|
|
934
|
+
"source": "seed:stack",
|
|
935
|
+
"tags": ["stripe", "connect"],
|
|
936
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000105",
|
|
940
|
+
"title": "Check PaymentIntent status before fulfillment",
|
|
941
|
+
"content": "Never fulfill an order based solely on the checkout.session.completed webhook. Always verify the PaymentIntent status is 'succeeded' before delivering goods: const pi = await stripe.paymentIntents.retrieve(session.payment_intent). A completed session can have a pending or failed payment.",
|
|
942
|
+
"category": "payments",
|
|
943
|
+
"source": "seed:stack",
|
|
944
|
+
"tags": ["stripe", "payments"],
|
|
945
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000106",
|
|
949
|
+
"title": "httpOnly cookie not accessible from JS",
|
|
950
|
+
"content": "Cookies set with httpOnly: true cannot be read by document.cookie or JavaScript—that's the point, it prevents XSS from stealing tokens. But this means you can't check if a user is logged in client-side. Use a separate non-httpOnly indicator cookie or a /me API endpoint to check auth status.",
|
|
951
|
+
"category": "auth",
|
|
952
|
+
"source": "seed:stack",
|
|
953
|
+
"tags": ["jwt", "cookies"],
|
|
954
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000107",
|
|
958
|
+
"title": "Refresh token rotation must invalidate old",
|
|
959
|
+
"content": "When issuing a new refresh token, you must invalidate the old one. Without rotation, a stolen refresh token grants permanent access. Store refresh tokens in the database and invalidate on use: if the old token is used again, assume theft and revoke all tokens for that user.",
|
|
960
|
+
"category": "auth",
|
|
961
|
+
"source": "seed:stack",
|
|
962
|
+
"tags": ["jwt", "security"],
|
|
963
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000108",
|
|
967
|
+
"title": "JWT exp is seconds not milliseconds",
|
|
968
|
+
"content": "JWT exp claim uses Unix timestamp in seconds, but JavaScript Date.now() returns milliseconds. Using Date.now() + 3600 sets expiry to 3.6 seconds, not 1 hour. Use Math.floor(Date.now() / 1000) + 3600 for 1 hour. The jsonwebtoken library handles this if you pass expiresIn: '1h'.",
|
|
969
|
+
"category": "auth",
|
|
970
|
+
"source": "seed:stack",
|
|
971
|
+
"tags": ["jwt", "time"],
|
|
972
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000109",
|
|
976
|
+
"title": "bcrypt compare is async—must await",
|
|
977
|
+
"content": "bcrypt.compare() returns a Promise, not a boolean. Writing if (bcrypt.compare(password, hash)) always evaluates to true because a Promise is truthy. You must await it: const match = await bcrypt.compare(password, hash). This bug lets anyone log in with any password.",
|
|
978
|
+
"category": "auth",
|
|
979
|
+
"source": "seed:stack",
|
|
980
|
+
"tags": ["auth", "bcrypt"],
|
|
981
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000110",
|
|
985
|
+
"title": "Password reset must revoke all sessions",
|
|
986
|
+
"content": "After a password reset, all existing sessions and refresh tokens should be invalidated. Otherwise, an attacker who stole a session continues to have access even after the password change. Increment a token version in the database and check it on every authenticated request.",
|
|
987
|
+
"category": "auth",
|
|
988
|
+
"source": "seed:stack",
|
|
989
|
+
"tags": ["auth", "sessions"],
|
|
990
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000111",
|
|
994
|
+
"title": "JWT payload shouldn't contain secrets",
|
|
995
|
+
"content": "JWT payloads are base64-encoded, not encrypted. Anyone can decode them: atob(token.split('.')[1]). Never include passwords, API keys, credit card numbers, or PII. Only include user ID, role, and organizational context. Keep tokens minimal—sensitive data belongs in your database.",
|
|
996
|
+
"category": "auth",
|
|
997
|
+
"source": "seed:stack",
|
|
998
|
+
"tags": ["jwt", "security"],
|
|
999
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000112",
|
|
1003
|
+
"title": "Clock skew causes JWT premature expiry",
|
|
1004
|
+
"content": "If the server issuing JWTs has a clock ahead of the server verifying them, tokens appear expired immediately. Even a few seconds of drift causes intermittent auth failures. Use NTP to sync clocks, and add clockTolerance to your verification: jwt.verify(token, secret, { clockTolerance: 30 }).",
|
|
1005
|
+
"category": "auth",
|
|
1006
|
+
"source": "seed:stack",
|
|
1007
|
+
"tags": ["jwt", "infrastructure"],
|
|
1008
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000113",
|
|
1012
|
+
"title": "JWT none algorithm attack",
|
|
1013
|
+
"content": "Some JWT libraries accept alg:'none' which means no signature verification. An attacker can craft a token with any payload and alg:none that passes verification. Always specify the algorithm explicitly: jwt.verify(token, secret, { algorithms: ['HS256'] }). Never allow 'none'.",
|
|
1014
|
+
"category": "security",
|
|
1015
|
+
"source": "seed:stack",
|
|
1016
|
+
"tags": ["jwt", "security"],
|
|
1017
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000114",
|
|
1021
|
+
"title": "Refresh token needs DB for revocation",
|
|
1022
|
+
"content": "JWTs are stateless—once issued, they can't be revoked until they expire. Refresh tokens must be stored in the database so you can delete them on logout, password change, or security events. Without DB storage, a stolen refresh token is valid for its entire lifetime.",
|
|
1023
|
+
"category": "auth",
|
|
1024
|
+
"source": "seed:stack",
|
|
1025
|
+
"tags": ["jwt", "database"],
|
|
1026
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000115",
|
|
1030
|
+
"title": "CSRF protection needed with cookie auth",
|
|
1031
|
+
"content": "Cookie-based auth is vulnerable to CSRF—a malicious site can make requests that include your auth cookie. Mitigation: use SameSite=Strict or Lax cookies, add a CSRF token to state-changing requests, or verify the Origin/Referer header. Token-based auth in headers is immune to CSRF.",
|
|
1032
|
+
"category": "security",
|
|
1033
|
+
"source": "seed:stack",
|
|
1034
|
+
"tags": ["auth", "csrf"],
|
|
1035
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000116",
|
|
1039
|
+
"title": "JWT token size impacts request overhead",
|
|
1040
|
+
"content": "Every claim added to a JWT increases its size. With many claims, tokens can exceed 4KB—the cookie size limit. Large tokens also add overhead to every HTTP request since they're sent in headers or cookies. Keep access tokens lean (ID, role, org) and fetch details from the database when needed.",
|
|
1041
|
+
"category": "performance",
|
|
1042
|
+
"source": "seed:stack",
|
|
1043
|
+
"tags": ["jwt", "performance"],
|
|
1044
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000117",
|
|
1048
|
+
"title": "SameSite=None requires Secure flag",
|
|
1049
|
+
"content": "Setting SameSite=None on a cookie (needed for cross-site requests) requires the Secure flag—browsers reject SameSite=None cookies without Secure. This means cross-site cookies only work over HTTPS. In local development over HTTP, use SameSite=Lax instead.",
|
|
1050
|
+
"category": "auth",
|
|
1051
|
+
"source": "seed:stack",
|
|
1052
|
+
"tags": ["cookies", "security"],
|
|
1053
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000118",
|
|
1057
|
+
"title": "Short access token with long refresh is wrong",
|
|
1058
|
+
"content": "If your access token is too long-lived, the refresh token mechanism is pointless. The standard pattern: access token expires in 15 minutes, refresh token in 7 days. The short access token limits the window of abuse if stolen, while the refresh token provides a smooth user experience.",
|
|
1059
|
+
"category": "auth",
|
|
1060
|
+
"source": "seed:stack",
|
|
1061
|
+
"tags": ["jwt", "auth"],
|
|
1062
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000119",
|
|
1066
|
+
"title": "Bearer prefix in Authorization header",
|
|
1067
|
+
"content": "The Authorization header format is 'Bearer <token>' with a space after Bearer. Forgetting the prefix or the space causes auth middleware to fail: const token = header.split(' ')[1]. Some APIs are strict about the 'Bearer' prefix being case-sensitive. Always validate the prefix before extracting the token.",
|
|
1068
|
+
"category": "auth",
|
|
1069
|
+
"source": "seed:stack",
|
|
1070
|
+
"tags": ["jwt", "http"],
|
|
1071
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000120",
|
|
1075
|
+
"title": "Password hash timing attack",
|
|
1076
|
+
"content": "String comparison (===) on password hashes is vulnerable to timing attacks—the comparison short-circuits on first mismatch, leaking information about correct prefix bytes. Use constant-time comparison: crypto.timingSafeEqual(). bcrypt.compare() already does this internally, so always use it instead of manual comparison.",
|
|
1077
|
+
"category": "security",
|
|
1078
|
+
"source": "seed:stack",
|
|
1079
|
+
"tags": ["auth", "security"],
|
|
1080
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000121",
|
|
1084
|
+
"title": "Missing index on FK kills JOIN performance",
|
|
1085
|
+
"content": "PostgreSQL doesn't automatically create indexes on foreign key columns. Without an index on orders.customer_id, a JOIN between customers and orders scans the entire orders table. Always create indexes on foreign keys: CREATE INDEX idx_orders_customer_id ON orders(customer_id).",
|
|
1086
|
+
"category": "database",
|
|
1087
|
+
"source": "seed:stack",
|
|
1088
|
+
"tags": ["postgresql", "performance"],
|
|
1089
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000122",
|
|
1093
|
+
"title": "Connection limit is per-pool not per-app",
|
|
1094
|
+
"content": "PostgreSQL's max_connections is the total across ALL clients. If your app pool is 25 connections and you run 4 instances, that's 100 connections. Managed databases like RDS have low defaults (100-200). Use a connection pooler like pgbouncer, and set pool size = max_connections / num_instances.",
|
|
1095
|
+
"category": "database",
|
|
1096
|
+
"source": "seed:stack",
|
|
1097
|
+
"tags": ["postgresql", "configuration"],
|
|
1098
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000123",
|
|
1102
|
+
"title": "SELECT FOR UPDATE can deadlock",
|
|
1103
|
+
"content": "Two transactions doing SELECT FOR UPDATE on rows in different order can deadlock. Transaction A locks row 1, Transaction B locks row 2, then each waits for the other. Always lock rows in a consistent order (e.g., by primary key ASC), or use NOWAIT/SKIP LOCKED to avoid blocking.",
|
|
1104
|
+
"category": "database",
|
|
1105
|
+
"source": "seed:stack",
|
|
1106
|
+
"tags": ["postgresql", "concurrency"],
|
|
1107
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000124",
|
|
1111
|
+
"title": "Always use timestamptz not timestamp",
|
|
1112
|
+
"content": "PostgreSQL's timestamp stores a datetime without timezone—it's ambiguous. timestamptz (timestamp with time zone) converts to UTC on storage and to local time on retrieval. Using timestamp causes bugs when servers or users are in different timezones. Always use timestamptz.",
|
|
1113
|
+
"category": "database",
|
|
1114
|
+
"source": "seed:stack",
|
|
1115
|
+
"tags": ["postgresql", "datetime"],
|
|
1116
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000125",
|
|
1120
|
+
"title": "VACUUM doesn't run on busy tables",
|
|
1121
|
+
"content": "PostgreSQL's autovacuum can fall behind on write-heavy tables, causing table bloat and slow queries. Dead rows accumulate because autovacuum can't keep up. Monitor with pg_stat_user_tables.n_dead_tup. Tune autovacuum_vacuum_scale_factor and autovacuum_vacuum_cost_delay for busy tables.",
|
|
1122
|
+
"category": "database",
|
|
1123
|
+
"source": "seed:stack",
|
|
1124
|
+
"tags": ["postgresql", "maintenance"],
|
|
1125
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000126",
|
|
1129
|
+
"title": "IN clause with empty array syntax error",
|
|
1130
|
+
"content": "WHERE id IN () is invalid SQL—PostgreSQL throws a syntax error. When building dynamic queries with empty arrays, add a guard: WHERE (array_length > 0 AND id = ANY($1)) or short-circuit in application code. Prisma handles this automatically, but raw queries don't.",
|
|
1131
|
+
"category": "database",
|
|
1132
|
+
"source": "seed:stack",
|
|
1133
|
+
"tags": ["postgresql", "queries"],
|
|
1134
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000127",
|
|
1138
|
+
"title": "NULL in unique index allows duplicates",
|
|
1139
|
+
"content": "PostgreSQL treats each NULL as distinct in unique indexes. A unique constraint on (email, deleted_at) allows multiple rows with the same email as long as deleted_at is NULL. Use a partial unique index instead: CREATE UNIQUE INDEX ON users(email) WHERE deleted_at IS NULL.",
|
|
1140
|
+
"category": "database",
|
|
1141
|
+
"source": "seed:stack",
|
|
1142
|
+
"tags": ["postgresql", "constraints"],
|
|
1143
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000128",
|
|
1147
|
+
"title": "EXPLAIN ANALYZE needed, not just EXPLAIN",
|
|
1148
|
+
"content": "EXPLAIN shows the query plan with estimated costs. EXPLAIN ANALYZE actually executes the query and shows real timing. Estimates can be wildly wrong due to stale statistics. Always use EXPLAIN (ANALYZE, BUFFERS) for real performance data. Be careful—ANALYZE runs the query, including writes.",
|
|
1149
|
+
"category": "database",
|
|
1150
|
+
"source": "seed:stack",
|
|
1151
|
+
"tags": ["postgresql", "performance"],
|
|
1152
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000129",
|
|
1156
|
+
"title": "Large IN clauses are slow—use ANY",
|
|
1157
|
+
"content": "WHERE id IN (1, 2, ..., 10000) generates a massive query plan. Use WHERE id = ANY($1::int[]) with a PostgreSQL array parameter instead—it's faster and avoids query plan cache pollution. For very large sets, use a temporary table or CTE joined with your target table.",
|
|
1158
|
+
"category": "performance",
|
|
1159
|
+
"source": "seed:stack",
|
|
1160
|
+
"tags": ["postgresql", "queries"],
|
|
1161
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000130",
|
|
1165
|
+
"title": "Prefer IDENTITY over SERIAL",
|
|
1166
|
+
"content": "PostgreSQL's SERIAL type is a legacy syntax that creates a sequence and default separately. GENERATED ALWAYS AS IDENTITY (SQL standard) is safer—it prevents accidental manual inserts that break the sequence. Use: id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY.",
|
|
1167
|
+
"category": "database",
|
|
1168
|
+
"source": "seed:stack",
|
|
1169
|
+
"tags": ["postgresql", "schema"],
|
|
1170
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000131",
|
|
1174
|
+
"title": "Partial index for soft delete queries",
|
|
1175
|
+
"content": "Queries filtering WHERE deleted_at IS NULL scan the full index including deleted rows. Create a partial index: CREATE INDEX idx_active_users ON users(email) WHERE deleted_at IS NULL. This index is smaller, faster, and only covers active records—perfect for soft-delete patterns.",
|
|
1176
|
+
"category": "performance",
|
|
1177
|
+
"source": "seed:stack",
|
|
1178
|
+
"tags": ["postgresql", "indexing"],
|
|
1179
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000132",
|
|
1183
|
+
"title": "Serverless needs connection pooler",
|
|
1184
|
+
"content": "Serverless functions (Lambda, Vercel) create a new database connection per invocation. With many concurrent requests, you quickly exhaust PostgreSQL's max_connections. Use an external connection pooler like PgBouncer, Supabase's built-in pooler, or Neon's serverless driver.",
|
|
1185
|
+
"category": "deployment",
|
|
1186
|
+
"source": "seed:stack",
|
|
1187
|
+
"tags": ["postgresql", "serverless"],
|
|
1188
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000133",
|
|
1192
|
+
"title": "Full text search needs tsvector index",
|
|
1193
|
+
"content": "PostgreSQL's LIKE '%term%' can't use regular indexes—it does a full table scan. For text search, use tsvector/tsquery with a GIN index: CREATE INDEX idx_search ON articles USING GIN(to_tsvector('english', title || ' ' || body)). It's significantly faster and supports stemming and ranking.",
|
|
1194
|
+
"category": "performance",
|
|
1195
|
+
"source": "seed:stack",
|
|
1196
|
+
"tags": ["postgresql", "search"],
|
|
1197
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000134",
|
|
1201
|
+
"title": "Batch insert faster than individual inserts",
|
|
1202
|
+
"content": "Inserting 1000 rows one at a time takes 1000 round trips. A single INSERT with VALUES for all rows takes one round trip and is 10-100x faster. Use Prisma's createMany() or construct a batch: INSERT INTO t(a,b) VALUES (1,2),(3,4),... For very large imports, use COPY for maximum speed.",
|
|
1203
|
+
"category": "performance",
|
|
1204
|
+
"source": "seed:stack",
|
|
1205
|
+
"tags": ["postgresql", "performance"],
|
|
1206
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000135",
|
|
1210
|
+
"title": "pg_stat_statements for slow query detection",
|
|
1211
|
+
"content": "pg_stat_statements is a PostgreSQL extension that tracks query execution statistics: call count, total/mean time, rows returned. Enable it in postgresql.conf and query pg_stat_statements to find your slowest queries. Essential for production performance monitoring—sort by total_time to find optimization targets.",
|
|
1212
|
+
"category": "performance",
|
|
1213
|
+
"source": "seed:stack",
|
|
1214
|
+
"tags": ["postgresql", "monitoring"],
|
|
1215
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000136",
|
|
1219
|
+
"title": "KEYS command blocks Redis in production",
|
|
1220
|
+
"content": "Redis KEYS pattern scans the entire keyspace and blocks all other operations while running. With millions of keys, it can block Redis for seconds. Use SCAN instead—it returns results in batches with a cursor and doesn't block: SCAN 0 MATCH 'user:*' COUNT 100.",
|
|
1221
|
+
"category": "performance",
|
|
1222
|
+
"source": "seed:stack",
|
|
1223
|
+
"tags": ["redis", "performance"],
|
|
1224
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000137",
|
|
1228
|
+
"title": "Redis TTL -1 means no expiry set",
|
|
1229
|
+
"content": "The Redis TTL command returns -1 when a key exists but has no expiry, and -2 when the key doesn't exist. Don't confuse them. Keys without TTL persist forever and can fill memory. Always set TTL on cache keys: SET key value EX 3600. Use PERSIST to remove an expiry if needed.",
|
|
1230
|
+
"category": "database",
|
|
1231
|
+
"source": "seed:stack",
|
|
1232
|
+
"tags": ["redis", "configuration"],
|
|
1233
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000138",
|
|
1237
|
+
"title": "Pub/sub messages lost without subscribers",
|
|
1238
|
+
"content": "Redis Pub/Sub is fire-and-forget. If no subscriber is listening when a message is published, the message is lost permanently. It's not a queue—there's no replay. For reliable messaging, use Redis Streams (XADD/XREAD) or a proper message queue like BullMQ which persists jobs.",
|
|
1239
|
+
"category": "architecture",
|
|
1240
|
+
"source": "seed:stack",
|
|
1241
|
+
"tags": ["redis", "messaging"],
|
|
1242
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000139",
|
|
1246
|
+
"title": "Redis connection pool exhaustion",
|
|
1247
|
+
"content": "Opening too many Redis connections crashes your app or gets connections refused. Use a connection pool (ioredis supports this natively) and set maxRetriesPerRequest. In serverless environments, reuse connections across invocations. Monitor with INFO clients to see connected_clients count.",
|
|
1248
|
+
"category": "performance",
|
|
1249
|
+
"source": "seed:stack",
|
|
1250
|
+
"tags": ["redis", "connections"],
|
|
1251
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000140",
|
|
1255
|
+
"title": "Redis key naming convention matters",
|
|
1256
|
+
"content": "Use a consistent key naming convention like 'entity:id:field' (user:42:sessions). This enables SCAN pattern matching for bulk operations and makes debugging easier. Avoid spaces and special characters. Prefix keys with your app name in shared Redis instances to prevent collisions.",
|
|
1257
|
+
"category": "architecture",
|
|
1258
|
+
"source": "seed:stack",
|
|
1259
|
+
"tags": ["redis", "conventions"],
|
|
1260
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000141",
|
|
1264
|
+
"title": "Redis memory full causes write failures",
|
|
1265
|
+
"content": "When Redis hits maxmemory with noeviction policy (the default), all write commands return OOM errors while reads still work. Your cache stops caching and your queue stops queuing. Set an appropriate maxmemory-policy like allkeys-lru to evict old keys, or monitor memory and scale proactively.",
|
|
1266
|
+
"category": "deployment",
|
|
1267
|
+
"source": "seed:stack",
|
|
1268
|
+
"tags": ["redis", "operations"],
|
|
1269
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000142",
|
|
1273
|
+
"title": "Lua scripts are atomic but block Redis",
|
|
1274
|
+
"content": "Redis Lua scripts execute atomically—no other command runs during execution. This is great for consistency but a long-running Lua script blocks ALL Redis operations. Keep scripts short and O(1) or O(log N). Use the EVALSHA command with script caching to reduce network overhead.",
|
|
1275
|
+
"category": "performance",
|
|
1276
|
+
"source": "seed:stack",
|
|
1277
|
+
"tags": ["redis", "scripting"],
|
|
1278
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000143",
|
|
1282
|
+
"title": "Redis pipeline reduces roundtrips",
|
|
1283
|
+
"content": "Sending 100 individual Redis commands takes 100 network roundtrips. Pipelining batches them into a single roundtrip: pipe = redis.pipeline(); pipe.get('a'); pipe.set('b', 1); results = await pipe.exec(). This can improve throughput by 5-10x for bulk operations.",
|
|
1284
|
+
"category": "performance",
|
|
1285
|
+
"source": "seed:stack",
|
|
1286
|
+
"tags": ["redis", "performance"],
|
|
1287
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000144",
|
|
1291
|
+
"title": "WATCH for optimistic locking in Redis",
|
|
1292
|
+
"content": "Redis WATCH implements optimistic locking: WATCH a key, read its value, start MULTI, modify, EXEC. If another client changed the watched key, EXEC returns null and you retry. This is how you implement check-and-set without blocking other clients. Essential for counters and inventory.",
|
|
1293
|
+
"category": "database",
|
|
1294
|
+
"source": "seed:stack",
|
|
1295
|
+
"tags": ["redis", "concurrency"],
|
|
1296
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000145",
|
|
1300
|
+
"title": "maxmemory-policy defaults to noeviction",
|
|
1301
|
+
"content": "Redis's default maxmemory-policy is noeviction—when memory is full, writes fail with OOM errors. For cache use cases, set allkeys-lru (evict least recently used) or volatile-lru (evict only keys with TTL). For queues, noeviction is correct because you don't want to lose jobs.",
|
|
1302
|
+
"category": "deployment",
|
|
1303
|
+
"source": "seed:stack",
|
|
1304
|
+
"tags": ["redis", "configuration"],
|
|
1305
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000146",
|
|
1309
|
+
"title": "Docker COPY order for layer caching",
|
|
1310
|
+
"content": "COPY package*.json ./ then RUN npm ci, THEN COPY . ./ for source code. Docker caches layers from top to bottom—the first changed line invalidates all layers below. Copying package.json first means npm install only reruns when dependencies change, not on every code change. Saves minutes per build.",
|
|
1311
|
+
"category": "deployment",
|
|
1312
|
+
"source": "seed:stack",
|
|
1313
|
+
"tags": ["docker", "optimization"],
|
|
1314
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000147",
|
|
1318
|
+
"title": "Non-root user can't bind port 80",
|
|
1319
|
+
"content": "Running a Docker container as non-root (USER node) prevents binding to ports below 1024. Your app on port 80 gets EACCES. Use a high port like 3000 inside the container and map it externally: docker run -p 80:3000. Or use setcap in the Dockerfile, but non-root is the better security practice.",
|
|
1320
|
+
"category": "deployment",
|
|
1321
|
+
"source": "seed:stack",
|
|
1322
|
+
"tags": ["docker", "security"],
|
|
1323
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000148",
|
|
1327
|
+
"title": "ENTRYPOINT vs CMD in Dockerfile",
|
|
1328
|
+
"content": "ENTRYPOINT defines the executable that always runs. CMD provides default arguments that can be overridden. Use ENTRYPOINT for the main process (node, python) and CMD for default arguments. Docker run args replace CMD but append to ENTRYPOINT. For flexibility, use CMD alone: CMD [\"node\", \"server.js\"].",
|
|
1329
|
+
"category": "deployment",
|
|
1330
|
+
"source": "seed:stack",
|
|
1331
|
+
"tags": ["docker", "configuration"],
|
|
1332
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000149",
|
|
1336
|
+
"title": ".dockerignore must exclude node_modules",
|
|
1337
|
+
"content": "Without a .dockerignore file, COPY . ./ sends node_modules (hundreds of MB) to the Docker daemon and into the image. This bloats the image, slows builds, and can cause platform-specific binary issues. Add node_modules, .git, .env, and dist to .dockerignore.",
|
|
1338
|
+
"category": "deployment",
|
|
1339
|
+
"source": "seed:stack",
|
|
1340
|
+
"tags": ["docker", "optimization"],
|
|
1341
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000150",
|
|
1345
|
+
"title": "Multi-stage build loses devDependencies",
|
|
1346
|
+
"content": "In a multi-stage Docker build, the production stage only copies built artifacts and production dependencies. If your build step needs devDependencies (TypeScript, build tools), install them in the build stage. The final stage runs npm ci --production or copies only node_modules from the build stage.",
|
|
1347
|
+
"category": "deployment",
|
|
1348
|
+
"source": "seed:stack",
|
|
1349
|
+
"tags": ["docker", "build"],
|
|
1350
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000151",
|
|
1354
|
+
"title": "Docker build args unavailable at runtime",
|
|
1355
|
+
"content": "ARG values in Dockerfile are only available during build, not at runtime. If you pass --build-arg API_KEY=xxx, it's available in RUN commands but not when the container runs. Use ENV to set runtime environment variables. For secrets, use Docker secrets or environment variables at run time, not build args.",
|
|
1356
|
+
"category": "deployment",
|
|
1357
|
+
"source": "seed:stack",
|
|
1358
|
+
"tags": ["docker", "configuration"],
|
|
1359
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000152",
|
|
1363
|
+
"title": "Alpine missing glibc breaks npm packages",
|
|
1364
|
+
"content": "Alpine Linux uses musl libc instead of glibc. Some npm packages with native bindings (bcrypt, sharp, prisma engines) crash with 'Error loading shared library'. Use the -slim Debian variant instead: FROM node:20-slim. It's slightly larger but avoids native module headaches.",
|
|
1365
|
+
"category": "deployment",
|
|
1366
|
+
"source": "seed:stack",
|
|
1367
|
+
"tags": ["docker", "nodejs"],
|
|
1368
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000153",
|
|
1372
|
+
"title": "HEALTHCHECK marks unhealthy but doesn't restart",
|
|
1373
|
+
"content": "Docker's HEALTHCHECK instruction marks a container as unhealthy but doesn't restart it. You need an orchestrator (Docker Compose restart: unless-stopped, Kubernetes liveness probe, or Docker Swarm) to take action on unhealthy status. HEALTHCHECK alone is just monitoring, not self-healing.",
|
|
1374
|
+
"category": "deployment",
|
|
1375
|
+
"source": "seed:stack",
|
|
1376
|
+
"tags": ["docker", "health"],
|
|
1377
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000154",
|
|
1381
|
+
"title": "Layer cache invalidates from first change",
|
|
1382
|
+
"content": "Docker builds layers sequentially. If layer 3 of 10 changes, layers 3-10 are all rebuilt even if 4-10 haven't changed. Order Dockerfile instructions from least to most frequently changed: OS setup > system deps > language deps > app config > source code. This maximizes cache hits.",
|
|
1383
|
+
"category": "deployment",
|
|
1384
|
+
"source": "seed:stack",
|
|
1385
|
+
"tags": ["docker", "optimization"],
|
|
1386
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000155",
|
|
1390
|
+
"title": "COPY --from needs stage name or number",
|
|
1391
|
+
"content": "In multi-stage builds, COPY --from=builder copies files from another stage. The stage must be named (FROM node:20 AS builder) or referenced by zero-based index (--from=0). Using an undeclared name silently copies nothing. Always name your stages explicitly for clarity.",
|
|
1392
|
+
"category": "deployment",
|
|
1393
|
+
"source": "seed:stack",
|
|
1394
|
+
"tags": ["docker", "multi-stage"],
|
|
1395
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000156",
|
|
1399
|
+
"title": "GitHub Actions secrets hidden in fork PRs",
|
|
1400
|
+
"content": "For security, GitHub Actions doesn't expose repository secrets to workflows triggered by fork pull requests. Tests that need API keys or database URLs fail silently. Use environment-specific secrets, mock external services in tests, or use pull_request_target (carefully—it runs with write access).",
|
|
1401
|
+
"category": "deployment",
|
|
1402
|
+
"source": "seed:stack",
|
|
1403
|
+
"tags": ["ci-cd", "github"],
|
|
1404
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000157",
|
|
1408
|
+
"title": "CI cache key must include lockfile hash",
|
|
1409
|
+
"content": "Cache keys like 'node-modules-v1' don't invalidate when dependencies change. Use a hash of the lockfile: key: node-modules-${{ hashFiles('package-lock.json') }}. This ensures the cache busts when any dependency changes, preventing stale module issues that cause mysterious CI failures.",
|
|
1410
|
+
"category": "deployment",
|
|
1411
|
+
"source": "seed:stack",
|
|
1412
|
+
"tags": ["ci-cd", "caching"],
|
|
1413
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000158",
|
|
1417
|
+
"title": "CI parallel jobs share nothing",
|
|
1418
|
+
"content": "Jobs in GitHub Actions run in separate VMs. Files created in one job don't exist in another. To share data, use artifacts (actions/upload-artifact + actions/download-artifact) or cache. This catches developers who expect 'build' job output to be available in the 'test' job.",
|
|
1419
|
+
"category": "deployment",
|
|
1420
|
+
"source": "seed:stack",
|
|
1421
|
+
"tags": ["ci-cd", "github"],
|
|
1422
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000159",
|
|
1426
|
+
"title": "npm ci faster and safer than npm install",
|
|
1427
|
+
"content": "npm ci deletes node_modules and installs from package-lock.json exactly. It's faster than npm install because it skips dependency resolution. It also fails if lock file is out of sync with package.json, catching inconsistencies. Always use npm ci in CI pipelines, npm install only for local development.",
|
|
1428
|
+
"category": "deployment",
|
|
1429
|
+
"source": "seed:stack",
|
|
1430
|
+
"tags": ["ci-cd", "npm"],
|
|
1431
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000160",
|
|
1435
|
+
"title": "actions/checkout only fetches 1 commit",
|
|
1436
|
+
"content": "actions/checkout@v4 with default settings performs a shallow clone (depth 1). Git history operations like git log, git diff between branches, or changelog generation fail or return wrong results. Use fetch-depth: 0 for full history, or a specific depth for performance balance.",
|
|
1437
|
+
"category": "deployment",
|
|
1438
|
+
"source": "seed:stack",
|
|
1439
|
+
"tags": ["ci-cd", "github"],
|
|
1440
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000161",
|
|
1444
|
+
"title": "Env vars in GitHub Actions run steps",
|
|
1445
|
+
"content": "In GitHub Actions, environment variables in run steps use $VARIABLE (shell syntax), not ${{ env.VARIABLE }} (expression syntax). The expression syntax works in with: and env: blocks but in run steps, use the shell's native dollar sign. Missing dollar signs result in empty strings, not errors.",
|
|
1446
|
+
"category": "deployment",
|
|
1447
|
+
"source": "seed:stack",
|
|
1448
|
+
"tags": ["ci-cd", "github"],
|
|
1449
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000162",
|
|
1453
|
+
"title": "Matrix strategy fail-fast is true by default",
|
|
1454
|
+
"content": "GitHub Actions matrix strategy has fail-fast: true by default. If one matrix job fails, all other running jobs are cancelled immediately. Set fail-fast: false if you want all combinations to run to completion, which is useful for identifying platform-specific failures across OS/Node versions.",
|
|
1455
|
+
"category": "deployment",
|
|
1456
|
+
"source": "seed:stack",
|
|
1457
|
+
"tags": ["ci-cd", "github"],
|
|
1458
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000163",
|
|
1462
|
+
"title": "GitHub Actions artifacts expire in 90 days",
|
|
1463
|
+
"content": "Build artifacts uploaded with actions/upload-artifact are automatically deleted after 90 days (configurable). If your deployment relies on artifacts from a previous workflow run, they may be gone. For long-term storage, push artifacts to S3, a container registry, or use GitHub Releases.",
|
|
1464
|
+
"category": "deployment",
|
|
1465
|
+
"source": "seed:stack",
|
|
1466
|
+
"tags": ["ci-cd", "github"],
|
|
1467
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000164",
|
|
1471
|
+
"title": "Reusable workflows need workflow_call trigger",
|
|
1472
|
+
"content": "To call a workflow from another workflow, the called workflow must have on: workflow_call trigger—not workflow_dispatch or push. Without it, you get 'error parsing called workflow'. The calling workflow uses: jobs: my-job: uses: ./.github/workflows/reusable.yml.",
|
|
1473
|
+
"category": "deployment",
|
|
1474
|
+
"source": "seed:stack",
|
|
1475
|
+
"tags": ["ci-cd", "github"],
|
|
1476
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000165",
|
|
1480
|
+
"title": "Docker layer cache in GitHub Actions",
|
|
1481
|
+
"content": "GitHub Actions runners don't persist Docker build cache between runs. Each build starts cold, taking minutes. Use actions/cache with docker buildx or the docker/build-push-action with cache-from/cache-to to persist layers. Without this, multi-stage builds are painfully slow in CI.",
|
|
1482
|
+
"category": "deployment",
|
|
1483
|
+
"source": "seed:stack",
|
|
1484
|
+
"tags": ["ci-cd", "docker"],
|
|
1485
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1486
|
+
},
|
|
1487
|
+
{
|
|
1488
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000166",
|
|
1489
|
+
"title": "Strict null checks break existing code",
|
|
1490
|
+
"content": "Enabling strictNullChecks in tsconfig catches real bugs but produces hundreds of errors in existing code. Every value that could be null/undefined must be explicitly checked. Enable it early in a project. For existing code, use the --strict flag incrementally with strict: false and individual strict options.",
|
|
1491
|
+
"category": "general",
|
|
1492
|
+
"source": "seed:stack",
|
|
1493
|
+
"tags": ["typescript", "configuration"],
|
|
1494
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000167",
|
|
1498
|
+
"title": "TypeScript enum is a runtime object",
|
|
1499
|
+
"content": "Unlike most TypeScript types that are erased at compile time, enums generate real JavaScript objects. enum Color { Red, Green } becomes { 0: 'Red', 1: 'Green', Red: 0, Green: 1 }. This increases bundle size. Use const enum for zero-cost or union types: type Color = 'red' | 'green'.",
|
|
1500
|
+
"category": "architecture",
|
|
1501
|
+
"source": "seed:stack",
|
|
1502
|
+
"tags": ["typescript", "enums"],
|
|
1503
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000168",
|
|
1507
|
+
"title": "Optional chaining returns undefined not null",
|
|
1508
|
+
"content": "The ?. operator returns undefined when the chain is broken, even if the missing value is null. user?.address?.city returns undefined if user is null. This matters when you check === null—use == null (loose equality) to catch both null and undefined, or check explicitly.",
|
|
1509
|
+
"category": "general",
|
|
1510
|
+
"source": "seed:stack",
|
|
1511
|
+
"tags": ["typescript", "operators"],
|
|
1512
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1513
|
+
},
|
|
1514
|
+
{
|
|
1515
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000169",
|
|
1516
|
+
"title": "as const for literal types in objects",
|
|
1517
|
+
"content": "TypeScript widens object property types: { status: 'active' } has type { status: string }. Use as const to preserve literal types: { status: 'active' } as const gives { readonly status: 'active' }. Essential for discriminated unions, action types, and any code that depends on exact string values.",
|
|
1518
|
+
"category": "general",
|
|
1519
|
+
"source": "seed:stack",
|
|
1520
|
+
"tags": ["typescript", "types"],
|
|
1521
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000170",
|
|
1525
|
+
"title": "Interface extends vs type intersection",
|
|
1526
|
+
"content": "Interface extends checks for property conflicts and errors on incompatible types. Type intersection (&) silently creates never for conflicting properties. interface A extends B is safer but less flexible. Type intersection works with primitives and unions. Use interfaces for objects you extend, types for unions.",
|
|
1527
|
+
"category": "general",
|
|
1528
|
+
"source": "seed:stack",
|
|
1529
|
+
"tags": ["typescript", "types"],
|
|
1530
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000171",
|
|
1534
|
+
"title": "Module augmentation for Express Request",
|
|
1535
|
+
"content": "To add custom properties to Express's Request type, use declaration merging: declare global { namespace Express { interface Request { user?: User } } }. Put this in a .d.ts file included in tsconfig. Without it, req.user causes a TypeScript error even though it works at runtime.",
|
|
1536
|
+
"category": "general",
|
|
1537
|
+
"source": "seed:stack",
|
|
1538
|
+
"tags": ["typescript", "express"],
|
|
1539
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000172",
|
|
1543
|
+
"title": "Generic default doesn't constrain the type",
|
|
1544
|
+
"content": "A generic default like <T = string> sets the default type when T isn't specified, but doesn't constrain T. Users can still pass <number>. To constrain, use extends: <T extends string = string>. Without extends, the default is just a suggestion, not a requirement.",
|
|
1545
|
+
"category": "general",
|
|
1546
|
+
"source": "seed:stack",
|
|
1547
|
+
"tags": ["typescript", "generics"],
|
|
1548
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000173",
|
|
1552
|
+
"title": "typeof in type vs value position",
|
|
1553
|
+
"content": "typeof behaves differently in type and value positions. In value: typeof x returns 'string', 'number', etc. at runtime. In type: type T = typeof x captures the full TypeScript type at compile time. They look identical but serve completely different purposes. The type-level typeof is much more powerful.",
|
|
1554
|
+
"category": "general",
|
|
1555
|
+
"source": "seed:stack",
|
|
1556
|
+
"tags": ["typescript", "operators"],
|
|
1557
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1558
|
+
},
|
|
1559
|
+
{
|
|
1560
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000174",
|
|
1561
|
+
"title": "Discriminated unions need literal member",
|
|
1562
|
+
"content": "Discriminated unions require a common property with literal types for TypeScript to narrow correctly: type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number }. If kind is just string instead of literal types, switch/if narrowing doesn't work—TypeScript can't distinguish the variants.",
|
|
1563
|
+
"category": "general",
|
|
1564
|
+
"source": "seed:stack",
|
|
1565
|
+
"tags": ["typescript", "types"],
|
|
1566
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000175",
|
|
1570
|
+
"title": "Declaration merging with interfaces",
|
|
1571
|
+
"content": "TypeScript interfaces with the same name in the same scope automatically merge. Declaring interface Window { myVar: string } extends the global Window type. This is powerful for augmentation but can cause accidental merges if you reuse interface names. Types (type aliases) don't merge—they error on duplicates.",
|
|
1572
|
+
"category": "general",
|
|
1573
|
+
"source": "seed:stack",
|
|
1574
|
+
"tags": ["typescript", "interfaces"],
|
|
1575
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000176",
|
|
1579
|
+
"title": "noUncheckedIndexedAccess catches bounds errors",
|
|
1580
|
+
"content": "With noUncheckedIndexedAccess enabled, array access (arr[0]) and object index access (obj[key]) return T | undefined instead of T. This catches out-of-bounds array access at compile time. Without it, accessing arr[100] on a 3-element array is typed as defined, causing runtime crashes.",
|
|
1581
|
+
"category": "general",
|
|
1582
|
+
"source": "seed:stack",
|
|
1583
|
+
"tags": ["typescript", "configuration"],
|
|
1584
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000177",
|
|
1588
|
+
"title": "Excess property checking only on literals",
|
|
1589
|
+
"content": "TypeScript only checks for excess properties on object literals. const x: {a: number} = {a: 1, b: 2} errors, but const obj = {a: 1, b: 2}; const x: {a: number} = obj compiles fine. The extra property b is allowed through a variable. This is intentional but surprising.",
|
|
1590
|
+
"category": "general",
|
|
1591
|
+
"source": "seed:stack",
|
|
1592
|
+
"tags": ["typescript", "types"],
|
|
1593
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000178",
|
|
1597
|
+
"title": "readonly doesn't make deep immutable",
|
|
1598
|
+
"content": "TypeScript's readonly modifier only applies to the immediate level: readonly arr: string[] prevents reassignment but arr.push('x') still works. For deep immutability, use Readonly<{items: readonly string[]}> or libraries like immer. readonly is a compile-time hint, not runtime enforcement.",
|
|
1599
|
+
"category": "general",
|
|
1600
|
+
"source": "seed:stack",
|
|
1601
|
+
"tags": ["typescript", "immutability"],
|
|
1602
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000179",
|
|
1606
|
+
"title": "satisfies preserves literal types",
|
|
1607
|
+
"content": "The satisfies operator validates a value matches a type without widening it. const config = { port: 3000 } satisfies Config preserves the literal type 3000, while const config: Config = { port: 3000 } widens it to number. Use satisfies for configuration objects where you want type checking and literal inference.",
|
|
1608
|
+
"category": "general",
|
|
1609
|
+
"source": "seed:stack",
|
|
1610
|
+
"tags": ["typescript", "types"],
|
|
1611
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000180",
|
|
1615
|
+
"title": "Mapped types lose method signatures",
|
|
1616
|
+
"content": "TypeScript mapped types like Partial<T> convert method signatures to function properties: { method(): void } becomes { method?: (() => void) | undefined }. This changes the type's structure and can break code that checks for method existence using typeof. Use Pick/Omit for more precise transformations.",
|
|
1617
|
+
"category": "general",
|
|
1618
|
+
"source": "seed:stack",
|
|
1619
|
+
"tags": ["typescript", "types"],
|
|
1620
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000181",
|
|
1624
|
+
"title": "SQL injection via string interpolation",
|
|
1625
|
+
"content": "Building SQL with template literals or concatenation is the #1 web vulnerability. Query(`SELECT * FROM users WHERE id = ${userId}`) lets attackers inject: '1; DROP TABLE users'. Always use parameterized queries: query('SELECT * FROM users WHERE id = $1', [userId]). ORMs do this automatically.",
|
|
1626
|
+
"category": "security",
|
|
1627
|
+
"source": "seed:stack",
|
|
1628
|
+
"tags": ["security", "database"],
|
|
1629
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000182",
|
|
1633
|
+
"title": "XSS through dangerouslySetInnerHTML",
|
|
1634
|
+
"content": "React's dangerouslySetInnerHTML renders raw HTML, enabling XSS if the content comes from user input. An attacker can inject <script>stealCookies()</script>. Sanitize with DOMPurify before rendering: dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(userContent)}}. Better yet, avoid raw HTML entirely.",
|
|
1635
|
+
"category": "security",
|
|
1636
|
+
"source": "seed:stack",
|
|
1637
|
+
"tags": ["security", "react"],
|
|
1638
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000183",
|
|
1642
|
+
"title": "CSRF without SameSite cookies",
|
|
1643
|
+
"content": "Without SameSite cookie attribute, a malicious site can make authenticated requests to your API using the user's cookies. Set SameSite=Lax (default in modern browsers) or Strict for sensitive operations. SameSite=None is needed for cross-site use but requires Secure and CSRF tokens.",
|
|
1644
|
+
"category": "security",
|
|
1645
|
+
"source": "seed:stack",
|
|
1646
|
+
"tags": ["security", "cookies"],
|
|
1647
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000184",
|
|
1651
|
+
"title": "Open redirect via unvalidated return URL",
|
|
1652
|
+
"content": "Login flows often redirect to a returnUrl parameter after auth. Without validation, attackers craft links like /login?returnUrl=https://evil.com that redirect authenticated users to phishing sites. Validate the URL is relative or matches an allowlist of domains before redirecting.",
|
|
1653
|
+
"category": "security",
|
|
1654
|
+
"source": "seed:stack",
|
|
1655
|
+
"tags": ["security", "auth"],
|
|
1656
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000185",
|
|
1660
|
+
"title": "Rate limit login by IP and user",
|
|
1661
|
+
"content": "Rate limiting login only by username lets attackers try one password across thousands of accounts. Rate limiting only by IP lets distributed attacks bypass limits. Apply both: 10 attempts per username per hour AND 100 attempts per IP per hour. Add exponential backoff and CAPTCHA after failures.",
|
|
1662
|
+
"category": "security",
|
|
1663
|
+
"source": "seed:stack",
|
|
1664
|
+
"tags": ["security", "rate-limiting"],
|
|
1665
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000186",
|
|
1669
|
+
"title": "Git secrets persist after deletion",
|
|
1670
|
+
"content": "Deleting a file with secrets and committing doesn't remove it from git history. Anyone with repo access can find it: git log --all --full-history -- .env. Use git-filter-repo or BFG Repo Cleaner to purge history. Immediately rotate any exposed credentials. Use .gitignore from the start.",
|
|
1671
|
+
"category": "security",
|
|
1672
|
+
"source": "seed:stack",
|
|
1673
|
+
"tags": ["security", "git"],
|
|
1674
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000187",
|
|
1678
|
+
"title": "CORS wildcard with credentials fails silently",
|
|
1679
|
+
"content": "Setting Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true is rejected by browsers silently. The request appears to work but cookies aren't sent. You must specify the exact origin. This is the most common reason authentication stops working after adding CORS.",
|
|
1680
|
+
"category": "security",
|
|
1681
|
+
"source": "seed:stack",
|
|
1682
|
+
"tags": ["security", "cors"],
|
|
1683
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1684
|
+
},
|
|
1685
|
+
{
|
|
1686
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000188",
|
|
1687
|
+
"title": "Helmet doesn't set all security headers",
|
|
1688
|
+
"content": "Helmet sets reasonable defaults for X-Content-Type-Options, X-Frame-Options, and others, but doesn't enable CSP, Permissions-Policy, or Cross-Origin-Embedder-Policy by default. Review your headers at securityheaders.com. Enable CSP and configure each header based on your specific needs.",
|
|
1689
|
+
"category": "security",
|
|
1690
|
+
"source": "seed:stack",
|
|
1691
|
+
"tags": ["security", "headers"],
|
|
1692
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000189",
|
|
1696
|
+
"title": "bcrypt truncates passwords at 72 bytes",
|
|
1697
|
+
"content": "bcrypt silently ignores characters after 72 bytes. A 100-character password and the same password truncated to 72 chars produce the same hash. For long passphrases, pre-hash with SHA-256 before bcrypt: bcrypt(sha256(password), salt). Or use argon2id which has no length limit.",
|
|
1698
|
+
"category": "security",
|
|
1699
|
+
"source": "seed:stack",
|
|
1700
|
+
"tags": ["security", "auth"],
|
|
1701
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000190",
|
|
1705
|
+
"title": "JWT none algorithm bypass",
|
|
1706
|
+
"content": "The JWT 'none' algorithm attack lets attackers create tokens without a signature that some libraries accept as valid. Always specify allowed algorithms in verification: jwt.verify(token, secret, { algorithms: ['HS256'] }). Never accept the algorithm from the token's header without validation.",
|
|
1707
|
+
"category": "security",
|
|
1708
|
+
"source": "seed:stack",
|
|
1709
|
+
"tags": ["security", "jwt"],
|
|
1710
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000191",
|
|
1714
|
+
"title": "Path traversal in file uploads",
|
|
1715
|
+
"content": "Accepting user-provided filenames for uploads enables path traversal: ../../etc/passwd overwrites system files. Always generate a random filename server-side: `${uuid()}.${ext}`. Validate file extensions against an allowlist and never use the original filename for storage paths.",
|
|
1716
|
+
"category": "security",
|
|
1717
|
+
"source": "seed:stack",
|
|
1718
|
+
"tags": ["security", "file-upload"],
|
|
1719
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000192",
|
|
1723
|
+
"title": "Timing attack on string comparison",
|
|
1724
|
+
"content": "Comparing secrets with === leaks information through timing: the comparison stops at the first mismatch, so longer correct prefixes take longer. Attackers can brute-force character by character. Use crypto.timingSafeEqual() for comparing tokens, API keys, and signatures.",
|
|
1725
|
+
"category": "security",
|
|
1726
|
+
"source": "seed:stack",
|
|
1727
|
+
"tags": ["security", "crypto"],
|
|
1728
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000193",
|
|
1732
|
+
"title": "SSRF through user-provided URLs",
|
|
1733
|
+
"content": "If your server fetches a URL provided by the user (e.g., webhook URL, avatar URL), attackers can make it request internal services: http://169.254.169.254/metadata for cloud credentials. Validate URLs against an allowlist, block private IP ranges, and use a DNS rebinding-resistant resolver.",
|
|
1734
|
+
"category": "security",
|
|
1735
|
+
"source": "seed:stack",
|
|
1736
|
+
"tags": ["security", "network"],
|
|
1737
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000194",
|
|
1741
|
+
"title": "Clickjacking without X-Frame-Options",
|
|
1742
|
+
"content": "Without X-Frame-Options or CSP frame-ancestors, attackers embed your site in an invisible iframe on their page. Users click what they think is the attacker's page but actually interact with your site. Set X-Frame-Options: DENY or SAMEORIGIN. Helmet sets this by default.",
|
|
1743
|
+
"category": "security",
|
|
1744
|
+
"source": "seed:stack",
|
|
1745
|
+
"tags": ["security", "headers"],
|
|
1746
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000195",
|
|
1750
|
+
"title": "Session fixation after login",
|
|
1751
|
+
"content": "If the session ID doesn't change after login, an attacker can set a known session ID (via URL or cookie), wait for the victim to log in, then use that same session ID. Always regenerate the session after authentication: req.session.regenerate(). This invalidates the pre-authentication session.",
|
|
1752
|
+
"category": "security",
|
|
1753
|
+
"source": "seed:stack",
|
|
1754
|
+
"tags": ["security", "sessions"],
|
|
1755
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1756
|
+
},
|
|
1757
|
+
{
|
|
1758
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000196",
|
|
1759
|
+
"title": "Mass assignment without allowlist",
|
|
1760
|
+
"content": "Passing req.body directly to a database update lets attackers modify fields they shouldn't: { role: 'admin', verified: true }. Always destructure or pick allowed fields: const { name, email } = req.body; await user.update({ name, email }). Never trust the full request body for writes.",
|
|
1761
|
+
"category": "security",
|
|
1762
|
+
"source": "seed:stack",
|
|
1763
|
+
"tags": ["security", "validation"],
|
|
1764
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000197",
|
|
1768
|
+
"title": "Insecure direct object reference (IDOR)",
|
|
1769
|
+
"content": "Accessing /api/orders/42 should verify the user owns order 42. Without authorization checks, any authenticated user can access any order by changing the ID. Always check: where: { id: orderId, organizationId: req.user.orgId }. This is the most common authorization vulnerability.",
|
|
1770
|
+
"category": "security",
|
|
1771
|
+
"source": "seed:stack",
|
|
1772
|
+
"tags": ["security", "authorization"],
|
|
1773
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000198",
|
|
1777
|
+
"title": "Error messages leak stack traces",
|
|
1778
|
+
"content": "Sending error.stack or detailed error messages to clients exposes internal file paths, library versions, and logic. In production, return generic messages: res.status(500).json({error: 'Internal server error'}). Log the full error server-side. Never set NODE_ENV=development in production.",
|
|
1779
|
+
"category": "security",
|
|
1780
|
+
"source": "seed:stack",
|
|
1781
|
+
"tags": ["security", "error-handling"],
|
|
1782
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000199",
|
|
1786
|
+
"title": "Default credentials in dev tools",
|
|
1787
|
+
"content": "Database GUIs (pgAdmin, Prisma Studio), monitoring dashboards (BullMQ Board), and admin panels often ship with no auth or default credentials. In production, these must be behind VPN/auth or disabled entirely. Attackers scan for exposed admin interfaces using known default ports and paths.",
|
|
1788
|
+
"category": "security",
|
|
1789
|
+
"source": "seed:stack",
|
|
1790
|
+
"tags": ["security", "devops"],
|
|
1791
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1792
|
+
},
|
|
1793
|
+
{
|
|
1794
|
+
"id": "a1b2c3d4-1234-4abc-8def-000000000200",
|
|
1795
|
+
"title": "Dependency confusion attack in npm",
|
|
1796
|
+
"content": "If your org uses private packages like @myorg/utils, an attacker can publish a public @myorg/utils with a higher version. npm may install the public malicious package instead. Mitigate with .npmrc registry scoping: @myorg:registry=https://your-private-registry. Always verify package sources.",
|
|
1797
|
+
"category": "security",
|
|
1798
|
+
"source": "seed:stack",
|
|
1799
|
+
"tags": ["security", "npm"],
|
|
1800
|
+
"created_at": "2026-03-25T00:00:00.000Z"
|
|
1801
|
+
}
|
|
1802
|
+
]
|