When a workout is in progress, there are certain responsibilities that sit outside the app’s normal UI-driven flow. These include things like:
- Persistent notifications
- Timers that must continue while the app is backgrounded
- Integration with platform-specific system UI (e.g. foreground notifications on Android)
These responsibilities are inherently platform-dependent, both in how they are executed and what they integrate with.
The purpose of the WorkoutWorker is to provide a platform-specific execution environment that reacts to a standardised stream of workout-related messages (events and commands) emitted by the main application.
The WorkoutWorker is not the authority for workout state:
- The source of truth lives in the main app (Redux / JS)
- If the app process crashes or is killed, the workout worker is not rehydrated automatically.
- The worker exists only while a workout is active and is fully disposable
The WorkoutWorker:
- Is started explicitly when a workout begins and stopped explicitly when it ends
- May run in the background using platform-appropriate mechanisms (e.g. Android Foreground Service)
- Does not access Redux state, application services, or business logic
- Does not decide what should happen next in a workout
- Reacts to a stream of messages:
- Events: Descriptive facts about what has occurred
- Commands: Instructions to perform an action (e.g. finish workout)
- May emit observational events back to the main app (e.g. notification tapped)
On Android, the WorkoutWorker is implemented as a ForegroundService.
The service owns an explicit, in-process message channel used to distribute workout messages to platform-specific handlers.
This event channel:
- Exists entirely within the app process
- Provides ordered delivery
- Does not rely on OS-level routing (e.g. broadcasts or Intents)
- Is owned by the service itself
The service is responsible only for execution concerns (notifications, timers, system UI), not workout progression.
WorkoutMessages can be either events or commands:
Events represent facts about what has occurred:
WorkoutStartedEventWorkoutUpdatedEventWorkoutEndedEvent
Commands represent instructions for actions to perform:
FinishWorkoutCommand
Messages are bidirectional:
- App → Worker: Events describing workout state changes (start, update, end)
- Worker → App: Commands requesting actions (e.g. finish workout from notification)
When the app broadcasts an event:
- The event is immediately echoed back to all JS listeners (enabling local observation)
- The event is forwarded to the native worker for platform-specific handling
For cross-platform communication (JS ↔ native, and parity with iOS), workout messages are serialized using json via the json schema we generate.
JSON schemas are generated from our types via (in ./app dir) npm run json-schema. This will generate schemas in the docs directory.
A dedicated translation layer is responsible for converting between:
- Json Schema
WorkoutMessagemessages - Internal domain
WorkoutMessageobjects (events and commands)
This translation layer:
- Lives on the JS side of the bridge
- Encapsulates versioning, defaults, and backward compatibility
The native bridge receives a json string directly and uses generated classes for parsing. The React Native module is responsible only for transport and service lifecycle.
- The main app broadcasts a
WorkoutMessage(event) - The message is serialized using json
- The broadcast immediately echoes the event back to all JS listeners
- The native bridge forwards the message to the platform worker
- The message is dispatched through the worker's in-process message channel
- Platform-specific handlers react (update notifications, timers, etc.)
- A platform-specific trigger occurs (e.g. notification action tapped)
- The worker creates a command message (e.g.
FinishWorkoutCommand) - The command is dispatched back to the app via the event bridge
- The app handles the command (e.g. dispatches Redux action to finish workout)
WorkoutStartedEventexplicitly starts the workerWorkoutEndedEventexplicitly stops the worker- Messages outside an active workout lifecycle are ignored