Skip to content
Documentation GitHub
Architecture

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 identifier
let (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_code validation 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

  1. Agent implemented prefix-based RefCode generation methods — created compile-time coupling between the domain identifier type and entity types
  2. Migration backfill used generate_with_prefix(prefix) — stored prefixed values in database
  3. 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:

StrategyType info lives in…Lookup requires…
Embedded prefixThe identifier value itselfOnly the identifier
Caller-providedThe URL scheme / API parameterIdentifier + 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 nanoid
pub fn generate() -> Self {
Self(nanoid::nanoid!(REF_CODE_LENGTH, &REF_CODE_ALPHABET))
}
// No for_page(), for_block(), etc. — the domain doesn't know entity types

2. Caller provides entity type (application layer)

// CORRECT: entity_type is a parameter, not derived from the ref_code
pub 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 table
let (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 scanning

4. Validate at the boundary (framework layer)

// CORRECT: Tauri command validates both entity_type and ref_code format
if !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_deleted flag 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 UNION or 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 design
  • crates/domain/src/identifiers.rs — RefCode implementation
  • crates/application/src/page/resolve_ref_code.rs — ResolveRefCodeUseCase

Was this page helpful?