How Macro Manages Database Migrations Across Its SQLx Database Clients

Macro centralizes all PostgreSQL schema changes in a single macro_db_client crate, exposing migrations through a shared macro_db_migrator library that every downstream service consumes.

The Macro codebase follows a crate-based migration strategy to keep database schema changes consistent across multiple microservices. Rather than duplicating migration logic, the engineering team maintains one authoritative source in crates/macro_db_client and distributes changes via Cargo dependencies and the sqlx::migrate! macro.

Centralized Migration Storage

All .sql migration files live in crates/macro_db_client/migrations/. The naming convention follows chronological ordering:

This directory serves as the single source of truth for the entire Macro platform. Any schema modification made here automatically propagates to every service that depends on macro_db_client.

The Migration Distribution Architecture

Macro separates migration storage from migration execution across two crates:

Component Purpose Key File
macro_db_client Stores .sql files; shared dependency for all services crates/macro_db_client/migrations/
macro_db_migrator Exposes sqlx::migrate! macro to run migrations crates/macro_db_migrator/src/lib.rs

In crates/macro_db_migrator/src/lib.rs, the migrations are exposed using SQLx's compile-time checked macro:

// Re-exports migrations from the canonical location
pub fn run(pool: &sqlx::PgPool) -> Result<()> {
    sqlx::migrate!("../macro_db_client/migrations")
        .run(pool)
        .await
}

This indirection allows services to execute migrations without hardcoding paths to the migration directory.

How Services Consume Shared Migrations

Every service that touches PostgreSQL adds macro_db_client to its Cargo.toml:

[dependencies]
macro_db_client = { path = "../../crates/macro_db_client" }

To apply migrations, services invoke the migrator at startup or during CI:

use macro_db_migrator::run;

async fn initialize_database(pool: &sqlx::PgPool) -> anyhow::Result<()> {
    run(pool).await?;
    Ok(())
}

Test Integration with SQLx

Integration tests use the #[sqlx::test] attribute with an explicit migrations path. This ensures each test runs against a fresh database with the latest schema:

// Example from crates/stream/src/outbound/redis_pg/queries/test.rs
#[sqlx::test(migrations = "../macro_db_client/migrations")]
async fn test_document_queries(pool: PgPool) -> sqlx::Result<()> {
    // Test logic executes against migrated schema
}

Local Development and CI Workflows

The xtask tooling standardizes migration commands across environments. In crates/macro_db_client/justfile:


# Create a fresh database for local development

create_db:
    # Database creation logic

# Run pending SQLx migrations

migrate_db:
    sqlx migrate run --source migrations/

These commands are invoked through the workspace-level justfile:

just crates/macro_db_client/create_db   # fresh database

just crates/macro_db_client/migrate_db  # apply migrations

Production and CI Safety

The same just commands wrap additional safety checks. In tooling/xtask/crates/xtask_workflows/src/workflows/preview_fly.rs, migrations are referenced when previewing Fly.io deployments, with prompts that warn before applying changes to production databases.

Key Implementation Files

Understanding these files clarifies how database migrations are managed across Macro's SQLx clients:

Summary

  • Single crate ownership: macro_db_client houses all .sql migrations, preventing drift across services
  • Macro-based distribution: sqlx::migrate!("../macro_db_client/migrations") in macro_db_migrator/src/lib.rs compiles migrations into consumable code
  • Dependency sharing: Services import macro_db_client via path dependencies in Cargo.toml
  • Test synchronization: #[sqlx::test(migrations = "...")] guarantees consistent schema in integration tests
  • Unified tooling: just commands in crates/macro_db_client/justfile standardize local and CI workflows

Frequently Asked Questions

How do Macro's microservices stay synchronized with database schema changes?

Services depend on macro_db_client via Cargo path dependencies. When the migration files in crates/macro_db_client/migrations/ change, developers update their lockfiles and receive the new schema on next build. The shared macro_db_migrator::run() function ensures all services apply identical migrations.

Can individual services override or skip migrations?

No. The architecture intentionally prevents this. Services call macro_db_migrator::run() which hardcodes the path to ../macro_db_client/migrations. There is no mechanism to inject alternative migration sources without forking the migrator crate.

How are migrations tested before production deployment?

Integration tests use #[sqlx::test(migrations = "../macro_db_client/migrations")] to spin up isolated PostgreSQL instances with the full schema. CI pipelines run these tests across all dependent crates. The xtask_workflows crate additionally validates migrations during Fly.io preview deployments.

What prevents accidental production database modifications?

The xtask tooling layers interactive prompts onto migration commands for production environments. While Local development uses just crates/macro_db_client/migrate_db directly, production workflows in tooling/xtask/crates/xtask_workflows/ require explicit confirmation before executing schema changes against live databases.

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 →