Architecture
Design principles, data flow, and critical architectural rules for VibORM's layered system
Design Principles
1. Types From Runtime Validation
VibORM infers TypeScript types from runtime validation schemas - a single source of truth for both compile-time types AND runtime validation.
const user = s.model({
id: s.string().id().ulid(),
name: s.string(),
email: s.string().unique(),
});
// Types are inferred from the schema definition
// No code generation needed
const users = await orm.user.findMany({
where: { name: "Alice" } // ← Fully typed!
});
2. Dual Validation
Every operation is validated twice:
- Compile-time - TypeScript catches type errors
- Runtime - VibORM’s validation engine validates actual data
3. Separation of Concerns
The architecture separates what to query from how to express it:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Schema Layer │────▶│ Client Layer │────▶│ Query Engine │
│ (Definition) │ │ (Operations) │ │ (WHAT) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────┐
│ Database │◀────│ Adapters │
│ (Execute) │ │ (HOW) │
└─────────────────┘ └─────────────┘
Critical Architectural Rules
Rule 1: Query Engine / Adapter Separation
The Golden Rule: Query engine decides WHAT to query. Adapters decide HOW to express it.
This enables VibORM to support PostgreSQL, MySQL, and SQLite with a single codebase. The query engine builds query structure; adapters translate to database-specific syntax.
Rule 2: Immutable State for Type Tracking
Every scalar modifier returns a NEW instance:
s.string() // StringScalar<{type: "string"}>
.nullable() // StringScalar<{type: "string", nullable: true}> ← NEW instance
.default("hello") // StringScalar<{..., default: "hello"}> ← NEW instance
TypeScript tracks the State generic through each transformation. If we mutated the scalar instead, the type system couldn’t track changes.
Rule 3: Lazy Evaluation for Circular References
Relations use thunks () => Model to break circular dependencies:
const user = s.model({
posts: s.toMany(() => post), // Thunk defers evaluation
});
const post = s.model({
authorId: s.string(),
author: s.toOne(() => user).fields("authorId").references("id"),
});
JavaScript can’t reference a variable before it’s declared. Thunks defer resolution until the model is actually used.
Data Flow
Query Execution
This shows the general two-phase path. When planning is empty and final compilation produces one representable statement, the executor uses the direct path and adds no transaction or batch envelope.
Layer Boundaries
Schema Layers (L1-L5)
These layers handle definition and validation - no SQL generation.
| Layer | Purpose |
|---|---|
| L1: Validation | Standard Schema V1 primitives (v.*) and SchemaRegistry |
| L2: Scalars | Scalar type definitions with State generic and base schemas |
| L3: Operation Schemas | Validation schemas for where/create/update |
| L4: Relations | Relationship definitions between models |
| L5: Schema Validation | Definition-time correctness checking |
Execution Layers (L6-L8)
These layers handle query building and execution.
| Layer | Purpose |
|---|---|
| L6: Query Engine | Compiles guard-free planning and one selected final fragment, executes it, and resolves its declared result (WHAT) |
| L7: Adapters | Database-specific SQL syntax (HOW) |
| L8: Drivers | Connection management and execution |
Interface Layer (L9)
The client layer provides the public API and type inference.
Extension and Management Layers (L10-L12)
| Layer | Purpose |
|---|---|
| L10: Cache | Caches read results and coordinates invalidation without owning query execution |
| L11: Instrumentation | Observes operations through tracing, logging, and diagnostics without owning query logic |
| L12: Migrations | Handles schema evolution through snapshots, diffs, and database-specific DDL |
File Structure
src/
├── validation/ # L1: Validation primitives
│ ├── model/ # L3: Operation schemas (where/create/update/args)
│ └── relations/ # L3: Relation operation schemas
├── schema/ # L2, L4, L5: Schema system
│ ├── scalars/ # L2: Scalar types
│ ├── relation/ # L4: Relations
│ └── validation/ # L5: Schema validation
├── query-engine/ # L6: Query engine
├── adapters/ # L7: Database adapters
├── drivers/ # L8: Database drivers
├── client/ # L9: ORM client
├── cache/ # L10: Query cache and invalidation
├── instrumentation/ # L11: Tracing, logging, and diagnostics
└── migrations/ # L12: Schema migrations