ctx0 Drizzle Schema Management
This document describes the Drizzle ORM-based schema management system for ctx0. This is the source of truth for database schema definitions.
Overview
ctx0 uses Drizzle ORM for TypeScript-first schema management. The system provides:
- Type-safe schema definitions - Tables defined in TypeScript with full type inference
- Automatic migration generation -
drizzle-kitgenerates SQL from schema changes - Smart migration command -
ctx0 migratehandles both Bytespace and self-hosted setups - Schema diffing -
ctx0 migrate --statusshows exactly what needs to change
Package Structure
packages/ctx0/
├── src/
│ ├── schema/ # Drizzle schema definitions (SOURCE OF TRUTH)
│ │ ├── index.ts # Exports all tables + ctx0Tables array
│ │ ├── meta.ts # Expected functions, triggers, extensions, buckets
│ │ ├── custom-types.ts # pgvector custom type
│ │ ├── users.ts # users, ctx0_schema_versions
│ │ ├── config.ts # ctx0_user_config
│ │ ├── entries.ts # ctx0_entries, ctx0_entry_versions
│ │ ├── sessions.ts # ctx0_sessions, ctx0_session_messages
│ │ ├── memory.ts # ctx0_working_memory, ctx0_learned_queries, ctx0_embedding_queue
│ │ ├── proxy.ts # ctx0_api_keys, ctx0_llm_usage, ctx0_db_credentials
│ │ ├── devices.ts # ctx0_devices, ctx0_daemons
│ │ ├── relations.ts # Foreign key relations
│ │ └── types.ts # Inferred TypeScript types
│ ├── db/ # Database utilities
│ │ ├── index.ts # Re-exports
│ │ ├── client.ts # Drizzle client creation
│ │ ├── migrate.ts # Migration runner + schema status
│ │ └── schema-diff.ts # Schema comparison logic
│ └── migrations.ts # Legacy SQL exports (for functions, triggers, RLS)
├── migrations/ # Raw SQL for non-Drizzle things
│ ├── 000_users.sql # Users table + helper functions
│ ├── 001_ctx0_schema.sql # Core tables + triggers
│ ├── 002_storage_policies.sql # Supabase storage policies
│ ├── 003_api_keys.sql # API key functions
│ ├── 004_llm_usage.sql # LLM usage tracking functions
│ ├── 005_db_credentials.sql # DB credentials functions
│ ├── 007_devices.sql # Device functions + triggers
│ └── 008_daemons.sql # Daemon triggers
└── drizzle.config.ts # Drizzle Kit configuration
Source of Truth
What Lives Where
| Component | Source of Truth | File(s) |
|---|---|---|
| Tables | ctx0Tables array | schema/index.ts |
| Columns | Drizzle table definitions | schema/*.ts |
| Functions | EXPECTED_FUNCTIONS | schema/meta.ts |
| Triggers | EXPECTED_TRIGGERS | schema/meta.ts |
| Extensions | EXPECTED_EXTENSIONS | schema/meta.ts |
| Storage Buckets | EXPECTED_STORAGE_BUCKETS | schema/meta.ts |
The ctx0Tables Array
All ctx0 tables are listed in a single array in schema/index.ts:
// All ctx0 tables - THE SOURCE OF TRUTH for schema diffing export const ctx0Tables: Table<any>[] = [ ctx0SchemaVersions, ctx0UserConfig, ctx0Entries, ctx0EntryVersions, ctx0Sessions, ctx0SessionMessages, ctx0WorkingMemory, ctx0LearnedQueries, ctx0EmbeddingQueue, ctx0ApiKeys, ctx0LlmUsage, ctx0DbCredentials, ctx0Devices, ctx0Daemons, ];
The meta.ts File
Non-Drizzle database objects are tracked in schema/meta.ts:
// Extensions export const REQUIRED_EXTENSIONS = ['uuid-ossp'] as const; export const OPTIONAL_EXTENSIONS = ['vector'] as const; // Functions (from SQL migration files) export const EXPECTED_FUNCTIONS = [ 'ctx0_update_updated_at', 'ctx0_semantic_search', 'ctx0_log_llm_usage', // ... all function names ] as const; // Triggers export const EXPECTED_TRIGGERS = [ 'trigger_users_updated_at', 'trigger_entries_updated_at', // ... all trigger names ] as const; // Storage buckets export const EXPECTED_STORAGE_BUCKETS = ['ctx0-vault'] as const;
Developer Workflows
Adding a New Table
-
Create or edit the schema file (e.g.,
schema/devices.ts):typescriptexport const ctx0NewTable = pgTable('ctx0_new_table', { id: uuid('id').primaryKey().default(sql`gen_random_uuid()`), userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), // ... columns }); -
Add to
ctx0Tablesarray inschema/index.ts:typescriptimport { ctx0NewTable } from './your-file'; export const ctx0Tables: Table<any>[] = [ // ... existing tables ctx0NewTable, // Add here ]; -
Run
pnpm db:statusto verify it shows as missing -
Run
pnpm db:migrateto apply
Adding a New Column
- Edit the table definition in the schema file
- Run
pnpm db:statusto see the diff - Run
pnpm db:migrateto apply
Adding a New Function
- Add the SQL to the appropriate migration file in
migrations/ - Add the function name to
EXPECTED_FUNCTIONSinschema/meta.ts - Run
pnpm db:statusto verify
Adding a New Trigger
- Add the SQL to the appropriate migration file
- Add the trigger name to
EXPECTED_TRIGGERSinschema/meta.ts - Run
pnpm db:statusto verify
CLI Commands
Check Schema Status
pnpm db:status
Shows comprehensive database status including:
- Extensions (required and optional)
- Users table existence
- All ctx0 tables with column counts, indexes, RLS status
- Functions (found and missing)
- Triggers (found and missing)
- Vector columns
- Storage buckets
- Schema Changes Needed - shows exactly what columns AND constraints need to be added
Example output:
Schema Changes Needed:
⚠ ctx0_daemons:
+ Add columns: device_id
+ Add UNIQUE constraint on (device_id)
+ Add FK: device_id → ctx0_devices.device_id
✓ All other tables match expected schema
Summary:
⚠ Issues found:
- 1 functions missing
- 1 triggers missing
- 2 constraints missing
Run `pnpm db:migrate` to apply fixes.
Verbose Status
pnpm db:status:verbose
Shows full column details, indexes, and more.
Dry Run Migration
pnpm db:migrate:dry
Shows what would be changed without applying.
Apply Migrations
pnpm db:migrate
Applies all pending schema changes:
- Missing columns are added via
ALTER TABLE - Missing UNIQUE and FK constraints are created
- Missing functions (
ctx0_upsert_user) are auto-created - Missing triggers (
trigger_users_updated_at) are auto-created
Example output:
✓ Users table exists (not modified)
✔ Schema synced successfully
✓ Added UNIQUE constraint on ctx0_daemons.device_id
✓ Added FK constraint ctx0_daemons.device_id -> ctx0_devices.device_id
✓ Added trigger trigger_users_updated_at on users table
✓ Added function ctx0_upsert_user
Result:
✓ Database schema is up to date
How It Works
Smart Detection (Bytespace vs Self-Hosted)
The migrate command automatically detects the environment:
- Bytespace: Users table exists → skip creating it, only sync
ctx0_*tables - Self-hosted: No users table → create it first, then sync
ctx0_*tables
Schema Diffing
The schema-diff.ts module provides comprehensive schema comparison:
- Column Detection: Imports
ctx0Tablesfromschema/index.tsand uses Drizzle'sgetTableColumns()to extract expected columns - Constraint Detection: Extracts UNIQUE constraints from column definitions (
.unique()) and FK constraints from Drizzle's internalSymbol(drizzle:PgInlineForeignKeys) - Database Comparison: Queries
information_schema.columnsandinformation_schema.table_constraintsfor actual state - Diff Reporting: Compares and reports missing columns and constraints
import { getTableColumns } from 'drizzle-orm/utils'; import { getTableName } from 'drizzle-orm'; import { ctx0Tables } from '../schema'; // Extract expected schema including columns export function getExpectedSchema(): ExpectedTable[] { return ctx0Tables.map((table) => { const tableName = getTableName(table); const columns = getTableColumns(table); return { name: tableName, columns: Object.values(columns).map((col) => ({ name: col.name, nullable: !col.notNull, hasDefault: col.hasDefault, isUnique: col.isUnique ?? false, })), }; }); } // Extract expected constraints (UNIQUE and FK) export function getExpectedConstraints(): ExpectedConstraint[] { // Extracts from column.isUnique and Symbol(drizzle:PgInlineForeignKeys) }
Automatic Migration
The applySchemaChanges() function in migrate.ts handles:
- Missing Columns: Adds columns with appropriate types and nullability
- Missing UNIQUE Constraints: Creates constraint for columns marked
.unique() - Missing FK Constraints: Creates foreign key references for columns with
.references() - Missing Functions: Auto-creates essential functions like
ctx0_upsert_user(schema-aware) - Missing Triggers: Auto-creates triggers like
trigger_users_updated_at
The ctx0_upsert_user function is schema-aware - it dynamically detects available columns in your users table, making it compatible with both simple self-hosted schemas and complex Bytespace schemas.
TypeScript Types
All table types are automatically inferred from Drizzle schemas:
import { type DrizzleDevice, type DrizzleDaemon, type NewDevice, type NewDaemon, } from '@bot0/ctx0'; // Select types (query results) const device: DrizzleDevice = await db.query.ctx0Devices.findFirst(); // Insert types const newDevice: NewDevice = { userId: user.id, deviceId: 'abc123', publicKey: 'key...', platform: 'macos', hardwareType: 'secure_enclave', };
Why SQL Files Still Exist
Drizzle ORM handles tables, columns, and constraints, but cannot declaratively create:
- PostgreSQL functions
- Triggers
- RLS policies
- Extensions
- Supabase storage buckets
The SQL files in migrations/ contain reference implementations for CREATE FUNCTION, CREATE TRIGGER, etc. The meta.ts file tracks what should exist so ctx0 migrate --status can report missing items.
Auto-Created Objects
Some essential functions and triggers are automatically created by pnpm db:migrate:
| Object | Description | Auto-Created |
|---|---|---|
ctx0_upsert_user | Schema-aware user upsert function | ✓ Yes |
trigger_users_updated_at | Auto-update updated_at on users | ✓ Yes |
| Other functions | Domain-specific functions | No (from SQL files) |
| RLS policies | Row-level security | No (from SQL files) |
This means for most setups, just running pnpm db:migrate will bring the database to a complete state.
Best Practices
- Always run
pnpm db:statusbefore and after changes to verify the state - Add new tables to
ctx0Tables- this is the single source of truth - Add new functions/triggers to
meta.ts- otherwise status won't track them - Use the Drizzle schema as documentation - it's type-safe and always up to date
- Test migrations on a staging database before production
Troubleshooting
"Table not showing in status"
Make sure it's added to the ctx0Tables array in schema/index.ts.
"Function shows as missing but exists"
Add the function name to EXPECTED_FUNCTIONS in schema/meta.ts.
"Column changes not detected"
Rebuild the ctx0 package: pnpm --filter @bot0/ctx0 build
"Migration fails"
Check the database connection URL. Run with --dry-run first to see what would change.