Files
dbtype/docs/architecture
glm-5.1 98764086f4 docs: resolve all open architecture questions with ADRs 009-014
Resolve OQ-01 through OQ-10 with formal ADRs and update all architecture
docs to reference decisions. Add two new tracked questions (OQ-11, OQ-12)
surfaced during review.

ADR-009: Keep FooRelations naming convention for relation entries
ADR-010: One module per database, include derived schemas by default
ADR-011: Support JSX/TSX as ergonomic authoring layer
ADR-012: Always .returning() with graceful fallback per dialect
ADR-013: Adapter generates relations from module entries (no <relation> element)
ADR-014: Leverage drizzle-kit for migrations, no native migration generator

Also upgrades elements.md, hosts.md, repo-adapter.md status to stable,
clarifies OQ-06 as design clarification, and specifies MySQL .returning()
detection mechanism in ADR-012.
2026-05-23 12:47:55 +00:00
..

status, last_updated
status last_updated
draft 2026-05-23

@alkdev/dbtype Architecture

Schema-first multi-dialect database type system. Define your database schema once as a UJSX element tree, validate it with TypeBox, and render it to any Drizzle dialect (SQLite, PostgreSQL, MySQL) — all from a single source of truth.

Why This Exists

Every project that uses Drizzle ORM across multiple backends defines schemas repeatedly: once per dialect (SQLite for local dev, PostgreSQL for production), once again for input validation (TypeBox, Zod, etc.), and once more for API contracts (GraphQL types, OpenAPI schemas). The existing drizzle-typebox only goes one direction (Drizzle → TypeBox) and requires defining schemas in Drizzle's dialect-specific API first.

The gap: there is no schema-first, dialect-agnostic way to define a database schema that simultaneously produces validation schemas, Drizzle table definitions, and (via the operations layer) CRUD operation specs — all from one source of truth.

Core Principle

The element tree is the schema. The module is the bundle. The host is the dialect.

  • The UJSX element tree (<table>, <column>) is the authoring surface — composable, reusable, and TypeBox-validatable
  • The Type.Module is the schema bundle — all tables, relations, and derived schemas in one namespace with Type.Ref resolving cross-table references
  • The HostConfig is the dialect adapter — render the same tree to sqliteTable, pgTable, mysqlTable, or any future target

Relationship to Sibling Packages

Package Relationship
@alkdev/typebox Type system foundation. Type.Module, Type.Ref, Value.Check/Diff/Edit, FormatRegistry
@alkdev/ujsx Element authoring. h(), createComponent, HostConfig, createRoot, reconciler
@alkdev/operations CRUD generation. OperationSpec, OperationRegistry, from-dbtype adapter (future)
@alkdev/pubsub Event transport. Used by operations call protocol
drizzle-orm Peer dependency. Dialect-specific column builders consumed by hosts

Current State

Phase 0: Exploration — Architecture probing complete. Probe scripts in scripts/probe-e2e.ts validate the core architecture. No implementation yet.

What Exists

  • Fork of drizzle-typebox with @alkdev/typebox support (current src/)
  • Research docs in docs/research/ (architecture, type-map, column diffs, typedef kind pattern)
  • E2E probe script validating: UJSX → Type.Module → Drizzle rendering pipeline

What Doesn't Exist Yet

  • Core dbtype package (the implementation)
  • Host configs for SQLite, PostgreSQL, MySQL
  • Schema extraction functions (createSelectSchema, createInsertSchema, createUpdateSchema from element trees)
  • Repo/CRUD generation adapter (from-dbtype for @alkdev/operations)
  • @alkdev/ujsx as a declared dependency (currently dev-only)

Architecture Documents

Document Content
schema.md Table/relation schema structure, schema derivation (insert/update/filter)
module.md Type.Module mechanics: construction, validation, serialization, migration diffing
elements.md UJSX element definitions, column types, type definitions, defaults, function components
hosts.md HostConfig implementations, column type mapping tables, rendering pipeline
repo-adapter.md from-dbtype adapter for @alkdev/operations, filter/schema generation, access control
build-distribution.md Package structure, sub-path exports, dependencies, tree-shaking
open-questions.md Cross-cutting unresolved questions

Design Decisions

ADR Decision
001 UJSX element tree as the IR, not a separate DbType builder API
002 Type.Module as the schema bundle, not a custom registry
003 HostConfig for dialect rendering, not a transform registry
004 Column formats are annotations, not validators — register explicitly
005 CRUD generation as an operations adapter, not a core feature
006 Column references as FK shorthand, explicit <fk> for complex FKs
007 Flat props with inner escape hatch for column validation
008 PG enum pre-declaration — return enums and tables from render context

