# How Macro's Bidirectional @linking System Functions Across Diverse Entity Types

> Discover how Macro's bidirectional @linking system connects messages documents projects email threads and more using a central EntityMention struct for seamless cross-entity relationships.

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

---

**Macro implements a generic @link (entity‑mention) system using a central `EntityMention` struct that records both source and target for every relationship, enabling bidirectional queries and notifications across messages, documents, projects, email threads, calls, and more.**

The macro-inc/macro codebase powers a unified collaboration platform where any object can reference any other object. The bidirectional @linking system serves as the backbone for cross-entity navigation, notifications, and relationship discovery. This article examines the architecture, data model, and API flow that make this polymorphic linking possible.

## The Core Data Model: EntityMention

At the heart of the system lies the **`EntityMention`** struct, defined in [`crates/channels/src/domain/models.rs`](https://github.com/macro-inc/macro/blob/main/crates/channels/src/domain/models.rs) (lines 1385–1400). This struct captures four essential identifiers that make any link traversable from either direction:

- `source_entity_type` + `source_entity_id` — the object creating the reference
- `entity_type` + `entity_id` — the object being referenced
- `created_by` — the user who created the link
- `created_at` — timestamp for ordering and auditing

```rust
// Conceptual representation based on EntityMention definition
pub struct EntityMention {
    pub id: Uuid,
    pub source_entity_type: String,  // e.g., "message", "document"
    pub source_entity_id: Uuid,      // UUID of the source
    pub entity_type: String,         // e.g., "Document", "Chat", "user"
    pub entity_id: Uuid,             // UUID of the target
    pub created_by: Uuid,
    pub created_at: DateTime<Utc>,
}

```

The **`CreateEntityMentionOptions`** struct (lines 1404–1415) mirrors these fields for API-level operations, while **`CreateEntityMentionRequest`** (lines 1420–1429) serves as the HTTP payload for `POST /channels/mentions`.

## Creating Links: The Insertion Flow

When a user creates an @link, the system validates the request and persists the relationship via **`insert_message_mentions`** in [`crates/channels/src/outbound/pg_channels_repo.rs`](https://github.com/macro-inc/macro/blob/main/crates/channels/src/outbound/pg_channels_repo.rs) (lines 62–70).

```rust
// Simplified flow showing the key operations
pub async fn insert_message_mentions(
    &self,
    message_id: Uuid,
    mentions: &[CreateEntityMentionOptions],
    created_by: Uuid,
) -> Result<Vec<Uuid>, Error> {  // Returns user IDs to notify
    // 1. Deduplicate existing mentions for this message
    // 2. Insert new rows into comms_entity_mentions
    // 3. If entity_type == "user", collect for notification
}

```

This function demonstrates polymorphic handling: it accepts any target type in the `entity_type` field, yet applies special logic when the target is a `user` (triggering push notifications).

## Querying Links: Bidirectional Retrieval

The system enables two fundamental query patterns through the same table structure.

### Forward Lookup: "What does this source mention?"

When fetching channel messages, **`get_latest_channel_messages_batch`** (lines 2120–2130) aggregates all mentions as `"entity_type:entity_id"` strings:

```sql
-- From pg_channels_repo.rs message retrieval query
SELECT 
    m.id,
    m.content,
    ARRAY(
        SELECT em.entity_type || ':' || em.entity_id
        FROM comms_entity_mentions em
        WHERE em.source_entity_id = m.id
    ) AS mentions
FROM comms_messages m
WHERE m.channel_id = $1
ORDER BY m.created_at DESC
LIMIT $2;

```

The frontend parses these strings to render clickable @links with appropriate icons and routing.

### Reverse Lookup: "What sources mention this target?"

By filtering on `entity_type` + `entity_id`, the system finds all incoming references. The thread participant logic uses this implicitly: **`get_thread_data`** and **`get_thread_messages`** join `comms_entity_mentions` to identify users @mentioned in any message, ensuring they receive thread notifications regardless of direct channel membership.

## Deletion and Consistency

Link lifecycle management relies on **`delete_entity_mentions_by_source`** (lines 33–40), which removes all rows matching a source entity ID. This generic deletion works across all source types:

```rust
pub async fn delete_entity_mentions_by_source(
    &self,
    source_entity_ids: &[Uuid],
) -> Result<u64, Error> {
    // DELETE FROM comms_entity_mentions 
    // WHERE source_entity_id = ANY($1)
}

```

This design ensures referential integrity without cascading constraints — when a message is deleted, its outgoing @links disappear automatically.

## Supported Entity Types and Extensibility

The **`ReferencedShareItemType`** enum defines built-in target types in [`models.rs`](https://github.com/macro-inc/macro/blob/main/models.rs):

| Type | Description |
|------|-------------|
| `Document` | Stored files and content |
| `Chat` | Channels and direct messages |
| `Project` | Workspace containers |
| `EmailThread` | Email conversations |
| `Call` | Voice/video call records |

Because `comms_entity_mentions` stores types as plain `String` values, new entity categories require no schema migration. The service layer need only recognize and validate the new type string.

## API Flow Summary

1. **Client** constructs a `CreateEntityMentionRequest` with source and target identifiers
2. **Router** accepts `POST /channels/mentions` and validates the payload
3. **Repository** calls `insert_message_mentions` (or generic equivalent) to write rows
4. **Database** stores bidirectional relationship in `comms_entity_mentions`
5. **Notification service** receives user IDs when @user mentions occur
6. **Query handlers** join the mentions table for rendering and reverse lookups
7. **Cleanup** triggers `delete_entity_mentions_by_source` on entity deletion

## Key Source Files

| File | Responsibility |
|------|---------------|
| [`crates/channels/src/domain/models.rs`](https://github.com/macro-inc/macro/blob/main/crates/channels/src/domain/models.rs) | `EntityMention`, request/response types, `ReferencedShareItemType` |
| [`crates/channels/src/outbound/pg_channels_repo.rs`](https://github.com/macro-inc/macro/blob/main/crates/channels/src/outbound/pg_channels_repo.rs) | SQL operations: `insert_message_mentions`, `delete_entity_mentions_by_source`, message queries with mention arrays |
| [`crates/channels/src/domain/mention_events.rs`](https://github.com/macro-inc/macro/blob/main/crates/channels/src/domain/mention_events.rs) | Event metadata generation for notifications |
| [`crates/channels/src/inbound/axum_router.rs`](https://github.com/macro-inc/macro/blob/main/crates/channels/src/inbound/axum_router.rs) | HTTP route handlers for mention endpoints |

## Summary

- **Bidirectional @linking** in Macro relies on a single table (`comms_entity_mentions`) with four identifier columns enabling traversal in either direction
- **`EntityMention`** struct (lines 1385–1400) provides the canonical representation, decoupled from any specific source or target type
- **Polymorphic insertion** via `insert_message_mentions` handles type-specific side effects (user notifications) while preserving generic storage
- **Deletion by source** with `delete_entity_mentions_by_source` maintains graph consistency without foreign key cascades
- **String-based typing** allows runtime extensibility — new entity types require code changes, not schema migrations

## Frequently Asked Questions

### How does Macro handle notification routing for @mentions?

The insertion function `insert_message_mentions` checks whether `entity_type == "user"` for each mention. When true, it resolves the referenced user's notification preferences and channel participation status, returning their UUIDs to the caller for enqueueing push/email notifications.

### Can two different entity types link to each other arbitrarily?

Yes. The data model imposes no restrictions on source-target type combinations. A `Call` can reference a `Document`, an `EmailThread` can reference a `Project`, and so on. The frontend determines how to render each combination based on the `entity_type` string.

### What happens to @links when a target entity is deleted?

The current implementation does not automatically cascade deletions when a *target* is removed. The `mentions` array in query results may contain stale references (dangling links), which the frontend typically handles by showing "unavailable" placeholders. Deletion cascades only from source entities via `delete_entity_mentions_by_source`.

### How scalable is the `comms_entity_mentions` table for high-volume workspaces?

The table structure supports indexing strategies for both query directions: `(source_entity_type, source_entity_id)` for forward lookups and `(entity_type, entity_id)` for reverse lookups. The batch message query uses `ARRAY` aggregation to minimize round-trips, and the deduplication logic in `insert_message_mentions` prevents redundant rows for frequently edited messages.