Skip to content
Data Architecture
AutoXXS (320px)XS (375px)SM (640px)MD (768px)LG (1024px)XL (1280px)XXL (1536px)
SketchMaterialiOSTamagui
DataInjectionKeyPatternsServiceTransactionProcessResearchProductQualityPerformanceSpecDomainFunctionTechnologyArchitectureConfigMiddlewareDataDatabaseDrizzleMigrationModelop-sqliteSchemaSQLState ManagementDraftKeystoneMergePatchPatchesPersistenceReactiveRedoStoreUndoTestingDeviceFactoryIsolationTypeScriptZodTopicsCommunicationBidsNVCDesignDesign ImplicationsEducationPedagogyFoundationsPsychologyAttachmentFloodingRelatingAuthentic RelatingUIEditorReact Native

Data Architecture

architecture

Reactive data pipeline for React Native: Drizzle ORM defines SQLite tables as the single schema source, Zod schemas are derived automatically, and MobX Keystone models are generated from Zod via a factory. Two data-flow modes coexist: SQLite-leads for read-heavy domain state (Option A) and Keystone-leads for UI-driven state (Option B).

Single Schema Source (Schema-First)

The Drizzle sqliteTable(...) definition is the sole authoritative schema. All other representations — Zod validation schemas, TypeScript types, and MobX Keystone models — are derived from it. Adding a column to the Drizzle table propagates through the entire stack without manual synchronisation.

sqliteTable(...) <- one definition
|-- Drizzle queries <- type-safe SQL, autocomplete on columns
|-- $inferSelect / Insert <- raw TS types
|-- createSelectSchema() <- Zod for query results + API responses
|-- createInsertSchema() <- Zod for writes + service layer input
|-- createUpdateSchema() <- Zod for partial updates
+-- modelFromZod() <- Keystone model for UI reactivity
+-- .extend() <- computed values, domain behavior
Rule Force Realm Reference Description
drizzle-is-source must global Schema definitions must originate in Drizzle table declarations. Hand-written Zod schemas are only acceptable for non-persisted state (drafts, UI-only models).
DrizzleZodSchema table-to-zod
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { createSelectSchema, createInsertSchema, createUpdateSchema } from 'drizzle-orm/zod'
// 1. Single source of truth
export const todos = sqliteTable('todos', {
id: text('id').primaryKey(),
title: text('title').notNull(),
description: text('description'),
completedAt: integer('completed_at'),
listId: text('list_id').notNull(),
createdAt: integer('created_at').notNull(),
})
// 2. Zod schemas derived for free
export const TodoSelectSchema = createSelectSchema(todos, {
title: (s) => s.min(1).max(200),
})
export const TodoInsertSchema = createInsertSchema(todos)
export const TodoUpdateSchema = createUpdateSchema(todos)
// 3. TS types flow through automatically
export type TodoRow = typeof todos.$inferSelect
export type TodoInsert = typeof todos.$inferInsert

Zod-to-Keystone Mapping (Factory)

A modelFromZod factory converts a z.ZodObject into a MobX Keystone Model class. It maps Zod primitives (ZodString, ZodNumber, ZodBoolean, ZodOptional, ZodNullable, ZodArray, ZodDefault) to Keystone tProp types. The generated class includes a static validate() that runs the Zod schema and a fromRaw() safe constructor.

For domain behaviour (computed values, actions), use ExtendedModel to layer behaviour on top of the generated base.