Open Questions

All unresolved questions tracked in open-questions.md.

Source Structure (Planned)

src/
  core/
    elements.ts          # h() wrappers, createComponent for table/column/index/fk
    schema.ts            # extractTable, createSelectSchema, createInsertSchema, createUpdateSchema
    module.ts            # buildModule, module construction helpers
    column-types.ts      # DbColumnType union, colToTypeBox mapping
    defaults.ts          # Symbolic default resolution ('now', 'uuid', 'autoincrement')
    guards.ts            # Type guards for element types
  hosts/
    sqlite.ts            # HostConfig for drizzle-orm/sqlite-core
    pg.ts                # HostConfig for drizzle-orm/pg-core
    mysql.ts             # HostConfig for drizzle-orm/mysql-core
  repo/
    from-dbtype.ts       # FromDbType adapter for @alkdev/operations
    filters.ts           # Filter schema generation per column type
    handlers.ts          # CRUD handler generation (findMany, insertOne, etc.)
   index.ts

End-to-End Pipeline

The dbtype pipeline has two walks over the same element tree:

  1. Schema extraction (extractTable) — walks the tree to produce TypeBox schemas and column metadata
  2. Host rendering (createRoot(host).render()) — walks the tree to produce Drizzle table definitions

These are separate walks because they serve different purposes: schema extraction feeds the Type.Module (for validation and serialization), while host rendering feeds Drizzle (for database operations). They consume the same element tree but produce different outputs.

Complete Flow

import { Type } from '@alkdev/typebox'
import { h, createComponent, createRoot } from '@alkdev/ujsx'
import { Value } from '@alkdev/typebox/value'

// 1. Define elements
const UsersEl = h('table', { name: 'users' },
  h('column', { name: 'id', type: 'uuid', primaryKey: true, default: 'uuid' }),
  h('column', { name: 'name', type: 'string', notNull: true }),
)

// 2. Schema extraction — produces TypeBox schemas and column metadata
const { name, schema, columns, indexes, foreignKeys } = extractTable(UsersEl)

// 3. Module construction — assemble into Type.Module
const defs = { Users: schema }
const M = Type.Module(defs)
const Users = M.Import('Users')

// 4. Validation — check data against module schema
Value.Check(Users, { id: '...', name: 'alice' })  // true

// 5. Host rendering — produce Drizzle tables
const root = createRoot(sqliteHost, {})
root.render(UsersEl)
const drizzleUsers = root.ctx.tables.users  // sqliteTable result

// 6. Serialization — JSON Schema with $defs (for migration diffing)
const serialized = JSON.parse(JSON.stringify(Users))

// 7. Schema derivation — insert, update, filter schemas (for API validation)
defs.InsertUsers = Type.Object({ name: Type.String() })  // omit auto-generated columns
defs.UpdateUsers = Type.Partial(Type.Ref('Users'))        // all optional

Key Relationships

Step Input Output Consumer
Extract UJSX elements TableMeta (schema + metadata) Module construction, Host rendering
Module Record<string, TSchema> Type.Module Validation, Serialization
Import Module key TImport (resolved schema) Value.Check, Value.Diff
Render UJSX elements + HostConfig Drizzle tables (in ctx) Database queries
Derive Module entries Insert/Update/Partial schemas API validation

Key Design Decisions

1. UJSX Element Tree as the IR, Not a Separate Builder API

The architecture docs in docs/research/ proposed a DbTypeBuilder class with methods like DbType.String(), DbType.Table(). The probe results show that UJSX elements (<table>, <column>) with props serve the same purpose — they carry all the metadata (type, notNull, primaryKey, default, references) and compose via function components (IdCol, AuditCols).

A separate builder API would be redundant: both the element tree and the builder produce the same TypeBox + metadata. The element tree is strictly more capable (function components for composition, reconciler for migration diffing in the future) and already exists as @alkdev/ujsx.

Alternative considered: DbTypeBuilder (the research docs pattern). Rejected because it duplicates what UJSX already provides and cannot compose as naturally.

2. Type.Module as the Schema Bundle

All table schemas and their relations live in a single Type.Module call. Type.Ref resolves cross-table references including mutual/circular ones. This eliminates the circular-import problem that the storage_sqlite pattern solves with a separate relations.ts file.

The module is also serializable as JSON Schema with $defs, enabling Value.Diff for schema migration and FromSchema for round-tripping.

Alternative considered: A custom registry with separate schema objects. Rejected because Type.Module already does everything needed (ref resolution, validation, serialization) and doesn't require a new abstraction.

3. HostConfig for Dialect Rendering, Not a Transform Registry

