/

Feature

Astrolabe: Managing Mac Fleets at scale

Ryan

No headings found on page

Start building
with Spectrum

Deploy AI agents
across every channel

Learn more about Spectrum

Preface

Preface

Terraform changed how teams think about cloud infrastructure: declare the desired state, apply it, and let the tool keep reality in sync. Astrolabe brings that same model to macOS.

If you run Macs in a fleet — inference nodes, CI runners, developer workstations, kiosks — you've probably stitched together shell scripts, MDM profiles, and manual checks to keep machines in shape. Astrolabe replaces that patchwork. Declare your macOS configuration in Swift with a SwiftUI-like syntax, and Astrolabe continuously enforces the state you described.

No setup scripts. No periodic audits. Just a declaration that enforces states.

Mac fleets are everywhere. The tooling still isn't.

Frontier AI labs rack Mac minis as inference servers. Platform teams run Mac Studios and Mac Pros as CI builders. Enterprises deploy MacBooks, kiosks, and shared workstations at scale.

These teams share the same problem: every machine needs to stay in a known, reproducible state — packages installed, services running, settings locked — not just at imaging, but continuously.

Traditional MDM handles enrollment, profiles, and compliance baselines well. But when you need to guarantee a specific LaunchDaemon is running, a Homebrew package is present, or a system setting hasn't drifted since last night, MDM is the wrong layer. It was designed for policy distribution, not runtime enforcement.

Astrolabe is built for that layer.

What Astrolabe is

Astrolabe is an open-source, declarative macOS configuration framework written in Swift. You describe the state a Mac should be in. Astrolabe runs as a root LaunchDaemon and continuously reconciles reality back to that description.

If you've written SwiftUI, the model will feel immediate:

import Astrolabe

@main
struct InferenceNode: Astrolabe {
    var body: some Setup {
        Pkg(.catalog(.homebrew))
        Brew("wget")
        Brew("ollama", type: .cask)

        LaunchDaemon("com.lab.healthcheck") {
            ProgramArguments(["/usr/local/bin/healthcheck", "--interval", "30"])
            RunAtLoad(true)
            KeepAlive(true)
        }

        Sys(.computerName("inference-node-04"))
        Sys(.power(.displaysOff(after: .minutes(5))))
    }
}
import Astrolabe

@main
struct InferenceNode: Astrolabe {
    var body: some Setup {
        Pkg(.catalog(.homebrew))
        Brew("wget")
        Brew("ollama", type: .cask)

        LaunchDaemon("com.lab.healthcheck") {
            ProgramArguments(["/usr/local/bin/healthcheck", "--interval", "30"])
            RunAtLoad(true)
            KeepAlive(true)
        }

        Sys(.computerName("inference-node-04"))
        Sys(.power(.displaysOff(after: .minutes(5))))
    }
}
import Astrolabe

@main
struct InferenceNode: Astrolabe {
    var body: some Setup {
        Pkg(.catalog(.homebrew))
        Brew("wget")
        Brew("ollama", type: .cask)

        LaunchDaemon("com.lab.healthcheck") {
            ProgramArguments(["/usr/local/bin/healthcheck", "--interval", "30"])
            RunAtLoad(true)
            KeepAlive(true)
        }

        Sys(.computerName("inference-node-04"))
        Sys(.power(.displaysOff(after: .minutes(5))))
    }
}

Homebrew, wget, Ollama, a health-check daemon, a hostname, a power setting — all declared in one place. Astrolabe installs whatever is missing, starts whatever should be running, and keeps checking. If something drifts, the next pass puts it back.

A shell script runs once and hopes nothing changes. Astrolabe treats your declaration as a continuous source of truth.

Why this matters

Drift correction, not drift detection

Every tick, Astrolabe compares live machine state against your declaration. Package uninstalled? Daemon crashed? Setting changed? Corrected on the next pass. The reconciliation loop is the monitoring — no separate tool required.

Configuration as code

Your fleet configuration lives in Swift — versioned in Git, reviewed in PRs, deployed like any other infrastructure artifact. No web console. No XML profiles. Just Swift code.

Why Swift

Swift isn't just a convenience choice. Its memory safety, strict type system, and compile-time guarantees mean your fleet configuration can't silently mishandle data, overflow a buffer, or pass the wrong type to a system API. That matters when your code runs as root on every machine in the fleet.

And because Swift bridges directly to C, Astrolabe can drop into low-level system work — calling Darwin APIs, interacting with IOKit, or invoking launchctl internals — without shelling out or marshaling through an intermediary. You get the safety of a modern language with the efficiency of native system access.

Runtime guarantees

Astrolabe doesn't configure a machine once and walk away. It runs as a persistent LaunchDaemon and provides a runtime guarantee: the state you declared will be maintained continuously. For inference nodes that need to stay healthy for weeks without human intervention, that's a fundamentally different operational model.

