Files
Stride/IMPLEMENTATION_PLAN.md

13 KiB
Raw Permalink Blame History

Multi-user running dashboard: implementation plan

Status: live local sync verified; first Flask app implemented with accounts, imports, charts, maps and friends-only sharing. See docs/APP_PROGRESS.md. Updated: 2026-10-01

Product and agreed decisions

Build Stride, a self-hosted running platform with user accounts, friends-only profiles, activity pages, and social features similar to Strava. Initial device is a Garmin Forerunner 265, but the data model must support other sources.

  • Python, Flask, Jinja templates, PostgreSQL, SQLAlchemy and Flask-Migrate.
  • Name/email/password signup; automatically generated internal profile identifiers.
  • Sharing requires an accepted friend relationship. No Explore or public activity pages.
  • Friends receive full recorded routes, with no start/end masking.
  • Leaflet for routes; Chart.js for graphs; Gunicorn for production HTTP.
  • python-garminconnect for unofficial Garmin integration; fitdecode for FIT parsing.
  • Users authenticate to Garmin on their own computer with a helper script and upload exported session tokens. The website does not collect Garmin passwords or MFA codes.
  • No dependency on approval for the official Garmin developer program.
  • Manual FIT uploads remain available independently of Garmin connectivity.
  • One repository, one Docker image, one Dokku application. Web and background worker are separate processes of that application.
  • Original activity files and provider credentials are private. Publishing is explicitly configured by the activity owner; new accounts default to private imports.

Architecture

Browser -> Flask web process -> PostgreSQL and private activity file storage. Local Garmin helper -> user uploads token bundle -> connection validation job. Background worker -> Garmin -> shared FIT import pipeline -> owned activities. Friends pages -> ownership/friendship checks -> full activity data for authorized viewers.

Start with a PostgreSQL-backed job table to avoid adding Redis for the initial deployment. The worker claims jobs transactionally, leases them, retries with backoff, and recovers expired leases. Keep this small; reconsider RQ/Celery and Redis if queue complexity or throughput warrants it. Do not run a scheduler in each Gunicorn worker. Schedule due connections from the background worker with unique job keys and per-connection locks.

Suggested layout:

app/
  __init__.py           # application factory
  config.py
  extensions.py
  models/
  auth/                # signup, sessions, recovery
  profiles/
  activities/
  connections/         # upload, validation, disconnect
  admin/               # owner controls and site administration
  services/
    garmin_adapter.py
    credentials.py
    fit_import.py
    publishing.py
    jobs.py
  templates/
  static/
tools/                 # local authentication/export helper
migrations/
tests/
docs/
Dockerfile
Procfile
pyproject.toml

Phase 0: prove the token workflow

  • Select and pin a compatible Python and python-garminconnect version for both helper and server; lock dependencies. Do not assume old garth exports work.
  • Write a local helper with hidden password input and interactive MFA. Authenticate directly with Garmin and export the supported session JSON.
  • Wrap exports with an explicit bundle format and library version, or accept the pinned library's raw token JSON with a documented compatibility check.
  • Test with the owner's account: local login, export, load in a Linux container, identify Garmin account, and download one recorded run.
  • Repeat from the intended Dokku host. Confirm that session reuse and refresh work from that host; record region/network limitations rather than assuming portability.
  • Confirm refreshed credentials are captured and persisted; exercise expired or revoked sessions and the reconnect state.
  • Define whether the exported session remains in local use. Avoid simultaneous helper/server refreshes of a shared credential set.
  • Document that session tokens grant ongoing Garmin account access and are not an app-specific read-only permission grant.

Exit: a documented end-to-end local-to-host import succeeds without sending a Garmin password to the website. If it fails, keep FIT uploads usable and record the integration blocker before building token-based onboarding.

Phase 1: application foundation and accounts

  • Scaffold Flask application factory, environment configuration, migrations, structured redacted logging, and health endpoints.
  • Add PostgreSQL local development configuration and dependency lockfile.
  • Implement signup, unique normalized email/username, password hashing, login/logout, email verification, and single-use expiring password reset links.
  • Use Flask-Login, CSRF protection, secure session cookies, login throttling, and a configured email delivery provider. Keep credentials out of cookies.
  • Add user time zone, display name, profile slug, and publication defaults.
  • Separate site administrator permissions from activity ownership.
  • Include an option to restrict signup to invitations during the pilot.

Exit: two users can register independently; protected pages and mutations enforce identity and ownership; recovery and verification work with test email delivery.

Phase 2: import and activity model

  • Create models and migrations:
    • User: identity, authentication, profile, time zone, settings.
    • ExternalConnection: owner, provider/account identity, encrypted credential blob/version, status, sync cursor, last success/error, refresh metadata.
    • Activity: owner, source ID, timestamps, sport, title, visibility, summary metrics.
    • ActivityLap and ActivitySample: splits and timestamped measurements.
    • ActivityFile: private storage key, checksum, size, import provenance.
    • ImportJob: type, owner/connection, status, attempts, lease, idempotency key.
  • Implement bounded FIT uploads and parsing through one shared service. Validate contents, use generated storage paths, and bound resource consumption.
  • Extract GPS, elapsed/timer time, distance, heart rate, cadence, elevation, laps, and available running metrics. Handle missing fields and paused periods.
  • Keep canonical units and UTC timestamps; render user time zones and units.
  • Deduplicate provider activities by connection/source ID and repeated files by owner/checksum. Define reconciliation for a manual file later fetched from Garmin.
  • Preserve original files privately and support controlled reprocessing.
  • Handle malformed/unsupported files with actionable import errors.

