Skip to content
Documentation GitHub
Architecture

Domain Rules & Business Invariants

Last Updated: February 2026 Context: Local-first Tauri desktop application with Rust backend and SQLite storage

This document captures the critical business rules and domain invariants that govern the Inklings knowledge management system. These rules are enforced in the domain layer (Rust crates) regardless of storage mechanism.

Principle: Business logic enforces business rules. The database stores data; the application enforces integrity.


Rule: Every page MUST have at least one block at all times.

Enforcement:

  • Creation: Creating a page atomically creates an initial empty block
  • Deletion: Cannot delete the last block of a page

Rationale:

  • UX Consistency: Pages without content are meaningless
  • Rendering Simplification: Frontend can always assume at least one block exists
  • State Machine Clarity: No “empty page” edge cases

Rule: Cannot move a page to become a descendant of itself (prevents cycles).

Enforcement: Check ancestry chain before move operations.

Example Violation:

Initial:
A
└── B
└── C
User attempts: Move A to be child of C
Result: Rejected - C is descendant of A
If allowed:
C (new root)
└── A
└── B
└── C ← CYCLE!

Rationale: Maintains tree integrity, prevents infinite loops in traversal.


Rule: Deleting a page deletes all its descendants.

UX Requirement: Frontend MUST warn user before deletion with descendant count.

Rationale:

  • Simplicity: No orphaned pages to manage
  • Predictability: Clear mental model for users
  • Data Integrity: No dangling references

Rule: All entities belong to exactly one workspace. Pages belong directly to the workspace; blocks inherit through page relationship.

Adaptation for Local-First:

  • Each workspace is a self-contained folder on disk with its own inklings.db
  • No cross-workspace references

Rationale:

  • Data Isolation: Clear boundaries per workspace
  • Portability: Each workspace is self-contained

Rule: Workspaces are flat containers (no nested workspaces).

Rationale:

  • Simplicity: Follows established PKM patterns (Obsidian, Notion spaces)
  • Clear Boundaries: Each workspace is independent
  • UX Clarity: No “workspace within workspace” confusion

Users can have multiple workspaces, but they don’t nest.


Rule: Blocks inherit workspace context through their Page relationship (blocks don’t directly track workspace membership).

Rationale:

  • Cleaner Domain Model: Blocks don’t “know” about workspaces
  • Referential Integrity: Workspace context always valid via page
  • Simplified Logic: No denormalization to maintain

Import functionality must respect existing invariants:

Existing RuleImport Impact
Min 1 block per pageEmpty imports still create initial block
Workspace scopingAll imported content belongs to target workspace
Flat workspacesSubfolder structure not preserved as workspace hierarchy

Design principles for import:

  • Atomic: Import succeeds completely or fails cleanly
  • Lossless: Wiki-links preserved as references (resolved or ghost)
  • Passthrough: Unrecognized frontmatter keys preserved, not discarded
  • Non-destructive: Conflicts prompt user, never silently overwrite

Entity-level validation (required fields, construction invariants):

impl Page {
pub fn new(title: impl Into<String>) -> Self {
let now = Utc::now();
// Atomically creates initial empty block (enforces min-1-block invariant)
let initial_block = Block::new(1, "");
Self {
id: Uuid::new_v4(),
title: title.into(),
page_type: PageType::default(),
blocks: vec![initial_block],
created_at: now,
updated_at: now,
// template, icon, frontmatter, type_assignments, layout: None/empty
}
}
}

Business rule validation (invariants):

impl CreatePageUseCase {
pub fn execute(&self, request: CreatePageRequest) -> Result<Page, BusinessRuleError> {
// Validate business rules (e.g., cycle detection for moves)
// ...
}
}
Error TypeLayerExample
ValidationErrorDomainInvalid title length
BusinessRuleErrorApplicationCycle detected
StorageErrorInfrastructureDatabase query failure

Rule: Every named area in a CSS Grid layout template MUST form a contiguous rectangle.

Enforcement: Layout::new() validates the template by computing the bounding box for each named area and verifying that the actual cell count equals the expected bounding box area: (max_row - min_row + 1) * (max_col - min_col + 1).

Additional constraints:

  • Template must contain at least one area
  • All rows must have the same column count
  • Area names may only contain alphanumeric characters, hyphens, or underscores
  • Templates are bounded to 20 rows, 20 columns, and 50 distinct area names

Example Violation:

a b
a a <- "a" forms an L-shape, not a rectangle: REJECTED

Rationale: CSS Grid grid-template-areas requires rectangular regions. Validating at the domain level prevents invalid layouts from reaching storage or the frontend.


Rule: Tag and tag group names must be non-empty, at most 100 Unicode scalar values, and must not have leading or trailing whitespace.

