Back to current projects

iOS engineering · Full technical article

How We Built Coach-Copilot for Equestic

A case study in end-to-end mobile systems design: SwiftUI architecture, lifecycle orchestration, authenticated networking, resilient audio workflows, and production-ready engineering.

In this article 31 sections
  1. Intro
  2. Context
  3. The Business Problem
  4. My Role
  5. Current Product Scope
  6. Engineering Goals
  7. Technical Baseline
  8. Application Architecture
  9. Dependency Composition and Shared State
  10. An Extensible Activity Model
  11. Activity Lifecycle Coordination
  12. Typed Navigation and Multi-Entry Flows
  13. Lifecycle Orchestration as a System Responsibility
  14. Authenticated Networking and Refresh Coordination
  15. Audio Capture as a Systems Problem
  16. Durable Local Recording State
  17. Segmented Recording and Continuity
  18. Safe Finalization and Ownership Handoff
  19. Local Audio Persistence
  20. Durable Upload Coordination
  21. AI-Assisted Summary Workflows
  22. Referrals, Invitations, and External Account Linking
  23. Memberships and Payments
  24. Runtime Configuration and Environments
  25. Observability and Production Diagnostics
  26. Testability
  27. Key Technical Tradeoffs
  28. Results
  29. What I’m Most Proud Of
  30. Key Takeaways
  31. Closing

Intro

Together with Sioux, we built the Coach-Copilot app for Equestic.

On the iOS side, I led the architectural direction and was responsible for how the mobile system was designed and implemented. That included defining the app structure, shaping shared state and navigation, designing the networking and authentication layers, handling lifecycle-aware behavior, and building the audio recording and upload workflows so they could hold up under real-world mobile conditions.

What made the project especially interesting was that it required much more than screen implementation. Coach-Copilot had to support authenticated session flows, push and deep-link entry points, lifecycle-heavy behavior, and durable audio workflows. From the mobile perspective, that pushed the work firmly into systems design rather than straightforward feature delivery.

Context

Coach-Copilot is an AI powered production mobile application built with SwiftUI on the iOS side in the equestrian world that helps coaching sessions be more structured, actionable, and continuous by capturing training context and turning it into useful follow-up insights.

The app had to support more than a simple linear user journey. It needed to coordinate state across:

  • authenticated and unauthenticated entry
  • lifecycle transitions between foreground, background, and inactive states
  • push-triggered and deep-link-triggered flows
  • asynchronous session updates
  • audio recording, persistence, and upload
  • environment-specific runtime behavior
  • production monitoring and diagnostics

Because the product was being built collaboratively across organizations, the iOS side needed clear architecture, strong boundaries, and predictable behavior. My responsibility was to ensure the mobile system could evolve cleanly while remaining reliable under operational complexity.

The Business Problem

A large part of equestrian coaching happens in fast-moving, high-context situations.

Important information is often communicated verbally during a training session:

  • observations about the horse
  • corrections for the rider
  • coaching decisions made in the moment
  • exercises that worked or failed
  • patterns that should be revisited
  • actions to take during the next session

Without a reliable digital workflow, much of this context remains ephemeral. It can be difficult to preserve consistently, revisit later, or transform into structured insight.

Coach-Copilot addresses that problem by creating continuity between the live training moment and what happens afterward.

The product helps:

  • coaches capture and preserve session context
  • riders revisit coaching feedback and training observations
  • individual users reflect on solo rides and groundwork sessions
  • training workflows become more consistent across activities
  • AI-assisted services transform captured information into structured summaries
  • coaches and riders maintain clearer follow-up between sessions

The reliability of the mobile implementation is directly connected to that value. If recording is fragile, uploads fail silently, or activity state becomes inconsistent after backgrounding, the product cannot deliver on its promise.

My Role

I worked as an external iOS contractor alongside Sioux to deliver the application for Equestic.

On the iOS side, I led the architectural direction and implemented the foundations behind the product. My responsibilities included:

  • defining the overall iOS architecture
  • structuring the application by feature and responsibility
  • designing shared observable state
  • building the SwiftUI navigation system
  • defining service and repository boundaries
  • composing production and stub dependency graphs
  • designing authenticated request execution
  • coordinating concurrent token refresh
  • centralizing lifecycle and system-event behavior
  • implementing audio capture and interruption recovery
  • designing local audio persistence and recording recovery state
  • building the background upload workflow
  • supporting coaching and individual-reflection activities
  • integrating summaries, memberships, telemetry, and runtime configuration
  • making critical layers independently testable

