Ashenmarch turns the real world into a medieval territory war. Every walk, run, ride, or swim you take is a march — your GPS route encloses real-world ground and claims it for your House. Hold it, wall it, and watch rivals try to take it back; neglect it and it decays.

Live on the App Store and Google Play, in 44 countries and four languages — two native clients over a shared Go/PostGIS backend, with a real subscription taking real money.

Inspiration

Fitness apps track your run and hand you a number. We wanted the run to mean something the moment it ends — to leave a mark on a shared world. The medieval feudal frame did the rest: land you can win and lose, Houses and rivalries, a Herald that narrates your conquests, coats of arms you design.

The result is a game where the motivation to go outside is territorial, not numeric — you run because someone is besieging your holdings, or because there's unclaimed ground three blocks over.

What it does

  • March to claim. Start a march and go walk, run, ride, or paddle/row on water in the real world. Your GPS route encloses area; enclosed ground becomes territory owned by your House, drawn on a live war-map.
  • Build & defend. Out-and-back routes raise walls; connected holdings form Districts. Territory decays over time, so an empire is something you maintain, not just win once.
  • Invade. Run through a rival's holding to drive down its defenses; enclose it and it flips to you — you take only what you actually enclose.
  • Every fitness level counts. Walking, running, Cavalry (cycling) and Navy (water) are each first-class, with their own baselines and identity. Fortification is presence-based, so a beginner walking slowly inside a court member's region contributes real defensive value — there is no pace at which you stop mattering.
  • A feudal world. Houses, Courts and vassals, contested Kingdom Regions, city leaderboards, and a Herald that turns your runs into feudal dispatches and re-engagement notifications.
  • Signature share cards. Every campaign renders as a heraldic parchment Conquest Share Card — your crest, your conquered ground, your chronicle — made to be shared.
  • An earned economy. Territory yields Gold tribute banked in your House Vault, spent on cosmetics. Earned only, never bought.

The grander the holding, the more often it demands you

Decay is not a single curve — it is a tolerance per fortification tier, and it runs backwards from what you'd expect:

Tier Days before it begins to decay
Citadel 5
Stronghold 7
Keep 10
Village 14
Outpost 21

A Citadel is the most powerful thing you can build and the most demanding to keep. Walls decay on their own clock (14 days, 7 if reinforced), and a House that can't pay its upkeep has its decay accelerated rather than its land confiscated — you are never locked out, only pressed.

How we built it

  • iOS — native Swift / SwiftUI: live-GPS capture with background activity sessions, Live Activities / Dynamic Island for the active march, MapLibre for the in-app map, and a bespoke parchment Canvas renderer for the heraldic war-map and share cards.
  • Android — native Kotlin / Jetpack Compose: a foreground-service march with the same background-survival guarantees, Compose Canvas parity for the war-map and share cards, and Glance widgets.
  • Backend — Go with PostgreSQL / PostGIS for the real-time territory geometry (enclosure, invasion splits, walls, decay), a runtime game-config layer so balance changes ship without a release, and a feudal narrative engine.
  • Web companion — SvelteKit / TypeScript portal that mirrors the game for imported runs.
  • Monetization — RevenueCat powers the premium Membership (monthly + annual, 30-day free trial) through one subscription group and the premium entitlement. One integration spans StoreKit and Google Play Billing, and entitlement is validated server-side — RevenueCat webhooks land on the Go backend, which is the only thing that decides what a player owns. The client never gets a vote.
  • Localization — English, Spanish, French and Portuguese in-app, generated from a single canonical source of truth in git, with regionally-targeted store listings (es-419, fr-CA, pt-BR).
  • Platform — Fly.io (app + Managed Postgres), Firebase Cloud Messaging / APNs push, Sentry + MetricKit for crash and exit-reason telemetry, PostHog analytics, GraphHopper route planning, Strava import.

Challenges we ran into

