Validation & Type Architecture
High-Level Strategy: Defense in Depth
The BusFlow monorepo divides validation and type-safety responsibilities across three distinct layers. This approach prevents duplication of effort while ensuring robust runtime and compile-time safety across frontend, AI workers, and the database.
1. PostgreSQL (Data Integrity)
PostgreSQL is the absolute source of truth for structural data integrity and persistence.
- Responsibility: Facts that never change and safeguards for relational integrity.
- Rules Enforced: Data types (
INTEGER,TEXT), nullability (NOT NULL), relationships (Foreign Keys), uniqueness (UNIQUE), and absolute baseline constraints (e.g.,price >= 0). - Why: Protects the database from malicious or erroneous access, even if someone bypasses the application layer. Rule changes here are expensive (require migrations).
2. Hasura & GraphQL (Network Boundary)
Hasura exposes the Postgres database as a GraphQL API, providing basic structural type enforcement.
- Responsibility: Network boundary structural typing and row-level authorization.
- Workflow:
graphql-codegengenerates strict TypeScript types (e.g., mutation inputs) derived 1:1 from the database structure. - Limitations:
graphql-codegentypes are compile-time only. They disappear at runtime and cannot validate dynamic payloads.
3. Valibot (Domain & Runtime Boundary)
Valibot acts as the Single Source of Truth for Domain Types within the isomorphic packages/types workspace.
- Responsibility: Runtime validation, conditional domain constraints, and mutable business logic.
- Rules Enforced: String formatting (emails, regex), conditional cross-field logic (e.g., maximum passengers based on vehicle type), and UI-specific limits.
- Why Valibot:
- AI/LLM Safety: Validates untyped, unpredictable JSON payloads from OpenAI/Nest.js workers before they reach the database.
- Isomorphic UI Validation: Powers real-time frontend form validation in Nuxt using the exact same schema the backend trusts.
- Bundle Size: Highly tree-shakable architecture optimizes performance for public-facing B2C apps.
- Domain Decoupling: Acts as an anti-corruption layer, allowing transformation of database shapes (e.g.,
user_id) to cleaner domain models (userId) at the edge.
Preventing Schema Drift
Because we write Valibot domain schemas manually alongside auto-generated GraphQL structural types, the system requires strict synchronization to prevent silent failures.
- Rule: A Valibot schema cannot exist in isolation. It must be explicitly bound to the database structure.
- Mechanism: We must use TypeScript utilities (such as the
satisfiesoperator or strict type inferences) to link the ValibotInferOutputto thegraphql-codegentypes. If someone adds or modifies a database column via Hasura, the Valibot schema must trigger a compile-time TypeScript error until they update it to match.