Loki — TCG Finance & Match Tracker
Loki is an iOS app for One Piece TCG players who want to track matches, manage trade expenses, and see how their decks actually perform over time. It started as a personal tool for logging locals and evolved into a full-featured companion app.
What it does
- Match logging with opponent, decks played, result, event type, and notes
- Expense tracking across cards, accessories, and tournament entries
- Win rate analytics broken down by deck, matchup, and event type
- Deck cost calculator to track investment per build
- iCloud sync so data follows you across devices without an account
The Stack
Expo / React Native — cross-platform UI
TypeScript — type safety throughout
Zustand — state management
AsyncStorage — local persistence
iCloud KV Store — cross-device sync
RevenueCat — subscription management
Expo Router — file-based navigation
Architecture Decisions
Zustand over Redux. Loki’s state is straightforward — match lists, expense records, user preferences. Zustand’s minimal API (create + selectors) keeps store files under 100 lines. There is no middleware chain to debug and no action/reducer boilerplate. For a solo-built app, that simplicity compounds fast.
iCloud KV Store dual-write pattern. Every store write goes to both AsyncStorage (local) and iCloud KV Store (cloud) through a custom syncedStorage adapter. This gives instant local reads with eventual cross-device consistency — no Supabase account required. The sync layer is transparent to the rest of the app; stores just call set() and the adapter handles both targets.
File-based navigation with Expo Router. Routes map 1:1 to the file system under app/. Adding a new screen is creating a file — no registration step, no navigation config to update. Deep linking works automatically.
Challenges
iCloud conflict resolution. When the same store key is written on two devices before a sync round-trip, iCloud picks a winner silently. Loki handles this by treating array data (matches, expenses) as append-only logs keyed by UUID. Conflicts on scalar values (settings, preferences) use last-write-wins, which is acceptable for non-critical data.
Excluding premium status from sync. isPremium is explicitly excluded from the persisted state via Zustand’s partialize option. Without this, a user could manipulate the iCloud KV Store value and bypass the paywall on another device. RevenueCat is the single source of truth for entitlement status — it gets checked fresh on each app launch.
App Store review compliance. Subscription apps get extra scrutiny. The paywall screen includes the auto-renewal disclaimer, links to both the privacy policy and Apple’s standard EULA, and restores purchases on tap. Missing any of these is an automatic rejection.