Keeping a march alive for 45 minutes. This was the number-one launch blocker for months, on both platforms. A live GPS session the OS is actively trying to reclaim is the product — if tracking dies at minute 38, the run didn't happen. It took background activity sessions, foreground services, crash-orphan recovery on next launch, and app-exit-reason telemetry so we could tell an OOM kill from a warm return instead of guessing.

One Spanish word cost a release. The longest Spanish tier name wrapped a pill at 375pt. Invisible in code review, invisible in English, and it shipped — version 1.0.2 exists to fix it. The lesson generalised: every screen now gets a render pass at the narrowest width and the largest font scale, in every language.

A double-charge that only appears on a timeout. Our spend flows minted the idempotency key inside the tap handler, so a request that timed out and retried generated a new key and charged twice. It hit three separate surfaces before we recognised it as one class of bug and moved the key to session scope.

Silent failures beat loud ones every time. A hardcoded country-suffix check decided what language to name a city's provinces in — so every Spanish-speaking market outside Mexico quietly got English place names. Nobody reports that, because an English place name still looks like a place name. Most of our worst bugs looked exactly like success.

Accomplishments that we're proud of

  • Two native clients, both stores, one window. iOS and Android are genuinely native — not a shared runtime — and reached production days apart.
  • Real-time territory geometry. PostGIS handles route enclosure, invasion splits, wall raising and time decay against live player data, fast enough to render a war-map the moment a march ends.
  • Four languages at launch, not retrofitted — every new player-facing string ships translatable by definition of done.
  • A real subscription, server-validated. Trial to paid, monthly and annual, across two stores, with entitlement decided by the backend rather than trusted from the device.

What we learned

Instrumentation you don't verify is decoration. We found a blocking CI gate that had been checking one of its four inputs for months — the other three silently passed on empty sets. A blind gate and a clean gate produce an identical log. Coverage is now explicit: a check that cannot see its input must say so and fail, never skip quietly.

Count the places that must agree. The same defect — "what languages exist, declared in two places" — appeared independently on iOS, Android, the backend, and the place-name generator. Four bugs, one habit. The fix was never a better process; it was one manifest and one assertion.

"It's on main" is not "it's live." Several shipped features were inert in production because a one-time job or config flag was never applied. We learned to verify the effect in the database, not the code in the branch.

What's next

  • Apple Watch and Wear OS — a march you start from your wrist, with the phone in your pocket or left at home.
  • German and Italian, shipping together. Languages don't interact, so one layout pass finds both sets of bugs.
  • Local place names as world content. The generator already builds feudal toponymy from real map data — English and Spanish today. Dutch and Nordic vocabularies are next, because a player in Utrecht or Aarhus should be conquering ground that sounds like their own country, not someone else's.

Built With

Share this project:

Updates

posted an update —

We went looking for why trial starts were low and found something worse.

Twenty-six real people had downloaded Ashenmarch, created an account, and clicked an email verification link. Three deliberate acts of intent. Twenty-four of them never completed a single march.

The cause was in our own launch sequence. A new player names their House. They design a coat of arms — pick a shield, a charge, a tincture. Then, before the app has even asked for location permission, they hit a paywall they cannot dismiss.

We had built a game whose entire promise is "the land is won on foot", and we were asking for a card before anyone had taken a step.

What makes it worth writing up is how invisible it was. Nobody complains about this. There is no crash, no error, no support ticket — the funnel just quietly ends. We only saw it because we went looking at the join between players and subscriptions for an unrelated reason. Our worst bugs keep turning out to look exactly like success, and this one looked like disinterest.

So the first conquest is free. Until a player has actually claimed ground, the paywall does not exist for them. They march, they close a loop, they watch their own street turn gold and hear the Herald name it — and only then does the app ask anything of them. Same price, same trial. Different order.

Two details we got wrong on the first pass and want to record:

