Collection Builder Architecture
Technical deep-dive into the Collection Builder architecture, explaining the reconciliation loop between Filesystem, Database, and UI.
On this page
This page is the compiler and the builder internals. The task is the Collection Builder guide.
This document details the technical architecture of the SveltyCMS Collection Builder. It explains how the system maintains a “Single Source of Truth” by reconciling filesystem definitions with database state and presenting a consistent view to the user.
System Overview
The Collection Builder operates on a Canonical Flat List architecture. Instead of storing complex nested trees, both the server and the UI operate on flat arrays of nodes that are linked via parentId.
Core Data Flow
The following diagram illustrates the reconciliation flow:
Key Components
1. Filesystem (The Configuration Truth)
The config/collections directory is the primary source of truth for Collections (existence and structure).
- If a collection file exists on disk, it MUST exist in the system.
- If a file is deleted from disk, the corresponding collection is marked for deletion (or becomes a “ghost” if not cleaned up).
Categories come from two sources:
- Path-derived (
source: "filesystem") — auto-created when collections live in subfolders, e.g.config/collections/test/posts.ts→ categorytest. - Builder-created (
source: "builder") — a real directory created from the category path./blogbecomesconfig/collections/blog/and.compiledCollections/blog/. The same node is stored incontent_nodesand backed up in.compilation-manifest.jsonunderstructureNodes. The compiler keeps that directory while the category exists, including when it is still empty.
GUI drag-and-drop updates organization (parentId, order) in the database and the manifest. Dropping a category inside another category moves its directory (blog/ becomes news/blog/ under config/collections/ and .compiledCollections/). Reordering siblings updates order and leaves the directory name in place. A folder created directly under config/collections/ is picked up by the Vite dev watcher, which runs compilation and records that folder as a category.
2. ContentSystem (The Reconciliation Engine)
Located at src/content/index.ts, this singleton is responsible for bridging the gap between static files and dynamic database state.
Key responsibilities:
- Startup Sync: On boot, it scans
compiledCollectionsand compares them against thesystem_content_structuretable/collection in the active database adapter. - Defensive Import: It actively filters out “garbage” nodes from the database (e.g., nodes where
pathis a UUID instead of a valid file path) to prevent database corruption from looping back into the application state. - ID Persistence: While collection definitions come from disk,
_id,parentId, andorderare preserved from the database and manifest (collectionOrder,structureNodes). - Save without destructive refresh:
executeGuiStructureSave()(via thesaveContentStructureremote) upserts organizationalmove/rename/create(category) /deleteoperations throughsyncContentState({ reason: "gui-save" }), applies the delta tocontentStore, persists the manifest, and broadcasts SSE — it does not callfullReload(). Collectioncreate/renameare rejected here (files own existence, C1): presets materialize viainstallTemplateCollectionsand duplicates viaduplicateCollection(a source.tscopy with an AST identity rewrite), both on the explicit Save click. - Schema save: Editor
saveCollectionwrites.tsthensyncContentState({ reason: "collection-save", targetFile })— compile + incremental refresh + model provision under a GUI compile lock so the Vite watcher does not double-compile. - Soft UI refresh: Presets, quick-start, and structure saves use
invalidate("app:content")only — nowindow.location.reload()(session, consent, and builder context stay intact). - Boot manifest watchdog: On
syncContentState({ reason: "boot" }),reconcileOrganizationalManifest()compares.compilation-manifest.json(collectionOrder,structureNodes) against the DB flat structure and re-aligns the manifest when drift is detected (e.g. after manual DB edits). - Cross-tab sync: Server-side saves call
notifyContentUpdate(); the browsercontentSystem.refresh()handler syncs bothcontentStoreandcollectionStore.contentStructureso the sidebar updates without navigation.
3. Organizational vs Filesystem Boundaries
| Operation | DB + manifest | Filesystem (config/collections/ and .compiledCollections/) |
|---|---|---|
| Drag category into a category | parentId, order updated |
Category directory moves under the parent folder |
| Reorder within the same parent | order updated |
Directory stays in place |
| Create a folder on disk | Category recorded on the next compile | config/collections/<name>/ is the source |
| Create category | New content_nodes row + structureNodes |
Directory from the path (/blog → blog/) |
| Delete builder category | Removed from DB + manifest | Empty category directory removed |
| Save new collection schema | collection-save reconcile + models |
.ts written; compile under GUI lock |
GUI drag-and-drop is organizational only. To change where a collection lives on disk, edit or move the .ts file under config/collections/ (or use the collection editor save path). The compile pipeline and boot drift detection (detectCompilationDrift) handle filesystem ↔ compiled output separately from organizational manifest drift (detectOrganizationalDrift).
4. Integrity & Safety Layer
Located in src/utils/schema/ and src/services/MigrationEngine.ts, this layer guards against data corruption:
- Tree Validator: Prevents cyclic dependencies (A->B->A) and path collisions during reordering.
- Drift Detection: Compares the Code Definition (Target) vs Database Schema (Current) to detect potentially destructive changes (e.g., removing a field, changing a widget type).
- Migration Engine: Orchestrates necessary DB updates agnostically via the
IDBAdapterinterface, ensuring compatibility across different database backends.
5. Database (State & Metadata)
The active database adapter stores the metadata that cannot live in static files, specifically:
_id: The stable UUID for the collection.parentId: Which category or folder the collection currently resides in.order: The sort order within that parent.
6. Client UI (The View)
The Collection Builder board (tree-view-board.svelte) and admin sidebar (collections.svelte → tree-view.svelte) both consume the same contentStructure store. The sidebar additionally applies page.data.collectionOrder from the manifest for user-defined sort overrides.
First paint is server data, not store state. The layout populates the store from data.contentStructure in a client-only $effect (so a Collection Builder draft cannot be overwritten by a stale layout snapshot). The sidebar therefore derives its structure from page.data.contentStructure until the store has data — otherwise SSR would render the empty state for one frame and the tree would appear only after hydration (the “No collections found.” → list flash). Once populated, the store stays authoritative for SSE and builder updates.
- Input: A flat array of
ContentNodeobjects from the API. - Process: A lightweight
buildTreefunction groups nodes byparentId. - Render: The UI renders the hierarchical tree based on this computed logical structure using accessible card nodes (
tree-view-node.svelte), token-based badges, category item counters, and a portal-based floating drag preview (tree-drag-preview.svelte). - Interaction: Drag-and-drop operations support nesting collections inside categories with real-time drop target feedback (
↳ Inside {category}), RTL-safe coordinate mapping, and full keyboard navigation (roving tabindex andAlt+Up/Downreordering). Operations emit updates to the flat list (e.g.,updateNode(id, { parentId: newParent })), persisting viasaveContentStructure().
7. Advanced Builder Subsystems (Enterprise Extensions)
Smart Type Inference (smart-inference.ts)
A zero-dependency client-side heuristic engine that parses natural language field names into fully-typed SveltyCMS widgets.
- Pattern Matching: Evaluates exact keywords and token prefixes/suffixes (e.g.
is_*,has_*,*_id,*_price,*_url). - Deterministic Field Naming: Converts arbitrary inputs to sanitized snake_case
db_fieldNameidentifiers. - Fallback Resolution: Queries available core/custom widgets in
widgetStoreto guarantee valid schema bindings.
Real-Time TypeScript Code Generator (collection-code-generator.ts)
Generates production-grade defineCollection({ ... }) TypeScript source code in real time on the client.
- Bi-Directional Parity: Produces an identical AST to the server-side compilation pipeline (
compilation/compile.ts). - Live Split-View: Embedded in
collection-widget.svelte, updating reactively as users reorder or edit fields. - Zero Disk Overhead: Operates entirely in-memory using Svelte 5 derived state runes.
Database DDL & Schema Ingestion Engine (schema-ingestion.ts → ingestSchema remote)
Reverse-engineers existing database schemas and API payloads directly into collections. The engine (schema-ingestion.ts) is a pure, dependency-free module, but it is executed on the server through the ingestSchema remote function in collectionbuilder.remote.ts — the client only imports its types, so the parser never ships to the browser.
- Multi-Dialect DDL Parsing: Supports PostgreSQL, MySQL, MariaDB, and SQLite
CREATE TABLEsyntaxes. - Context-Aware Relations: Resolves
FOREIGN KEY ... REFERENCES table(id)and*_idcolumns intoRelationwidgets, matching the target against the live collection list (singular/plural aware) so suggestions point at real collections. - Enum & Check Constraints: Translates
ENUM(...)andCHECK (col IN (...))into populatedSelectoptions. - Canonical Widget Resolution: Normalizes every inferred key to a registered widget factory
Name(Checkbox,DateTime,MediaUpload,JsonEditor, …), guaranteeing the field binds to a real widget. - Type Coverage: Boolean, integer, numeric (price-aware), date/time, text/long-text, string, UUID, JSON/JSONB, binary, and array (
tags TEXT[]→Tags, structured arrays →Repeater). - Defaults & Nullability:
NOT NULL→ required;DEFAULT true→ the widget’s default value. - JSON Payload Reverse-Engineering: Synthesizes schemas from sample JSON objects/arrays (ISO strings →
DateTime, URL strings → urlInput, string arrays →Tags, object arrays →Repeater, nested objects →Relation).
Unified Metadata & Tagging (in collection-store.svelte.ts)
A cross-cutting reactive surface (collectionMetadata, pinnedStore, getTagColor) that maintains favorites and tag metadata across all administrative views. It was consolidated out of collection-metadata-store.svelte.ts into collection-store.svelte.ts so the board/sidebar read one store.
- Reactive State Sync: Powered by Svelte 5
$staterunes, immediately broadcasting favorite toggles and tag changes between the Collection Builder tree, the Collection Editor form, and the LeftSidebar navigation tree. - Cross-Tab Storage Sync: Listens to the
storageevent to propagate changes across concurrent browser tabs. - Deterministic Color Hashing: Maps tag names to WCAG 2.2 AA compliant status-shade tokens (
tertiary,primary,secondary,warning,success) without requiring manual color picker configuration.
⌨️ Keyboard-Driven UX
The Collection Builder is designed for rapid iteration using a unified hotkey system.
Standard Controls
Mod + S: Save the entire collection and trigger database reconciliation.Escape: Cancel current operation or exit the builder shell.
BuzzForm (Visual Canvas)
Delete: Instantly remove the selected field from the canvas.Mod + D: Duplicate the selected field, automatically generating a uniquedb_fieldName.
Widget Editor (Multi-Step)
Mod + Enter: Advance to the next configuration step or “Finish” the widget.Escape: Navigate back to the previous step.
Failure Scenarios & Recovery
| Scenario | Handling |
|---|---|
| Invalid Component Loaders | The createWidget factory and TypeScript/Vite validate loaders at compile-time. Broken imports fail during build/development. |
| Orphaned Database Nodes | The ContentSystem validation logic detects nodes with invalid structure and prevents them from loading into memory. |
| Disk/DB Mismatch | The Filesystem is treated as the “Definition of Done”. If a collection is in DB but not Disk, it is flagged for removal. |
| Circular Dependencies | The buildTree logic includes cycle detection to prevent infinite recursion loop crashing the UI. |
Related Architecture
- Configuration Promotion API — Schema as Code, drift detection, and multi-environment promotion
- Compilation Pipeline — ContentSync, atomic compile, HMR payload
- Content System Architecture — coordinator, stores, SSE
- Collection Store Data Flow — active collection SSR flow
Tests
| Layer | Path |
|---|---|
| Unit | tests/unit/collectionbuilder/*, tests/unit/content/sync-content-* |
| Integration | tests/integration/collectionbuilder/* |
| E2E | tests/e2e/routes/collection-builder/builder.spec.ts |