The shape of the problem
Land measurement has two markets that never talk to each other. At the top, total-station software and GIS suites: correct, expensive, desktop, and priced per seat. At the bottom, a long tail of free "area calculator" apps that put a pin under your thumb, wrap it in interstitials, and produce a number with no provenance.
The gap is the person who needs a defensible figure in a field. A surveyor checking a parcel before the crew arrives. A farmer splitting a paddock. An agent who has to tell a buyer what they're actually buying. They need the accuracy of the first market and the accessibility of the second, and they need it when the nearest cell tower is eleven kilometres away.
That constraint, correct and correct with no network, is what determined the architecture.
Local first, because the field has no signal
The device is the source of truth. Every measurement, place, category and group is written to sqflite first and is fully usable with no account and no connectivity. The database has been migrated forward seventeen times; the _onUpgrade path is forward-only and never destructive, because the alternative is telling somebody their boundary is gone.
Sync is an addition to that, not a prerequisite for it. The Supabase Postgres tables mirror the local schema column-for-column, deliberately, so that a push is a straight upsert. It reads as duplication in a code review. It is the reason the round trip is lossless.
Access control belongs in the database
Acrea has team workspaces: organisations, membership, invites, and a scope switch that changes what "my measurements" means. None of that is enforced in the Flutter client, because a client-side check is a suggestion. It lives in Postgres row-level security policies, and the app simply cannot see a row it has no claim to.
The share feature took the same reasoning further. A share link has to work for a recipient with no session, and may be created by someone with no account. So the payload is a frozen snapshot, self-contained rather than a live view of a row, and the shares table is not readable with the public key at all. The website resolves a link through a single token-scoped function. There is no query shape that lists shares, which means there is no accident that turns the feature into a public register of who owns what.
Pro workspaces can brand a share page. The resolution order (workspace brand, then team name and logo, then the owner's profile) is resolved server-side at mint time, so an older app build that knows nothing about workspace branding still produces a correctly branded page.
Two engines, one interface
The free tier runs on Google Maps. Acrea Pro runs on Mapbox, which is what makes downloadable offline regions possible. Both sit behind a single MapEngine interface and the entitlement layer picks the implementation at runtime.
The alternative, a build flavour per engine or a Pro fork of every map screen, would have doubled the surface area of the most-used screen in the app. This way, the paid capability is an engine swap. Tile spend stays proportional to revenue, and the feature code has no idea which engine it is drawing on.
Geometry that owns no pixels
Area, distance, bearing, and the COGO traverse maths live in a pure Dart kernel with no dependency on any map SDK. That separation is why the same computation drives three very different outputs: the polygon on the map, the figures in the generated PDF report, and the metes-and-bounds description that reads like a deed.
It is also why the maths is unit-testable without a widget tree, which matters when the unit conversion table runs from square metres to perches.
Shipping as a system
Trunk-based development. main is always releasable. Release branches are cut for a minor, and a version tag on one of them triggers Codemagic to build, sign and submit both platforms. Dart-only hotfixes get a patch tag and go out through Shorebird over the air, skipping store review entirely.
Store release notes are generated from the changelog, and a user-visible change is only merged with its changelog entry in the same pull request, because release copy written three weeks later from memory is always worse than release copy written by the person who made the change.
Twelve locales ship with the app, and CI fails any pull request that adds a string to the English template without translating it. At runtime a missing key falls back to English and nothing breaks locally, which is exactly why it needs a gate in the pipeline rather than a convention in a document.