In practice, this meant combining technical leadership with hands-on implementation. The goal was not only to deliver features, but to create an iOS system that could continue evolving without losing predictability.

Current Product Scope

The current application has grown beyond a single coaching-session flow.

It includes:

  • authentication, OTP verification, and guided onboarding
  • referral-aware registration and deep-link entry
  • coach, rider, buddy, user-profile, and horse management
  • live coaching sessions
  • solo-ride reflections
  • groundwork reflections
  • activity-specific setup workflows
  • audio capture and local persistence
  • background-capable upload and recovery
  • structured summaries with editing and approval
  • activity history, search, and in-progress restoration
  • QR and referral-based coaching invitations
  • SaddleClip account linking
  • news and community updates
  • Subscription-backed memberships and purchases
  • push notifications and universal links
  • network reachability and remote configuration
  • analytics, telemetry, and crash reporting
  • production and backend-independent stub environments

This broader activity model is one of the most important evolutions from the original implementation.

Engineering Goals

I optimized the iOS architecture around five goals.

1. Explicit system boundaries

The application includes authentication, activities, audio, uploads, summaries, memberships, notifications, and external entry points. These concerns needed clear ownership so that changes in one area would not destabilize unrelated features.

2. Reliability under real mobile conditions

The app needed predictable behavior during:

  • network degradation
  • token expiration
  • audio interruptions
  • input-route changes
  • background transitions
  • local finalization
  • upload retries
  • app relaunches
  • push and deep-link entry
  • partially completed activities

3. Extensible activity workflows

Live coaching, solo rides, and groundwork reflections share some concepts but require different participants, steps, permissions, and completion behavior.

The architecture needed to support those differences without duplicating an entire feature for every new activity type.

4. Operational visibility

Failures involving authentication, recording, finalization, or uploads are difficult to diagnose from a screenshot. Logging, telemetry, and crash reporting therefore had to be part of the architecture rather than added after release.

5. Testability

Routing, repositories, services, onboarding, activity state, and presentation logic needed to be isolated behind replaceable dependencies.

Testability was treated as feedback on the design itself.

Technical Baseline

The current application is built with:

  • SwiftUI
  • the Observation framework and @Observable
  • Swift concurrency with async/await and actors
  • NavigationStack and typed route values
  • AVFoundation for recording
  • background URLSession uploads
  • Swift Package Manager through Xcode
  • an iOS 18+ deployment baseline
  • Swift 5 language mode
  • an Xcode 26.2-verified toolchain

The application targets iPhone and uses a portrait-oriented interface.

The current project does not claim a completed Swift 6 strict-concurrency migration. Instead, concurrency-sensitive subsystems use explicit actor isolation, while UI and lifecycle coordination remain main-actor-bound.

Application Architecture

I structured CoachCopilot as a pragmatic layered SwiftUI application:

  1. SwiftUI views
  2. Observable view models and workflow state
  3. Feature services
  4. Repositories and stores
  5. HTTP execution, Keychain, UserDefaults, and file storage

The main responsibilities are separated as follows:

  • Views render state and forward user intent.
  • View models manage presentation logic and screen-level orchestration.
  • Workflow types define multi-step activity behavior.
  • Services coordinate feature-level operations.
  • Repositories isolate backend requests and persistence boundaries.
  • Stores own tokens, files, upload jobs, and other durable data.
  • Shared observable state coordinates application-wide information.
  • Dependency injection assembles production or stub implementations.
  • System components own startup, lifecycle, push notifications, links, background-session bridging, and telemetry.

This keeps SwiftUI screens focused on presentation while moving business and infrastructure behavior into independently understandable layers.

Dependency Composition and Shared State

Application composition happens at the app boundary.

At launch, CoachCopilotApp:

  1. loads the configuration for the selected environment
  2. configures third-party integrations
  3. constructs stores and repositories
  4. assembles either real or stub services
  5. creates the dependency container
  6. injects that container into SwiftUI
  7. connects lifecycle, push, deep-link, connectivity, and upload handling

