Identifier Strategy
Overview
Section titled “Overview”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.
| Identifier | Format | Scope | Mutable | Used For |
|---|---|---|---|---|
| UUID | xxxxxxxx-xxxx-4xxx-… | Internal | No | Database PKs, Tauri commands, foreign keys |
| slug | my-page-title | Workspace | Yes | Wiki-links, file paths, human-readable URLs |
ref_code | k3RmNpQaXwY (11 chars) | External | No | Deep 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_idreferencespages.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_slugreferencespages.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.
ref_code
Section titled “ref_code”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()incrates/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.
Generating a ref_code
Section titled “Generating a ref_code”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.
Entity Matrix
Section titled “Entity Matrix”| Entity | UUID | slug | ref_code | Notes |
|---|---|---|---|---|
| Page | ✓ | ✓ | ✓ | All three; slug drives wiki-links and hierarchy |
| Block | ✓ | — | ✓ | No slug; ref_code used for block-level deep links |
| Bookmark | ✓ | — | ✓ | No slug; ref_code used for timeline anchoring |
| Attachment | ✓ | — | ✓ | No slug; ref_code used for file share links |
| Workspace | ✓ | — | — | UUID only; identified by name in the UI |
| Channel | ✓ | — | — | UUID only at present |
| Conversation | ✓ | — | — | UUID only at present |
| Tag | ✓ | ✓ | — | slug used for property bridge and display |
| Type | ✓ | ✓ | — | slug used for type assignment and collection views |
| Property | ✓ | ✓ | — | slug used for property insertion and FTS5 indexing |
Deep Link URL Scheme
Section titled “Deep Link URL Scheme”The inklings:// custom URL scheme routes deep links within the desktop application.
| Resource | URL Pattern | Example |
|---|---|---|
| Page | inklings://p/{page_ref_code} | inklings://p/k3RmNpQaXwY |
| Block anchor | inklings://p/{page_ref_code}#{block_ref_code} | inklings://p/k3RmNpQaXwY#Xp7TmLqVwNa |
| Workspace | inklings://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.
Collision Analysis
Section titled “Collision Analysis”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.
Database Storage
Section titled “Database Storage”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):
| Table | Column | Index |
|---|---|---|
pages | ref_code | idx_pages_ref_code |
blocks | ref_code | idx_blocks_ref_code |
bookmarks | ref_code | idx_bookmarks_ref_code |
attachments | ref_code | idx_attachments_ref_code |
Developer Decision Guide
Section titled “Developer Decision Guide”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:
- Add
ref_code: Stringto the domain entity struct. - Initialize with
RefCode::generate().to_string()in the constructor. - Add
ALTER TABLE … ADD COLUMN ref_code TEXT NOT NULL DEFAULT ''in the next migration. - Add
CREATE UNIQUE INDEX … ON …(ref_code) WHERE ref_code != ''in the same migration.
Related
Section titled “Related”- Database Schema — Full schema reference including
ref_codecolumn details - Domain Rules — Business invariants enforced in Rust
- Source:
crates/domain/src/identifiers.rs—RefCodetype implementation - Source:
crates/infrastructure/sqlite/src/migrations/mod.rs— V002 migration SQL
Was this page helpful?
Thanks for your feedback!