Busflow Docs

Internal documentation portal

Skip to content

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-codegen generates strict TypeScript types (e.g., mutation inputs) derived 1:1 from the database structure.
  • Limitations: graphql-codegen types 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 satisfies operator or strict type inferences) to link the Valibot InferOutput to the graphql-codegen types. 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.

Internal documentation — Busflow