How Macro’s Bidirectional @linking System Works Across Different Block Types
Macro’s bidirectional @linking system uses a generic entity_mentions table to store typed relationships between any two blocks, enabling both forward and reverse lookups through a single database row.
The Macro platform treats documents, chats, projects, and other content types as interchangeable blocks that can reference each other via @link syntax. Rather than implementing custom linking logic per block type, the codebase centralizes this capability in a purpose-built entity_mentions crate. This article examines the architecture, database schema, service integration patterns, and API surface that make bidirectional linking work uniformly across all block types.
The Core Data Model: entity_mentions Table
The foundation of Macro's linking system is a single, type-agnostic database table defined in crates/entity_mentions/src/db.rs. Each row represents one directed relationship between two blocks:
| Column | Purpose |
|---|---|
source_entity_type |
Enum string identifying the block type originating the link (e.g., "Document", "Chat") |
source_entity_id |
UUID of the specific source block instance |
target_entity_type |
Enum string identifying the block type being linked to |
target_entity_id |
UUID of the specific target block instance |
created_at / updated_at |
Timestamps for cache invalidation and sorting |
metadata |
Optional JSONB for context like anchor text position |
Because a single row captures both the outbound relationship (source → target) and the inbound relationship (target ← source), no duplicate "reverse" entries are required. The schema includes composite indexes on both (source_entity_type, source_entity_id) and (target_entity_type, target_entity_id) to keep bidirectional queries performant at scale.
Database Layer: Compiled Queries with sqlx
The entity_mentions crate exposes a thin, type-safe database interface built on sqlx. All queries use sqlx::query! for compile-time schema validation. The four primary operations are:
create_mention(source, target)– Inserts a new link row after validating that both entities exist.delete_mention(source, target)– Removes a specific link by its composite key.get_outbound_links(entity)– Returns all targets linked from a given block.get_inbound_links(entity)– Returns all sources linking to a given block.
This design decouples the linking mechanism from any specific block type. Services import these functions and supply their own EntityType variants.
Service Integration: How Block Types Participate
Each microservice that owns a block type integrates with the linking system through a consistent pattern. When a block is persisted, the service:
- Parses its content for
@linktokens. - Validates target existence via service-specific DAOs.
- Calls
entity_mentions::db::create_mentionfor each valid link. - Calls
entity_mentions::db::delete_mentionfor any links that were removed during editing.
The document storage service (crates/document_storage_service/src/service.rs) demonstrates this flow. On document save, it extracts @link annotations from the Markdown AST, resolves each target through the appropriate service client, and batch-inserts mentions. Similarly, the chat service (crates/chat_service/src/service.rs) scans message content for @link syntax and maintains mentions accordingly.
For read operations, services call get_inbound_links to populate "referenced by" UI panels. A project page, for example, queries for all documents and chats linking to it without needing to know the storage details of those referencing blocks.
API Surface: REST Endpoints for the Frontend
The entity_mentions crate exposes HTTP handlers in crates/entity_mentions/src/api.rs that the Tauri-based frontend consumes:
// POST /entity-mentions
// Creates a new bidirectional link
async fn create_mention_handler(
Json(body): Json<CreateMentionRequest>,
) -> Result<impl IntoResponse, AppError> {
let (source_type, source_id) = parse_entity(&body.source)?;
let (target_type, target_id) = parse_entity(&body.target)?;
db::create_mention(&mut conn, (source_type, source_id), (target_type, target_id)).await?;
Ok(StatusCode::CREATED)
}
// GET /entity-mentions?entity_type=...&entity_id=...&direction=outbound|inbound
// Retrieves linked entities in either direction
async fn list_mentions_handler(
Query(params): Query<ListMentionsQuery>,
) -> Result<Json<Vec<MentionResponse>>, AppError> {
let links = match params.direction {
Direction::Outbound => db::get_outbound_links(&mut conn, params.entity_type, params.entity_id).await?,
Direction::Inbound => db::get_inbound_links(&mut conn, params.entity_type, params.entity_id).await?,
};
Ok(Json(links))
}
// DELETE /entity-mentions
// Removes a specific link
async fn delete_mention_handler(
Json(body): Json<DeleteMentionRequest>,
) -> Result<StatusCode, AppError> {
db::delete_mention(&mut conn, (body.source_type, body.source_id), (body.target_type, body.target_id)).await?;
Ok(StatusCode::NO_CONTENT)
}
These endpoints power the frontend's @link autocomplete, link creation UI, and "linked items" sidebars.
Block-Type-Agnostic Parsing and Resolution
The Markdown parser extracts @link tokens using the pattern @{type}:{id}. A central entity resolver maps the type string to the EntityType enum and validates target existence through pluggable service clients:
// crates/entity_mentions/src/resolver.rs
pub enum EntityType {
Document,
Chat,
Project,
// Extensible: new block types add variants here
}
pub async fn resolve_and_link(
conn: &mut PgConnection,
source: (EntityType, Uuid),
target_ref: &str, // e.g., "chat:c7b2e4..."
) -> Result<(), ResolutionError> {
let (target_type, target_id) = parse_entity_ref(target_ref)?;
validate_target_exists(target_type, target_id).await?;
db::create_mention(conn, source, (target_type, target_id)).await?;
Ok(())
}
Adding a new block type requires only extending EntityType, registering a validator, and updating the parser's allowed prefix set—no changes to the core linking logic.
Code Examples: Working with @linking
Creating a link from a document to a chat:
use entity_mentions::db::create_mention;
use entity_mentions::EntityType;
use sqlx::Acquire;
async fn link_document_to_chat(
conn: &mut sqlx::PgConnection,
doc_id: Uuid,
chat_id: Uuid,
) -> anyhow::Result<()> {
let source = (EntityType::Document, doc_id);
let target = (EntityType::Chat, chat_id);
create_mention(&mut *conn.begin().await?, source, target).await?;
Ok(())
}
Fetching all blocks that reference a specific project:
use entity_mentions::db::get_inbound_links;
use entity_mentions::EntityType;
async fn get_project_references(
conn: &mut sqlx::PgConnection,
project_id: Uuid,
) -> anyhow::Result<Vec<(EntityType, Uuid)>> {
let inbound = get_inbound_links(conn, EntityType::Project, project_id).await?;
Ok(inbound)
}
Frontend JavaScript (Tauri) creating a link:
async function createDocumentToChatLink(docId, chatId) {
const response = await fetch('/entity-mentions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source_type: 'Document',
source_id: docId,
target_type: 'Chat',
target_id: chatId
})
});
if (!response.ok) throw new Error(`Link failed: ${response.status}`);
return response.status === 201;
}
Fetching linked items for a sidebar:
async function getLinkedItems(entityType, entityId, direction = 'inbound') {
const params = new URLSearchParams({ entity_type: entityType, entity_id: entityId, direction });
const response = await fetch(`/entity-mentions?${params}`);
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
return response.json(); // [{ entity_type, entity_id, created_at }, ...]
}
Key Source Files
| Path | Responsibility |
|---|---|
crates/entity_mentions/src/lib.rs |
Public API exports and EntityType enum definition |
crates/entity_mentions/src/db.rs |
SQLx queries for create_mention, delete_mention, get_outbound_links, get_inbound_links |
crates/entity_mentions/src/api.rs |
Axum HTTP handlers for POST/GET/DELETE endpoints |
crates/entity_mentions/src/resolver.rs |
@link token parsing and target validation |
crates/document_storage_service/src/service.rs |
Example service integrating mentions on document save |
crates/chat_service/src/service.rs |
Example service scanning messages for @link syntax |
docs/STYLE_GUIDE.md |
Guidelines for extending EntityType when adding block types |
Summary
- Macro's bidirectional @linking system relies on a unified
entity_mentionstable where each row encodes both directions of a relationship. - The
entity_mentionscrate provides type-safe database operations and HTTP endpoints that any service can consume. - Block-type agnosticism is achieved through the
EntityTypeenum and pluggable resolvers; new block types extend the system without modifying core linking code. - Performance comes from composite indexes on both source and target columns, enabling fast outbound and inbound lookups.
- Consistency is maintained because link creation and deletion are atomic operations on single rows, instantly affecting both sides of the relationship.
Frequently Asked Questions
How does Macro handle link integrity when a target block is deleted?
The entity_mentions crate subscribes to block deletion events via the internal event bus. When a block is permanently removed, a background job queries for all rows where that block appears as either source or target and deletes them. Services may also implement soft-delete checks in their validators to prevent creating links to archived blocks.
Can links span across different Macro workspaces or organizations?
No. The entity_mentions table includes an implicit workspace scope inherited from the source block's tenancy. Cross-workspace links would require explicit federation support not currently implemented. All link operations are validated against the calling user's workspace permissions before database insertion.
What happens if two users simultaneously create the same link?
The database schema enforces a unique constraint on (source_entity_type, source_entity_id, target_entity_type, target_entity_id). Concurrent identical insertions result in one success and one duplicate key error, which the API layer catches and returns as a 409 Conflict with the existing link's metadata.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →