Monarch: Minimal, Manageable, Migrations 🦋

Oct 23, 2024
5 minute read

Note: When I left Twitter to become an indie developer I committed to helping others with my work as much as I could. My goal has always been to make the lives of developers a little easier, so they can turn their ideas into, apps, products, and businesses. Every week this month I will be open sourcing something I've built for Plinky, my very own personal Open Source October. My career rests on the foundation that others have created, and I want to continue paying it forward by lowering the barrier for others to make what matters to them. Now, onto the post.

You don't always get it right the first time. That's why pencils have erasers — and why apps have migrations. In over two years of building Plinky, I've made a lot of mistakes changes. I've fixed bugs, rebuilt features completely, and overhauled the app's data architecture with Boutique.

All these changes required migrating user data to prevent a broken experience. Initially, I used a straightforward solution, running a handful of functions on app launch. But to simplify things over time, Monarch — a simple yet flexible migrations library.

Photo of many Monarch butterflies

Fun fact alert!

Monarch is named after the Monarch butterfly, the only butterfly that makes a bird-like, bidirectional migration to escape the cold. In that way, the Monarch butterfly and I are similar. As I get older I too dread surviving another New York City winter.

Migrations (of the programmatic variety)

Migrations need to be minimal, manageable, and mistake-free. The more frequently they need to be run, the more true this is. People trust me with their links in Plinky, so I'm very serious about data loss and data corruption. Monarch eliminates the patchwork and complexity, allowing you to focus on writing clean, simple Swift code. Let’s dive into why Monarch is a good choice for your running migrations in your app.

  • Simple Migrations: Define independent migrations without worrying about state management, making it easy to evolve your app over time.

  • Built for Swift and SwiftUI: Monarch’s migrations feel right at home in any Swift app, with a clean SwiftUI API.

  • Dependency Injection: Whether your migration is simple or complex, Monarch’s easy-to-use dependency injection provides structure to prevent data-related issues.

Instead of just telling you how simple Monarch is, let me show you.

Imagine this scenario: You've been storing user data for your app in UserDefaults. Your app is doing well, and now you're ready to build a new feature which requires a share extension. It would be helpful to have this user data available to you in the share extension, which means migrating the data from UserDefaults to an AppGroup shared between your app and share extension.

First, let’s define a migration.

struct MigrateUserDataToAppGroup: Migration {
    @MigrationDependency private var userDefaultsAppState
    @MigrationDependency private var sharedAppState

    static let id: MigrationID = "MigrateUserDataToAppGroup"

    func run() async throws {
        // Migrate the user data from UserDefaults to an AppGroup, so you can share data across targets
        sharedAppState.userData = userDefaultsAppState.userData

        userDefaultsAppState.userData = nil
    }
}

Next, we’ll create a MigrationGroup. This group lets us inject any necessary dependencies from our app, making them accessible to each Migration within the group.

let migrations = MigrationGroup {
    MigrateUserDataToAppGroup()
    // Add more migrations here
}
.migrationDependency(self.userDefaultsAppState)
.migrationDependency(self.sharedAppState)

Finally, we run our migrations!

struct ContentView: View {
    var body: some View {
        Text("Hello, Monarch! 🦋")
            .runMigrations {
                migrations
            }
    }
}

How easy was that? In just a few lines of code, we’ve moved our user data from UserDefaults to an AppGroup. While you could write this code and all of the boilerplate necessary yourself, Monarch makes it seamless to focus on what matters. As your migrations grow, this structure helps prevent bugs and race conditions.

Advanced Usage

While migrations often solve problems, occasionally they may introduce new ones. In such cases, developers need more control. Monarch offers a MigrationRunner, which lets you build custom and complex migration pipelines. In the small app below, we’ll explore how MigrationRunner can manage multiple migration tasks.

struct ButterflyTrackerApp: App {
    @State var appState = AppState()
    @State var preferences = Preferences()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task { 
						try await self.runMigrations()
                }
        }
    }
    
	func runMigrations() async throws {
		// 1. Remove all the migrations
		MigrationRunner.removeAllMigrations()
		
		// 2. Mark the ResetButterflyListMigration as completed
		MigrationRunner.markMigrationAsCompleted(withID: ResetButterflyListMigration.id)

		// 3. Create a MigrationGroup, with two dependencies and two migrations to run.
		let migrationGroup = MigrationGroup {
		    ProvideButterlyFansPremiumAccountAccessMigration()
		    RemoveAccidentallyAddedMothMigration()
		}
		.migrationDependency(self.appState)
		.migrationDependency(self.preferences)
		
		// 4. We run our migrations
		try await MigrationRunner.runMigrations({ migrationGroup })
    }
}

In the example above we:

  1. Removed all previous migrations, giving us a clean slate. (Note: You can also remove individual migrations without clearing all of them.)

  2. Marked the ResetButterflyListMigration as completed without running it. This allows us to skip a migration, which can be useful on a per-user basis or when transitioning from another migration system.

  3. Created a MigrationGroup with two migrations, which will run in the specified order. We also added two dependencies, to ensure access to the necessary state in our migrations.

  4. Ran our migrations! After all, that’s the goal.


When I open source a project, I’m usually excited to share what’s coming next. Monarch is a handy tool, but as the Brits say, it “does exactly what it says on the tin.” It’s been a go-to solution for me over the last few years across multiple apps, and I hope it helps solve similar problems in your apps too.

I'm always open to feedback, so I'd love to hear any thoughts or suggestions you may have. You can find me on Threads, where I welcome any and all polite discussions.

Joe Fabisevich is an indie developer creating software at Red Panda Club Inc. while writing about design, development, and building a company here at build.ms. Before all that he was working as an iOS developer on societal health issues @Twitter.

Like my writing? You can keep up with it in your favorite RSS reader, or get posts emailed in newsletter form. I promise to never spam you or send you anything other than my posts, it's just a way for you to read my writing wherever's most comfortable for you.

If you'd like to know more, wanna talk, or need some advice, feel free to sign up for office hours at no charge, I'm very friendly. 🙂