Enforcement: Tag::new() and TagGroup::new() both validate via validate_label_name() before construction.

Additional constraints:

  • Tag slugs are derived from names via slugify() (lowercase, hyphens)
  • Tag colors must be valid 6-digit hex format (e.g., #ff5733)

Rationale: Consistent naming prevents display issues, slug collisions, and whitespace-related bugs in search and autocomplete.


Rule: Attachment filenames must be non-empty, at most 255 characters (Unicode char count), and must not contain path separators (/ or \).

Enforcement: Attachment::new() validates filename, size, and extension before construction.

Additional constraints:

  • File size must be greater than zero
  • Only known-safe file extensions are allowed via an allowlist (images, documents, common office formats)
  • Content hash (SHA-256) is stored for dedup

Rationale: Prevents path traversal attacks, empty uploads, and unknown file types from entering the system.


Rule: Block content type (Markdown or Image) is fixed at creation time and cannot be changed.

Enforcement: BlockContentType is set during block creation; no mutation method exists.

Additional constraints:

  • Image blocks require valid ImageBlockMetadata (attachment ID, scale, alignment)
  • Image captions must not exceed 500 characters
  • Image link URLs must use http:// or https:// scheme and not exceed 2048 bytes
  • Image scale percentage must be 1-100

Rationale: Changing a block’s fundamental type would invalidate its stored content (CRDT doc vs. image metadata). Type-specific rendering and FTS indexing depend on a stable content type.


Section titled “Rule 11: Wiki-Link References Track Resolution State”

Rule: References (wiki-links) are either resolved (pointing to an existing page) or unresolved (ghost links). Resolution state is tracked explicitly via the resolved flag and target_page_id.

Enforcement: Reference::resolved() and Reference::unresolved() constructors enforce the correct combination of fields. The UpdateReferencesUseCase re-resolves all links on content save.

Additional constraints:

  • Renaming a page propagates slug changes to all referencing pages
  • Deleting a page converts resolved references to ghost (unresolved) links

Rationale: Maintaining explicit resolution state enables ghost link detection, backlink queries, and rename propagation without full-text scanning.


Rule: Builtin layout definitions (is_builtin = true) cannot be updated or deleted by the user.

Enforcement: LayoutDefinition has is_builtin flag; application layer rejects mutations on builtins.

Additional constraints:

  • Layout definition names must be non-empty, at most 100 characters, and not whitespace-only
  • Descriptions must not exceed 500 characters
  • Builtin IDs are deterministic (UUID v5 from name) and stable across restarts

Rationale: Builtins provide a consistent baseline; user-created definitions extend but never replace them.


RuleEnforcementRationale
Min 1 block per pageDomain/ApplicationUX consistency
No page cyclesApplicationTree integrity
Cascade deletionApplicationNo orphans
Workspace scopingDomain designData isolation
Flat workspacesSchema designSimplicity
Block-workspace inheritanceDomain designClean model
Layout rectangle validationDomain (Layout::new())CSS Grid compliance
Tag name validationDomain (Tag::new())Naming consistency
Attachment filename validationDomain (Attachment::new())Security, data integrity
Block content type immutabilityDomain designStable rendering/indexing
Reference resolution trackingDomain/ApplicationGhost links, backlinks
Builtin layout immutabilityApplicationConsistent baseline
Type definition name validationDomain (validate_name())Naming consistency
Property definition name valid.Domain (validate_name())Naming consistency
Container rule depth constraintDomain (closed enum)Unambiguous propagation

Rule: Type definition names must be non-empty and at most 100 Unicode characters.

Enforcement: TypeDefinition::validate_name() checks constraints before creation or update.

Rationale: Consistent naming prevents display issues and ensures type names fit within UI constraints.

Rule 14: Property Definition Name Validation

Section titled “Rule 14: Property Definition Name Validation”

Rule: Property definition names must be non-empty and at most 100 Unicode characters.

Enforcement: PropertyDefinition::validate_name() checks constraints before creation or update.

Rationale: Same consistency guarantees as type names, plus property names appear in inline {{name:value}} syntax where excessively long names would degrade readability.

Rule: Container rules specify a depth (DirectChildren or Recursive) that controls how deep auto-type-assignment propagates within a folder hierarchy.

Enforcement: ContainerRuleDepth is a closed enum — only the two valid variants can be constructed. Parsing from storage rejects unknown values.

Rationale: Unconstrained depth semantics would create ambiguous behavior for nested folder structures.


Key Principle: Business rules are the most stable part of the system. They live in the domain and application layers, not the storage layer. Storage enforces data integrity; the application enforces business integrity.

Was this page helpful?