---
title: "Acrea"
description: "A land measurement and surveying toolkit for surveyors, farmers and real-estate professionals. Measure area and distance on a map or on foot, run COGO traverses, generate metes-and-bounds descriptions, and work offline in the field, backed by a Postgres sync engine, team workspaces and a tag-triggered release pipeline."
url: "https://dewmina.dev/projects/acrea"
canonical: "https://dewmina.dev/projects/acrea"
kind: "PRODUCTION"
year: "2026"
status: "Live on the App Store and Google Play · v1.3.1"
role: "Founder and sole engineer: Flutter app, Postgres backend, marketing site and the CI/CD between them"
author: "Dewmina Udayashan"
---

# Acrea

Survey-grade land measurement that works with no signal and no subscription to a desktop suite.

A land measurement and surveying toolkit for surveyors, farmers and real-estate professionals. Measure area and distance on a map or on foot, run COGO traverses, generate metes-and-bounds descriptions, and work offline in the field, backed by a Postgres sync engine, team workspaces and a tag-triggered release pipeline.

**My role:** Founder and sole engineer: Flutter app, Postgres backend, marketing site and the CI/CD between them

**Status:** Live on the App Store and Google Play · v1.3.1

**Links:** [App Store](https://apps.apple.com/app/acrea-gps-land-area-measure/id6775723115) · [Google Play](https://play.google.com/store/apps/details?id=com.acrea.app)

**Stack:** Flutter, Dart, Riverpod, Supabase, PostgreSQL, SQLite, Google Maps, Mapbox, RevenueCat, Firebase, Codemagic, Shorebird, Next.js, Vercel

## By the numbers

- **17** Schema versions migrated forward without data loss
- **12** Locales shipped, enforced by a CI translation check
- **2** Map engines behind a single interface
- **4** Codebases in one monorepo, on independent release cadences

## The problem

Professional surveying software is desktop-bound, licensed by the seat, and assumes an office. Consumer area-measure apps solve the opposite problem badly: they run in a browser tab of ads, lose the plot the moment connectivity drops, and produce nothing you could put in front of a client. Acrea had to sit between them: accurate enough that a surveyor will stake a boundary on it, cheap enough that a smallholder will install it, and functional in a field with one bar of signal. That is a systems problem long before it is a UI problem: two map engines with different licensing models, a local database that must survive seventeen schema changes without losing a measurement, and a sync layer that has to be correct when the same account is open on three phones.

## The approach

- **One map interface, two engines.** A `MapEngine` abstraction with Google Maps and Mapbox implementations. The entitlement layer picks at runtime, so free users never cost Mapbox tiles and offline regions stay a Pro capability without forking a single screen of feature code.
- **A geometry kernel that owns no pixels.** Geodesic area, distance, bearing and COGO traverse maths live in a pure Dart core with no map SDK dependency, which is why the same numbers drive the map overlay, the PDF report and the metes-and-bounds description.
- **Offline-first, then synced.** sqflite is the source of truth with forward-only migrations through seventeen schema versions. Supabase tables mirror those columns exactly, so a sync push is a straight upsert rather than a translation layer that drifts.
- **Shipping as a system.** A version tag on a release branch triggers Codemagic to build, sign and submit both platforms. Dart-only hotfixes ship over the air through Shorebird, with no store review in the loop.

## System architecture

- **Map engine.** One interface, two implementations (Google Maps free / Mapbox Pro); the entitlement layer selects, offline regions ride on Mapbox
- **Geometry kernel.** Pure Dart geodesic area, distance, bearing and COGO traverse maths with no SDK dependency, unit-testable in isolation
- **Local persistence.** sqflite, 17 forward-only migrations; the device works fully without a network or an account
- **Sync engine.** Push/pull scheduler, wire codec and status surface over Supabase Postgres, mirroring the local schema column-for-column
- **Teams & access.** Organisations, membership, invites and scope switching enforced in Postgres row-level security, not in the client
- **Share links.** An Edge Function mints a frozen, self-contained snapshot; the website renders it by token only, so links can never be enumerated
- **Release pipeline.** Tag-triggered Codemagic builds for both stores, Shorebird OTA for Dart-only patches, release notes generated from the changelog

## Engineering decisions

### The share payload is a frozen snapshot, not a view of a live row

A person sharing a measurement may have no account at all, and the recipient opens the link with no session. So the snapshot is self-contained and the table is unreadable with the public key, so the site can only reach a share through a token-scoped function. A policy that exposed rows without the token would quietly turn every link into a public directory of everyone's land.

### Two map engines, chosen by entitlement rather than by build flavour

Mapbox gives offline regions and a better cartographic base; Google Maps is free at our free-tier volume. Putting the choice behind one interface at runtime meant the paid capability was an engine swap, not a second implementation of every map screen, and it kept tile spend proportional to revenue.

### Forward-only migrations, mirrored on the server

The Supabase tables shadow the sqflite schema down to column names. It looks like duplication; it is the reason a sync push is an upsert instead of a mapping layer that rots. Add a column on one side and the round-trip is lossy, which is the sort of bug that shows up as a missing boundary three weeks later.

### A monorepo with two independent cadences

The app ships on a version tag through Codemagic and Shorebird; the marketing and documentation site ships continuously to Vercel. They live together so a feature and the documentation explaining it land in the same pull request, and they deploy apart so neither blocks the other.

## 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.

---

Written by Dewmina Udayashan. Full site: https://dewmina.dev
