v0.20.0-beta2 - Event-driven activities, background jobs and scale-out
⚠️ Warning: beta release
- This is a pre-release. Do not run it against data you cannot restore.
- The DB has schema changes (a large set of new migrations). Backup your DB and
datafolder before updating; restoring that backup is the only way back to v0.19.x. - This release contains breaking API changes (see below). Third-party clients and integrations using the REST API will need updating.
- Review the environment variable changes below.
Highlights
The backend foundation for this release is tracked in #785; related PRs are linked below.
- Activity pipeline rebuilt on an event-driven core. Activities now publish events (
activity.created, ...) and independent subscribers react to them: map thumbnails, reverse geocoding, notifications, source-file retention. Failures in one subscriber no longer affect the request or the others, and scheduled backfills repair anything missed. - Durable background jobs (#796). Optional job queue (
JOBS_ENABLED=true) with a transactional outbox, retries with backoff, leases and a dead-letter state. File uploads, bulk import and provider refresh (Strava/Garmin) now run as jobs. - Ready to scale out (#771). New deployment profiles (
local/distributed), shared Redis state, S3-compatible storage, PostgreSQL advisory locks for scheduled jobs, multiple web workers and a dedicated worker container (APP_ROLE=worker). The default single-container setup needs no changes. See Scaling and background jobs in the docs. - Platform package. Storage, events, jobs, geocoding and migrations now come from the
jasilpackage. - Hardened security, see below.
- Timezone-correct stats (#910). Week/month boundaries, gear component dates and health data now use the user's configured timezone and week start; activities without a GPS track use the user timezone too.
Import folders moved (action required if you import from disk)
Folder-based imports are now isolated per user, and the Strava export layout changed:
- Bulk import:
data/activity_files/bulk_import/becomesdata/activity_files/bulk_import/<your user id>/. - Strava export:
data/activity_files/strava_import/(withactivities.csv,activities/,media/) becomesdata/activity_files/strava_import/<your user id>/, or just upload the export ZIP from the Import page. - Files that fail to import are no longer left in
bulk_import/import_errors; they are kept in thebulk_import_errorsstorage area under<user id>/<import id>/<file name>.
Files already sitting in the old locations are not picked up; move them before starting an import. Details in the Importing data docs page.
Environment Variables & Configuration
New Variables
DEPLOYMENT_PROFILE(default:local) -local,distributedorcustom. Endurain refuses to start on an inconsistent topology and names the variable at fault.WEB_WORKERS(default:1) - number of web-server processes. Above 1 requiresREDIS_URL.REDIS_URL(default:empty) - default DSN for every Redis-backed capability (rate limits, login lockout, pending MFA, websocket tickets, event bus).STATE_URI/EVENTS_URI/LOCK_URI/STORAGE_URI- fine-grained overrides.STORAGE_URIacceptslocal://(default) ors3://bucket/prefix.JOBS_ENABLED(default:false) plusJOBS_RUN_IN_PROCESS_WORKER,JOBS_MAX_ATTEMPTS,JOBS_BACKOFF_BASE_SECONDS,JOBS_BACKOFF_MAX_SECONDS,JOBS_LEASE_SECONDS,JOBS_BATCH_SIZE,JOBS_POLL_INTERVAL_SECONDS,JOBS_RETENTION_DAYS- durable job queue tuning.APP_ROLE(default:api) -workerruns a dedicated job-worker container from the same image.EVENT_LOG_ENABLED/EVENT_LOG_RETENTION_DAYS(defaults:true/90).- Docker build arg
PYTHON_EXTRAS=distributed- installs the Redis/S3 client libraries. The default image does not include them; if you useREDIS_URLors3://storage, build your own image with this extra.
Changed
SMTP_FROM- Now falls back toSMTP_USERNAMEwhen unset (it was auto-detected by Apprise before). If neither is set, email is disabled with a startup warning. A bare address is sent asEndurain <address>;Name <address>is kept as written.SMTP_SECURE-false(no TLS) is now only accepted for a private, loopback or link-localSMTP_HOST, and not together withSMTP_USERNAME/SMTP_PASSWORD. Otherwise email is disabled with a warning. If you relied on unencrypted authenticated SMTP, switch tostarttlsorssl.SMTP_HOST/SMTP_PORT/SMTP_USERNAME/SMTP_PASSWORD- validated at startup:SMTP_HOSTmust be a hostname or IP address (no scheme, port or path),SMTP_PORT1-65535, and username and password must be set together and contain only ASCII characters. An invalid combination disables email with one warning per problem naming the setting (never its value); Endurain still starts.
Removed
RATE_LIMIT_STORAGE_URIandAUTH_SECURITY_STORAGE_URI- replaced byREDIS_URL/STATE_URI. Endurain refuses to start while either is still set, so replace them withREDIS_URLin your compose file before updating.JAEGER_ENABLED,JAEGER_PROTOCOL,JAEGER_HOST,JAEGER_PORT- OpenTelemetry/Jaeger tracing has been removed, along with the Jaeger service in the example compose files.
Other
- New data folders are created at start:
activity_files/upload_incomingandactivity_files/upload_staging.
Changes from v0.20.0-beta1
- Apprise removed; email now uses the
jasilmail provider (#825). Password reset, sign-up verification, admin approval request (one per admin) and sign-up approved emails are published as events (auth.password_reset_requested,auth.sign_up_verification_requested,auth.sign_up_approval_requested,auth.sign_up_approved) and handled by subscribers. WithJOBS_ENABLED=truethey run as durable jobs with retries and backoff; otherwise delivery is best-effort on the in-process event bus (no retries, lost on restart). Theapprisedependency is gone from the image. - Email is now sent asynchronously. The API no longer reports delivery failure: the
500 Unable to send password reset emailresponse and the "Failed to send verification email" sign-up message are gone. Failures show up in the logs and, with jobs enabled, in the Jobs dashboard. Permanent failures (SMTP 5xx, email not configured, invalid message) are logged and not retried; transient ones (connection, TLS, 4xx) are retried when jobs are enabled. - Only the newest reset or verification link works. Requesting a new password reset or sign-up verification email invalidates the previous unused link. Before, every unexpired link stayed valid.
- No secrets in events. Event payloads carry user ids only; the reset/verification token is created by the handler and committed before the email is sent, so no token or address is stored in events or jobs.
- Clear startup log for email. One info line when
SMTP_HOSTis unset, one warning per problem when the SMTP settings are invalid (email disabled), one info line when enabled. - Email requests from async routes run in the thread pool so they no longer block the event loop (CodeRabbit review on #825).
- Docs: SMTP variable table updated (
SMTP_FROMfallback,SMTP_SECURE=falserestrictions, retries needJOBS_ENABLED); Apprise removed from the tech stack and feature lists. - Send a test email from the admin settings. New Email card on Settings > Server: shows whether the SMTP settings are valid (and, when they are not, which settings are at fault, by name only) and a Send test email button. The test is sent synchronously to the admin's own address, never to an arbitrary recipient, and a failure shows the mail server's failure category and SMTP status code (for example "authentication failed (status 535)"). API:
GET /server_settings/email(server_settings:read) andPOST /server_settings/email/test(server_settings:write, rate limited;503when email is not configured,502when the mail server refuses the message). The email text is English only for now; other languages fall back to English. - OpenStreetMap default tile URL (#812). The default and the OpenStreetMap preset now use
https://tile.openstreetmap.org/{z}/{x}/{y}.png(noa/b/csubdomains), as OSM's tile usage policy asks; the thumbnail fallback and the frontend fallbacks match. A new migration rewrites a stored legacy OSM URL to the new one; custom tile servers are left unchanged. Fixes #811. - SSO redirect honors
ALLOWED_REDIRECT_SCHEMESin the frontend (#826). The backend already accepted the schemes configured inALLOWED_REDIRECT_SCHEMES, but the frontend only allowedendurain:, so mobile SSO handoff to other apps (for example Gadgetbridge) never happened. The public server settings now expose the configured schemes and the frontend validates redirects against them; the allow-list is still deny-by-default and unsafe schemes are never accepted as redirect targets. - Spoofed
X-Forwarded-Forhops are ignored (#829). The client IP was taken from the leftmostX-Forwarded-Forentry, which a client can prepend when the proxy appends to the header, so a forged value could bypass rate limiting and poison session IPs. The header is now read right to left and the first hop that is not a trusted proxy is the client. Check thatTRUSTED_PROXIESlists your reverse proxy(ies) whenBEHIND_PROXY=true. - Garmin sleep sync no longer overwrites existing data (#830). For a date without sleep, Garmin returns a sleep record with every field empty. The sync treated it as real data and overwrote any existing sleep row for that date, including rows written by another source, storing
Noneas the Garmin sleep id. Such dates are now skipped, and the sync only updates rows that Garmin wrote. - Docker compose examples simplified. The multiple-backends example is removed and Redis is no longer part of the default examples:
REDIS_URLis commented out in.env.example, andDEPLOYMENT_PROFILEis no longer set there. Add Redis yourself only for adistributeddeployment (see Scaling and background jobs). - Docs: new Nike Run Club export guide in the import docs (#819).
- Docs: new Map tile server section in Advanced setup explaining how the tile URL and the tile server API key are used by browser maps and by backend thumbnails, which providers receive the key in a header (Stadia Maps), and the workaround for providers that expect a different query parameter name (#831).
- Dependency updates. Backend and frontend dependencies bumped to their latest releases and minimum versions raised to match (for example SQLAlchemy 2.0.54, uvicorn 0.53, timezonefinder 9, garminconnect 0.3.16, ruff 0.16.8; Vue 3.5.44, Pinia 4.1, vitest 5.0.5, eslint 10.13). TypeScript stays on 6.0 because TypeScript 7 does not work with
vue-tscyet. - Tooling and images.
uv0.12.24 (still 0.12.x),astral-sh/setup-uvv10.3.0 pinned by commit hash, and refreshed digests for thenode:24-slimandpython:3.13-alpinebase images.
Breaking API changes
If you only use the Endurain web UI you do not need to do anything. API consumers, note the main route changes (full list in the OpenAPI reference):
| Before (v0.19.x) | Now |
|---|---|
POST /activities/create/upload
| POST /activities/upload (returns 202, poll GET /activities/ingestion-jobs/{job_id}; supports Idempotency-Key)
|
POST /activities/create/bulkimport
| POST /activities/bulk-import and POST /activities/bulk-import/folder (one durable job)
|
GET /activities/refresh
| POST /activities/refresh (returns 202)
|
POST /strava/import/activities
| removed; Strava export imports go through POST /activities/bulk-import (upload the export ZIP as-is) or POST /activities/bulk-import/folder
|
POST /activities_media/upload/activity_id/{id}
| POST /activities/{id}/media
|
GET /activities_media/activity_id/{id}, DELETE /activities_media/{media_id}
| GET /activities/{id}/media, DELETE /activities/{id}/media/{media_id}
|
GET /activity_media/{media} (static)
| GET /activities/{id}/media/{media_id}/file (signed, expiring token)
|
GET /activity_thumbnails/{thumbnail} (static)
| GET /activities/{id}/thumbnail (signed, expiring token)
|
GET /user_images/{img} (static)
| GET /users/{user_id}/photo (signed, expiring token)
|
PUT /activities/edit, PUT /activities/visibility/{v}
| PATCH /activities (bulk) and PATCH /activities/{id} (supports If-Match)
|
DELETE /activities/{id}/delete
| DELETE /activities/{id}
|
GET /activities/number, /user/{id}/page_number/{n}/num_records/{m}, /name/contains/{name}
| GET /activities and GET /activities/users/{user_id} with pagination and filters; totals are in the response envelope
|
GET /activities/user/{id}/followed/...
| GET /activities/feed (keyset pagination)
|
GET /activities/user/{id}/thisweek/stats, /thismonth/stats
| GET /activities/users/{user_id}/stats
|
GET /activities/gear/{id}, /gear/{id}/list, /gear/{id}/number
| GET /activities/gears/{gear_id}
|
GET /activities_summaries/{view_type}
| GET /activities/summaries
|
GET /activities_streams/activity_id/{id}/all (also laps, sets, workout steps)
| GET /activities/{id}/streams, /laps, /sets, /workout-steps
|
GET /activities_streams/activity_id/{id}/stream_type/{t}
| GET /activities/{id}/streams/{stream_type}
|
GET /activities_exercise_titles/all
| GET /activities/exercise-titles/all
|
GET /public/activities_*/activity_id/{id}/all
| GET /public/activities/{id}/streams, /laps, /sets, /workout-steps (same pattern as above)
|
POST /followers/create/targetUser/{id}
| POST /followers/users/{user_id}/followers
|
PUT /followers/accept/targetUser/{id}
| PATCH /followers/follow-requests/{follower_id} to accept; DELETE on the same path to decline a pending request
|
DELETE /followers/delete/follower/..., /delete/following/...
| DELETE /followers/users/{user_id}/followers/{follower_id}
|
GET /followers/user/{id}/followers/all, /following/all and the count/... routes
| GET /followers/users/{user_id}/followers, /following (paginated, counts in the envelope)
|
GET /followers/user/{id}/targetUser/{t}
| GET /followers/users/{user_id}/relationship
|
Authentication routes (login, refresh, MFA) are unchanged in this release.
General changes:
- Paginated list envelopes. Activity, follower, lap, set, workout-step and stream list endpoints return a paginated envelope with totals instead of bare arrays. The activity feed uses keyset pagination. Empty results are
[], nevernull. - Nested child resources. Activity child resources (laps, sets, streams, workout steps, media, exercise titles) live under
/activities/{id}/.... - 202 Accepted for uploads and provider refresh. File upload and Strava/Garmin refresh now return
202and run as durable jobs. Uploads support anIdempotency-Keyheader. PATCHfor bulk activity edit and activity ETags.- RFC 9457 problem documents for all error responses (replaces
{"detail": ...}), documented in the OpenAPI schema. - Signed, token-gated URLs for activity media, thumbnails and user profile photos. Tokens expire; fetch fresh URLs from the API rather than caching them.
- Followers API reworked (new model, request/accept notifications, privacy-aware lists; unknown fields in follow decisions are rejected).
- Activity responses use
Readschemas; timezone formatting fields were removed from activity and lap schemas. Datetimes are normalised to UTC. - Thumbnails are now WebP, generated through the event bus and storage provider (#774). Existing PNGs are migrated.
Security
This release includes security fixes, including high-severity issues. Details will be published with v0.20.0.
Thank you to everyone who reported these responsibly.
Backend
- Backend restructured into modules with enforced boundaries; services own transactions and publish events.
- Unified activity ingestion (
ActivityCore) with content-hash anddedup_keydeduplication, UTC normalisation and idempotent re-imports; activity media deduplicated bycontent_hash. - Activity source-file storage, a pluggable file-parser registry and orphan sweeps for files, media and thumbnails.
- Reverse geocoding with location backfill and defer logic for unresolved activities.
- HR zone percentages are no longer stored; they are computed from your current max heart rate when streams are read.
- Thumbnail regeneration with retry scheduling.
- Distributed locking for scheduled jobs; user activity locking during deletion; bulk-deletion events.
- Follower invariants enforced with check constraints; activity exercise-title uniqueness.
- Legacy timezone aliases canonicalised.
- Notifications are created idempotently from source event IDs.
- Week start respected in stats (#910).
- Pruning of
event_log,processing_jobsandevent_outboxin bounded batches. - Dependency updates: FastAPI, SQLAlchemy, Alembic, Pydantic, psycopg.
Frontend
- Pending-thumbnail placeholders while map images are generated.
- Feed and search updated for paginated results.
- Gear component dates use calendar dates in the local timezone.
- Vite PWA precache excludes the large login background image.
Docs
- New API reference (OpenAPI) page under Integrate: browse every REST endpoint, parameters and responses without installing anything. It is generated from the backend code at docs build time, so it always matches the release.
- New: scaling and background jobs, reverse proxy, operations, troubleshooting, export/import, error handling, API reference for Garmin/Strava, events and subscribers, activity ingestion, supported types.