This avoids constructing service graphs inside individual screens.

The root AppState uses Observation and groups cross-cutting state into focused sub-states, including:

  • authentication
  • permissions
  • connectivity
  • notifications
  • active activity and session information
  • user data
  • UI state
  • deep-link data
  • membership state
  • force-update requirements

Navigation is owned separately by an observable, main-actor-isolated router. This prevents backend state and navigation-stack mutations from becoming the same concern.

The result is centralized coordination without turning every feature into a dependency on one monolithic object.

An Extensible Activity Model

One of the most significant architectural developments is the unified activity system.

The application currently supports three primary activity types:

  • live coaching sessions
  • solo-ride reflections
  • groundwork reflections

These activities share lifecycle concepts such as creation, recording, finalization, upload, and summary processing, but their setup requirements differ.

Rather than hard-coding one large conditional screen, the app models activity creation through an ActivityWorkflowEngine. Each activity defines:

  • its availability rules
  • supported training specifications
  • an ordered list of setup steps
  • its completion action
  • whether rider creation is available
  • whether it is an individual-reflection activity

For example, a live coaching session follows a flow similar to:

  1. choose a training specification
  2. select a rider
  3. select a horse
  4. review the selections
  5. send the coaching invitation

A solo ride or groundwork reflection follows a different flow:

  1. choose a training specification
  2. select a horse
  3. choose the activity date and time
  4. review the selections
  5. confirm the individual activity

Activity availability is also explicit. Live coaching is coach-specific, while individual reflection activities are available to a broader user group.

Supported specifications currently span riding and groundwork contexts such as dressage, show jumping, hunter, cross-country, trail riding, lunging, in-hand training, lead-rope training, and liberty training.

This design allows each activity to evolve independently while reusing the shared presentation and lifecycle infrastructure.

Activity Lifecycle Coordination

The application does not treat recording as an isolated media action. It is part of a larger backend activity lifecycle.

A shared activity lifecycle service provides operations for:

  • cancelling an activity
  • marking recording as started
  • finalizing recording metadata
  • marking uploaded audio as complete

The service routes those operations to the appropriate coaching-session or individual-reflection implementation.

This allows the recording and upload layers to work with a consistent contract without embedding activity-specific backend logic inside the audio subsystem.

The activity domain also supports:

  • activity history
  • activity search
  • backend state restoration
  • detection of locally in-progress activity information
  • coaching invitation and referral states
  • summary-ready and summary-approved states

This is more robust than treating each screen transition as an independent callback. The app and backend cooperate around an explicit activity lifecycle.

Typed Navigation and Multi-Entry Flows

Navigation is implemented through an observable router and typed route values.

Routes cover flows such as:

  • login and registration
  • OTP verification
  • onboarding
  • home
  • activity creation
  • recording
  • summary review
  • account management
  • rider and coach management
  • horse management
  • memberships
  • external account linking

The root navigation layer decides whether the authenticated or unauthenticated application hierarchy should be visible, while the router manages the current NavigationPath.

External signals are passed into this system through centralized app state and lifecycle handling. These signals include:

  • universal links
  • custom URLs
  • referral links
  • push-notification interactions
  • restored activity state

This separation makes it possible to accept external entry points without distributing routing logic across unrelated views.

Lifecycle Orchestration as a System Responsibility

Lifecycle behavior is centralized through SystemEventsHandler, with AppDelegate acting as the bridge to UIKit and operating-system callbacks.

This layer coordinates:

  • launch-time bootstrap
  • authentication revalidation
  • active, inactive, foreground, and background transitions
  • push-notification configuration
  • APNs token registration and backend synchronization
  • universal-link and deep-link handling
  • remote configuration refresh
  • API compatibility and force-update evaluation
  • recording and audio-monitoring policy
  • background upload completion events
  • lifecycle telemetry

Push setup is intentionally separated from the user-facing permission request. The app can configure notification handling and reuse an existing authorization state without unexpectedly displaying the system prompt during launch.

Device-token synchronization is also conditional on authentication, user availability, and the presence of a stored APNs token.

