wcz-layout 7.6.0 → 7.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wcz-layout might be problematic. Click here for more details.

Files changed (34) hide show
  1. package/dist/{RouterListItemButton-CvfZk2zD.js → RouterListItemButton-DeaQB4ym.js} +1 -1
  2. package/dist/{RouterListItemButton-CvfZk2zD.js.map → RouterListItemButton-DeaQB4ym.js.map} +1 -1
  3. package/dist/components/core/Layout.d.ts +1 -1
  4. package/dist/components/core/navigation/NavigationList.d.ts +4 -4
  5. package/dist/components/core/navigation/NavigationListItem.d.ts +3 -3
  6. package/dist/components.js +2 -2
  7. package/dist/hooks.js +1 -1
  8. package/dist/index.js +617 -621
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/auth/msalClient.d.ts +8 -2
  11. package/dist/middleware.js +11 -11
  12. package/dist/models/Navigation.d.ts +23 -11
  13. package/dist/{queries-DzKY6YXz.js → queries-D-DV5lCw.js} +3 -3
  14. package/dist/{queries-DzKY6YXz.js.map → queries-D-DV5lCw.js.map} +1 -1
  15. package/dist/query.js +2 -2
  16. package/dist/{queryClient-uWNhcABg.js → queryClient-B__OEZ70.js} +1 -1
  17. package/dist/{queryClient-uWNhcABg.js.map → queryClient-B__OEZ70.js.map} +1 -1
  18. package/dist/{msalClient-BLrbVP5z.js → utils-C4oJ0tr5.js} +58 -47
  19. package/dist/utils-C4oJ0tr5.js.map +1 -0
  20. package/dist/utils.js +5 -5
  21. package/package.json +38 -6
  22. package/skills/api-routes/SKILL.md +251 -0
  23. package/skills/auth/SKILL.md +268 -0
  24. package/skills/data-grid/SKILL.md +229 -0
  25. package/skills/database-schema/SKILL.md +182 -0
  26. package/skills/dialogs-notifications/SKILL.md +241 -0
  27. package/skills/forms-validation/SKILL.md +331 -0
  28. package/skills/forms-validation/references/field-components.md +212 -0
  29. package/skills/layout-navigation/SKILL.md +259 -0
  30. package/skills/project-initialization/SKILL.md +181 -0
  31. package/skills/project-structure/SKILL.md +157 -0
  32. package/skills/tanstack-db-collections/SKILL.md +270 -0
  33. package/skills/ui-pages/SKILL.md +278 -0
  34. package/dist/msalClient-BLrbVP5z.js.map +0 -1
