# How Macro’s Bidirectional @linking System Works Across Different Block Types

> Discover how Macro's bidirectional @linking system efficiently connects different block types using a unified entity_mentions table for seamless forward and reverse lookups.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: internals
- Published: 2026-08-16

---

**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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/crates/entity_mentions/src/api.rs) that the Tauri-based frontend consumes:

```rust
// 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:

```rust
// 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:**

```rust
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:**

```rust
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:**

```javascript
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:**

```javascript
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`](https://github.com/macro-inc/macro/blob/main/crates/entity_mentions/src/lib.rs) | Public API exports and `EntityType` enum definition |
| [`crates/entity_mentions/src/db.rs`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/crates/entity_mentions/src/api.rs) | Axum HTTP handlers for POST/GET/DELETE endpoints |
| [`crates/entity_mentions/src/resolver.rs`](https://github.com/macro-inc/macro/blob/main/crates/entity_mentions/src/resolver.rs) | `@link` token parsing and target validation |
| [`crates/document_storage_service/src/service.rs`](https://github.com/macro-inc/macro/blob/main/crates/document_storage_service/src/service.rs) | Example service integrating mentions on document save |
| [`crates/chat_service/src/service.rs`](https://github.com/macro-inc/macro/blob/main/crates/chat_service/src/service.rs) | Example service scanning messages for `@link` syntax |
| [`docs/STYLE_GUIDE.md`](https://github.com/macro-inc/macro/blob/main/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

### 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.