Centralizing these policies reduces the chance that multiple screens will respond differently to the same operating-system event.

Authenticated Networking and Refresh Coordination

The networking stack is divided into:

  • typed endpoint definitions
  • repositories
  • a shared HTTP client
  • request signing
  • token persistence
  • refresh coordination
  • request and failure logging

Authentication tokens are stored through Keychain-backed storage rather than in view state.

The most important concurrency concern is token refresh.

When several authenticated requests receive 401 responses at approximately the same time, a naive implementation can start several refresh requests. That creates race conditions around token replacement and can cause requests to retry with inconsistent credentials.

TokenRefreshCoordinator solves this as an actor-isolated coordination problem.

The first failing request starts refresh work. Other callers wait through checked continuations rather than starting additional refresh operations. After a successful refresh, the new credentials are saved and all waiting requests can continue.

If the refresh token itself is rejected, the stored credentials are cleared and all waiters receive the same terminal authentication failure.

This gives the app one coordinated recovery path instead of requiring every repository or feature to invent its own behavior.

Audio Capture as a Systems Problem

The audio workflow remains one of the most technically demanding parts of the application.

The recording service uses AVFoundation and handles more than starting and stopping a file. It coordinates:

  • AVAudioSession configuration
  • microphone input validation
  • Bluetooth hands-free routes
  • audio-engine and tap installation
  • recording and level monitoring
  • elapsed-time tracking
  • interruption handling
  • route-change recovery
  • media-services reset recovery
  • background visual-update policy
  • local file output
  • telemetry and failure reporting

The audio callback performs only the necessary signal work before crossing to the main actor for file and presentation updates. Equalizer and level updates are throttled so that SwiftUI is not invalidated for every captured audio buffer.

When the app moves out of the active state, visual updates can be disabled while the recording policy remains controlled separately. This avoids unnecessary UI work while the application is backgrounded.

The service also listens directly for AVAudioSession interruptions rather than using foreground transitions as a proxy for phone calls or other system audio events.

During recovery, it can:

  • pause the engine
  • rebuild the engine and tap
  • reactivate the audio session
  • account for an interruption gap
  • resume recording or monitoring when iOS allows it

Route changes and media-services resets trigger dedicated recovery paths because the input format or audio engine may no longer be valid after those events.

Durable Local Recording State

The latest evolution of the audio architecture introduces an explicit local recording journal through LocalRecordingSession.

This model is deliberately independent from the backend ActivitySessionState. The two state machines solve different problems:

  • backend activity state describes the business workflow
  • local recording state describes the recoverability and ownership of audio on the device

The local recording lifecycle distinguishes between:

  1. active recording
  2. local finalization
  3. availability of a finalized local asset
  4. durable upload handoff
  5. complete backend acknowledgement
  6. explicit discard or recoverable failure

A stable localFileId is created once and remains unchanged across finalization attempts, upload retries, and application relaunches. This gives the recording, local asset, and upload saga a shared durable identity.

The journal also records:

  • an immutable snapshot of the associated activity
  • persisted elapsed time
  • pending explicit silence
  • ordered recording segments
  • the finalized AudioAsset identity
  • the durable UploadJob identity
  • the most recent local-processing error

This design makes recovery decisions explicit. A session can indicate that it still requires reconciliation without implying that every recovery action means restarting the microphone. Depending on its lifecycle, recovery may instead involve validating segments, retrying finalization, rebuilding the upload handoff, or observing an existing upload job.

Segmented Recording and Continuity

The durable recording model represents capture as a sequence of short, independently recoverable compressed segments.

Each LocalRecordingSegment has:

  • a stable identifier and sequence index
  • immutable file naming
  • start and end timestamps
  • validated duration and byte size
  • sample rate and channel count
  • successfully written frame count
  • explicit silence to insert before the segment
  • a durable segment state

A closed segment is treated as immutable. After an interruption, route change, crash-recovery event, or normal rotation point, recording continues into a new segment instead of attempting to append to a previously closed file.

This is important because audio format can change after a route transition. Persisting format information per segment allows later finalization to reason about those differences instead of assuming that the whole recording used one unchanged input format.

