26 KiB
Project Template: Native Betting Study App
This is the canonical working plan and progress log for the project. Use this file for progress notes, next steps, and status updates.
Current Status
Done So Far
- Android was renamed from the old study app into
com.hermes.app, with the app entry, theme, repository, media, and feature screens rewritten around the backend-backed Hermes flow. - Android no longer relies on the sample fixture; it boots from backend session and round data, uses
/health.server_timefor clock sync, buffers analytics, and flushes them to the backend. - Android localization is in place for English and Swedish, including the remaining locale toggle labels and session language display.
- Backend now exposes
server_timefrom/health, publishes the matching OpenAPI contract, and records audit events for session, bet, event, market, odds, and settlement actions. - Backend smoke coverage was added for
server_timeand audit logging. - iOS now follows the backend-backed flow, syncs clock from
/health, flushes analytics, and uses localized language labels instead of hardcodedENandSV. - iOS source was updated for the backend-backed session and round flow, including the real preview cue points and localized session strings.
- iOS settings now uses localized copy instead of a scaffold placeholder, and the screen is wired into the root scroll flow.
- Android debug build passes with
./gradlew :app:assembleDebug. - Backend tests pass with
cargo test. - Backend localization bundles now have a contract test, and the localization catalog matches the shipped English and Swedish keys.
- The research export path is documented under
docs/research/export-path.md. - Backend bet coverage now includes a direct regression for late-lock rejection.
- Backend bet coverage now includes invalid-session and invalid-market rejection regressions, plus settlement override coverage.
- Backend analytics coverage now includes a representative key user flow batch.
- iOS project scaffolding now exists as
mobile/ios-app/HermesApp.xcodeprojwith a shared scheme and XCTest target. - iOS sources and tests typecheck against the iPhone simulator SDK, and a standalone localization/error-mapping harness passes.
Still Open
- Continue through the remaining plan phases and finish any leftover localization and polish work.
- Run Xcode build/test validation once the local Xcode license is accepted;
xcodebuildis currently blocked by the license gate in this environment. - Keep expanding tests around session, odds, settlement, and analytics behavior.
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:
controlmodern
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-appandroid-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
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
-
The server is authoritative
- lock time is determined by the server
- odds acceptance is determined by the server
- settlement is determined by the server
-
The client must not decide fairness
- the client sends intent
- the server accepts or rejects
-
Video must never block on UI updates
- player and overlay must be architecturally separate
-
Explicit state machines are required
- client and server must follow the same state model
-
Optimize for smoothness
- low input latency
- consistent animation timing
- clear hierarchy
- readable odds and timers
-
Localization is first-class
- all UI strings in English and Swedish
- no hardcoded copy in views
- use stable translation keys
-
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:
- Prefetch the next event
- Show the preview segment
- Update odds during preview
- User selects an outcome before
lock_at - Show locked state
- Play reveal segment
- Show result
- 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
scheduledprefetch_readypreview_openlockinglockedreveal_opensettledarchived
Client round state
idleprefetchingreadypreview_playingselection_pendingselection_submittingselection_acceptedlockedreveal_playingresult_visibletransitioningerror
Bet acceptance state
receivedvalidatedacceptedrejected_too_laterejected_invalid_marketrejected_invalid_sessionrejected_duplicatesettled
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
controlormodern - 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
timestamptzfor 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_idsessions.started_atevents.statusmarkets.event_idmarkets.lock_atbet_intents.session_idbet_intents.market_idbet_intents.idempotency_keyanalytics_events.session_idanalytics_events.user_idanalytics_events.analytics_event_type_idaudit_logs.created_at
Notes
For analytics and audit data, avoid schemaless storage. Use parent-child tables such as:
analytics_eventsanalytics_event_attributesaudit_logsaudit_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/startPOST /api/v1/session/endGET /api/v1/session/me
Feed and events
GET /api/v1/feed/nextGET /api/v1/events/{event_id}GET /api/v1/events/{event_id}/manifest
Markets and odds
GET /api/v1/events/{event_id}/marketsGET /api/v1/markets/{market_id}/odds/currentGET /api/v1/stream
Bets
POST /api/v1/bets/intentGET /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/eventsPOST /api/v1/admin/marketsPOST /api/v1/admin/oddsPOST /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
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
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
ensv
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_logsaudit_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:
localdevstagingstudyprod
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.
After each major completed change set, create a git commit and push it before starting the next major change.
Phase 1: Documents
- Write root README
- Write architecture document
- Write API spec
- Write relational schema
- Write SQLx migrations
- Write state machine spec
- Write localization key catalog
- Write analytics taxonomy
Phase 2: Backend foundation
- Initialize Rust workspace
- Add Axum, Tokio, SQLx, tracing, serde, config
- Add config system
- Add app state
- Add error handling
- Add health endpoint
- Connect PostgreSQL
- Add migrations
- Add Redis
- Add session endpoints
Phase 3: Domain and API
- Implement users
- Implement sessions
- Implement events
- Implement event media
- Implement markets
- Implement outcomes
- Implement odds versions
- Implement bet intents
- Implement settlement
- Implement experiments
- Implement localization
- Implement analytics
Phase 4: Fixtures and admin
- Add seed scripts
- Add fixture loading
- Add admin endpoints
- Add test event generator
Phase 5: iOS app
- Initialize project structure
- Build design system
- Build localization layer
- Build networking client
- Build feed screen
- Build round screen
- Integrate AVPlayer
- Build countdown and odds overlay
- Build selection flow
- Build reveal and result
- Add analytics
Phase 6: Android app
- Initialize project structure
- Build design system
- Build localization layer
- Build networking client
- Use Kotlin coroutines and Flow broadly
- Use Kotlinx Serialization for API models
- Use kotlinx-datetime for time handling
- Build feed screen
- Build round screen
- Integrate Media3
- Build countdown and odds overlay
- Build selection flow
- Build reveal and result
- Add analytics
Phase 7: Quality
- Add tests
- Validate localization
- benchmark critical paths
- review gesture edge cases
- validate analytics coverage
- 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