@@ -0,0 +1,268 @@
1
+ ---
2
+ name: auth
3
+ description: >
4
+ MSAL/Entra ID authentication and AD-group authorization. Use
5
+ authorizationMiddleware(permissionKey) which includes authentication.
6
+ Configure permissions.ts and scopes.ts for type-safe keys via
7
+ virtual:wcz-layout. Client: getAccessToken(scopeKey). Server:
8
+ getAppToken(scopeKey), getTokenOnBehalfOf(userToken, scopeKey).
9
+ Route guards with requirePermission. Public routes via LayoutOptions.
10
+ serverFnAccessTokenMiddleware in start.ts. Activate when configuring
11
+ authentication, authorization, or token acquisition.
12
+ type: core
13
+ library: wcz-layout
14
+ library_version: "7.6.1"
15
+ sources:
16
+ - "wcz-layout:src/middleware/authMiddleware.ts"
17
+ - "wcz-layout:src/lib/auth/msalClient.ts"
18
+ - "wcz-layout:src/lib/auth/msalServer.ts"
19
+ - "wcz-layout:src/lib/auth/permissions.ts"
20
+ - "wcz-layout:src/lib/auth/scopes.ts"
21
+ ---
22
+
23
+ # Authentication & Authorization
24
+
25
+ ## Setup
26
+
27
+ Two configuration files define the app's security surface. Both are consumed by the `viteWczLayout()` plugin via `virtual:wcz-layout` to generate TypeScript types:
28
+
29
+ ```typescript
30
+ // src/lib/auth/permissions.ts
31
+ export const permissions = {
32
+ admin: ["wcz-developers"],
33
+ all: ["wcz-all-employees", "wscz-all-employees"],
34
+ } as const;
35
+ // keyof typeof permissions → "admin" | "all"
36
+ ```
37
+
38
+ ```typescript
39
+ // src/lib/auth/scopes.ts
40
+ export const scopes = {
41
+ graph: ["User.Read"],
42
+ api: ["api://wistron.com/your-app/access_as_user"],
43
+ } as const;
44
+ // keyof typeof scopes → "graph" | "api"
45
+ ```
46
+
47
+ Permission keys map to AD group names. A user has a permission if their JWT `groups` claim contains at least one group from the array.
48
+
49
+ ## Core Patterns
50
+
51
+ ### API route authorization
52
+
53
+ ```typescript
54
+ import { authorizationMiddleware } from "wcz-layout/middleware";
55
+
56
+ export const Route = createFileRoute("/api/todos")({
57
+ server: {
58
+ middleware: [authorizationMiddleware("all")],
59
+ handlers: createHandlers({
60
+ GET: async ({ context }) => {
61
+ // context.user is typed as User with hasPermission()
62
+ return Response.json(await context.db.select().from(todoTable));
63
+ },
64
+ }),
65
+ },
66
+ });
67
+ ```
68
+
69
+ `authorizationMiddleware(permissionKey)`:
70
+
71
+ 1. Verifies the JWT Bearer token (includes `authenticationMiddleware` internally)
72
+ 2. Builds a `User` object with `hasPermission(key)` method
73
+ 3. Returns 401 if token is invalid, 403 if user lacks the specified permission
74
+ 4. Injects `context.user: User`
75
+
76
+ ### Public (authentication-only) route
77
+
78
+ For routes that need authentication but no specific permission:
79
+
80
+ ```typescript
81
+ import { authenticationMiddleware } from "wcz-layout/middleware";
82
+
83
+ export const Route = createFileRoute("/api/profile")({
84
+ server: {
85
+ middleware: [authenticationMiddleware()],
86
+ handlers: createHandlers({
87
+ GET: async ({ context }) => {
88
+ // context.user — any authenticated user
89
+ return Response.json(context.user);
90
+ },
91
+ }),
92
+ },
93
+ });
94
+ ```
95
+
96
+ For optional authentication (public endpoints that may have user context):
97
+
98
+ ```typescript
99
+ middleware: [authenticationMiddleware({ optional: true })],
100
+ // context.user is User | null
101
+ ```
102
+
103
+ ### Route-level permission guard
104
+
105
+ ```typescript
106
+ import { requirePermission } from "wcz-layout/utils";
107
+
108
+ export const Route = createFileRoute("/admin/")({
109
+ beforeLoad: requirePermission("admin"),
110
+ component: AdminPage,
111
+ });
112
+ ```
113
+
114
+ `requirePermission` runs in `beforeLoad` and redirects unauthorized users.
115
+
116
+ ### Client-side token acquisition
117
+
118
+ ```typescript
119
+ import { getAccessToken } from "wcz-layout/utils";
120
+
121
+ // In an axios interceptor or client-side code
122
+ const token = await getAccessToken("api");
123
+ ```
124
+
125
+ `getAccessToken` is a client-only function (uses MSAL `PublicClientApplication`). It acquires tokens silently and triggers interaction listeners on `InteractionRequiredAuthError`.
126
+
127
+ ### Server-side token acquisition
128
+
129
+ ```typescript
130
+ import { getAppToken, getTokenOnBehalfOf } from "wcz-layout/utils";
131
+
132
+ // App-only token (client credentials flow — no user context)
133
+ const appToken = await getAppToken("graph");
134
+
135
+ // On-behalf-of flow (exchange user token for downstream API token)
136
+ const oboToken = await getTokenOnBehalfOf(userBearerToken, "graph");
137
+ ```
138
+
139
+ Both are server-only functions using MSAL `ConfidentialClientApplication`.
140
+
141
+ ### Global server function middleware
142
+
143
+ ```typescript
144
+ // src/start.ts
145
+ import { serverFnAccessTokenMiddleware } from "wcz-layout/middleware";
146
+
147
+ export const startInstance = createStart(() => ({
148
+ defaultSsr: false,
149
+ functionMiddleware: [serverFnAccessTokenMiddleware("api")],
150
+ }));
151
+ ```
152
+
153
+ `serverFnAccessTokenMiddleware` attaches the Bearer token to all server function calls globally. Without it, server functions won't include authorization headers.
154
+
155
+ ### Getting the current user
156
+
157
+ ```typescript
158
+ import { getUser } from "wcz-layout/utils";
159
+
160
+ // Isomorphic — works on client (returns User | null) and server (returns null)
161
+ const user = getUser();
162
+ ```
163
+
164
+ On the client, `getUser` reads the active MSAL account's ID token claims.
165
+
166
+ ## Common Mistakes
167
+
168
+ ### HIGH Chaining authenticationMiddleware before authorizationMiddleware
169
+
170
+ Wrong:
171
+
172
+ ```typescript
173
+ middleware: [authenticationMiddleware(), authorizationMiddleware("admin")],
174
+ ```
175
+
176
+ Correct:
177
+
178
+ ```typescript
179
+ middleware: [authorizationMiddleware("admin")],
180
+ ```
181
+
182
+ `authorizationMiddleware` already composes `authenticationMiddleware` internally. Chaining both duplicates JWT verification.
183
+
184
+ Source: maintainer interview
185
+
186
+ Cross-skill: See also skills/api-routes/SKILL.md § Common Mistakes
187
+
188
+ ### CRITICAL Calling getAccessToken on the server
189
+
190
+ Wrong:
191
+
192
+ ```typescript
193
+ // In a server handler or serverFn
194
+ const token = await getAccessToken("api");
195
+ ```
196
+
197
+ Correct:
198
+
199
+ ```typescript
200
+ // Server-side — use app token or OBO
201
+ const token = await getAppToken("api");
202
+ // or
203
+ const token = await getTokenOnBehalfOf(userToken, "api");
204
+ ```
205
+
206
+ `getAccessToken` is `createClientOnlyFn` — it uses MSAL `PublicClientApplication` which only exists in the browser. Calling it on the server throws.
207
+
208
+ Source: wcz-layout:src/lib/auth/msalClient.ts
209
+
210
+ ### HIGH Forgetting serverFnAccessTokenMiddleware in start.ts
211
+
212
+ Wrong:
213
+
214
+ ```typescript
215
+ // src/start.ts
216
+ export const startInstance = createStart(() => ({
217
+ defaultSsr: false,
218
+ // Missing functionMiddleware
219
+ }));
220
+ ```
221
+
222
+ Correct:
223
+
224
+ ```typescript
225
+ // src/start.ts
226
+ export const startInstance = createStart(() => ({
227
+ defaultSsr: false,
228
+ functionMiddleware: [serverFnAccessTokenMiddleware("api")],
229
+ }));
230
+ ```
231
+
232
+ Without `serverFnAccessTokenMiddleware`, server function calls won't include the Authorization header, causing 401 errors.
233
+
234
+ Source: wcz-layout:src/start.ts
235
+
236
+ ### MEDIUM Using string literals for permission/scope keys
237
+
238
+ Wrong:
239
+
240
+ ```typescript
241
+ authorizationMiddleware("superadmin"); // not in permissions.ts → compile error
242
+ getAccessToken("custom-api"); // not in scopes.ts → compile error
243
+ ```
244
+
245
+ Correct:
246
+
247
+ ```typescript
248
+ // First add to permissions.ts / scopes.ts, then use the typed key
249
+ authorizationMiddleware("admin");
250
+ getAccessToken("api");
251
+ ```
252
+
253
+ Permission and scope keys are typed from the `as const` exports via `virtual:wcz-layout`. Using arbitrary strings causes TypeScript compile errors.
254
+
255
+ Source: wcz-layout:src/types/wcz-layout.d.ts
256
+
257
+ ### HIGH Missing Entra redirect URI configuration
258
+
259
+ The MSAL `redirectUri` defaults to `"/"`. The Entra app registration must include this exact redirect URI, or the authentication flow fails silently with a redirect error.
260
+
261
+ Source: wcz-layout:src/routes/runbook/entra-id.tsx
262
+
263
+ ---
264
+
265
+ See also:
266
+
267
+ - skills/api-routes/SKILL.md — Every API route uses authorizationMiddleware.
268
+ - skills/layout-navigation/SKILL.md — publicRoutes bypass auth; permission guards need auth context.
@@ -0,0 +1,229 @@
1
+ ---
2
+ name: data-grid
3
+ description: >
4
+ MUI X DataGrid Premium with wcz-layout helpers. ChipInputCell for chip
5
+ rendering in cells (getLabel for complex objects). EditableColumnHeader
6
+ for pencil icon headers. RouterGridActionsCellItem for type-safe row
7
+ action navigation. Column definitions with renderCell/renderHeader.
8
+ Feed rows from useLiveQuery collection data. Activate when building
9
+ data tables or grids.
10
+ type: composition
11
+ library: wcz-layout
12
+ library_version: "7.6.1"
13
+ requires:
14
+ - tanstack-db-collections
15
+ - ui-pages
16
+ sources:
17
+ - "wcz-layout:src/components/data-grid/ChipInputCell.tsx"
18
+ - "wcz-layout:src/components/data-grid/EditableColumnHeader.tsx"
19
+ - "wcz-layout:src/components/router/RouterGridActionsCellItem.tsx"
20
+ ---
21
+
22
+ # Data Grid
23
+
24
+ ## Setup
25
+
26
+ Install `@mui/x-data-grid-premium` (peer dependency). Use `DataGridPremium`, not the community `DataGrid`.
27
+
28
+ ## Core Patterns
29
+
30
+ ### Basic data grid with collection data
31
+
32
+ ```typescript
33
+ import { DataGridPremium, type GridColDef } from "@mui/x-data-grid-premium";
34
+ import { useLiveQuery } from "@tanstack/react-db";
35
+ import { todoCollection } from "~/lib/db/collections/todoCollection";
36
+ import type { Todo } from "~/schemas/todo";
37
+
38
+ const columns: GridColDef<Todo>[] = [
39
+ { field: "name", headerName: "Name", flex: 1 },
40
+ { field: "isCompleted", headerName: "Completed", type: "boolean", width: 120 },
41
+ { field: "createdAt", headerName: "Created", type: "dateTime", width: 180 },
42
+ ];
43
+
44
+ function TodoGrid() {
45
+ const { data: todos } = useLiveQuery((query) =>
46
+ query.from({ todos: todoCollection })
47
+ );
48
+
49
+ return (
50
+ <DataGridPremium
51
+ rows={todos}
52
+ columns={columns}
53
+ getRowId={(row) => row.id}
54
+ autoHeight
55
+ />
56
+ );
57
+ }
58
+ ```
59
+
60
+ ### Row actions with RouterGridActionsCellItem
61
+
62
+ ```typescript
63
+ import { RouterGridActionsCellItem } from "wcz-layout/components";
64
+ import EditIcon from "@mui/icons-material/Edit";
65
+ import DeleteIcon from "@mui/icons-material/Delete";
66
+ import type { GridColDef } from "@mui/x-data-grid-premium";
67
+
68
+ const columns: GridColDef<Todo>[] = [
69
+ { field: "name", headerName: "Name", flex: 1 },
70
+ {
71
+ field: "actions",
72
+ type: "actions",
73
+ width: 100,
74
+ getActions: (params) => [
75
+ <RouterGridActionsCellItem
76
+ key="edit"
77
+ icon={<EditIcon />}
78
+ label="Edit"
79
+ to="/todos/edit/$id"
80
+ params={{ id: params.row.id }}
81
+ />,
82
+ <RouterGridActionsCellItem
83
+ key="delete"
84
+ icon={<DeleteIcon />}
85
+ label="Delete"
86
+ onClick={() => handleDelete(params.row)}
87
+ showInMenu
88
+ />,
89
+ ],
90
+ },
91
+ ];
92
+ ```
93
+
94
+ `RouterGridActionsCellItem` wraps MUI's `GridActionsCellItem` with TanStack Router's `createLink` for type-safe SPA navigation.
95
+
96
+ ### ChipInputCell for array/tag columns
97
+
98
+ ```typescript
99
+ import { ChipInputCell } from "wcz-layout/components";
100
+ import type { GridColDef } from "@mui/x-data-grid-premium";
101
+
102
+ const columns: GridColDef<Todo>[] = [
103
+ {
104
+ field: "tags",
105
+ headerName: "Tags",
106
+ flex: 1,
107
+ renderCell: (params) => <ChipInputCell params={params} />,
108
+ },
109
+ ];
110
+ ```
111
+
112
+ For complex objects (not plain strings), provide `getLabel`:
113
+
114
+ ```typescript
115
+ renderCell: (params) => (
116
+ <ChipInputCell
117
+ params={params}
118
+ getLabel={(item) => item.displayName}
119
+ slotProps={{ size: "small", color: "primary" }}
120
+ />
121
+ ),
122
+ ```
123
+
124
+ `ChipInputCell` renders a horizontal scrollable stack of `Chip` components. It handles both scalar and array values automatically.
125
+
126
+ ### EditableColumnHeader
127
+
128
+ ```typescript
129
+ import { EditableColumnHeader } from "wcz-layout/components";
130
+
131
+ const columns: GridColDef<Todo>[] = [
132
+ {
133
+ field: "name",
134
+ headerName: "Name",
135
+ flex: 1,
136
+ editable: true,
137
+ renderHeader: (params) => <EditableColumnHeader {...params} />,
138
+ },
139
+ ];
140
+ ```
141
+
142
+ Renders the column header text with a trailing pencil (Edit) icon to visually indicate editable columns. Purely visual — no interaction.
143
+
144
+ ## Common Mistakes
145
+
146
+ ### HIGH Using DataGrid instead of DataGridPremium
147
+
148
+ Wrong:
149
+
150
+ ```typescript
151
+ import { DataGrid } from "@mui/x-data-grid";
152
+ ```
153
+
154
+ Correct:
155
+
156
+ ```typescript
157
+ import { DataGridPremium } from "@mui/x-data-grid-premium";
158
+ ```
159
+
160
+ The library peer-depends on `@mui/x-data-grid-premium`. Using the community `DataGrid` loses Premium features like column grouping, row grouping, tree data, and aggregation.
161
+
162
+ Source: package.json peerDependencies
163
+
164
+ ### HIGH Using GridActionsCellItem instead of RouterGridActionsCellItem
165
+
166
+ Wrong:
167
+
168
+ ```typescript
169
+ import { GridActionsCellItem } from "@mui/x-data-grid-premium";
170
+
171
+ <GridActionsCellItem
172
+ icon={<EditIcon />}
173
+ label="Edit"
174
+ onClick={() => navigate({ to: `/todos/${id}` })}
175
+ />
176
+ ```
177
+
178
+ Correct:
179
+
180
+ ```typescript
181
+ import { RouterGridActionsCellItem } from "wcz-layout/components";
182
+
183
+ <RouterGridActionsCellItem
184
+ icon={<EditIcon />}
185
+ label="Edit"
186
+ to="/todos/edit/$id"
187
+ params={{ id }}
188
+ />
189
+ ```
190
+
191
+ Plain `GridActionsCellItem` doesn't support TanStack Router navigation. `RouterGridActionsCellItem` wraps it with `createLink` for type-safe routing without manual `onClick` + `navigate`.
192
+
193
+ Source: wcz-layout:src/components/router/RouterGridActionsCellItem.tsx
194
+
195
+ ### MEDIUM Not passing getLabel to ChipInputCell for complex objects
196
+
197
+ Wrong:
198
+
199
+ ```typescript
200
+ // params.value is Array<{ id: string; name: string }>
201
+ <ChipInputCell params={params} />
202
+ // Renders [object Object] chips
203
+ ```
204
+
205
+ Correct:
206
+
207
+ ```typescript
208
+ <ChipInputCell
209
+ params={params}
210
+ getLabel={(item) => item.name}
211
+ />
212
+ ```
213
+
214
+ `ChipInputCell` defaults `getLabel` to identity (value as-is). For objects, you must provide `getLabel` to extract the display string.
215
+
216
+ Source: wcz-layout:src/components/data-grid/ChipInputCell.tsx
217
+
218
+ ### HIGH Tension: MUI component API vs. TanStack Router types
219
+
220
+ `RouterGridActionsCellItem` merges MUI grid action props with TanStack Router `LinkProps`. Use `to` + `params` for internal navigation. Using `onClick` + `navigate` bypasses the type-safe link and router prefetching.
221
+
222
+ See also: skills/ui-pages/SKILL.md § Common Mistakes
223
+
224
+ ---
225
+
226
+ See also:
227
+
228
+ - skills/tanstack-db-collections/SKILL.md — Grid rows come from useLiveQuery results.
229
+ - skills/ui-pages/SKILL.md — Grid pages follow the same routing patterns.
@@ -0,0 +1,182 @@
1
+ ---
2
+ name: database-schema
3
+ description: >
4
+ Design PostgreSQL tables with Drizzle ORM pgTable and derive Zod validation
5
+ schemas via createSelectSchema from drizzle-orm/zod. Covers uuid primary keys,
6
+ column types, Zod refinements (trim, min, max), and type inference. Drizzle
7
+ tables live in src/lib/db/schemas/, Zod schemas in src/schemas/. Activate
8
+ when creating or modifying database tables or validation schemas.
9
+ type: core
10
+ library: wcz-layout
11
+ library_version: "7.6.1"
12
+ sources:
13
+ - "wcz-layout:src/lib/db/schemas/"
14
+ - "wcz-layout:src/schemas/"
15
+ - "drizzle-orm:docs/zod.md"
16
+ ---
17
+
18
+ # Database Schema Design
19
+
20
+ ## Setup
21
+
22
+ Database schema files live in two directories:
23
+
24
+ ```
25
+ src/lib/db/schemas/todo.ts # Drizzle pgTable definition
26
+ src/schemas/todo.ts # Zod schema derived from the table
27
+ ```
28
+
29
+ ## Core Patterns
30
+
31
+ ### Drizzle table definition
32
+
33
+ ```typescript
34
+ // src/lib/db/schemas/todo.ts
35
+ import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
36
+
37
+ export const todoTable = pgTable("todos", {
38
+ id: uuid().primaryKey(),
39
+ name: text().notNull(),
40
+ description: text(),
41
+ isCompleted: boolean().notNull().default(false),
42
+ createdBy: text().notNull(),
43
+ createdAt: timestamp().notNull().defaultNow(),
44
+ updatedAt: timestamp().notNull().defaultNow(),
45
+ });
46
+ ```
47
+
48
+ All tables use `uuid().primaryKey()`. IDs are generated client-side with `uuidv7()`.
49
+
50
+ ### Deriving Zod schema from Drizzle
51
+
52
+ ```typescript
53
+ // src/schemas/todo.ts
54
+ import { createSelectSchema } from "drizzle-orm/zod";
55
+ import { todoTable } from "~/lib/db/schemas/todo";
56
+
57
+ export const TodoSchema = createSelectSchema(todoTable, {
58
+ name: (schema) => schema.trim().min(1),
59
+ description: (schema) => schema.trim().nullish(),
60
+ });
61
+
62
+ export type Todo = typeof TodoSchema._type;
63
+ ```
64
+
65
+ `createSelectSchema` generates a Zod schema matching the table columns. The second argument lets you refine individual fields — add `.trim()`, `.min()`, `.max()`, or replace the schema entirely.
66
+
67
+ ### Insert schema for mutations
68
+
69
+ ```typescript
70
+ // src/schemas/todo.ts
71
+ import { createInsertSchema } from "drizzle-orm/zod";
72
+
73
+ export const TodoInsertSchema = createInsertSchema(todoTable, {
74
+ name: (schema) => schema.trim().min(1),
75
+ });
76
+ ```
77
+
78
+ Use `createInsertSchema` when you need a schema for insert operations that omits fields with defaults (like `id`, `createdAt`).
79
+
80
+ ### Type inference
81
+
82
+ ```typescript
83
+ import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
84
+ import { todoTable } from "~/lib/db/schemas/todo";
85
+
86
+ type Todo = InferSelectModel<typeof todoTable>;
87
+ type NewTodo = InferInsertModel<typeof todoTable>;
88
+ ```
89
+
90
+ Prefer the Zod `_type` for runtime-validated types. Use Drizzle inference types when you need the raw DB shape without validation.
91
+
92
+ ## Common Mistakes
93
+
94
+ ### CRITICAL Writing Zod schemas manually instead of deriving from Drizzle
95
+
96
+ Wrong:
97
+
98
+ ```typescript
99
+ import { z } from "zod";
100
+
101
+ export const TodoSchema = z.object({
102
+ id: z.string().uuid(),
103
+ name: z.string().trim().min(1),
104
+ isCompleted: z.boolean(),
105
+ });
106
+ ```
107
+
108
+ Correct:
109
+
110
+ ```typescript
111
+ import { createSelectSchema } from "drizzle-orm/zod";
112
+ import { todoTable } from "~/lib/db/schemas/todo";
113
+
114
+ export const TodoSchema = createSelectSchema(todoTable, {
115
+ name: (schema) => schema.trim().min(1),
116
+ });
117
+ ```
118
+
119
+ Manual Zod schemas drift from the database when columns are added or renamed. `createSelectSchema` keeps them in sync automatically.
120
+
121
+ Source: maintainer interview
122
+
123
+ ### HIGH Placing schema files in wrong directory
124
+
125
+ Wrong:
126
+
127
+ ```
128
+ src/lib/db/schemas/todo.ts # Contains both pgTable AND Zod schema
129
+ ```
130
+
131
+ Correct:
132
+
133
+ ```
134
+ src/lib/db/schemas/todo.ts # Only pgTable definition
135
+ src/schemas/todo.ts # Zod schema derived from the table
136
+ ```
137
+
138
+ Drizzle table definitions and Zod validation schemas live in separate directories. Mixing them in one file violates the project structure convention.
139
+
140
+ Source: consumer project structure
141
+
142
+ ### HIGH Forgetting uuid primary key convention
143
+
144
+ Wrong:
145
+
146
+ ```typescript
147
+ import { serial, pgTable, text } from "drizzle-orm/pg-core";
148
+
149
+ export const todoTable = pgTable("todos", {
150
+ id: serial().primaryKey(),
151
+ name: text().notNull(),
152
+ });
153
+ ```
154
+
155
+ Correct:
156
+
157
+ ```typescript
158
+ import { uuid, pgTable, text } from "drizzle-orm/pg-core";
159
+
160
+ export const todoTable = pgTable("todos", {
161
+ id: uuid().primaryKey(),
162
+ name: text().notNull(),
163
+ });
164
+ ```
165
+
166
+ All tables use `uuid().primaryKey()`. IDs are generated client-side with `uuidv7()` for optimistic inserts through TanStack DB collections.
167
+
168
+ Source: consumer project example
169
+
170
+ ### HIGH Tension: Type safety vs. rapid prototyping
171
+
172
+ Deriving Zod schemas from Drizzle tables and wiring them through forms requires more setup than quick `useState` + manual validation. Always use the enforced pattern — `createSelectSchema` + `useLayoutForm` — even for simple forms.
173
+
174
+ See also: skills/forms-validation/SKILL.md § Core Patterns
175
+
176
+ ---
177
+
178
+ See also:
179
+
180
+ - skills/api-routes/SKILL.md — Schemas feed into validationMiddleware(Schema).
181
+ - skills/tanstack-db-collections/SKILL.md — Same Zod schema used as collection schema option.
182
+ - skills/forms-validation/SKILL.md — Zod schemas used as form validators.