Rule Force Realm Reference Description
extend-for-behaviour should global Generated models should be extended via to add computed values and model actions, keeping the schema-derived base clean.
modelFromZod factory and ExtendedModel usage
ZodKeystoneFactory model-from-zod-factory
import { z } from 'zod'
import { Model, prop, tProp, types, ExtendedModel } from 'mobx-keystone'
function zodToKeystoneType(zodType: z.ZodTypeAny): any {
if (zodType instanceof z.ZodString) return types.string
if (zodType instanceof z.ZodNumber) return types.number
if (zodType instanceof z.ZodBoolean) return types.boolean
if (zodType instanceof z.ZodOptional) return types.maybe(zodToKeystoneType(zodType.unwrap()))
if (zodType instanceof z.ZodNullable) return types.maybeNull(zodToKeystoneType(zodType.unwrap()))
if (zodType instanceof z.ZodArray) return types.array(zodToKeystoneType(zodType.element))
throw new Error(`Unsupported Zod type: ${zodType.constructor.name}`)
}
function zodToKeystoneProp(zodType: z.ZodTypeAny) {
if (zodType instanceof z.ZodDefault)
return tProp(zodToKeystoneType(zodType.removeDefault()))
.withDefault(zodType._def.defaultValue())
return tProp(zodToKeystoneType(zodType))
}
function modelFromZod<T extends z.ZodObject<any>>(modelType: string, schema: T) {
const keystoneProps = Object.fromEntries(
Object.entries(schema.shape).map(([key, zodType]) => [
key, zodToKeystoneProp(zodType as z.ZodTypeAny),
])
)
@model(modelType)
class DerivedModel extends Model(keystoneProps as any) {
static validate(data: unknown): z.infer<T> {
return schema.parse(data)
}
static fromRaw(data: unknown) {
return new DerivedModel(DerivedModel.validate(data))
}
}
return DerivedModel
}
// Usage: extend for domain behaviour
const BaseTodo = modelFromZod('BaseTodo', TodoSelectSchema)
@model('Todo')
class Todo extends ExtendedModel(BaseTodo, {}) {
@computed get isCompleted() {
return this.completedAt !== null
}
}

Option A — SQLite Leads (Reactive Cache)

SQLite is the source of truth for persisted domain state. Writes go through a service layer directly to the database. Reactive queries (reactiveExecute) push fresh results into MobX Keystone models, which serve as a view-cache. The UI observes Keystone models via observer().

This option excels for read-heavy data, complex joins/aggregates, background sync, and offline-first scenarios where SQLite handles the heavy lifting.

UI components -> observe MobX Keystone models (read)
-> call services (write)
MobX Keystone models -> shaped data + computed/derived values
-> NO write logic
Services / use-cases -> domain logic, validation, business rules
-> all writes go through here
-> orchestrate transactions
op-sqlite reactive -> bridge: DB changes -> model updates
queries -> registered at app init or model lifecycle
SQLite -> single source of truth for persisted state
Rule Force Realm Reference Description
models-no-writes must global In Option A, MobX Keystone models must not contain write logic. All mutations flow through service functions that write to SQLite. Models expose only computed/derived values.

Option B — Keystone Leads (Event Sourcing Lite)

MobX Keystone is the source of truth. Models are mutated directly via @modelAction. The onPatches listener translates Keystone patches into SQL statements that persist changes asynchronously. Inverse patches enable undo/redo that automatically generates compensating SQL.

This option excels for UI-driven state, complex domain logic, optimistic updates, and scenarios requiring undo/redo or time-travel debugging.

See blueprint patch-persistence for the full patch-to-SQL mechanism.

Rule Force Realm Reference Description
action-boundary-persistence must global In Option B, SQL persistence must be triggered at the top-level action boundary, not per individual patch. Use to group all patches from one action into a single SQL transaction.

Hybrid Architecture

Option A and Option B coexist cleanly because they serve different data lifecycles. Domain state that needs queries, sync, and durability uses Option A. Ephemeral UI state (drafts, editor state) uses Option B. The two never conflict because they operate on disjoint model subtrees.

UI editors -> bind to Draft models (Option B, MobX snapshot persistence)
UI readers -> observe persisted models (Option A, DB reactive queries)
DraftStore -> MobX Keystone + JSON blob in SQLite kv table
-> owns draft lifecycle, recovery, unsaved indicators
Services -> receive draft on save, write to domain SQLite tables
-> discard draft on success
Domain models -> DB-backed, reactive queries, thin

Architecture

architecture Data Architecture architecture
Gallery

