SurrealDB IDs: Why We Use `table:id` Everywhere
Stripping table prefixes from IDs is a common mistake that breaks multi-model databases. Here is why we keep the full SurrealDB ID format across Rust, the API, and the Frontend.
Stripping table prefixes from IDs is a common mistake that breaks multi-model databases. Here is why we keep the full SurrealDB ID format across Rust, the API, and the Frontend.

In most relational databases, IDs are integers or UUIDs. When you send them to your frontend, they are just strings: "550e8400-e29b-41d4-a716-446655440000".
If you use SurrealDB, you've seen IDs like user:550e8400.... The temptation for many developers is to strip that user: prefix. "The frontend knows it's a user, why clutter the ID?"
In my system, we made a strict architectural decision: IDs are always table:id everywhere.
From the SurrealDB record to the Rust struct, through the JSON API, and into the React component—the prefix stays. Here is why.
SurrealDB is a multi-model database with first-class graph capabilities. Relationships are first-class citizens. When you have a has_skill edge, the in and out fields can point to anything.
If you have a generic Activity feed, one item might be message:123 and the next might be execution:456. If you strip the prefixes, the frontend sees 123 and 456 and has no idea which component to render or which API route to call.
By keeping the prefix, every ID is self-describing.
Consider a vertical slice (see Full-Stack Vertical Slices) that handles workspaces and agents.
If an API endpoint accepts an ID, and you've stripped the prefixes, you need to be extremely careful with your naming: workspace_id, agent_id, user_id.
If you keep the prefixes, your code becomes simpler and more resilient:
// Rust: The ID itself tells you what it is
pub fn get_resource(id: RecordId) {
match id.table.as_str() {
"workspace" => handle_workspace(id),
"agent" => handle_agent(id),
_ => return_error(),
}
}
In SurrealDB, if you have a RecordId, you can use it directly in queries without knowing the table name in advance.
-- This works even if $target_id points to an agent, a user, or a workspace
-- Note: $target_id must be a typed RecordId parameter, not a plain string
SELECT * FROM $target_id;
If your application layer (Rust/TypeScript) passes around stripped strings, you have to manually reconstruct the ID before every database call—and since SurrealDB 2.0 no longer eagerly casts strings to record IDs, getting this wrong means silent failures. If you keep the full ID, the data flows from the UI back to the database without transformation.
In our TanStack Start frontend, we use these IDs in the URL.
URL: /workspace/workspace:abc123/agent/agent:xyz789
When the frontend agent (or a human developer) looks at that URL, there is zero ambiguity. We know exactly which tables are being queried. When we generate breadcrumbs or logs, we have the table context built into the data.
To make this work in Rust, we use a custom serde module to ensure that RecordId types are serialized as strings including the prefix.
#[derive(Serialize, Deserialize)]
pub struct Agent {
#[serde(default, with = "surreal_id", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
The custom surreal_id serde module normalizes SurrealDB's various RecordId representations into the flat "table:id" string. This ensures that the JSON sent over the wire is always {"id": "agent:uuid"}.
The downside? The colon (:) is a reserved character in URLs. While RFC 3986 technically allows colons unencoded in path segments, some frameworks (like Express route patterns) treat them specially, so we encode them to be safe.
/workspace/workspace%3Aabc123
It's slightly "uglier." But aesthetics are a small price to pay for a system where every piece of data knows exactly what it is and where it belongs.
Don't strip your prefixes. SurrealDB IDs are not just identifiers; they are pointers in a global graph. By keeping the table:id format across your entire stack, you eliminate a massive category of mapping bugs and unlock the full potential of a multi-model database.
In my system, the ID is the source of truth. One format, one standard, zero ambiguity.