Schema Builder
Schema Builder
The Schema Builder lets users design their own database tables, fields, and relationships from scratch — runtime DDL on SQLite with safety, auditability, and interoperability with pre-built modules.
Metadata Model
Three metadata tables describe user-defined schemas:
_custom_tables— table definitions (name, display name, icon, establishment scope)_custom_fields— field definitions (name, type, required, default, type-specific config JSON)_custom_relations— relationships between tables (belongs_to, has_many, many_to_many; can target custom or pre-built tables)_custom_table_versions— DDL change history for each table
Field Types
| Schema Builder Type | SQLite Type | Config Examples |
|---|---|---|
text |
TEXT | {"multiline": true, "max_length": 500} |
number |
INTEGER | {"min": 0, "max": 100} |
decimal |
REAL | {"precision": 2} |
date |
TEXT | stored as YYYY-MM-DD |
datetime |
TEXT | stored as ISO 8601 |
boolean |
INTEGER | 0/1 |
select |
TEXT | {"options": ["Low", "Medium", "High"]} |
relation |
INTEGER | {"target_table": "cx_projects", "display_field": "name"} |
Naming and Safety
All user-created tables get the cx_ prefix. The validator enforces:
- Table names:
^[a-z][a-z0-9_]{1,58}$(after prefix, max 63 chars) - Field names:
^[a-z][a-z0-9_]{1,62}$ - Reserved names blocked:
id,establishment_id,created_at,updated_at(auto-added to every custom table) - Cannot collide with pre-built table names
- Relation targets must exist
DDL Execution
The executor translates metadata into DDL:
Table creation adds id, establishment_id, created_at, updated_at
automatically, plus an index on establishment_id.
Field addition uses ALTER TABLE ADD COLUMN.
Field deactivation sets is_active = 0 in metadata — hides from UI and
query builder. SQLite column remains. Explicit compaction can drop it later
(SQLite 3.35.0+).
Dynamic Query Builder
Safe parameterized queries for custom tables. Column names validated against metadata are interpolated; all data values are parameterized:
func (qb *QueryBuilder) Select(tableID int64, opts SelectOpts) (string, []any, error)
func (qb *QueryBuilder) Insert(tableID int64, values map[string]any) (string, []any, error)
func (qb *QueryBuilder) Update(tableID int64, id int64, values map[string]any) (string, []any, error)
func (qb *QueryBuilder) Delete(tableID int64, id int64) (string, []any, error)
Interoperability
Custom tables can relate to whitelisted pre-built tables:
establishments,employeesincidents,chemicalstraining_courses,training_completionsstorage_locations,work_areas
The query builder generates appropriate JOINs. RecordForm.tsx renders
searchable dropdowns for relation fields.
Table Designer UI
- Table metadata — display name, description, icon
- Fields list — drag-to-reorder, type, required, config
- Relations panel — source field, target table, relation type
- Preview — live record form preview from current field definitions
- Save — diffs against current state, executes necessary DDL
Schema Versioning
Every DDL change is recorded in _custom_table_versions — enables future undo,
export, and schema sharing between establishments.