Components

contract

drizzle-schema

Drizzle ORM

Defines all SQLite tables via . Serves as the single schema source from which Zod schemas, TS types, and Keystone models are derived. Also produces type-safe query builders.

contract

zod-schemas

Zod

Select, insert, and update schemas derived from Drizzle via , , . Used for runtime validation of query results and service inputs.

Collaborates with
  • drizzle-schema — Schemas are generated from Drizzle table definitions.
store

keystone-models

MobX Keystone

Domain models generated from Zod schemas via , extended with computed values and model actions. Observed by UI components for reactive rendering. In Option A these are view-caches; in Option B they are the source of truth.

Collaborates with
  • zod-schemas — Model shape derived from Zod select schemas.
adapter

reactive-query-bridge

op-sqlite / Drizzle

Bridges Drizzle’s type-safe query builder to op-sqlite’s . Uses Drizzle’s to extract SQL + params, subscribes via op-sqlite’s native change hook, and validates results through Zod before applying to Keystone models.

Collaborates with
  • drizzle-schema — Builds queries using Drizzle's .
  • zod-schemas — Validates reactive query results before applying to models.
  • keystone-models — Applies validated results to Keystone stores via surgical reconciliation.
Type-safe reactive query bridge utility
ReactiveDrizzleop-sqlite reactive-query-utility
import { QueryBuilder } from 'drizzle-orm/sqlite-core'
import { type SQLiteTable } from 'drizzle-orm/sqlite-core'
import { type z } from 'zod'
const qb = new QueryBuilder()
// Extract table names from Drizzle table objects
function tableNames(...tables: SQLiteTable[]): string[] {
return tables.map(t => t[Symbol.for('drizzle:Name')])
}
type ReactiveQueryOptions<TSchema extends z.ZodTypeAny> = {
query: ReturnType<typeof qb.select>
tables: string[]
schema: TSchema
callback: (rows: z.infer<TSchema>[]) => void
}
function reactiveQuery<TSchema extends z.ZodTypeAny>({
query, tables, schema, callback,
}: ReactiveQueryOptions<TSchema>) {
const { sql, params } = query.toSQL()
return opsqliteDb.reactiveExecute({
query: sql,
arguments: params,
tables,
callback: (rows: unknown[]) => {
const parsed = z.array(schema).parse(rows)
callback(parsed)
},
})
}
// Usage — fully type-safe, no raw SQL strings
const unsub = reactiveQuery({
query: qb
.select({ id: todos.id, title: todos.title, completedAt: todos.completedAt })
.from(todos)
.where(eq(todos.listId, listId))
.orderBy(desc(todos.createdAt)),
tables: tableNames(todos),
schema: TodoSelectSchema,
callback: (rows) => {
// rows: z.infer<typeof TodoSelectSchema>[] — fully typed
applySnapshot(store.todos, rows)
},
})
service

service-layer

TypeScript

Domain logic, validation, and business rules. All database writes flow through services. Services accept (Drizzle type) via constructor injection, making them testable with any compatible SQLite driver.

Collaborates with
  • drizzle-schema — Executes type-safe Drizzle queries for reads and writes.
  • zod-schemas — Validates inputs using insert/update Zod schemas.
Type-safe service with Drizzle and Zod
ServiceDrizzle typed-service
type AppDatabase = ReturnType<typeof drizzle<typeof schema>>
class TodoService {
constructor(private db: AppDatabase) {}
async create(input: z.infer<typeof TodoInsertSchema>) {
await this.db.insert(todos).values(input)
}
async update(id: string, input: z.infer<typeof TodoUpdateSchema>) {
await this.db.update(todos).set(input).where(eq(todos.id, id))
}
async complete(id: string) {
const todo = await this.db.select().from(todos)
.where(eq(todos.id, id)).then(rows => rows[0])
if (todo.blockedBy) throw new Error('Cannot complete a blocked todo')
await this.db.update(todos)
.set({ completedAt: Date.now() })
.where(eq(todos.id, id))
}
}
store

