Skip to content
Documentation GitHub
Content

Property System

Status: Implemented Depends On: Page System, Type System


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.


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)

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,
}
TypeJSON ShapeValidation
Text"string"Must be a string
Number42 or 3.14Must be a number
Booleantrue / falseMust 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)

Four built-in properties with deterministic UUIDs are seeded on workspace creation:

PropertySlugTypeUUID Suffix
SummarysummaryText...0011
Cover Imagecover-imageText...0012
TagstagsMultiSelect...0013
AliasesaliasesMultiSelect...0014

System properties cannot be deleted but can be updated (e.g., adding select options).


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.


The GetPagePropertiesUseCase resolves properties for a page in two phases:

  1. 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.

  2. Freeform properties: Any frontmatter keys not covered by typed properties are returned as freeform entries with is_from_type: false and property_id: nil.

The result is a Vec<PagePropertyView> — a unified view consumed by the frontend property panel.


Properties can be referenced inline in editor content using double-brace syntax:

SyntaxBehavior
{{name}}Display-only: shows resolved value, no write-back
{{name:value}}Write-back: value is extracted on markdown re-import

The PropertyRef TipTap node is an inline atom (no cursor inside). It stores two attributes:

  • propertyName: the property slug
  • propertyValue: the inline authored value (for {{name:value}} syntax)

The node view renders as a clickable pill:

  • Resolved value: shows the current property value if getPropertyValue is provided
  • Missing property: styled with property-ref-pill--missing CSS 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.

Two triggers activate the property suggestion popup:

TriggerDetectionExample
{{textBefore.lastIndexOf("{{") with no closing }}The age is {{ag
/property or /propRegex /\/prop(erty)?(\s.*)?$//property sum

Mutual exclusion prevents overlapping popups: when property suggestions activate, wiki-link suggestions are dismissed via dismissWikiLinks().

When a suggestion is selected, the hook uses cascading prefix detection to delete the correct trigger text:

  1. Try to find {{ trigger — delete from {{ to cursor
  2. If no {{, try /property or /prop trigger — delete from / to cursor
  3. Insert a propertyRef node with the selected slug

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}}).


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.


ScenarioEntry Point
Resolve properties for a pageGetPagePropertiesUseCase::execute()
Set a property valueSetPropertyValueUseCase::execute()
Create property definitionCreatePropertyUseCase::execute()
Autocomplete trigger detectionusePropertyAutocomplete.checkForPropertyRefTrigger()
Property pill renderingPropertyRef TipTap extension addNodeView()
Markdown round-tripmarkdownUtils.ts markdownToHtml() / htmlToMarkdown()

Structured metadata for pages. See also: Page System.

Was this page helpful?