
This engineering journal documents the real architecture decisions, production failures and durable rules learned while building Taht ve Kan.
1. Architecture: the client requests, the server decides
React client → Server Actions → pure game logic → Supabase/PostgreSQL
The client is never trusted. It can request an attack, but turn ownership, action points, adjacency and authorization are verified on the server. Combat, economy, Elo and league thresholds live as database-independent pure functions. RLS is enabled on every table and anonymous access cannot read rows.
2. Identity: guest and account players share one resolver
Guests use an httpOnly per-game token; signed-in players use a 30-day session backed by a scrypt hash. resolvePlayer() accepts both. The secret game token is never added to a broad select list, and fields returned to the client are explicitly enumerated.
3. The real performance bottleneck was geography
The database ran in Frankfurt while server functions initially ran in a distant default region. Dozens of sequential calls repeatedly paid roughly 90 ms of network latency. Pinning functions to fra1 and consolidating account loading from seven round trips to two fixed the actual source of the perceived slowness.

4. Migration resilience
Schema changes and code deployments are not guaranteed to happen together. A query reading a new column is isolated and guarded; when the column does not exist, the default value is used. Breaking this rule with daily_streak caused the entire main select to fail and made cosmetics and the admin flag appear to reset. The permanent rule is simple: never put a guarded column in the main select.
5. Three defenses against stalled matches
- A turn deadline advances play automatically.
- Players, timeouts and AI all pass through the same
advanceTurnCore()function. - Long-abandoned matches can be taken over by AI.
Unused action points also pay a small gold and food bonus, creating an economic nudge to finish a turn instead of holding it.
6. Three generations of the map
The first version used procedural SVG hexes: clean engineering, generic presentation. The second used a painted tile atlas but still looked like a board. The current map is one painted world divided into 22 asymmetric regions by terrain-aware Voronoi segmentation. Artificial white borders were removed; ownership is communicated through soft color influence. Legacy 40-region matches still open with their original renderer so old games remain valid.

7. Android is not a WebView
The Android client began as a TWA wrapper and was rebuilt as a native Kotlin and Jetpack Compose application. The game engine was rewritten in Kotlin, supports offline play and uses the same backend through dedicated mobile endpoints when online. Defaults for new fields and tolerance for unknown fields protect older local saves and APK versions.
8. Decoupling server cost from registered users
- Completed matches skip battle, espionage and event logs.
- Polling stops while
document.hiddenis true. - Chat polls every eight seconds and turn refresh every eighteen seconds.
- The expensive map is memoized and the one-second timer lives in a separate component.
As a result, cost tracks active turns more closely than total accounts.
9. Platform-specific traps
"use server"modules may export only async functions.- Server-only modules cannot be imported into client components.
- Windows case-insensitivity created a real collision between
recentGames.tsandRecentGames.tsx. - Path invalidation cannot be called from a server action during page rendering, so the email verification flow had to be reorganized.
10. Current state
The game is in closed beta with registration, approval, sign-in, email verification, invitations and feedback collection active. Thirty database migrations have been applied. The native Android client is at version 1.8.1. Real payment integration remains intentionally deferred while debugging, balance and retention take priority.