Segment state explicitly distinguishes files that are:

  • currently recording
  • safely closed
  • missing from disk
  • present but corrupted
  • intentionally deleted

Continuity gaps are also modeled explicitly. Silence associated with an interruption or recovery period is attached to the next segment. The finalization layer must not infer silence from arbitrary wall-clock differences, which could otherwise convert unrelated timing noise into fabricated audio duration.

The new journal establishes the persistence contract and recovery semantics required for crash-resilient capture. Recovery execution can then be integrated around explicit state rather than reconstructed from temporary URLs or UI memory.

Safe Finalization and Ownership Handoff

The transition from local recording to background upload is treated as an ownership boundary.

The local journal remains responsible until:

  1. closed segments have been validated
  2. local stitching or export has produced a durable AudioAsset
  3. the resulting asset identity has been persisted
  4. a durable upload job has been created
  5. that upload-job identity has also been persisted

Only after both the finalized asset and upload job exist can raw segments become eligible for deletion. The journal itself remains relevant until the complete upload saga reaches its terminal completed state.

This ordering prevents a dangerous class of failures where raw audio is deleted after export but before durable upload ownership has been established.

It also makes every transition recoverable by identity rather than relying on volatile in-memory URLs.

Local Audio Persistence

Finalized audio assets are managed through an actor-isolated local file store.

The store owns:

  • temporary recording URLs
  • application-support storage
  • asset import
  • metadata encoding and decoding
  • local file identity
  • lookup and persistence operations

Using an actor for this boundary prevents concurrent recording, upload, and restoration operations from mutating the local file library inconsistently.

It also separates AVFoundation concerns from storage policy. The recording pipeline produces audio, while the local persistence layer determines how that audio becomes a durable asset.

Durable Upload Coordination

Uploads are modeled as a persisted workflow rather than a single HTTP request.

Each upload job progresses through three logical stages:

  1. notify the backend that recording has been finalized
  2. transfer the audio blob
  3. acknowledge that the upload workflow has completed

This distinction is critical. A successful file transfer does not necessarily mean the business workflow is complete.

AudioUploadService is implemented as an actor and owns:

  • upload-job state
  • activity type and identity
  • saga phase
  • deduplication
  • retry state
  • progress
  • cancellation
  • persistence
  • backend acknowledgement
  • telemetry

Enqueueing is idempotent for the same activity and local file identity. If a matching active job already exists, the existing job is reused rather than creating a competing upload.

Failures use persisted retry information and staged backoff. Users can also trigger an immediate retry when a job remains eligible.

The transfer itself uses a background URLSession configured to:

  • wait for connectivity
  • send launch events
  • use a stable background identifier
  • continue independently of the foreground UI
  • report progress
  • rebind existing tasks after relaunch

A stable job identifier is associated with each transfer so restored background tasks can be connected to their persisted upload records.

AppDelegate forwards background-session completion handling through a dedicated bridge, allowing iOS to wake the app and finish background URL session events correctly.

This architecture separates:

  • recording-journal durability
  • finalized-asset durability
  • transfer durability
  • retry policy
  • backend workflow state
  • UI progress presentation

That separation is what turns audio capture and upload from one fragile chain into a set of recoverable ownership transitions.

AI-Assisted Summary Workflows

The iOS application supports the product’s AI-generated summary lifecycle without coupling the UI to how the backend produces those summaries.

The summary domain supports:

  • retrieving an initial structured summary
  • presenting multiple summary sections
  • editing section content
  • saving user changes
  • approving the completed summary
  • retrieving approved summaries later

The same overall review model supports coaching sessions and individual reflection activities.

The responsibility of the iOS app is not to perform the AI processing locally. Its role is to model the asynchronous product workflow around that processing:

  • captured audio is finalized and uploaded
  • backend processing progresses
  • a structured summary becomes available
  • the user reviews and edits it
  • the final summary is approved

This keeps server-side generation separate from mobile presentation and approval state.

Referrals, Invitations, and External Account Linking

Live coaching involves more than selecting a rider from a local list.

The application includes referral and invitation flows that support:

  • generating coaching referrals
  • QR-based invitations
  • referral expiration handling
  • deep-link entry
  • accepting a coaching session
  • passing referral information through onboarding
  • restoring the intended flow after authentication