Exit: a manual FIT upload produces correct summary, route, laps, and charts; repeated imports do not create duplicates; failed imports do not leave partial activities.

Phase 3: user-managed Garmin connections and automatic sync

  • Add authenticated token upload with CSRF, strict size/schema/version checks, and rejection of client-supplied paths or arbitrary provider URLs.
  • Validate the uploaded session against Garmin before enabling sync. Obtain provider account identity from Garmin rather than trusting uploaded metadata.
  • Bind connections to the current user; enforce one owner per Garmin account initially and require an explicit replacement flow for changing accounts.
  • Encrypt credentials at application level; supply versioned encryption keys through Dokku config, separately from database backups; document rotation.
  • Adapt library session loading/saving to per-user encrypted persistence. If temporary files are necessary, restrict access and remove them reliably. Never use the library's default shared home-directory token store for all users.
  • Provide connection status, last sync, manual sync, reconnect, and disconnect.
  • Poll connected accounts approximately every 15–30 minutes with jitter, bounded concurrency, rate-limit backoff, and one active refresh/sync per connection.
  • Persist refreshed credentials atomically and protect against stale writes.
  • Backfill history in bounded jobs with progress and retry support.
  • Periodically revisit recent activities for edits; define upstream deletion handling explicitly instead of deleting local data based on an absent result.
  • Disconnect cancels pending jobs and deletes stored credentials; prevent an in-flight worker from restoring credentials or importing after disconnect.
  • Explain that deleting the server's token copy may not revoke other copies. Do not claim provider-side revocation unless verified for this integration.

Exit: two accounts sync without crossing credentials or data; restarts preserve progress; expired sessions request reconnect; Garmin downtime leaves the site usable.

Phase 4: dashboard and friend access

  • Build responsive profile, activity list, run detail, and personal dashboard pages.
  • Add weekly mileage, pace, splits, and heart-rate charts.
  • Use Leaflet routes with attribution; review tile provider for hosted deployment.
  • Add owner title editing and private/friends visibility controls.
  • Enforce ownership and friendship on profiles, HTML and JSON routes.
  • Show the full route to authorized viewers without geographic masking.
  • Exclude private activities from friends' totals and feeds.
  • Add configurable automatic sharing, deletion and owner data export.

Exit: anonymous visitors and non-friends cannot see activity data. Accepted friends see only friends-shared activities, including the full recorded route.

Phase 5: social MVP

  • Add requests using an existing account's email and explicit recipient acceptance.
  • Add friends-only feed and profiles; remove public Explore functionality.
  • Add decline/cancel/remove; removal immediately revokes data access.
  • Paginate the feed beyond its initial recent-50 limit.
  • Add account blocking, reporting, and basic moderation before broad signup.
  • Add kudos and comments after the core feed, with ownership checks and abuse limits.

Deferred: challenges, segments, leaderboards, live tracking, mobile apps, coaching, additional wearable integrations, and a separate frontend application.

Phase 6: Dokku deployment and operations

  • Build one Linux image with web and worker commands; configure a Procfile.
  • Provision PostgreSQL and private persistent storage; deploy one Dokku app with one web and one worker process initially.
  • Configure HTTPS/proxy handling, domain, email, Flask signing key, encryption keys, database URL, and storage paths outside source control.
  • Run migrations as a controlled release step; avoid racing worker startup.
  • Add worker heartbeat, import failure visibility, bounded logs, and alerts for stuck jobs or repeated authentication failures without logging secrets.
  • Back up database and private files; protect encryption keys separately; rehearse restore and document image rollback/migration compatibility.
  • Implement account deletion covering connections, files, activities, social data, pending jobs, and documented backup retention.
  • Publish onboarding and operations documentation, including token generation, upload, reconnect, privacy settings, and manual FIT fallback.

Verification strategy

  • Unit tests: FIT metric calculation, malformed files, duplicate detection, friendship authorization and full-route preservation, and credential serialization.
  • Integration tests against PostgreSQL: per-user authorization, refresh races, leases/retries, disconnect races, migrations, and transactional imports.
  • Browser tests: register -> upload FIT -> inspect private run -> share with friends -> accept friendship -> view -> remove friendship -> denied; local-token connection with mocked Garmin responses.
  • Use synthetic or explicitly consented and sanitized FIT fixtures. Never commit real tokens, passwords, home coordinates, or personal raw recordings.
  • Keep live Garmin tests opt-in; run the Phase 0 smoke test with the owner's account.
  • Deployment smoke test: restart web/worker, retry a failed job, verify persistent files, and restore a backup in an isolated environment.

Delivery milestones

  1. Token portability proven and integration constraints recorded.
  2. Accounts plus manual FIT upload and private activity view.
  3. Friends-only profiles, sharing controls and full-route authorized views.
  4. Per-user token upload, automatic sync, and reconnect flow.
  5. Following/feed and operational readiness for invited users.
  6. Broader signup after privacy, recovery, moderation, and restore checks pass.

Decisions to resolve during implementation

  • Hosted domain for Stride; repository folder is a working name.
  • Initial signup mode, email provider, and map tile provider.
  • History backfill limit and file/account quotas.
  • Friends-only aggregate semantics and handling of activity updates/deletions upstream.
  • Remote Git hosting/provider, visibility, and repository name if remote hosting is desired.

References

The library's authentication format is version-sensitive. Verify these sources against the pinned release during Phase 0 rather than relying on moving master.