Composable, fine-grained control

Declare individual packages, LaunchDaemons, LaunchAgents, system settings, and Jamf properties. Compose them like SwiftUI views — group, branch conditionally, attach lifecycle hooks, and extract reusable modules.

var body: some Setup {
    if isInferenceNode {
        MLToolchain()
        MonitoringStack()
    } else {
        DeveloperWorkstation()
    }
}
var body: some Setup {
    if isInferenceNode {
        MLToolchain()
        MonitoringStack()
    } else {
        DeveloperWorkstation()
    }
}
var body: some Setup {
    if isInferenceNode {
        MLToolchain()
        MonitoringStack()
    } else {
        DeveloperWorkstation()
    }
}

Profile-based MDM can't express this. The smallest unit is usually an entire policy. Astrolabe gives you per-package, per-daemon, per-setting control.

The SwiftUI mental model

Astrolabe borrows from SwiftUI deliberately:

SwiftUI

Astrolabe

App

Astrolabe (entry point)

View

Setup (declaration protocol)

@ViewBuilder

@SetupBuilder (composable tree)

@State

@State, @Storage (reactive state)

@Environment

@Environment (system-derived values like MDM enrollment)

Modifiers attach behavior — priority, drift cadence, lifecycle hooks, dialogs, and background tasks. Your body is a pure function of state. When state changes, Astrolabe rebuilds the tree, diffs it, and applies only what changed.

Built-in declarations

Astrolabe ships with the building blocks fleet operators reach for most:

  • HomebrewBrew("ffmpeg"), Brew("firefox", type: .cask)

  • .pkg installers — from Homebrew's catalog, GitHub Releases, or custom sources

  • LaunchDaemons and LaunchAgents — define, install, and keep them alive

  • System settings — hostname, power management, energy schedules

  • Jamf properties — computer name and other managed fields

  • Custom steps — anything that doesn't fit a built-in declaration

Declarations are Swift protocols. Extend Astrolabe with your own package sources, system settings, and inline steps.

Self-updating

Once you ship an Astrolabe binary to your fleet, it can keep itself current. Opt in with a simple configuration:

@main
struct InferenceNode: Astrolabe {
    static var version: String { "1.2.3" }

    static var update: UpdateConfiguration? {
        UpdateConfiguration(.gitHub("your-org/fleet-setup"))
            .interval(.hours(1))
            .verify(.pkgSignatureRequired)
    }

    var body: some Setup {
        // ...
    }
}
@main
struct InferenceNode: Astrolabe {
    static var version: String { "1.2.3" }

    static var update: UpdateConfiguration? {
        UpdateConfiguration(.gitHub("your-org/fleet-setup"))
            .interval(.hours(1))
            .verify(.pkgSignatureRequired)
    }

    var body: some Setup {
        // ...
    }
}
@main
struct InferenceNode: Astrolabe {
    static var version: String { "1.2.3" }

    static var update: UpdateConfiguration? {
        UpdateConfiguration(.gitHub("your-org/fleet-setup"))
            .interval(.hours(1))
            .verify(.pkgSignatureRequired)
    }

    var body: some Setup {
        // ...
    }
}

Astrolabe installs a sibling updater daemon that checks your release source, downloads newer packages, verifies signatures, and restarts the main daemon — zero manual intervention. Push a new release to GitHub, and your entire fleet picks it up.

Battle-tested at Photon

Astrolabe isn't a side project we're releasing cold. We've run it internally at Photon for over six months.

Photon provides iMessage APIs for AI agents, backed by a fleet of Macs that relay iMessage traffic. Every machine in that fleet needs the right services running, the right packages installed, and the right system configuration locked in — continuously, not just at setup.

Before Astrolabe, that meant shell scripts, manual checks, and a growing ops burden. After adopting it, we cut ops time on our macOS fleet by 60%. We no longer need dedicated DevOps for fleet maintenance. Rolling out a new feature is a code change and a release — Astrolabe's self-update mechanism propagates it across the fleet. Rolling back is the same. And every machine stays in the state we designed, not the state it drifted into.

That's the confidence we want every team running Macs at scale to have.

Who it's for

AI/ML labs running Mac fleets for inference, training, or research. Every node in a known state, packages pinned, services guaranteed, drift corrected — without babysitting.

Enterprise IT and DevOps teams that have outgrown what MDM profiles can express. Infrastructure-as-code for macOS — version-controlled, composable, testable — that works alongside your existing MDM, not instead of it.

Platform engineers managing CI fleets, kiosk deployments, or shared developer machines. Any Mac — mini, Studio, Pro, or laptop — that needs to stay in a declared state without someone SSHing in to fix it.

Get started

Astrolabe is fully open source under the MIT license.

Declare what your Macs should look like. Astrolabe keeps them there.


Subscribe Photon Newsletter

Subscribe
Photon Newsletter