It has to key on ever conquered, not currently holds ground. Territory decays in Ashenmarch. Key it off current holdings and anyone who lets their land lapse is silently un-walled — free play forever, and an incentive to play badly.

And it has to key on a successful claim, not a completed march. A first run that fails to close a loop must not burn the free conquest, or we wall someone who experienced nothing — strictly worse than what we have now.

We considered a permanent free tier instead — Strava import only, no Market. We're not doing it yet, because the data genuinely cannot tell us whether people refused the price or the timing of the ask, and those need opposite designs. Moving the wall is cheap, reversible, and answers the question. If players who taste a conquest then subscribe, the wall was in the wrong place. If they still don't, the objection is price and we'll design the tier knowing what it has to overcome.

One more thing worth noting for anyone building this: the whole change is server-side. Our clients already honour an "enforcement off" signal from the backend, and that signal is computed per player. No app release, no review cycle, both platforms at once, and revertable with a single config toggle. Deciding entitlement on the server rather than trusting the device was a call we made for correctness months ago. It just paid for itself in shipping speed.

Coming in 1.0.6

German and Italian, and the EU.

The last update promised these two shipping together, on the logic that languages don't interact so one layout pass finds both sets of bugs. They're now drafted end to end — 3,762 translatable strings per language across the seven canonical sources that feed all four clients, every row checked for placeholder parity, register and orthography before it was allowed near a build.

German is the one that finds your layout bugs. It runs about 30% longer than English, and the first casualty was already waiting for us: on the Home rank ladder, Freigelassener — Freeman — rendered as Freigelassene. One clipped letter, in the label naming the rank you currently hold.

Alongside them, Ashenmarch opens across the EU and EEA with localised store listings in German, Italian, French and Spanish. Six languages, and a map that keeps getting bigger.

Log in or sign up for Devpost to join the conversation.

posted an update —

Update: 1.0.5 approved and live on iOS and Android before the deadline

Our last update said 1.0.5 was in progress. It's done: Ashenmarch 1.0.5 was approved by Apple and Google and is live on the App Store and Google Play, ahead of the extended Shipaton deadline (October 1, 12:00 PM).

1.0.5 is the First Conquest release, a rebuilt first day for every new runner:

  • The Herald's charge: on Day 1 the app names a real Crown holding near you and frames the map on it, so your first run has a clear target.
  • Live enclosure: shading and a live readout during every march show how much ground your loop is closing.
  • A first conquest that teaches: after your first claim, "what's next" guidance shows where to go next. A march that doesn't close a loop now shows what to fix instead of an error.
  • Teaching empty states on the Armoury, Empire and Market screens replace the old tutorial tour.
  • Fixes and polish: house naming, Strava import checks, push re-registration, and translation fixes across English, Spanish, French and Portuguese.

Every change came from watching real players' first days since our August 31 launch.

App Store: https://apps.apple.com/app/id6784425372 Google Play: https://play.google.com/store/apps/details?id=com.cunningplay.ashenmarch

Log in or sign up for Devpost to join the conversation.

posted an update —

** 1.0.4 is live on iOS, Android's in Play review **

iOS 1.0.4 shipped this week with a fix for a real data-loss bug — a forced sign-out mid-march was destroying the run before it ever reached the server — plus a fully localized first-run tour (EN/ES/FR/PT). Android's 1.0.4 carries the same fixes and is in Google Play's review queue now; Android players are still on 1.0/1.0.1 until it clears.

** Up next: 1.0.5, "The First Conquest" **

We're close to done rebuilding the entire new-player onboarding arc — the moment you register, see your first real nearby holding to claim, and complete your first march. The goal (D-308): whatever we show a brand-new player, we show every player, forever — no separate "tutorial mode" that quietly diverges from the real game. One clarifier left (disambiguating the On Foot / Cavalry / Navy activity picker) before it heads into a full device QA pass on both platforms, four locales.

Log in or sign up for Devpost to join the conversation.

Submission history