The research docs proposed a TransformRegistry with priority-sorted rules. UJSX's HostConfig serves the same purpose — each dialect is a host, createInstance maps element types to Drizzle column builders, appendChild assembles columns into tables.

This leverages existing infrastructure (createRoot, createComponent, reconciler) rather than building a parallel dispatch system. It also positions the project for future migration support via prepareUpdate/commitUpdate.

Alternative considered: TransformRegistry from docs/research/architecture.md. Rejected because HostConfig is the same pattern with more capabilities (reconciler, function components, context) and already exists in @alkdev/ujsx.

4. Column Formats as Annotations

Type.String({ format: 'uuid' }) and Type.String({ format: 'email' }) are annotations by default in TypeBox. Value.Check does not enforce format unless validators are explicitly registered via FormatRegistry.Set. This is correct JSON Schema behavior.

For dbtype, formats serve as metadata that hosts can use (e.g., the PG host maps format: 'uuid' to uuid() column type, the SQLite host maps it to text() with $defaultFn). Validation is opt-in via format registration.

5. CRUD Generation as an Operations Adapter

The repo pattern (auto-generated CRUD for each table) is not a core feature of dbtype. It's an adapter for @alkdev/operations that consumes the same element tree and module bundle. This keeps dbtype focused on schema definition and rendering, while the operations integration is a separate concern.

Error Handling Strategy

dbtype uses a layered validation approach:

  1. TypeScript compile-time enforcement: DbColumnType, column props, and element types are enforced by TypeScript types. Invalid type values, missing required props, and incorrect prop types are caught at compile time.

  2. Runtime validation at extraction: extractTable() validates the element tree — missing name or type on columns, duplicate column names, invalid DbColumnType values. These throw descriptive errors.

  3. TypeBox validation at module compile: Type.Module(defs) validates the schema map. Invalid Type.Ref targets, duplicate keys, and malformed schemas throw TypeBox errors.

  4. Host rendering validation: createInstance() in the host validates dialect-specific constraints — unknown column types fall back to text(), invalid symbolic defaults throw errors. PG enum names must be unique within a module.

The general principle: catch errors as early as possible. Type errors at compile time, structural errors at extraction time, schema errors at module compile time, dialect errors at render time.

Glossary

Term Definition
UJSX Universal JSX — @alkdev/ujsx's element system. Uses h() and createComponent to create element trees that render to different hosts via HostConfig. Not an acronym, a project name.
Element tree A tree of UJSX elements (<table>, <column>, <index>, <fk>) representing a database schema. The tree is the IR (intermediate representation).
HostConfig UJSX's host configuration interface. Defines how elements map to output objects (createInstance, appendChild, finalizeInstance). Each dialect (SQLite, PG, MySQL) is a host.
Type.Module @alkdev/typebox's module system. Holds all schemas in a flat namespace with Type.Ref for cross-references. The module is dbtype's schema bundle.
Type.Ref A TypeBox reference type that resolves within a Type.Module. Enables forward and circular references without import ordering issues.
TImport The type returned by M.Import(key). Embeds the full $defs namespace and resolves $ref pointers for validation.
$defs JSON Schema keyword used by serialized Type.Modules. Contains all referenced schemas. $ref pointers reference entries in $defs.
extractTable() Core function that walks an element tree, resolves function components, and produces TableMeta (TypeBox schema + column metadata + indexes + FKs).
Function component A UJSX component created with createComponent that returns element(s). Transparent to the host — only resolved intrinsic elements (column, table) reach createInstance.
Intrinsic element A built-in element type (table, column, index, fk). Not a function component — directly dispatched to the host's createInstance.
Reconciler UJSX's diffing engine. Can compare old and new element trees to produce update instructions. Not used in phase 1, but positions dbtype for future schema migration support.
Host rendering The process of walking an element tree through a HostConfig to produce output (Drizzle tables). Done via createRoot(host).render(element).
Schema derivation Producing insert, update, and filter schemas from the module. E.g., Type.Partial(Type.Ref('Users')) for update schemas.

Document Lifecycle

From To Condition
draft reviewed All open questions resolved
reviewed stable Implementation complete and verified by tests
stable deprecated Superseded by new architecture

References

  • Research: docs/research/architecture.md, docs/research/typemap-architecture.md, docs/research/dizzle-column-diffs.md, docs/research/typedef-kind-pattern.md
  • Probe results: scripts/probe-e2e.ts
  • Sibling projects: @alkdev/typebox, @alkdev/ujsx, @alkdev/operations, @alkdev/pubsub
  • Reference implementation: @alkdev/drizzle-typebox (current src/), drizzle-graphql (workspace)