Referral data is consumed deliberately from shared state so that it does not repeatedly trigger navigation after it has been handled.

The user domain also supports linking a SaddleClip account. That integration is exposed through account management and can participate in coaching-session acceptance where the user chooses to include the relevant data.

This demonstrates why onboarding, links, authentication, and activity state cannot be designed independently. They are different entry points into the same product workflow.

Memberships and Payments

Membership behavior is implemented through service and repository boundaries backed by a third party.

The payment domain supports:

  • membership information
  • free-trial state
  • subscription state
  • subscription customer-information synchronization
  • membership display information
  • purchase and paywall presentation
  • customer-state refresh
  • backend membership synchronization
  • subscription-specific logout behavior

Membership state is represented separately in the shared application state rather than being inferred independently by each screen.

This gives the product one place to coordinate backend membership information with subscriptions customer state.

Runtime Configuration and Environments

The project contains four application schemes.

Each target selects its own runtime configuration.

Configurable behavior includes:

  • backend base URL
  • API client identity
  • supported API versions
  • real versus stub dependency graphs
  • launch bootstrap behavior
  • active-state revalidation
  • background-recording policy
  • subscription configuration

The API compatibility configuration is used alongside remote configuration to evaluate whether the current app version remains supported and whether a forced update is required.

Stub services can replace backend-backed implementations without changing the UI contracts. This improves local development and allows feature work to proceed without depending on every remote environment being available.

Observability and Production Diagnostics

Production behavior is observable through several complementary systems:

  • Third party analytics
  • structured application logging
  • scoped networking logs
  • audio and upload telemetry
  • lifecycle breadcrumbs

Analytics is integrated into network and system failure paths.

For long-running workflows, telemetry includes more than a final error. Breadcrumbs and state snapshots make it possible to understand which stage failed and what the application believed the current activity, recording, or upload state to be.

That is especially valuable for failures that depend on timing, background execution, or operating-system behavior.

Testability

The current test target contains:

  • 29 *Tests.swift test files
  • 22 Swift mock, spy, stub, and support files
  • 51 Swift files in total

Coverage includes:

  • authentication repositories and services
  • activity repositories and services
  • coaching-session behavior
  • rider, buddy, horse, user, and payment boundaries
  • Keychain storage
  • shared activity and membership state
  • app-delegate integration
  • typed routing
  • onboarding and OTP presentation logic
  • recording and summary view models
  • activity-list presentation
  • networking repository behavior

Mocks and spies are organized alongside the test target so services, repositories, routers, and system behavior can be replaced independently.

The production dependency graph also has a parallel stub graph, which supports backend-independent development in addition to automated testing.

The architectural value of these tests is not only regression protection. They also confirm that important responsibilities can be isolated without bootstrapping the entire application.

The durable segmented-recording journal is a newer architectural area. Its recovery and persistence invariants are intentionally explicit so dedicated tests can exercise schema compatibility, segment validation, interruption continuity, finalization retries, upload handoff, and cleanup safety as the production integration evolves.

Key Technical Tradeoffs

Explicit layering over direct feature coupling

The architecture introduces more types than placing API calls directly inside view models. That additional structure was intentional because the product’s complexity comes from interactions between systems, not from the number of screens.

A unified activity abstraction without erasing differences

Coaching, solo rides, and groundwork share lifecycle behavior, but they do not have identical participants or setup steps. Shared contracts are used where the behaviors genuinely align, while each activity still defines its own workflow.

Separate local and backend state machines

The backend activity lifecycle and local recording lifecycle are deliberately independent. Combining them would make device-level recovery depend on remote state and would make backend transitions responsible for local file ownership.

Keeping them separate allows reconciliation at explicit boundaries.

SwiftUI with targeted UIKit integration

The product interface is built with SwiftUI. UIKit remains where the operating system requires it, particularly app-delegate callbacks, remote notifications, and background URL session integration.

Actor isolation for mutable concurrent subsystems

Actors are used around token refresh, local audio storage, and upload coordination because these components own mutable state accessed by multiple asynchronous callers.

UI, routing, lifecycle coordination, and presentation state remain main-actor-isolated.

Immutable segments over appending after disruption

