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:

  1. Parses its content for @link tokens.
  2. Validates target existence via service-specific DAOs.
  3. Calls entity_mentions::db::create_mention for each valid link.
  4. Calls entity_mentions::db::delete_mention for 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_mentions table where each row encodes both directions of a relationship.
  • The entity_mentions crate provides type-safe database operations and HTTP endpoints that any service can consume.
  • Block-type agnosticism is achieved through the EntityType enum 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

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.

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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →