Identifier Entity-Type Dispatch: Caller-Provided vs Embedded
Identifier Entity-Type Dispatch: Caller-Provided vs Embedded
Problem
When implementing a resolve_ref_code() function to look up entities by their short external identifier, the
implementation embedded entity-type prefixes (pg_, bk_, bm_, at_) directly into the ref_code values. This
created 14-char identifiers (3-char prefix + 11-char nanoid) instead of the canonical 11-char base62 format.
The resolution function then used starts_with() prefix parsing to determine which table to query:
// WRONG: Prefix-based dispatch embedded in the identifierlet (table, entity_type) = if ref_code.starts_with("pg_") { ("pages", "page")} else if ref_code.starts_with("bk_") { ("blocks", "block")} else if ref_code.starts_with("bm_") { ("bookmarks", "bookmark")} else if ref_code.starts_with("at_") { ("attachments", "attachment")} else { return Ok(None); // Unknown prefix};Symptoms:
ref_codevalidation checked for 14 chars instead of 11- Entity-specific
RefCode::for_page(),RefCode::for_block()etc. methods polluted the domain - Migration backfill generated prefixed values
- Deep-link URL scheme (
inklings://p/{ref_code}) would carry redundant type info - Cross-table coupling: the repository had to know about all entity tables
Investigation
Steps Tried
- Agent implemented prefix-based
RefCodegeneration methods — created compile-time coupling between the domain identifier type and entity types - Migration backfill used
generate_with_prefix(prefix)— stored prefixed values in database - Command validation checked
ref_code.len() != 14— broke the contract with the documented 11-char format
What Revealed the Error
The user caught the drift: “The entity designators were part of the URL, as in inklings://{type}/{ref_code} or
inklings://p/k3RmNpQaXw.” Verification against the canonical identifier-strategy.mdx confirmed ref_codes are plain
11-char base62 with no embedded prefix.
Root Cause
Confusion between two valid dispatch strategies:
| Strategy | Type info lives in… | Lookup requires… |
|---|---|---|
| Embedded prefix | The identifier value itself | Only the identifier |
| Caller-provided | The URL scheme / API parameter | Identifier + context |
The project’s design (documented in identifier-strategy.mdx) uses caller-provided dispatch: the deep-link URL
scheme path segment (inklings://p/, inklings://a/) carries the entity type. The ref_code is an opaque lookup key
within a known table.
The agent defaulted to the embedded-prefix pattern (common in systems like Stripe’s sk_live_, pi_ prefixes) without
checking the existing architectural documentation.
Solution
1. Keep ref_codes opaque (domain layer)
// CORRECT: RefCode is a plain 11-char base62 nanoidpub fn generate() -> Self { Self(nanoid::nanoid!(REF_CODE_LENGTH, &REF_CODE_ALPHABET))}// No for_page(), for_block(), etc. — the domain doesn't know entity types2. Caller provides entity type (application layer)
// CORRECT: entity_type is a parameter, not derived from the ref_codepub fn execute( &self, guard: &PermissionGuard, entity_type: &str, // "page", "block", "bookmark", "attachment" ref_code: &str,) -> PageResult<Option<ResolvedRef>> { ... }3. Single-table dispatch (infrastructure layer)
// CORRECT: Match on caller-provided entity_type, query single tablelet (table, filter_deleted) = match entity_type { "page" => ("pages", true), "block" => ("blocks", false), "bookmark" => ("bookmarks", false), "attachment" => ("attachments", false), _ => return Ok(None),};// Single indexed query, no cross-table scanning4. Validate at the boundary (framework layer)
// CORRECT: Tauri command validates both entity_type and ref_code formatif !matches!(entity_type.as_str(), "page" | "block" | "bookmark" | "attachment") { return Ok(None);}if ref_code.len() != 11 || !ref_code.chars().all(|c| c.is_ascii_alphanumeric()) { return Ok(None);}Implementation Notes
- Entity type flows from URL scheme → command layer → use case → repository
- Repository never infers type from the identifier — it’s a pure lookup
filter_deletedflag is entity-specific (only pages support soft delete)- TypeScript bindings regenerated:
resolveRefCode(entityType, refCode, opId)
Prevention
Best Practices
- Check architectural docs before implementing identifier patterns. The canonical design is in
identifier-strategy.mdx— it specifies format, length, alphabet, and URL scheme. - Identifiers should be opaque at the domain level. If entity type needs to be known, it comes from the caller’s context (URL, API parameter), not from parsing the identifier.
- Single Responsibility for resolution: The repository resolves within one table. The caller (framework/command layer) decides which table.
Warning Signs
RefCode::for_X()entity-specific constructors appearing in the domain layer- Validation checking for lengths other than the documented 11 chars
- Sequential
if starts_with()chains in resolution logic - Cross-table
UNIONor fallback queries in a single resolution call
Decision Framework
Use embedded prefixes when:
- Identifiers are the only context (no URL scheme, no API envelope)
- System needs to route opaque tokens from untrusted sources
- Example: Stripe API keys (
sk_live_...), where the key alone must identify its type
Use caller-provided type when:
- A structured envelope carries context (URL scheme, API parameter, message header)
- Identifiers are scoped within a known entity type at the call site
- Example:
inklings://p/{ref_code}— the path segment/p/provides type
References
apps/codex/src/content/docs/architecture/identifier-strategy.mdx— canonical identifier designcrates/domain/src/identifiers.rs— RefCode implementationcrates/application/src/page/resolve_ref_code.rs— ResolveRefCodeUseCase
FTS5 unicode61 Tokenizer Implicitly Indexes Custom Syntax Next
Identity vs Auth vs Analytics: Three-Concern Separation
Was this page helpful?
Thanks for your feedback!