MotoRun - Offline-First Race Administration System MotoRun - Offline-First Race Administration System

MotoRun - Offline-First Race Administration System


Overview

MotoRun is a modern Android app for the Silkolene motoRun motorcycle navigation races. It makes logging at the checkpoints simpler for both the organizers and the participants. The app was built offline-first: the main goal was reliable, to-the-second digital logging even without an internet connection.

Throughout the project I paid special attention to Clean Architecture principles, the MVVM pattern and offline-first data handling, so that critical race data never gets lost because of connectivity problems.

The system has kept growing since:

  • 2024-2025: the Android app with a Firebase backend. Most of this page describes this version.
  • Spring 2026: my own Spring Boot backend instead of Firebase, as my independent lab project.
  • Fall 2026: auditable race scoring and a web admin interface in my BSc thesis.

Architecture and design patterns

Clean Architecture

  • Presentation layer: @HiltViewModel ViewModels with unidirectional data flow (StateFlow)
  • Domain layer: repository interfaces and use cases that keep the business logic separate
  • Data layer: a hybrid of a Room database and Firestore, offline-first
  • UI layer: stateless composables built with Jetpack Compose

State management and reactive programming

data class CheckpointUiState(
    val sorter: Sorter = Sorter.TimeDESC,
    val filter: Filter = Filter.Arrived,
    val selectedEntry: Entry? = null,
    val sortedEntries: List<Entry> = emptyList(),
    val isFilterHidingEntries: Boolean = false
)
  • Sealed classes for type-safe state and error modeling (AuthState, AuthError)
  • Flow operators (combine, distinctUntilChanged) for reactive data flow

Dependency injection and modularity

  • Hilt with full DI coverage
  • Modular structure: AuthRepository, EntryRepository, NetworkManager
  • Interface segregation for better testability

Key features and implementation

NFC-based team registration

@Singleton
class NFCHandler @Inject constructor(
    private val authRepository: AuthRepository,
    private val entryRepository: EntryRepository
) {
    sealed class NFCState {
        object Disabled : NFCState()
        data class Enabled(val checkpointId: Int) : NFCState()
    }
    
    fun handleNfcIntent(intent: Intent) {
        // Role-based authorization and automatic entry creation
    }
}

Offline-first data sync

  • Room database for local persistence
  • Firestore for cloud backup and multi-device sync
  • Conflict resolution with a version-based update strategy
  • Network state monitoring with automatic reconnection and retry logic

Role-based access control

enum class Role {
    UNKNOWN, TEAM, CHECKPOINT, ORGANIZER, ADMIN
}

sealed class AuthState {
    data object Idle : AuthState()
    data object Loading : AuthState() 
    data class LoggedIn(val role: Role, val isOnline: Boolean) : AuthState()
    data class Error(val error: AuthError) : AuthState()
}

Robust networking

  • NetworkManager for lifecycle-aware network handling
  • Exponential backoff reconnection strategy
  • Rate limiting to avoid overloading the Firestore API (free tier :))
  • Health checks and automatic recovery

Technical implementation

Offline capabilities

  • AndroidKeyStore for storing credentials and keys securely
  • Room with complex relational queries and indexing
  • DataStore for configuration

UI/UX design

  • Material 3 design system
  • Compose UI for a declarative interface
  • State hoisting for a clean separation of concerns
  • Dialog management with centralized UI state handling

Data export and reports

  • Apache POI for the Excel export
  • Google Apps Script for the Google Sheets export
  • FileProvider for offline file sharing
  • Progress tracking callbacks for long-running operations

Hardware integration

  • NFC card reading with NDEF message parsing
  • Device identification for tracking individual devices
  • Hardware sensor access for device-specific data

Technologies

Android development:

  • Kotlin 2.0 with modern language features
  • Jetpack Compose declarative UI
  • Navigation Compose type-safe navigation
  • Hilt dependency injection
  • Room + DataStore data persistence

Backend and sync (first version):

  • Firebase Auth role-based authentication
  • Firebase Firestore sync database with versioning
  • Firebase Crashlytics crash reporting

Architecture and patterns:

  • MVVM + Clean Architecture
  • Repository pattern as the data abstraction layer
  • Observer pattern for reactive programming
  • Singleton pattern for system services

Developer tooling:

  • Gradle KTS build configuration
  • ProGuard code obfuscation and minification
  • BuildConfig environment-specific settings

Results and performance

  • Offline operation: 100% of the functionality works without internet
  • Data safety: zero data loss thanks to conflict-resolving sync
  • NFC response time: < 200 ms from card to entry creation
  • Multi-role support: 4 different user roles
  • Excel export: batch export of the complete race results
  • Network resilience: automatic reconnection and retry logic

The project demonstrates modern Android best practices, with a focus on reliability, scalability and maintainability in a critical environment.


2026: my own backend (independent lab project)

Firebase served the app well at the start, but it outgrew it: Firestore’s pricing forced me to add client-side rate limiting, which delayed sync by minutes, all business logic ran on the clients, and there was no server-side source of truth. So in the spring 2026 semester I replaced the whole Firebase backend with my own modular monolith built on Spring Boot. The Room-based offline-first behaviour stayed untouched, and only the remote sync layer changed.

  • Spring Boot 4 + Spring Modulith (Java 25): auth and entry modules, PostgreSQL 18, Liquibase migrations.
  • JWT authentication and RBAC: access and refresh tokens, TEAM, CHECKPOINT, ORGANIZER and ADMIN roles with Spring Security.
  • Deterministic server-side conflict resolution: combined push/pull sync for checkpoint devices that can be offline for hours, with the server as the authority.
  • Real-time broadcasting: STOMP over WebSocket, so organizers see every checkpoint’s data without delay.
  • Android client: the Firebase SDKs replaced by a remote sync layer built on OkHttp and Retrofit.
  • Infrastructure: Docker Compose, GitHub Actions CI/CD, Cloudflare, Prometheus and Grafana monitoring.

I built it with OpenRouter + OpenCode based agentic development. The plan and schedule are in my independent lab plan (in Hungarian).


Thesis: auditable race scoring (fall 2026, in progress)

The final results still pass through a manual trust boundary: the organizers copy the data from Google Sheets into a multi-sheet Excel workbook, where a shifted reference or a bad copy-paste can change the outcome in a way only a long manual check would catch. My thesis replaces that process:

  • Deterministic scoring based on versioned rules, with a detailed calculation log: every total can be traced back to the raw checkpoint entries.
  • Compose Multiplatform web admin interface: race configuration, a team-by-section heatmap, a penalty ledger, corrections and individual rulings.
  • Immutable, versioned publication: each team gets its own link (with a QR code) that shows the full breakdown of its result.
  • Provable behaviour: verification against the complete 2026 race dataset, Playwright E2E tests, automated backup and restore.
  • AI as a second opinion: the LLM reviews values that the deterministic checks have flagged, and explains results to participants based on their own published data, but it has no say in the scoring itself.

The detailed plan and schedule are in my thesis plan (in Hungarian).


Back to projects