commit 8be455ba989dd694dc8791951dd7a704d24b5d83 Author: Love Billenius Date: Thu Apr 9 14:42:19 2026 +0200 plan diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..682dbab --- /dev/null +++ b/PLAN.md @@ -0,0 +1,1412 @@ +# Project Template: Native Betting Study App + +## 1. Purpose + +Build a native mobile prototype for iOS and Android for a research +study. Users watch a short sports clip before a critical moment, choose +an outcome before a lock time, and then watch the reveal segment. + +The app must feel premium, fast, clear, mobile-first, and highly +polished. It must feel as easy and fun to use as possible as it's needed +for the research study. As mentioned, it is for research and +prototype use, not first-release real-money gambling. +However, the app will be tests on a small group and compared against traditional +betting sites. We thereful need to make this as premium and good as possible +to make sure that we can make meaningful observations and discussionpoints from the outcome. + +The app must support: + +- English +- Swedish + +Every user-facing string must be localizable from day one. + +--- + +## 2. Product Goals + +### Primary goals + +- Native feel on both iOS and Android +- Very high responsiveness +- Smooth video playback +- Strong gesture support +- Clear and fair lock timing +- Excellent readability and touch ergonomics +- Premium visual quality +- Stable architecture for long-term iteration + +### Secondary goals + +- Low initial app size +- Reusable backend +- Easy experimentation between: + - `control` + - `modern` + +### Non-goals + +- Real money betting in the first version +- Full regulatory launch in version one + +--- + +## 3. Technology Stack + +## Backend + +- Rust +- Axum +- Tokio +- SQLx +- PostgreSQL +- Valkey (redis) +- tracing +- thiserror + anyhow +- validator + +- OpenTelemetry +- Docker +- docker compose + +## iOS + +- Swift +- SwiftUI +- UIKit where needed +- AVFoundation / AVPlayer +- URLSession +- async/await +- native haptics +- native gesture handling + +## Android + +- Kotlin +- Jetpack Compose +- Media3 / ExoPlayer +- Kotlin coroutines +- Kotlin Flow +- Kotlinx Serialization +- kotlinx-datetime +- kotlinx-collections-immutable +- native haptics +- native gesture handling + +## Shared principles + +- No cross-platform UI +- Native UI on each platform +- Shared contracts through generated schemas and strict API specs +- Prefer Kotlinx libraries broadly inside Android and Kotlin tooling +- If a small shared Kotlin module is added later, prefer: + - coroutines + - Flow + - Kotlinx Serialization + - kotlinx-datetime + - kotlinx-collections-immutable + +--- + +## 4. Architecture Overview + +The system has three layers: + +### 4.1 Client layer + +Two separate native apps: + +- `ios-app` +- `android-app` + +Client responsibilities: + +- UI rendering +- video playback +- gestures +- odds overlay +- local state +- media prefetch +- clock sync +- sending user selections +- analytics and telemetry capture +- localization in English and Swedish + +### 4.2 API layer + +Rust backend responsible for: + +- auth +- sessions +- event feed +- odds distribution +- bet acceptance +- settlement +- experiment assignment +- analytics ingestion +- audit trail + +### 4.3 Data layer + +- PostgreSQL for durable relational data +- Redis for short-lived state and caching +- object storage / CDN for HLS assets and media metadata + +--- + +## 5. Repository Structure + +```text +root/ + README.md + docs/ + product/ + architecture/ + api/ + ux/ + research/ + localization/ + analytics/ + backend/ + Cargo.toml + Cargo.lock + migrations/ + src/ + main.rs + config.rs + app_state.rs + error.rs + telemetry.rs + db/ + auth/ + users/ + sessions/ + events/ + media/ + markets/ + outcomes/ + odds/ + bets/ + settlement/ + analytics/ + experiments/ + localization/ + admin/ + jobs/ + utils/ + tests/ + mobile/ + ios-app/ + android-app/ + contracts/ + openapi/ + protobuf/ + localization/ + fixtures/ + videos/ + manifests/ + test-data/ + scripts/ + infra/ +``` + +--- + +## 6. Core Principles + +1. The server is authoritative + - lock time is determined by the server + - odds acceptance is determined by the server + - settlement is determined by the server + +2. The client must not decide fairness + - the client sends intent + - the server accepts or rejects + +3. Video must never block on UI updates + - player and overlay must be architecturally separate + +4. Explicit state machines are required + - client and server must follow the same state model + +5. Optimize for smoothness + - low input latency + - consistent animation timing + - clear hierarchy + - readable odds and timers + +6. Localization is first-class + - all UI strings in English and Swedish + - no hardcoded copy in views + - use stable translation keys + +7. Log aggressively + - log as many meaningful user actions as possible + - support later UX optimization and research analysis + - keep logs structured and queryable + +--- + +## 7. Main Product Flow + +A user session contains multiple rounds. + +Each round: + +1. Prefetch the next event +2. Show the preview segment +3. Update odds during preview +4. User selects an outcome before `lock_at` +5. Show locked state +6. Play reveal segment +7. Show result +8. Prepare the next round + +--- + +## 8. Domain Model + +## Core entities + +### User + +- id +- external_ref +- created_at +- preferred_language +- device_platform + +### Session + +- id +- user_id +- started_at +- ended_at +- experiment_variant +- app_version +- device_model +- os_version +- locale_code + +### Event + +- id +- sport_type +- source_ref +- title_en +- title_sv +- status +- preview_start_ms +- preview_end_ms +- reveal_start_ms +- reveal_end_ms +- lock_at +- settle_at + +### EventMedia + +- id +- event_id +- media_type +- hls_master_url +- poster_url +- duration_ms +- preview_start_ms +- preview_end_ms +- reveal_start_ms +- reveal_end_ms + +### Market + +- id +- event_id +- question_key +- market_type +- status +- lock_at +- settlement_rule_key + +### Outcome + +- id +- market_id +- outcome_code +- label_key +- sort_order + +### OddsVersion + +- id +- market_id +- version_no +- created_at +- is_current + +### OutcomeOdds + +- id +- odds_version_id +- outcome_id +- decimal_odds +- fractional_num +- fractional_den + +### BetIntent + +- id +- user_id +- session_id +- event_id +- market_id +- outcome_id +- idempotency_key +- client_sent_at +- server_received_at +- accepted +- acceptance_code +- accepted_odds_version_id + +### Settlement + +- id +- market_id +- settled_at +- winning_outcome_id + +### ExperimentAssignment + +- id +- user_id +- session_id +- variant +- assigned_at + +### LocalizationKey + +- id +- key_name +- description + +### LocalizationValue + +- id +- localization_key_id +- locale_code +- text_value + +### AnalyticsEventType + +- id +- event_name +- description + +### AnalyticsEvent + +- id +- session_id +- user_id +- analytics_event_type_id +- occurred_at +- app_screen_id +- event_sequence_no + +### AnalyticsEventAttribute + +- id +- analytics_event_id +- attribute_name +- attribute_value + +### AuditLog + +- id +- actor_type +- actor_id +- action_name +- created_at +- target_type +- target_id + +### AuditLogAttribute + +- id +- audit_log_id +- attribute_name +- attribute_value + +--- + +## 9. State Machines + +### Event lifecycle + +- `scheduled` +- `prefetch_ready` +- `preview_open` +- `locking` +- `locked` +- `reveal_open` +- `settled` +- `archived` + +### Client round state + +- `idle` +- `prefetching` +- `ready` +- `preview_playing` +- `selection_pending` +- `selection_submitting` +- `selection_accepted` +- `locked` +- `reveal_playing` +- `result_visible` +- `transitioning` +- `error` + +### Bet acceptance state + +- `received` +- `validated` +- `accepted` +- `rejected_too_late` +- `rejected_invalid_market` +- `rejected_invalid_session` +- `rejected_duplicate` +- `settled` + +--- + +## 10. Backend Modules + +## 10.1 auth + +Responsibilities: + +- anonymous study sign-in or simple participant sign-in +- token issuance +- session binding + +## 10.2 users + +Responsibilities: + +- user creation +- locale preference +- participant metadata + +## 10.3 sessions + +Responsibilities: + +- start and end sessions +- attach device metadata +- track experiment group + +## 10.4 events + +Responsibilities: + +- list events +- return next round +- return event manifest + +## 10.5 media + +Responsibilities: + +- expose media metadata +- expose HLS URLs +- expose cue points + +## 10.6 markets + +Responsibilities: + +- create and query markets +- attach outcomes +- manage lock time + +## 10.7 odds + +Responsibilities: + +- publish odds versions +- stream odds changes +- store full relational odds history + +## 10.8 bets + +Responsibilities: + +- receive bet intents +- validate requests +- accept or reject against lock time +- enforce idempotency + +## 10.9 settlement + +Responsibilities: + +- store winning outcome +- expose settlement result +- audit all settlement actions + +## 10.10 experiments + +Responsibilities: + +- assign `control` or `modern` +- return feature flags +- keep assignment stable per session + +## 10.11 analytics + +Responsibilities: + +- ingest all telemetry +- normalize event types +- write event attributes to child tables +- support analysis exports + +## 10.12 localization + +Responsibilities: + +- serve language bundles +- support English and Swedish +- version translation payloads + +## 10.13 admin + +Responsibilities: + +- create events +- create markets +- create odds versions +- publish fixtures +- set settlements + +--- + +## 11. Database Design + +Use SQLx migrations only. +Do not use JSONB. +Use normalized relational tables instead. + +### Required tables + +- users +- sessions +- events +- event_media +- markets +- outcomes +- odds_versions +- outcome_odds +- bet_intents +- settlements +- experiment_assignments +- localization_keys +- localization_values +- analytics_event_types +- analytics_events +- analytics_event_attributes +- audit_logs +- audit_log_attributes + +### General rules + +- UUID primary keys +- `timestamptz` for all timestamps +- no JSONB columns +- use child tables for attributes and event payloads +- store translation text in relational tables +- use foreign keys everywhere possible + +### Key indexes + +- `sessions.user_id` +- `sessions.started_at` +- `events.status` +- `markets.event_id` +- `markets.lock_at` +- `bet_intents.session_id` +- `bet_intents.market_id` +- `bet_intents.idempotency_key` +- `analytics_events.session_id` +- `analytics_events.user_id` +- `analytics_events.analytics_event_type_id` +- `audit_logs.created_at` + +### Notes + +For analytics and audit data, avoid schemaless storage. +Use parent-child tables such as: + +- `analytics_events` +- `analytics_event_attributes` +- `audit_logs` +- `audit_log_attributes` + +This allows queryable, indexable telemetry without JSONB. + +--- + +## 12. API Design + +## 12.1 Style + +- REST for initialization and main flows +- WebSocket or SSE for live odds and state updates +- JSON transport is acceptable over the API +- database storage must remain relational +- version prefix: `/api/v1` + +## 12.2 Core endpoints + +### Session + +- `POST /api/v1/session/start` +- `POST /api/v1/session/end` +- `GET /api/v1/session/me` + +### Feed and events + +- `GET /api/v1/feed/next` +- `GET /api/v1/events/{event_id}` +- `GET /api/v1/events/{event_id}/manifest` + +### Markets and odds + +- `GET /api/v1/events/{event_id}/markets` +- `GET /api/v1/markets/{market_id}/odds/current` +- `GET /api/v1/stream` + +### Bets + +- `POST /api/v1/bets/intent` +- `GET /api/v1/bets/{bet_intent_id}` + +### Results + +- `GET /api/v1/events/{event_id}/result` + +### Analytics + +- `POST /api/v1/analytics/batch` + +### Experiments + +- `GET /api/v1/experiments/config` + +### Localization + +- `GET /api/v1/localization/{locale_code}` + +### Admin + +- `POST /api/v1/admin/events` +- `POST /api/v1/admin/markets` +- `POST /api/v1/admin/odds` +- `POST /api/v1/admin/settlements` + +--- + +## 13. Native Client Architecture + +## 13.1 Common structure + +Each app must have: + +- Presentation +- Feature state +- Domain +- Networking +- Media +- Localization +- Analytics +- Persistence + +## 13.2 iOS structure + +```text +ios-app/ + App/ + Core/ + Networking/ + DesignSystem/ + Analytics/ + Media/ + Haptics/ + Gestures/ + Localization/ + Features/ + Onboarding/ + Feed/ + Round/ + Selection/ + Reveal/ + Result/ + Session/ + Settings/ + Resources/ + Tests/ +``` + +### iOS rules + +- SwiftUI by default +- UIKit only where precision control is needed +- AVPlayer for playback +- separate player state from overlay state +- all user strings loaded from localization layer +- English and Swedish supported at launch + +## 13.3 Android structure + +```text +android-app/ + app/ + core/ + network/ + designsystem/ + analytics/ + media/ + haptics/ + gestures/ + localization/ + data/ + domain/ + feature/ + onboarding/ + feed/ + round/ + selection/ + reveal/ + result/ + session/ + settings/ + benchmark/ + androidTest/ +``` + +### Android rules + +- Jetpack Compose by default +- Media3 / ExoPlayer for playback +- ViewModel + immutable state +- Kotlin coroutines and Flow everywhere practical +- Kotlinx Serialization for API models +- kotlinx-datetime for time handling +- kotlinx-collections-immutable for UI collections +- optimize recomposition carefully +- English and Swedish supported at launch + +--- + +## 14. Gesture Specification + +The app must feel mobile-native and gesture-friendly. + +### Supported gestures + +- tap to select outcome +- vertical swipe between rounds or cards where appropriate +- swipe down to dismiss modal +- edge gestures only where platform expectations exist +- no horizontal scrubbing during an active betting round + +### Rules + +- gestures must never make selection state ambiguous +- a critical choice must get clear visual confirmation +- avoid complex multi-touch gestures +- no gesture may fight system navigation +- touch targets must be large and comfortable + +### Haptics + +- subtle feedback when a selection is accepted +- subtle feedback when the market locks +- never overuse haptics + +--- + +## 15. Screens + +### Onboarding + +- study intro +- consent +- language selection +- start session + +### Feed + +- next round preview card +- compact event information +- clear call to action + +### Active round + +- video +- countdown +- odds panel +- selection controls +- lock status +- pause or exit control + +### Locked state + +- selected outcome highlighted +- lock status clearly visible +- odds frozen for the user + +### Reveal + +- reveal segment +- simple visual emphasis +- no chaotic effects + +### Result + +- what happened +- user selection +- outcome display +- next round entry + +### Pause and exit + +- easy to reach +- easy to confirm +- no manipulative friction + +--- + +## 16. Design System + +### Visual direction + +- premium dark base +- strong contrast +- limited accent colors +- clean typography +- large video surface +- restrained motion +- high readability + +### Design tokens + +Define centrally: + +- colors +- spacing +- radii +- elevation +- typography scale +- icon sizes +- motion durations +- motion curves + +### Motion rules + +- quick feedback on touch +- calm transitions +- one consistent motion language +- no flashing or overly aggressive effects +- reveal should feel polished, not loud + +--- + +## 17. Localization Requirements + +The app must ship with: + +- English +- Swedish + +### Rules + +- every visible string uses a localization key +- no hardcoded strings in views +- backend can serve localization bundles if needed +- use pluralization support where relevant +- event and market text must support both languages +- analytics must include active locale for each session + +### Suggested locale codes + +- `en` +- `sv` + +--- + +## 18. Video and Media Pipeline + +### Requirements + +- HLS as the default streaming format +- short segments +- correct keyframes around: + - preview start + - preview end + - lock cue + - reveal start + - reveal end + +### Playback goals + +- prefetch the next clip early +- keep player and overlay independent +- recover gracefully from playback errors +- support poster fallback + +### Cue points + +Each media item must define: + +- preview start +- preview end +- lock cue +- reveal start +- reveal end + +--- + +## 19. Fairness and Clock Sync + +### Rule + +The client may display a local countdown based on synced server time, +but the server always decides final bet acceptance. + +### Requirements + +- regular server clock sync +- client clock drift compensation +- request tracing for all bet intents +- strict idempotency keys +- duplicate submissions must be safe + +### Logged timestamps + +For all critical actions, capture: + +- client timestamp +- server receive timestamp +- request id +- session id +- app version +- locale +- device platform + +--- + +## 20. Analytics and Telemetry + +Log as many meaningful actions as possible to support later +optimization, analysis, and UX refinement. + +## 20.1 Core rule + +All analytics events must be structured. +Do not store opaque analytics blobs in JSONB. +Use event tables and attribute tables. + +## 20.2 Minimum tracked actions + +- app_opened +- app_backgrounded +- app_closed +- language_selected +- consent_viewed +- consent_accepted +- session_started +- session_ended +- feed_viewed +- round_card_viewed +- round_loaded +- event_manifest_received +- preview_prefetch_started +- preview_prefetch_completed +- preview_started +- preview_paused +- preview_resumed +- preview_completed +- countdown_visible +- countdown_warning_threshold_hit +- odds_panel_viewed +- odds_version_received +- odds_changed +- outcome_focused +- outcome_selected +- selection_submitted +- selection_accepted +- selection_rejected +- duplicate_selection_attempt +- market_locked +- reveal_started +- reveal_completed +- result_viewed +- next_round_requested +- session_paused +- exit_prompt_viewed +- exit_confirmed +- playback_error +- network_error +- stream_reconnected +- localization_bundle_loaded +- locale_changed +- haptic_triggered +- gesture_swipe +- gesture_tap +- gesture_cancelled +- screen_viewed +- cta_pressed + +## 20.3 Analytics event schema + +Store: + +- event row in `analytics_events` +- one row per attribute in `analytics_event_attributes` + +Example attributes: + +- screen_name +- event_id +- market_id +- outcome_id +- odds_version_id +- countdown_ms_remaining +- locale_code +- experiment_variant +- network_type +- device_orientation +- playback_position_ms +- latency_ms + +## 20.4 Research metrics + +- decision latency +- round completion rate +- missed rounds +- post-lock selection attempts +- replay desire +- exit rate +- difference between control and modern +- effect of locale +- effect of device platform + +--- + +## 21. Audit Logging + +Audit logging is separate from analytics. + +### Audit goals + +- fairness +- traceability +- admin accountability +- settlement trace + +### Audit examples + +- event created +- market opened +- odds version published +- market locked +- bet accepted +- bet rejected +- result settled +- admin override applied +- localization updated + +Store: + +- `audit_logs` +- `audit_log_attributes` + +--- + +## 22. Security + +- signed tokens +- strict input validation +- rate limiting +- request ids +- full tracing +- secrets outside the repo +- environment-based config +- server-side experiment assignment +- server-side odds authority + +--- + +## 23. Testing Strategy + +## Backend + +- unit tests per module +- integration tests with PostgreSQL +- SQLx compile-time query checks +- state transition tests +- API contract tests +- migration tests + +## iOS + +- unit tests for view models and reducers +- snapshot tests for core screens +- UI tests for the full round flow +- playback error tests +- localization tests for English and Swedish + +## Android + +- unit tests for reducers and view models +- Compose UI tests +- macrobenchmark tests +- playback tests +- localization tests for English and Swedish +- coroutine and Flow tests + +## End-to-end + +- start session +- assign variant +- load event +- prefetch preview +- start preview +- submit selection +- lock market +- play reveal +- settle result +- ingest analytics + +--- + +## 24. Performance Goals + +## Backend + +- p95 standard GET < 100 ms in staging-like conditions +- p95 `POST /bets/intent` < 150 ms +- low odds stream latency + +## Mobile + +- no visible jank in the round flow +- very fast touch feedback +- next round prefetched before the current one ends +- low memory churn on screen transitions + +--- + +## 25. CI and CD + +## Backend pipeline + +- rustfmt +- clippy +- tests +- SQLx checks +- migration validation +- build container + +## iOS pipeline + +- lint +- unit tests +- UI tests +- localization checks + +## Android pipeline + +- ktlint or equivalent +- detekt if adopted +- unit tests +- Compose tests +- benchmark checks where practical +- localization checks + +## General + +- PR template +- changelog +- release tags +- API contract versioning + +--- + +## 26. Configuration Profiles + +Minimum environments: + +- `local` +- `dev` +- `staging` +- `study` +- `prod` + +Each environment must define: + +- backend URL +- stream URL +- CDN base URL +- analytics mode +- experiment config +- localization source +- log verbosity + +--- + +## 27. Feature Flags + +Must support flags for: + +- control vs modern +- animation intensity +- haptics enabled +- autoplay next enabled or disabled +- countdown style +- odds update visualization +- result card style +- localization source mode + +--- + +## 28. AI Agent Execution Plan + +The AI agent must work in this order. + +### Phase 1: Documents + +1. Write root README +2. Write architecture document +3. Write API spec +4. Write relational schema +5. Write SQLx migrations +6. Write state machine spec +7. Write localization key catalog +8. Write analytics taxonomy + +### Phase 2: Backend foundation + +1. Initialize Rust workspace +2. Add Axum, Tokio, SQLx, tracing, serde, config +3. Add config system +4. Add app state +5. Add error handling +6. Add health endpoint +7. Connect PostgreSQL +8. Add migrations +9. Add Redis +10. Add session endpoints + +### Phase 3: Domain and API + +1. Implement users +2. Implement sessions +3. Implement events +4. Implement event media +5. Implement markets +6. Implement outcomes +7. Implement odds versions +8. Implement bet intents +9. Implement settlement +10. Implement experiments +11. Implement localization +12. Implement analytics + +### Phase 4: Fixtures and admin + +1. Add seed scripts +2. Add fixture loading +3. Add admin endpoints +4. Add test event generator + +### Phase 5: iOS app + +1. Initialize project structure +2. Build design system +3. Build localization layer +4. Build networking client +5. Build feed screen +6. Build round screen +7. Integrate AVPlayer +8. Build countdown and odds overlay +9. Build selection flow +10. Build reveal and result +11. Add analytics + +### Phase 6: Android app + +1. Initialize project structure +2. Build design system +3. Build localization layer +4. Build networking client +5. Use Kotlin coroutines and Flow broadly +6. Use Kotlinx Serialization for API models +7. Use kotlinx-datetime for time handling +8. Build feed screen +9. Build round screen +10. Integrate Media3 +11. Build countdown and odds overlay +12. Build selection flow +13. Build reveal and result +14. Add analytics + +### Phase 7: Quality + +1. Add tests +2. Validate localization +3. benchmark critical paths +4. review gesture edge cases +5. validate analytics coverage +6. document research export path + +--- + +## 29. Definition of Done + +A version is done when: + +- the user can start a session +- a variant is assigned +- the next event is loaded +- preview video plays +- odds are shown and updated +- the user can select an outcome +- the server accepts or rejects correctly +- lock state is clear +- reveal plays +- result is shown +- analytics are written relationally +- English and Swedish both work +- the flow passes end-to-end tests + +--- + +## 30. Future Extensions + +- small shared Kotlin module for non-UI state logic +- researcher admin web panel +- analytics export to CSV or Parquet +- accessibility pass +- tablet and foldable layouts +- richer localization management +- advanced telemetry dashboards + +--- + +## 31. Important Constraints + +- no real money flow in the first version +- localization in English and Swedish is mandatory +- normalized relational logging is mandatory +- avoid JSONB in database design +- log broadly enough to enable later UX optimization diff --git a/README.md b/README.md new file mode 100644 index 0000000..175ce60 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Hermes + +Hermes is the internal name of this project