work-hub-server/openspec/changes/swap-superstruct-valibot/design.md
Nicolas HOARAU 9314e72f3e feat(schema): add shared schema package with Drizzle ORM and Valibot validation
Create @workspace/schema package with Project and Task table definitions, Drizzle relations, Valibot validation schemas (select/insert/update), and wire as workspace dependency in API and Web apps.
2026-06-04 12:46:51 +04:00

2.4 KiB

Context

The schema package currently has two manually maintained modules (validation/ and types/) that mirror Drizzle table definitions. Drizzle ORM has built-in Valibot support (drizzle-orm/valibot) that can derive insert, update, and select schemas directly from pgTable definitions — eliminating the need for hand-written validators and separate type files.

The constant arrays in drizzle/enums.ts (PROJECT_STATUSES, TASK_STATUSES, PRIORITIES) already define the allowed values. These can feed into Valibot picklist() refinements via the customization callback in createInsertSchema/createUpdateSchema.

Goals / Non-Goals

Goals:

  • Replace Superstruct with Valibot as the validation library
  • Derive insert/update schemas from Drizzle tables using drizzle-orm/valibot
  • Remove the validation/ and types/ source directories
  • Remove superstruct dependency; add valibot
  • Keep DB-level CHECK constraints — Valibot refinements layer on top
  • Ensure createUpdateSchema produces all-optional shapes (replacing manual partial logic)

Non-Goals:

  • No change to drizzle/ module structure or table definitions
  • No change to the public import paths consumers use
  • No migration or database changes

Decisions

  1. Use drizzle-orm/valibot not drizzle-valibot package — the integration is built into drizzle-orm itself (since v1.0.0-beta.15). No separate package needed.

  2. One composer file for all Valibot schemas — a single src/valibot.ts (or inline in drizzle/) containing createInsertSchema and createUpdateSchema calls for each table. No separate validation/ directory.

  3. Customization via the second argument callback — enum-constrained fields use (schema) => pipe(schema, picklist([...ARRAY])) to add the allowed-value constraint on top of Drizzle's auto-generated type info.

  4. Types are inferred, not declaredv.InferOutput<typeof projectInsert> replaces the manual type aliases in types/. Consumers import the Valibot schema and derive types as needed.

Risks / Trade-offs

  • [New dependency] Valibot replaces Superstruct. The API surface is similar but not identical — consumers may need to adjust error handling (Valibot uses ValiError not StructError).
  • [Picklist sync] The picklist() values reference the constant arrays, so they stay in sync automatically. No new drift risk.