collection-store

MobX Keystone

Uses keyed by entity ID for O(1) lookups during reconciliation. Surgical per-instance updates replace naive on arrays, avoiding the performance cliff at 1000+ items on low-end devices.

Collaborates with
  • keystone-models — Holds model instances in an ObjectMap.
ObjectMap store with surgical reconciliation
KeystonePerformance map-based-store
import { Model, prop, objectMap, ObjectMap, modelAction } from 'mobx-keystone'
import { runInAction } from 'mobx'
@model('TodoStore')
class TodoStore extends Model({
todosMap: prop<ObjectMap<Todo>>(() => objectMap()),
}) {
@computed get todos() {
return Array.from(this.todosMap.values())
}
@modelAction addTodo(todo: Todo) {
this.todosMap.set(todo.id, todo)
}
@modelAction removeTodo(id: string) {
this.todosMap.delete(id)
}
}
// Surgical reconciliation in reactive callback
function reconcileTodos(store: TodoStore, rows: TodoRow[]) {
runInAction(() => {
const rowMap = new Map(rows.map(r => [r.id, r]))
// Remove deleted
for (const [id] of store.todosMap.items()) {
if (!rowMap.has(id)) store.removeTodo(id)
}
// Add new, update existing
for (const [id, row] of rowMap) {
const existing = store.todosMap.get(id)
if (existing) {
applySnapshot(existing, row) // per-instance reconcile
} else {
store.addTodo(Todo.fromRaw(row))
}
}
})
}
infrastructure

database-provider

op-sqlite

Provides the SQLite database connection. In production, wraps with extensions (FTS5, sqlite-vec, rtree). Configured via .

op-sqlite configuration with extensions
op-sqliteConfig op-sqlite-config
{
"op-sqlite": {
"fts5": true,
"sqliteVec": true,
"rtree": true,
"performanceMode": true
}
}
infrastructure

migration-runner

Drizzle Kit

Generates migration SQL files at dev time via . Bundles migrations into the JS bundle via Babel inline-import plugin. Runs migrations on device at app startup via hook before any UI renders. Tracks applied migrations in table.

Collaborates with
  • drizzle-schema — Generates migrations from schema diffs.
  • database-provider — Applies migrations to the on-device database.
Mobile migration bundling and runtime execution
MigrationDrizzle mobile-migration-setup
// babel.config.js — inline .sql files into the JS bundle
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [['inline-import', { extensions: ['.sql'] }]],
}
// App.tsx — run migrations before rendering
import { useMigrations } from 'drizzle-orm/op-sqlite/migrator'
import migrations from './drizzle/migrations'
const opsqliteDb = open({ name: 'app.db' })
const db = drizzle(opsqliteDb, { schema })
export default function App() {
const { success, error } = useMigrations(db, migrations)
if (error) return <MigrationErrorScreen error={error} />
if (!success) return <SplashScreen />
return <RootNavigator />
}

Rules

Rule Force Realm Reference Description
drizzle-is-source must global Schema definitions must originate in Drizzle table declarations. Hand-written Zod schemas are only acceptable for non-persisted state (drafts, UI-only models).
extend-for-behaviour should global Generated models should be extended via to add computed values and model actions, keeping the schema-derived base clean.
models-no-writes must global In Option A, MobX Keystone models must not contain write logic. All mutations flow through service functions that write to SQLite. Models expose only computed/derived values.
action-boundary-persistence must global In Option B, SQL persistence must be triggered at the top-level action boundary, not per individual patch. Use to group all patches from one action into a single SQL transaction.
forward-only-migrations must global Shipped migrations can never be rolled back. Any fix must be a forward migration. Destructive changes (dropping columns/tables) must be split into a deprecation migration followed by a removal migration in a later version.
debounce-reactive-callbacks should global Reactive query callbacks should be debounced (50-100ms) when the underlying tables receive high-frequency writes (sync, bulk imports) to prevent redundant reconciliation work.