DEV Community

Tavexa Mini
Tavexa Mini

Posted on

Building a server-authoritative "mining" loop in Flutter + Supabase

We recently shipped TAVEXA MINI, a free Android app where a one-tap "mining session" runs on a server timer and credits TVX, an in-app unit with no cash value. There's no token, no blockchain and nothing is mined on the phone.

Phone "mining" apps have a deserved bad reputation, mostly because the "value" is just a counter running on the client. We wanted the opposite: nothing important is decided on the phone. This post is about how we set that up.

The rule: the server decides, the client predicts

A session starts with one tap. From that moment, the server owns the clock. The app shows a live countdown and an estimated balance, but that's only a prediction. When the session ends, the server calculates the result and the client accepts it.

This matters because the app also has crews (bonuses when several friends mine on the same day), weekly leagues and badges. If the client could decide rewards, the leaderboard would be meaningless within a week.

Two copies of the economy rules

We keep the reward rules in two places:

  • Dart in the Flutter app, so the UI can react instantly (progress bars, "you'll get +40 if one more crewmate mines today", and so on).
  • TypeScript in Supabase Edge Functions, which is the real authority.

Two copies of the same logic sound like a bug factory, so we treat them like a contract. A shared set of test vectors (inputs plus expected outputs) runs against both sides, and a change to one side without the other fails the build. In practice this has caught several "the app says +30 but the server gives +25" mistakes before they shipped.

Offline taps: an outbox in front of a gateway

Mobile networks are unreliable, especially in the countries where most of our players are. So the app never calls the reward endpoints directly from the UI. Every action goes into a small local outbox, and a gateway sends them in order with backoff. If the server rejects or adjusts something, the client simply takes the server's answer.

One atomic write, and no exceptions as control flow

All state changes for a player go through a single atomic write on the database side. Early on, that write signalled "someone else changed this first" by raising an error. Under load, the client treated that error as a temporary failure and retried, which caused a burst of retries in production.

The fix was simple: the write now returns a boolean ("applied" or "not applied"), and the caller decides what to do. Lesson learned: don't use exceptions as normal control flow across the database boundary.

What we'd love feedback on

  • How do you keep business rules in sync between a client and a server? Shared test vectors work for us, but code generation is tempting.
  • If you've built offline-first flows with Supabase, how do you handle conflicts?

The app is free on Google Play if you want to see the result: https://play.google.com/store/apps/details?id=com.tavexa.mini

More about the project: https://tavexa.app

Top comments (0)