Property System
Status: Implemented Depends On: Page System, Type System
Overview
Section titled “Overview”The Property System provides structured metadata for pages through two complementary mechanisms: typed properties defined by property definitions and linked to page types, and freeform properties stored as raw frontmatter key-value pairs. Properties are surfaced in the editor via inline {{name}} references and in the context panel as an editable property sheet.
The system spans all layers — domain definitions in Rust, application use cases with type validation, SQLite persistence, and a rich frontend with dual-trigger autocomplete, click-to-navigate highlighting, and bidirectional markdown serialization.
Data Flow
Section titled “Data Flow”Architecture
Section titled “Architecture”Frontend (React) ├── PropertyRef.ts TipTap inline node extension (atom, inline) │ Renders {{property}} as clickable pill with resolved value ├── usePropertyAutocomplete.ts Hook: dual-trigger ({{ and /property) autocomplete ├── PropertySection.tsx Context panel: editable property sheet ├── markdownUtils.ts Bidirectional {{name:value}} <-> HTML conversion └── appStore.ts highlightProperty state for click-to-navigate
Application ├── CreatePropertyUseCase crates/application/src/property/create.rs ├── UpdatePropertyUseCase crates/application/src/property/update.rs ├── DeletePropertyUseCase crates/application/src/property/delete.rs ├── ListPropertiesUseCase crates/application/src/property/list.rs ├── GetPagePropertiesUseCase crates/application/src/property/get_page_properties.rs │ Resolves typed + freeform properties for a page ├── SetPropertyValueUseCase crates/application/src/property/set_property_value.rs │ Validates value against definition type, stores in frontmatter └── PropertyRepository (trait) crates/application/src/property/services.rs
Domain ├── PropertyDefinition crates/domain/src/property_definition.rs ├── PropertyValueType crates/domain/src/property_definition.rs ├── PropertyConfig crates/domain/src/property_definition.rs ├── SelectOption crates/domain/src/property_definition.rs └── PagePropertyView crates/domain/src/page_property.rs
Infrastructure └── SqlitePropertyRepository crates/infrastructure/sqlite/src/workspace/ (via mod.rs)Property Definitions
Section titled “Property Definitions”A PropertyDefinition is a workspace-scoped schema for a property:
pub struct PropertyDefinition { pub id: Uuid, pub slug: String, // lowercase alphanumeric + hyphens, <= 100 chars pub name: String, // display name, <= 100 chars pub description: Option<String>, pub value_type: PropertyValueType, pub config: PropertyConfig, pub is_system: bool, // system properties cannot be deleted pub created_at: String, pub updated_at: String,}Value Types
Section titled “Value Types”| Type | JSON Shape | Validation |
|---|---|---|
Text | "string" | Must be a string |
Number | 42 or 3.14 | Must be a number |
Boolean | true / false | Must be a boolean |
Date | "2025-01-15" | Must be a string (ISO 8601) |
Select | "option-label" | Must be a string |
MultiSelect | ["a", "b"] | Must be an array of strings |
Relation | "uuid" or ["uuid", ...] | Must be valid UUID(s) |
System Properties
Section titled “System Properties”Four built-in properties with deterministic UUIDs are seeded on workspace creation:
| Property | Slug | Type | UUID Suffix |
|---|---|---|---|
| Summary | summary | Text | ...0011 |
| Cover Image | cover-image | Text | ...0012 |
| Tags | tags | MultiSelect | ...0013 |
| Aliases | aliases | MultiSelect | ...0014 |
System properties cannot be deleted but can be updated (e.g., adding select options).
Database Schema
Section titled “Database Schema”CREATE TABLE properties ( id TEXT PRIMARY KEY, slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL, description TEXT, value_type TEXT NOT NULL, config TEXT NOT NULL DEFAULT '{}', -- JSON (PropertyConfig) is_system INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE INDEX idx_properties_slug ON properties(slug);
CREATE TABLE type_property_refs ( type_id TEXT NOT NULL, property_id TEXT NOT NULL, sort_order INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (type_id) REFERENCES types(id) ON DELETE CASCADE, FOREIGN KEY (property_id) REFERENCES properties(id) ON DELETE CASCADE, PRIMARY KEY (type_id, property_id));Property values are stored in the page’s frontmatter JSON column (in the pages table), keyed by the property slug.
There is no separate property_values table — frontmatter is the single source of truth for values.
Typed vs Freeform Properties
Section titled “Typed vs Freeform Properties”The GetPagePropertiesUseCase resolves properties for a page in two phases:
-
Typed properties: For each type assigned to the page, load linked property definitions (via
type_property_refs). Deduplicate across types (a property linked to multiple types appears once). Look up current values from the page’s frontmatter. -
Freeform properties: Any frontmatter keys not covered by typed properties are returned as freeform entries with
is_from_type: falseandproperty_id: nil.
The result is a Vec<PagePropertyView> — a unified view consumed by the frontend property panel.
Inline Property References
Section titled “Inline Property References”Markdown Syntax
Section titled “Markdown Syntax”Properties can be referenced inline in editor content using double-brace syntax:
| Syntax | Behavior |
|---|---|
{{name}} | Display-only: shows resolved value, no write-back |
{{name:value}} | Write-back: value is extracted on markdown re-import |
TipTap Extension (PropertyRef)
Section titled “TipTap Extension (PropertyRef)”The PropertyRef TipTap node is an inline atom (no cursor inside). It stores two attributes:
propertyName: the property slugpropertyValue: the inline authored value (for{{name:value}}syntax)
The node view renders as a clickable pill:
- Resolved value: shows the current property value if
getPropertyValueis provided - Missing property: styled with
property-ref-pill--missingCSS class - Empty value: styled with
property-ref-pill--empty, shows “(no value)”
Clicking a property pill triggers onPropertyClick, which sets appStore.highlightProperty to scroll and highlight the
property in the context panel.
Dual-Trigger Autocomplete
Section titled “Dual-Trigger Autocomplete”Two triggers activate the property suggestion popup:
| Trigger | Detection | Example |
|---|---|---|
{{ | textBefore.lastIndexOf("{{") with no closing }} | The age is {{ag |
/property or /prop | Regex /\/prop(erty)?(\s.*)?$/ | /property sum |
Mutual exclusion prevents overlapping popups: when property suggestions activate, wiki-link suggestions are dismissed
via dismissWikiLinks().
Insertion Logic
Section titled “Insertion Logic”When a suggestion is selected, the hook uses cascading prefix detection to delete the correct trigger text:
- Try to find
{{trigger — delete from{{to cursor - If no
{{, try/propertyor/proptrigger — delete from/to cursor - Insert a
propertyRefnode with the selected slug
Markdown Serialization
Section titled “Markdown Serialization”The markdownUtils.ts module handles bidirectional conversion:
Markdown to HTML (markdownToHtml):
{{age:34}} -> <property-ref data-property="age" data-value="34">34</property-ref>{{name}} -> <property-ref data-property="name"></property-ref>HTML to Markdown (htmlToMarkdown via serializePropertyRef):
<property-ref data-property="age" data-value="34"> -> {{age:34}}<property-ref data-property="name"> -> {{name}}The data-value attribute distinguishes write-back refs ({{name:value}}) from display-only refs ({{name}}).
FTS5 Integration
Section titled “FTS5 Integration”Property references are naturally indexed by FTS5’s unicode61 tokenizer. The tokenizer treats braces and colons as
separator characters, so {{age:34}} is tokenized to ["age", "34"] without any preprocessing. This means property
names and values are searchable by default.
Key Code Paths
Section titled “Key Code Paths”| Scenario | Entry Point |
|---|---|
| Resolve properties for a page | GetPagePropertiesUseCase::execute() |
| Set a property value | SetPropertyValueUseCase::execute() |
| Create property definition | CreatePropertyUseCase::execute() |
| Autocomplete trigger detection | usePropertyAutocomplete.checkForPropertyRefTrigger() |
| Property pill rendering | PropertyRef TipTap extension addNodeView() |
| Markdown round-trip | markdownUtils.ts markdownToHtml() / htmlToMarkdown() |
Structured metadata for pages. See also: Page System.
Was this page helpful?
Thanks for your feedback!