# How Macro's Contact Management System Works and Syncs with CRM Data

> Discover Macro's contact management system. Learn how it centralizes contacts in a CRM database and syncs them via an SQS pipeline powered by the email service and teams crate.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: deep-dive
- Published: 2026-08-15

---

**Macro's contact management system stores contacts in a central CRM database and keeps them synchronized through an asynchronous SQS pipeline driven by the email service and the `teams` crate.**

This article explains the architecture and data flow behind contact synchronization in the Macro codebase. The system ensures that every contact extracted from email activity is consistently reflected in the central CRM store, enabling unified contact management across the platform.

---

## Core Architecture Overview

The contact management system spans multiple services and crates. The primary components include the **email service** for ingestion, the **`teams` crate** for queuing logic, and the **CRM service** for persistent storage.

| Layer | Primary Crate/Service | Responsibility |
|-------|----------------------|---------------|
| Ingestion | `services/email_service` | Extracts contact data from email activity |
| Queuing | `crates/teams` | Builds and dispatches contact sync jobs |
| Persistence | `crm` (via `services/mcp_service`) | Maintains source-of-truth contact records |
| Querying | `apps/web` | Serves contact data to the frontend |

---

## Contact Ingestion from Email Activity

When a user sends or receives an email, the email service extracts address information and enqueues a contact-upsert job. This is handled by `crm::domain::service::CrmService::populate_contact` as implemented in [`services/mcp_service/src/context.rs`](https://github.com/macro-inc/macro/blob/main/services/mcp_service/src/context.rs).

The job is placed on the **contacts SQS queue** (`email-service-crm-cleanup-queue`), where workers in the email service process it asynchronously.

```rust
// Conceptual flow based on services/mcp_service/src/context.rs (lines 171-173)
// The CRM service is instantiated here and made available to workers
let crm_service = CrmServiceImpl::new(db_pool, config);
context.set_crm_service(Arc::new(crm_service));

```

---

## The Contacts Enqueuer in the Teams Crate

The `teams` crate contains the core logic for creating SQS messages when contacts need synchronization. This operates through two primary modules.

### Outbound Enqueuer

[`crates/teams/src/outbound/contacts_enqueuer.rs`](https://github.com/macro-inc/macro/blob/main/crates/teams/src/outbound/contacts_enqueuer.rs) builds the payload and sends it to the contacts queue. This module handles the actual AWS SQS interaction.

### Domain Logic

[`crates/teams/src/domain/contacts_enqueuer.rs`](https://github.com/macro-inc/macro/blob/main/crates/teams/src/domain/contacts_enqueuer.rs) determines *when* a contact should be synced. Trigger conditions include:

- New email message received
- Draft email sent
- Explicit user-triggered sync

This separation of concerns allows the domain layer to define business rules while the outbound layer handles infrastructure concerns.

---

## CRM Synchronization Service

The CRM crate provides the single source of truth for contact data. The implementation in `crm::domain::service::CrmServiceImpl` exposes several key methods:

- `populate_contact` — Creates or updates a contact record
- `get_team_id_for_user` — Resolves team context for contact association
- `upsert_contact` — Idempotent contact write operation

When the enqueuer processes a contact, it invokes `crm_service.populate_contact`, which writes to the `crm_contacts` table and maintains the `crm_contact_sources` foreign-key relationship to track the originating email link.

---

## Frontend Contact Queries

The web application fetches contacts through GraphQL queries that delegate to the CRM client. Two primary query modules handle this:

[`apps/web/src/lib/queries/crm/contacts.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/queries/crm/contacts.ts) — Primary GraphQL interface for CRM-backed contacts

[`apps/web/src/lib/queries/contacts/contacts.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/queries/contacts/contacts.ts) — Alternative query interface used by specific UI components

These queries return normalized contact objects with fields including `name`, `email`, and `photoUrl`.

---

## Complete Data Flow

| Step | Component | Action |
|------|-----------|--------|
| 1 | **Email Service** (`services/email_service`) | Extracts contact info from sent/received email |
| 2 | **SQS Contacts Queue** | Holds `populate_contact` job payload |
| 3 | **Contacts Enqueuer** ([`crates/teams/src/outbound/contacts_enqueuer.rs`](https://github.com/macro-inc/macro/blob/main/crates/teams/src/outbound/contacts_enqueuer.rs)) | Processes queue, calls CRM service |
| 4 | **CRM Service** (`crm::domain::service::CrmServiceImpl`) | Persists to `crm_contacts` and `crm_contact_sources` tables |
| 5 | **Web App** (`apps/web`) | GraphQL queries serve synced contacts to users |

---

## Key Source Files Reference

| File Path | Role |
|-----------|------|
| [`crates/teams/src/outbound/contacts_enqueuer.rs`](https://github.com/macro-inc/macro/blob/main/crates/teams/src/outbound/contacts_enqueuer.rs) | SQS message construction and dispatch |
| [`crates/teams/src/domain/contacts_enqueuer.rs`](https://github.com/macro-inc/macro/blob/main/crates/teams/src/domain/contacts_enqueuer.rs) | Sync trigger logic and business rules |
| [`services/mcp_service/src/context.rs`](https://github.com/macro-inc/macro/blob/main/services/mcp_service/src/context.rs) | CRM service instantiation (lines 171-173) |
| `services/email_service/src/pubsub/crm_cleanup/*` | Queue workers consuming contact jobs |
| [`apps/web/src/lib/queries/crm/contacts.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/queries/crm/contacts.ts) | Frontend GraphQL queries for CRM contacts |
| [`apps/web/src/lib/queries/contacts/contacts.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/queries/contacts/contacts.ts) | Secondary frontend query interface |
| `apps/docs/product/crm.mdx` | Product documentation for CRM features |

---

## Summary

- **Ingestion** happens in the email service, which enqueues contact jobs to SQS
- **Queuing logic** resides in the `teams` crate, with clear separation between domain rules and outbound infrastructure
- **Persistence** is handled by the CRM service, which maintains relational data in `crm_contacts` with source tracking via `crm_contact_sources`
- **Queries** are served through GraphQL endpoints consumed by the web application
- **All contact data** lives in the CRM database and is kept synchronized through this asynchronous pipeline

---

## Frequently Asked Questions

### How does Macro decide when to sync a contact?

The domain logic in [`crates/teams/src/domain/contacts_enqueuer.rs`](https://github.com/macro-inc/macro/blob/main/crates/teams/src/domain/contacts_enqueuer.rs) evaluates multiple trigger conditions: new email messages, draft sends, and explicit user actions. This centralized decision logic prevents duplicate syncs and ensures contacts are only queued when meaningful changes occur.

### Where is the CRM service instantiated?

The CRM service is instantiated in [`services/mcp_service/src/context.rs`](https://github.com/macro-inc/macro/blob/main/services/mcp_service/src/context.rs) at lines 171-173, where `CrmServiceImpl` is created with database pool and configuration dependencies, then wrapped in an `Arc` for shared access across request handlers.

### What database tables store contact information?

Contacts are stored in the `crm_contacts` table with source metadata tracked in `crm_contact_sources`. This foreign-key relationship enables auditing of how each contact entered the system and which email link originated the record.

### Can the frontend query contacts directly from SQS?

No. The web application queries through GraphQL endpoints that hit the CRM service's database layer. The SQS queue is purely for asynchronous ingestion; it is not exposed to frontend clients.