How we built MetiRecon and shipped it to the Mac App Store and Microsoft Store
MetiRecon is ours end to end: an offline desktop app with a matching engine written in Go, embedded inside a Flutter interface, published on both the Mac App Store and the Microsoft Store. We chose the problem ourselves and carried it all the way to two store releases. This case study is the engineering behind that work - the architecture decisions, the hard parts of getting a native desktop app through two store reviews at once, and the way we take a product from an empty repository to something people can install and pay for. Client work runs on exactly these rails.
- Platforms
- macOS, Windows
- Stack
- Flutter, Go, native FFI
- Distribution
- Mac App Store, Microsoft Store
- Data
- 100% offline
- Problem
- Finance and audit teams reconcile in spreadsheets that quietly break the moment files get large, rules get intricate, or an auditor asks how a match was made.
- What we built
- A fully offline desktop app - a Go matching engine inside a Flutter interface - that classifies every difference and produces an audit-ready report.
- Outcome
- Shipped to the Mac App Store and Microsoft Store, native on Apple Silicon and Intel, reconciling millions of rows in seconds - with data that never leaves the machine.
What this case study covers
- The product we set out to build
- How we approached the build
- Why we built it fully offline
- The domain we had to model
- Modeling the break types
- Designing the matching engine in Go
- Embedding Go inside Flutter
- Reading seven file formats
- Designing the desktop interface
- Audit-ready reports
- Shipping to the Mac App Store
- Shipping to the Microsoft Store
- One binary for Apple Silicon and Intel
- Store-managed subscriptions
- Testing a tool that must be correct
- What this build shows about how we work
- What we would tell another team
- Frequently asked questions
The product we set out to build
MetiRecon began as our own idea, not a client brief, which meant we owned every decision: what to build, for whom, and how far to take it. We picked a problem we understood well. Finance and audit teams reconcile data constantly, almost always in a spreadsheet, and the spreadsheet quietly fails them the moment the files get large, the rules get intricate, or an auditor asks how a given match was made. The choice on the market is binary today: a spreadsheet that gives up as the files grow, or a platform that arrives with a procurement cycle and an implementation project. For someone holding two extracts who needs an answer this afternoon, there is nothing in between.
So the brief we wrote for ourselves was concrete. Build a desktop tool that compares two sets of data, classifies every difference, and produces a report an auditor can sign off on. Make it fast enough to handle files a spreadsheet would refuse to open. Make it usable by someone who has never written a line of code. Keep the data on the user's machine. And ship it through the app stores so it installs like any other professional app. What follows is how that brief became a product on two stores, and the engineering decisions that got it there.
The real question is never "how do these two files differ" but "which instruments moved, and why" - across millions of rows, in seconds. A unix diff cannot answer that, and a complex SELECT has to be written first.
How we approached the build
Order of work is itself a design decision, and here we made it against the obvious one: the interface came last. We started with the two things most likely to sink the project: the matching engine, because if it could not be both correct and fast on a local machine the product had no reason to exist, and the delivery path, because shipping a native desktop app with an embedded native library to two different app stores is exactly where first releases stall.
Tackling the riskiest parts first is a deliberate habit. It meant that by the time we were polishing the interface, the questions that could have forced a rewrite - can the Go engine live inside a Flutter app cleanly, will a sandboxed and notarized build actually pass review, will the same code package for the Microsoft Store - had already been answered. It is far cheaper to discover a hard constraint in week two than in week eight, and building in that order is one of the main reasons the products we take on reach the store instead of stalling just short of it.
What follows is ordered the way the build was: engine and architecture first, then the product around them, then the long stretch of getting through two store reviews. We work in that sequence whenever the concept is not the hard part and the delivery is.
Why we built it fully offline
The single most important design decision in MetiRecon is one that never appears on screen: the data never leaves the machine. There is no upload, no cloud processing, no account that has to be created before the first comparison, and no internet connection required to run a reconciliation at all. Everything happens locally, on the computer in front of the person doing the work.
This matters because of what reconciliation data usually is. Bank statements, trading records, payroll extracts, intercompany ledgers, customer transaction histories: this is some of the most sensitive information a company holds. A great many organizations have policies that flatly prohibit uploading that kind of file to an external service, and for good reason. The moment data leaves the building it becomes someone else's security problem, someone else's breach exposure, and a line item in every vendor risk assessment and data processing agreement the company has to sign.
By keeping everything on the device, MetiRecon removes that entire category of objection. There is no data processing agreement to negotiate because we never process the data. There is no region to worry about because the file never travels. A security team can approve the tool on the basis of a single, easily verified claim: the files a user loads never leave the machine. For a product sold into finance and audit, that claim is worth more than any feature. It turns a months-long procurement conversation into a download.
Choosing offline also shaped everything downstream. It ruled out the easy path of doing the heavy computation on a server and made the local machine responsible for the whole job, which is exactly why the matching engine had to be fast and memory-aware rather than merely correct. Privacy alone will not hold anyone. If comparing two files takes a quarter of an hour, the user is back in a spreadsheet within a week.
The domain we had to model
Before writing an engine we had to model the problem properly, because building software for a domain means understanding it better than the people who will use it. People outside finance imagine reconciliation as a simple equality check: line up two lists and see where they match. Real reconciliation is considerably more involved, and getting the nuances right in the data model is most of the work. Three of them shaped the whole engine.
The first nuance is the key. Two records represent the same underlying event when some combination of their fields agrees, and that combination is rarely a single tidy identifier. It might be a reference number, or a date plus an amount plus a counterparty, or an account plus a value date with the amount allowed to differ by a rounding tolerance. MetiRecon lets the user define what "the same" means, because only they know the shape of their data, and then applies that definition consistently across every row.
The second nuance is the break. When two records do not match, the interesting question is not whether they differ but how. An amount that is off by a few cents is a rounding difference. An entry that appears in one file today and the other file tomorrow is a timing difference, not an error. A transaction that exists on one side and nowhere on the other is a genuine unmatched item that someone needs to investigate. Lumping all of these together as "differences" is what makes a manual reconciliation so slow, because a human then has to re-sort them by hand. MetiRecon classifies each difference so the exceptions that actually need attention rise to the top.
The third nuance is cardinality. Not every match is one to one: a single invoice can be settled by several payments, and a single payment can cover several invoices. For the first release we deliberately built around a key the user defines - they decide what "the same record" means, and they narrow the key where cardinality comes into play. Full one-to-many and many-to-many support is the next step on the roadmap.
Modeling the break types
Classifying differences was one of the core modeling decisions in the engine, and we kept the taxonomy deliberately small. A classification a user cannot predict is worse than none at all, because they stop trusting the label and go back to reading rows by hand. Every record that does not match cleanly lands in one of three buckets.
Mismatched records are the interesting ones: the key matched on both sides, so this is the same underlying event, but one or more of the compared fields disagree. Both systems know about the transaction and they are telling different stories about it.
Pre-orphans and post-orphans are records present on one side and absent on the other. Splitting orphans by the side they came from matters more than it first sounds. "Missing from the ledger" and "missing from the statement" are two different investigations, they often belong to two different people, and collapsing both into a single "unmatched" pile hands the analyst the job of re-sorting them by hand - which is exactly the work the tool is supposed to remove.
Which bucket a record lands in depends on how the comparison is configured, and that is the user's decision rather than ours. Numeric fields can be compared with a tolerance expressed either as a percentage or as an absolute amount. Two systems that round at different points, or convert at slightly different rates, produce differences that are noise rather than errors - and a tool that reports every one of them buries the real problems under a pile of trivia.
Text fields get their own switches: case sensitivity on or off, and whitespace stripped or preserved. Real exports disagree about both constantly, because one system pads a reference and another upper-cases it, and neither difference means the underlying records differ. Making these explicit toggles rather than silent cleverness is what keeps a run reproducible: the same two files with the same settings classify identically every time, which is precisely what an audit trail requires.
Designing the matching engine in Go
The heart of MetiRecon is a matching engine, and we wrote it in Go. That choice was deliberate and it drove much of the architecture that followed.
Go is fast, it compiles to a single native binary with no runtime to install, and it has genuinely good support for cross-compilation, which matters enormously when the same engine has to run on Apple Silicon, Intel Macs and Windows. Just as importantly, Go is comfortable with the kind of work reconciliation demands: streaming through large files, holding structured records in memory efficiently, and running comparisons in parallel across all the cores a modern laptop has to offer. Where a spreadsheet recalculates the world on every change, the Go engine reads each file once, builds the indexes it needs, and does the matching in a single deliberate pass.
Exact, tolerant and fuzzy matching
The engine supports several matching modes because real data needs all of them. Exact matching is the baseline: the key fields agree byte for byte. Tolerant matching allows numeric fields to differ within a configured band, which is how rounding and small foreign-exchange differences are absorbed without being flagged as errors. Fuzzy matching handles the messier cases where a reference has been transcribed slightly differently on each side, or a name is abbreviated in one system and spelled out in the other. Each mode is a tool, and the user decides which applies to which field.
Keys, indexes and performance
Performance on a local machine comes down to not doing unnecessary work. The engine builds hash indexes on the key fields so that finding candidate matches is close to constant time rather than a nested scan of every row against every other row. Composite keys are hashed together, tolerances are handled by bucketing values so that near matches land in adjacent buckets, and the whole comparison is designed to hold only what it needs in memory at once. The result is a tool that reconciles millions of rows in seconds on ordinary hardware, well past the point a spreadsheet would give up, without a server in sight.
Memory was treated as a hard limit rather than something to hope about. The size of a reconciliation is not bounded by how much RAM the machine has: the engine splits both sides into fragments, reconciles fragment against fragment, and keeps resident only what the current comparison needs. A user can therefore reconcile a data set considerably larger than their available memory, with a footprint that stays flat instead of growing with the file. A spreadsheet will not open such a file at all, and a naive tool loads both sides and dies.
Embedding Go inside Flutter
MetiRecon has a Go engine and a Flutter interface, and making those two worlds talk to each other cleanly was one of the more technically involved parts of the build.
The obvious alternative would have been to rewrite the matching logic in Dart, the language Flutter uses, and avoid the interoperability problem entirely. We chose not to, for two reasons. First, the engine is the part of the product where correctness and speed matter most, and Go gives us both with far less effort than reimplementing indexing, hashing and parallel matching in Dart would have taken. Second, a self-contained engine is reusable. The same core that powers the desktop app could, in principle, be driven from a command line or a batch job without touching the UI at all.
So instead of rewriting, we compiled the Go engine into a native library, a dynamic library on each platform, and called into it from Flutter through Dart's foreign function interface. The Flutter side is responsible for everything the user sees: loading files, configuring the match, showing progress and rendering results. The Go side is responsible for the computation. Data crosses the boundary through a carefully designed interface so that large datasets are not copied back and forth needlessly, and the heavy work runs off the UI thread so the interface never freezes while a reconciliation is in progress.
Flutter owns everything the user sees. Go owns the computation. The boundary between them is small, explicit and fast.
This architecture has a pleasant property: each side plays to its strengths and the seam between them is small. Flutter is excellent at building a consistent, native-feeling desktop interface from a single codebase across macOS and Windows. Go is excellent at chewing through data. Keeping the boundary between them narrow, a handful of well-defined calls rather than a chatty back-and-forth, kept the whole thing maintainable and made it far easier to reason about performance.
Reading seven file formats
A reconciliation tool is only useful if it can read the files people already have, and finance data comes in a remarkable variety of shapes. MetiRecon reads CSV, TSV, Excel, JSON, XML, fixed-width text and FIX protocol messages, and each of those brought its own small ordeal.
CSV sounds simple and never is. Delimiters vary between commas, semicolons and tabs depending on the system and the locale that produced the file. Text encodings differ, and a file exported on one machine can carry a byte order mark or use a legacy code page that turns accented characters into nonsense if you assume everything is modern Unicode. Quoting rules differ, embedded newlines inside quoted fields are common in exported data, and numbers are formatted with different thousands and decimal separators around the world. The parser has to be forgiving of all of this without silently misreading a value, because a misread number in a reconciliation is worse than a crash.
Excel means reading the real binary structure of a spreadsheet, including multiple sheets, typed cells, dates stored as serial numbers and formulas whose cached results have to be read rather than recomputed. JSON and XML bring nesting: financial data in those formats is often deeply structured, and the tool has to let the user flatten the parts they care about into rows to compare. Fixed-width files, still common in older banking systems, carry no delimiters at all and depend entirely on column positions defined in a separate specification. And FIX, the messaging standard that underpins electronic trading, is a stream of tag-value pairs with its own vocabulary that has to be parsed into meaningful fields before it can be reconciled against anything.
Supporting all seven means a user almost never has to reshape their exports before comparing them. They point MetiRecon at whatever their systems produce, and the tool meets the data where it is. That sounds like a small convenience. In practice it removes the most tedious and error-prone step of the entire process, the manual reformatting that people do before they can even begin to reconcile.
Designing the desktop interface
The engine does the thinking, but the interface is what makes MetiRecon usable by someone who has never written a line of code, and desktop interface design has its own expectations that a web app does not.
Desktop users expect to work with local files naturally, to open several windows, to use the keyboard, and to have the application feel like it belongs on their operating system rather than like a web page in a frame. Flutter gave us a single codebase that could deliver that on both macOS and Windows while still respecting the conventions of each. The core workflow is deliberately linear: choose two files, tell the tool which fields identify a record and which should be compared, run the reconciliation, and then explore the result.
Exploring the result is where a lot of care went. After a run, the user sees matched records, unmatched records from each side, and the classified breaks, and they can filter and drill into any of these. A finance professional does not want a raw dump of differences; they want to answer questions. Which items are unmatched on our side? Which differences are just rounding and can be ignored? Which single transaction is causing this whole set to fail? The interface is built around answering those questions quickly, so that the human spends their time on judgment, which is the part that actually needs a human, rather than on sorting and searching, which the software should do for them.
Audit-ready reports
A reconciliation that lives only on one person's screen is not finished. The output has to become evidence: something that can be attached to a month-end close, handed to a manager, or produced for an auditor months later. Generating that report properly is a feature in its own right.
An audit-ready report has requirements that a casual export does not. It has to state exactly what was compared, which files, which rules, which tolerances, so that the conclusion is reproducible. It has to present matched and unmatched items clearly, with the classified breaks, so a reviewer can see not just the answer but the working. And it has to be stable, so that the same inputs and the same rules produce the same report every time, because reproducibility is the whole point of an audit trail. MetiRecon produces reports with that discipline built in, which turns the tool from something that helps you do the work into something that also proves the work was done.
Shipping to the Mac App Store
Building the app was one thing. Getting it into the Mac App Store was a separate project with its own rules, and it is exactly the stage where first-time desktop developers get stuck.
Apps in the Mac App Store run inside the App Sandbox, a security boundary that restricts what the app can touch by default. A reconciliation tool that reads files the user chooses has to request precisely the right entitlements to do that, and no more, because asking for broad access is both a review risk and a genuine security smell. Getting the file-access model right, so that the app can open the documents a user deliberately points it at while still living inside the sandbox, took real care.
On top of sandboxing sits notarization, Apple's process for scanning and signing software so that macOS will run it without warnings, and a review process that scrutinizes everything from how the subscription is presented to how the app behaves on first launch. Embedding a Go native library inside a sandboxed, notarized, store-distributed app added its own wrinkles, because the library has to be signed and packaged correctly along with the rest of the bundle. None of this is visible to the user, and that is the point: the reward for getting it right is an app that installs from the store as smoothly as any other, with the operating system's full trust.
Shipping to the Microsoft Store
Windows repeats the shape of the problem with an entirely different set of tools, and treating it as "the same app, different button" is how projects fall behind.
Distribution through the Microsoft Store means packaging the app as MSIX, Microsoft's modern installer and packaging format, which brings its own model of application identity, signing and capabilities. The concepts rhyme with Apple's, there is a packaging step, a signing step and a set of declared capabilities, but the mechanics, the tooling and the failure modes are all different. An app that is signed and notarized perfectly for macOS still has to be packaged and signed all over again, in the Windows way, to earn the same trust from the Microsoft Store and from Windows itself.
Running the same Go engine and the same Flutter interface on Windows also meant confirming that the native library loaded and behaved identically there, that file handling respected Windows conventions, and that the packaged app passed the store's own review. Doing this twice, once for each platform, is a large part of why cross-platform desktop distribution is where inexperienced teams underestimate the work. The code that computes a reconciliation is shared. The work of making that code shippable is paid twice.
One binary for Apple Silicon and Intel
Macs come in two processor architectures, Apple Silicon and Intel, and a professional app is expected to run natively on both. That expectation reached all the way down into the Go engine.
On macOS, MetiRecon ships as a universal binary that contains native code for both arm64 and x86_64, so the operating system runs the right version automatically and the user never thinks about it. Because the matching engine is compiled Go rather than interpreted code, that meant cross-compiling the engine for each architecture and combining the results into the universal build, then making sure the interoperability layer between Flutter and Go behaved identically on both. Getting this right is the difference between an app that runs natively at full speed on a modern Mac and one that limps along under translation, and it is the sort of detail that is invisible when it works and glaringly obvious when it does not.
Store-managed subscriptions
MetiRecon is a subscription product, and the subscriptions are billed through the stores themselves rather than through any external payment system. That was a deliberate choice with real consequences.
Selling through the store's own billing means the user pays with the account they already have, the one tied to their Apple or Microsoft profile, with a payment method already on file. There is no separate checkout, no card details to enter into a third-party form, and no external service that has to be trusted with payment data. For a tool that sells its privacy as a core value, that consistency matters: the same app that never uploads a user's data also never sends them off to an unfamiliar payment page. It also happens to be a requirement of both stores for this kind of product, so building the app around store-managed subscriptions, entitlement checks and receipt validation from the start avoided a painful retrofit later.
Testing a tool that must be correct
For most apps, a bug is an annoyance. For a reconciliation tool, a bug can be a wrong number in a financial report, and that changes how seriously the testing has to be taken.
The correctness of the matching engine was tested against known inputs and expected outputs, including the awkward cases that make reconciliation hard: rounding differences, timing differences, one-to-many relationships, duplicate keys, empty and malformed files, unusual encodings and the full spread of supported formats. Beyond correctness there is behavior under load, because an engine that is right on a hundred rows but falls over on a few million is not fit for purpose. And beyond the engine there is the whole app, tested on both macOS and Windows, on both Mac architectures, and through the store distribution path that real users actually take.
This is also where a dedicated tester, someone who did not write the code, earns their place. A developer testing their own reconciliation logic tends to test the cases they already thought of when they wrote it. A separate person, deliberately trying to break the tool with the ugliest files they can construct, finds the cases the developer did not imagine. For a product whose entire value rests on being trustworthy, that independent scrutiny is not a luxury.
What this build shows about how we work
A working reconciliation prototype is a weekend for a competent engineer. Everything after that weekend is the real project: two store reviews, two processor architectures, financial data that cannot lose a cent, and an auditor who has to be satisfied. Most of the calendar goes there, and most abandoned products are abandoned there.
Sandboxing and packaging were settled by someone who had taken software through both stores before and knew exactly which entitlement decides the outcome - the reason review here cost days rather than weeks. The engine's designer was also its author, and its author was deliberately not its tester: an engine checked by the person who wrote it only ever meets the cases that person already imagined.
Read this as evidence of a route rather than a domain. The transferable part is not reconciliation - it is carrying a product through certificates, entitlements, notarization, MSIX packaging and two store reviews to the point where a stranger can install it and pay for it. If your product has to make that same trip, it is the trip we know.
What we would tell another team
If you are considering building something in this shape, a serious desktop tool with a native computation core, distributed through the app stores, a few things are worth knowing before you start.
- The app is half the project. Sandboxing, entitlements, notarization, MSIX packaging, signing and store review are not an afterthought you bolt on at the end. They are a substantial body of work, they differ completely between platforms, and they are where schedules slip. Plan for them from day one.
- Offline is a feature, not a limitation. Choosing to keep data on the device removes an entire class of security, compliance and procurement friction. For sensitive-data tools it is often the single most valuable thing about the product.
- Pick the right language for each job. A compiled engine in Go behind a Flutter interface gave us speed where we needed it and a single UI codebase everywhere else. The interoperability cost was real but paid for itself.
- File parsing is deceptively deep. "We support CSV" hides a week of edge cases. Budget for the messiness of real-world data, because that is what your users will feed the tool.
- Correctness needs an independent tester. For a tool that produces numbers people rely on, someone other than the author has to try to break it.
We raise every one of these with a client before the first sprint. A desktop product that has to install cleanly from two stores and satisfy a finance team fails in predictable places, and knowing them in week one is worth more than any amount of enthusiasm in week eight.
Questions we get from potential clients
Is MetiRecon your own product, or did you build it for a client?
It is our own. We came up with it, designed it, built it and shipped it ourselves. We show it because it is proof of what we deliver: not a demo, but a real product on two app stores that people pay for. We build for clients the same way.
Could you build a desktop product like this for us?
Yes. A native desktop app for macOS and Windows, with a high-performance engine at its core, is squarely the kind of work we do. Your product will differ in the details, but the shape of the job - design, build, test and ship to the stores - is the same one described above.
Do you handle publishing to the Mac App Store and Microsoft Store?
Yes, end to end. Sandboxing, entitlements, notarization, MSIX packaging, signing, store listings and getting through review are part of the work we take on, not an extra cost you discover later. It is also the stage where most first releases stall, which is exactly why we own it.
Can you embed a fast engine written in Go, Rust or C++ inside a Flutter app?
Yes. MetiRecon runs a Go engine inside a Flutter interface through a native interface, and the same approach works for other compiled languages. It keeps a single cross-platform UI while running heavy computation at native speed.
Do we own the code and the store accounts?
Yes. You own the code, the repositories and the store accounts. We can hand everything over to your in-house team whenever you need it, with no lock-in.
How long does a build like this take?
It depends on scope, but our standard first release is eight weeks, at a fixed price and a fixed deadline. We start by agreeing what goes into that first version, then build in two-week iterations, each ending with something you can install.
Have a product that needs building?
We take mobile and desktop products from an empty repository to shipped - review and app stores included. A 30-minute call is enough to tell you what yours realistically takes.
Book a call