Skip to content
Documentation GitHub
Architecture

Identifier Strategy

Every entity in Inklings uses one or more of three identifier types, each with a distinct purpose and scope. Using the wrong identifier in the wrong context causes subtle bugs, so this document establishes clear rules for when to use each.

IdentifierFormatScopeMutableUsed For
UUIDxxxxxxxx-xxxx-4xxx-…InternalNoDatabase PKs, Tauri commands, foreign keys
slugmy-page-titleWorkspaceYesWiki-links, file paths, human-readable URLs
ref_codek3RmNpQaXwY (11 chars)ExternalNoDeep links, share URLs, cross-workspace refs

Format: UUID v4 (e.g., 550e8400-e29b-41d4-a716-446655440000)

Where used:

  • Database primary keys for all entity tables
  • Tauri command parameters for all create, update, and delete operations
  • Foreign key references between tables (e.g., blocks.page_id references pages.id)
  • Internal service and repository method signatures

Properties:

  • Generated at entity creation via Uuid::new_v4()
  • Immutable — never changes after creation
  • Never exposed directly to users in the UI
  • Universally unique — no collision concern at any realistic scale

Rule: All Tauri commands that operate on a specific entity take a UUID. If you are adding a new command that targets a page, block, bookmark, or attachment, it accepts a UUID — not a slug and not a ref_code.


Format: URL-safe lowercase string derived from the entity title (e.g., my-page-title)

Where used:

  • Wiki-link syntax: [[My Page Title|my-page-title]]
  • Parent-child hierarchy: pages.parent_slug references pages.slug
  • Markdown file paths during import and export
  • Human-readable display in navigation

Properties:

  • Generated by slugify() from the entity title at creation time
  • Mutable — changes when the page is renamed
  • Workspace-scoped — two different workspaces can have the same slug
  • Not guaranteed unique across time — a deleted page’s slug can be reused by a new page

Rename propagation: When a page is renamed, UpdateReferencesUseCase rewrites all [[…|old-slug]] references to use the new slug. check_links_resolution detects any ghost links (references to slugs with no matching page).

Rule: Use slugs only for wiki-link resolution, display purposes, and import/export. Never pass a slug to a command that modifies data — use the UUID instead.


Format: 11-character base62 string (e.g., k3RmNpQaXwY)

Alphabet: A-Z, a-z, 0-9 (62 characters, no look-alike ambiguity problems beyond base62 itself)

Entropy: 62^11 ≈ 5.2 × 10^19 — approximately 65.5 bits. At one billion entities, the probability of any single collision is under 10^-10.

Where used:

  • Deep links: inklings://p/{ref_code} (page), inklings://ws/{ref_code} (workspace)
  • Block anchoring: inklings://p/{page_ref_code}#{block_ref_code}
  • MCP API responses — external callers reference entities by ref_code
  • Cross-workspace references where UUID alone is insufficient
  • Share URLs when cloud sharing is enabled

Properties:

  • Generated at entity creation via RefCode::generate() in crates/domain/src/identifiers.rs
  • Immutable — never changes after creation, even on rename
  • Globally meaningful — survives page renames and workspace moves
  • Stored as TEXT NOT NULL DEFAULT '' in SQLite with a partial unique index

Rule: Use ref_code for anything that leaves the app boundary: deep links, share URLs, external API responses, and clipboard-copy operations.

use domain::RefCode;
let code = RefCode::generate();
// "k3RmNpQaXwY" — 11 chars, [A-Za-z0-9]

Loading from storage uses RefCode::from_existing() — this bypasses generation and wraps a value already in the database.


EntityUUIDslugref_codeNotes
PageAll three; slug drives wiki-links and hierarchy
BlockNo slug; ref_code used for block-level deep links
BookmarkNo slug; ref_code used for timeline anchoring
AttachmentNo slug; ref_code used for file share links
WorkspaceUUID only; identified by name in the UI
ChannelUUID only at present
ConversationUUID only at present
Tagslug used for property bridge and display
Typeslug used for type assignment and collection views
Propertyslug used for property insertion and FTS5 indexing

The inklings:// custom URL scheme routes deep links within the desktop application.

ResourceURL PatternExample
Pageinklings://p/{page_ref_code}inklings://p/k3RmNpQaXwY
Block anchorinklings://p/{page_ref_code}#{block_ref_code}inklings://p/k3RmNpQaXwY#Xp7TmLqVwNa
Workspaceinklings://ws/{workspace_ref_code}(workspace ref_code not yet assigned)

Deep links are resolved by the Tauri URL scheme handler. The handler looks up the entity by ref_code, navigates to the matching page, and optionally scrolls to the block.


At 11 characters with a 62-character alphabet:

  • Total keyspace: 62^11 ≈ 5.2 × 10^19
  • With 1,000 entities: probability of any collision ≈ 9.6 × 10^-17
  • With 1,000,000 entities: probability of any collision ≈ 9.6 × 10^-11
  • With 1,000,000,000 entities: probability of any collision ≈ 9.6 × 10^-5

A typical workspace holds fewer than 100,000 entities. The collision risk is negligible at all realistic scales.


ref_code columns are stored as TEXT NOT NULL DEFAULT '' with a partial unique index that ignores empty-string migration backfill values:

ALTER TABLE pages ADD COLUMN ref_code TEXT NOT NULL DEFAULT '';
CREATE UNIQUE INDEX idx_pages_ref_code ON pages(ref_code) WHERE ref_code != '';

The V002 migration adds the columns with empty defaults. A post-migration Rust hook backfills each empty ref_code with a proper RefCode::generate() value (11-char nanoid, base62 alphabet).

Tables with ref_code columns (added in schema V002):

TableColumnIndex
pagesref_codeidx_pages_ref_code
blocksref_codeidx_blocks_ref_code
bookmarksref_codeidx_bookmarks_ref_code
attachmentsref_codeidx_attachments_ref_code

When adding a new feature, use this guide to choose the right identifier:

Q: Is this an internal operation (create, update, delete) within the app? Use UUID. Pass id: Uuid in the Tauri command and use case request.

Q: Is this a wiki-link, a navigation breadcrumb, or a markdown path? Use slug. Slugs are designed for human-readable, mutable references within a workspace.

Q: Is this a URL, share link, deep link, or any identifier that will leave the app boundary? Use ref_code. It is stable (rename-proof) and short enough for URLs.

Q: I am adding a new entity. Should it get a ref_code? Yes, if the entity will ever be addressable from outside the app (via deep link, MCP tool, or share URL). Entities used purely as internal junction tables (e.g., page_tags) do not need a ref_code. To add one:

  1. Add ref_code: String to the domain entity struct.
  2. Initialize with RefCode::generate().to_string() in the constructor.
  3. Add ALTER TABLE … ADD COLUMN ref_code TEXT NOT NULL DEFAULT '' in the next migration.
  4. Add CREATE UNIQUE INDEX … ON …(ref_code) WHERE ref_code != '' in the same migration.

  • Database Schema — Full schema reference including ref_code column details
  • Domain Rules — Business invariants enforced in Rust
  • Source: crates/domain/src/identifiers.rsRefCode type implementation
  • Source: crates/infrastructure/sqlite/src/migrations/mod.rs — V002 migration SQL

Was this page helpful?