Internals Overview
The 10-layer architecture transforming type-safe schema definitions into database queries without code generation
The 10-Layer Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ User Code │
│ s.model({ name: s.string() }) → orm.user.findMany({...}) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ L1: Validation │ v.* primitives, Standard Schema V1 compliance │
├───────────────────────┼─────────────────────────────────────────────────┤
│ L2: Fields │ Field classes with State generic pattern │
├───────────────────────┼─────────────────────────────────────────────────┤
│ L3: Query Schemas │ where, create, update, args schema generation │
├───────────────────────┼─────────────────────────────────────────────────┤
│ L4: Relations │ oneToOne, manyToOne, oneToMany, manyToMany │
├───────────────────────┼─────────────────────────────────────────────────┤
│ L5: Schema Validation│ Definition-time schema correctness checks │
├───────────────────────┼─────────────────────────────────────────────────┤
│ L6: Query Engine │ Database-agnostic query structure building │
├───────────────────────┼─────────────────────────────────────────────────┤
│ L7: Adapters │ Database-specific SQL dialect generation │
├───────────────────────┼─────────────────────────────────────────────────┤
│ L8: Drivers │ Connection management and query execution │
├───────────────────────┼─────────────────────────────────────────────────┤
│ L9: Client │ Type-safe ORM interface via recursive proxies │
├───────────────────────┼─────────────────────────────────────────────────┤
│ L10: Migrations │ Schema diffing and database synchronization │
└───────────────────────┴─────────────────────────────────────────────────┘Layer Responsibility Matrix
| Layer | Location | Owns | Doesn't Own |
|---|---|---|---|
| L1: Validation | src/validation/ | v.* primitives, Standard Schema V1 | Field logic, domain rules |
| L2: Fields | src/schema/fields/ | Field classes, State generics | Query schemas |
| L3: Query Schemas | src/schema/model/schemas/ | where, create, update, args schemas | SQL generation |
| L4: Relations | src/schema/relation/ | Relation types, nested schemas | Query execution |
| L5: Schema Validation | src/schema/validation/ | Definition-time validation | Runtime validation |
| L6: Query Engine | src/query-engine/ | Query structure, logic | Database SQL |
| L7: Adapters | src/adapters/ | Database-specific SQL | Query logic |
| L8: Drivers | src/drivers/ | Connection, execution | Query building |
| L9: Client | src/client/ | Result types, proxies | Query construction |
| L10: Migrations | src/migrations/ | Schema diffing, push | Schema definition |
Why This Architecture?
VibORM was designed to solve three problems that plagued existing ORMs:
1. Code Generation Lock-in
Prisma requires prisma generate after every schema change. We wanted instant type updates on save.
// VibORM: Types update instantly when you change this
const user = s.model({
email: s.string().unique(), // Change here → types update immediately
});2. Multi-Database Support Without Forking
Drizzle has separate packages per database. We wanted one codebase supporting PostgreSQL, MySQL, and SQLite through the adapter pattern.
// Same code, different adapters
const pg = new PostgresAdapter();
const mysql = new MySQLAdapter();
const sqlite = new SQLiteAdapter();3. Types From Runtime Validation (Single Source of Truth)
We wanted types inferred from runtime schema validators - a single source of truth for both compile-time types AND runtime validation. Maintaining separate type definitions and validation logic would make the codebase difficult to evolve.
The key challenge: ORM models reference each other recursively. User has Posts, Post has Author. None of the existing libraries handled this well:
- Zod -
z.lazy()loses type info, becomesany - ArkType - Good types, but thunks evaluated eagerly causing issues
- Valibot - Poor lazy evaluation caused infinite recursion at runtime
So we built our own validation layer using thunks + lazy evaluation (inspired by ArkType's approach) that correctly handles recursive schemas.
Type Flow Through Layers
User writes: s.string().nullable()
↓
L2: Field creates: StringField<{type: "string", nullable: true}>
↓
L1: Schema factory: v.string({nullable: true}) (lazy, on first access)
↓
Type inference: InferInput<schema> → string | null
↓
L9: Client uses: orm.user.findMany({ where: { name: ... }}) // Fully typed!Key insight: Types flow DOWN through this chain. If types are wrong at the client level, the bug is upstream in schema or field definition.
The Golden Rule
Query Engine / Adapter Separation
Query engine NEVER generates dialect-specific SQL. ALWAYS delegate to adapter.
// ❌ WRONG: Hardcoded PostgreSQL in query-engine
sql`COALESCE(json_agg(...), '[]'::json)`
// ✅ RIGHT: Delegate to adapter
ctx.adapter.json.agg(subquery)This is the most important architectural boundary. The query engine decides WHAT to query. The adapter decides HOW to express it.
Explore the Layers
| Layer | Purpose |
|---|---|
| L1: Validation | Recursive schemas via thunks + lazy evaluation |
| L2: Fields | Field types with State generic for type tracking |
| L3: Query Schemas | Schema generation for where/create/update |
| L4: Relations | Model relationships with thunks for circular refs |
| L6: Query Engine | Database-agnostic query structure |
| L7: Adapters | Database-specific SQL dialects |
| L8: Drivers | Connection management and execution |
| L9: Client | The public orm.model.operation() API |
| L10: Migrations | Schema diffing and synchronization |
Next Steps
- Architecture - Design principles and data flow
- L1: Validation - The foundation layer
- L6: Query Engine - Core query building
- L7: Adapters - Database dialect support