Expo SDK 55 React Native app with Reanimated 4, self-hosted Supabase, and a custom OdometerText animation
Satta Matka is an Indian numbers lottery with roots going back to the 1960s — originally based on opening and closing cotton rates transmitted from New York to Bombay, evolving over decades into a draw format where players bet on digit combinations across game types: Single Digit, Jodi, Panna, Half Sangam, Full Sangam. The market is grey-area legal in India. It operates openly, it has a significant user base, and clients who build platforms for it have real technical requirements around live data delivery, wallet integrity, and result presentation. I am not going to moralize about the category. The engineering problem is legitimate and the client relationship was straightforward.
This was the third iteration. The first was a Flutter prototype that did not progress far — the client's requirements clarified mid-build and the architecture did not fit what they actually needed. The second was an earlier React Native version that worked but felt unpolished: the animations were janky on low-end devices, the backend was a shared host with no real-time capability, and the admin workflow for declaring results was awkward enough that the operator preferred using a spreadsheet. This version — 11,190 lines across 53 TypeScript files on Expo SDK 55 and React Native 0.83.2 — was where I finally had enough accumulated context about what the client actually needed to make the right architectural choices from the start. Each iteration carried lessons forward. The third one has the structure I would have designed on day one if I had known then what I know now.
The core product requirement that all three iterations had to solve: operators declare results on a schedule, users are watching live, and the moment a result is declared the display must update — number by number, with the tactile feel of a physical Satta board. Users of physical boards have a sensory expectation for what a result reveal looks like. A text swap does not satisfy it.
The second iteration displayed results as plain text updates. They arrived correctly but they felt wrong. The client described it as "not live-like." That feedback sounds vague; what it actually means is that the UI did not match the psychological expectation users bring from the physical context. Understanding that was more useful than any technical decision I made on this project.
The third iteration answered that feedback with OdometerText — a custom animation component built specifically to meet the physical board expectation. That was the design centre. Everything else — the backend choice, the admin workflow, the wallet logic — was built to support that core experience reliably at scale.
OdometerText. Each digit in a result is an independent animated column. When a value changes from 4 to 7, the column for that digit scrolls through 5, 6, and lands on 7 — like a physical odometer reading forward, not a number teleporting. The implementation uses Reanimated 4's worklet model: each digit column is a SharedValue tracking its scroll offset, driven by withTiming to a derived target position. The animation logic runs entirely on the UI thread, bypassing the JS bridge. This matters on the low-end Android devices that represent the majority of this app's user base — bridge-mediated animations at 60fps produce visible jitter when the device is also handling a WebSocket connection. Running on the UI thread eliminates that jitter. The final display composites a vertical strip of digit characters in a clipped Animated.View, producing a scroll that decelerates mechanically. When Supabase Realtime pushes a new result, the digits roll before the number reads as settled — which is exactly the tactile expectation.
Self-hosted Supabase on Coolify. I default to self-hosting for client projects when the data is sensitive and the client needs ownership. Supabase's managed cloud is a good product, but it means a third party holds the wallet transaction records, deposit history, referral ledger, and user identity data. For a client in a grey-area market, that is an uncomfortable dependency with no obvious upside. Coolify on a VPS gives the client a Supabase instance they control — backups they configure, data they can migrate, no vendor lock-in on the infrastructure layer.
The backend schema is eight relational tables. Supabase Realtime subscriptions push live market status and result declarations to connected clients via WebSocket. When the operator declares a result through the hidden admin tab, the update propagates to every active session in real time. I implemented stale-while-revalidate caching in AsyncStorage for users on flaky connections — the last known state is shown immediately while the WebSocket reconnects, rather than an empty screen.
Wallet and game math. Seven game types with distinct payout multipliers. The wallet layer includes tiered deposit bonuses — the bonus percentage is calculated on the backend before the transaction is confirmed, not on the client. The referral system tracks downline depth and credits the referrer on each deposit by a referred user. Every debit and credit is a row in the transaction ledger with enough detail that a user can audit their full history. The payout calculation for each game type is server-side and logged — the user's claim is verified against the declared result before the wallet is credited.
Hidden admin tab. The operator's result-declaration interface is a sixth tab in the user-facing app, conditionally rendered for authenticated operator accounts. The design trades a clean user/operator separation for operational simplicity: the client runs one app, not two. A future iteration should split this into a dedicated operator panel, but that requires a client who is ready to manage two applications and two sets of credentials. For the deployment context this app actually operates in, the single-binary approach was the right call.
Victory Native for charts. Market history and result frequency distributions are displayed using Victory Native's Skia renderer, which keeps chart drawing on the native thread consistent with the rest of the UI's performance posture.
Did this resonate?
11,190 lines of TypeScript, 53 files, delivered as a versioned archive per contract terms. No git history — the client specified file delivery, which is a common arrangement in this market. I documented the delivery clearly and included an architecture notes file so any developer who picks this up later understands the structure without having to reverse-engineer it from the code.
Two honest caveats I want to be explicit about. First: Postgres RLS is disabled on the demo instance. In production, RLS is the mechanism that prevents one user's wallet records from being queryable by another user's authenticated session. I documented this gap in the handoff notes. It was a pragmatic choice to reach a demonstrable state faster, not a permanent decision — the schema is structured correctly for RLS enforcement and adding it is a defined next step. Second: the admin tab's co-location with the user app means a determined reverse-engineer of the APK could find the operator route. For the current deployment scale this is an acceptable risk; at growth it needs to be addressed. I noted both gaps explicitly rather than quietly leaving them as someone else's problem to discover.