Collection Builder Architecture
Technical deep-dive into the Collection Builder architecture, explaining the reconciliation loop between Filesystem, Database, and UI.
On this page
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") — virtual folders created in the GUI. Persisted incontent_nodesand backed up in.compilation-manifest.jsonunderstructureNodes.
GUI drag-and-drop updates organization (parentId, order) in DB + manifest. It does not move .ts files on disk unless you save a new collection path or move files manually.
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()(viasaveContentStructureremote or legacy?/saveConfigaction) upsertsmove/rename/create/deleteoperations throughsyncContentState({ reason: "gui-save" }), syncscontentStore, writes the manifest, and broadcasts SSE — it does not callfullReload(). - 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/) |
|---|---|---|
| Drag collection into category | parentId, order updated |
Unchanged — org-only |
| Create virtual category | New content_nodes row + structureNodes |
No folder created |
| Delete builder category | Removed from DB + manifest | N/A |
| 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.
- 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.
- Interaction: Drag-and-drop operations do not mutate the tree structure directly. Instead, they emit updates to the flat list (e.g.,
updateNode(id, { parentId: newParent })), which triggers a server save and a reactive re-render.
⌨️ 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
- 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 |