Continuing into a new segment after an interruption or input-format change creates more finalization work, but it avoids trusting a file that may have been incompletely closed or created with a different format.

Explicit silence over inferred timing gaps

Persisting known continuity gaps requires additional state, but it prevents arbitrary wall-clock differences from being converted into incorrect audio duration.

Safe ownership transfer over early cleanup

Keeping raw segments until both the finalized asset and upload job exist consumes more temporary storage, but it avoids irrecoverable loss between local export and durable upload ownership.

Operational readiness over invisible failure

Telemetry and persisted workflow state add implementation cost, but they substantially improve the ability to recover from and diagnose production failures.

Results

The result is an iOS application that operates as a coordinated production system rather than a collection of independent screens.

The architecture provides:

  • one extensible model for different equestrian activities
  • clear boundaries between presentation, workflows, services, and I/O
  • coordinated recovery from concurrent authentication failures
  • interruption-aware audio capture
  • an explicit local recording journal and segmented-recovery model
  • safe handoff from raw capture to finalized assets and upload jobs
  • persisted and resumable upload jobs
  • distinct transport and business-completion states
  • typed routing across authentication and external entry points
  • environment-aware runtime composition
  • backend-independent development through stubs
  • production diagnostics for lifecycle-heavy workflows
  • testable critical layers

Most importantly, these technical foundations support the product’s actual value: preserving real training context and turning it into structured information users can revisit and act on.

What I’m Most Proud Of

The strongest part of CoachCopilot is not one isolated feature.

It is the way the iOS system connects:

  • real-world audio capture
  • activity-specific workflows
  • authentication
  • local persistence
  • recording recovery state
  • background transfer
  • backend lifecycle state
  • AI-assisted summaries
  • navigation
  • subscriptions
  • operational telemetry

I am particularly proud of:

  • leading the iOS architectural direction
  • evolving the application toward a unified activity model
  • designing the audio workflow as a complete subsystem
  • separating local recording state from backend activity state
  • modeling capture, finalization, and upload as durable ownership transitions
  • separating file transfer from logical workflow completion
  • centralizing lifecycle and multi-entry behavior
  • treating token refresh as a concurrency problem
  • preserving clear production and stub dependency graphs
  • keeping critical services and presentation logic testable

That combination represents the kind of engineering work I value most: taking ownership of a complex mobile system and making its behavior explicit, recoverable, and maintainable.

Key Takeaways

Product reliability begins below the UI

For a product based on capturing real-world activity, recording, persistence, finalization, upload, and restoration are part of the user experience even when they are invisible.

Shared workflows should model genuine common behavior

The activity abstraction works because it shares lifecycle concepts without pretending that coaching and individual reflection have identical requirements.

Local and remote state solve different problems

A backend activity and a device recording can progress independently. Modeling them separately makes recovery and reconciliation explicit instead of accidental.

Recovery needs durable identity

A stable local file identity, finalized asset identity, and upload-job identity allow the system to reconstruct ownership without depending on volatile memory or temporary URLs.

Mobile lifecycle events are system inputs

Foregrounding, backgrounding, notifications, links, interruptions, route changes, and relaunches should be modeled as inputs into the architecture rather than scattered callbacks.

Authentication recovery is a concurrency concern

Coordinating refresh centrally prevents duplicated work and inconsistent token state when several requests fail together.

Transport completion is not business completion

Durable workflows need to distinguish between uploading bytes and completing the backend process associated with those bytes.

SwiftUI benefits from disciplined surrounding architecture

SwiftUI provides an effective presentation layer, but production resilience still depends on dependency boundaries, concurrency control, lifecycle design, persistence, and observability.

Closing

Equestic Coach-Copilot is one of the clearest examples of the engineering work I enjoy most: building a production mobile system where architecture directly supports product value.

The project was delivered for Equestic through a collaboration between me as an external iOS contractor and Sioux. On the iOS side, I led the architectural direction and implemented the foundations behind the application.

What makes the project meaningful is the connection between technical depth and real-world use. The architecture is not complex for its own sake. It exists so coaches and riders can reliably capture training moments, preserve their context, and turn them into structured, actionable follow-up through an AI-assisted mobile experience.

Have a mobile system to build or improve?