mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-08 23:38:01 -05:00
1 line
112 KiB
JSON
1 line
112 KiB
JSON
{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"Endurain <p> A self-hosted fitness tracking service - Mastodon profile - Discord server </p>"},{"location":"#try-the-demo","title":"\ud83d\ude80 Try the Demo","text":"<p>Experience Endurain without installation:</p> <p>Demo URL: https://demo.endurain.com</p> <ul> <li>Username: <code>admin</code></li> <li>Password: <code>admin</code></li> <li>Reset Schedule: Daily at midnight (Europe/Lisbon timezone)</li> </ul> <p>Demo Environment</p> <p>The demo environment resets every day at midnight. Do not store important data or expect persistence.</p>"},{"location":"#what-is-endurain","title":"What is Endurain?","text":"<p>Endurain is a self-hosted fitness tracking service designed to give users full control over their data and hosting environment. Built with:</p> <ul> <li>Frontend: Vue.js, Notivue and Bootstrap CSS</li> <li>Backend: Python FastAPI, Alembic, SQLAlchemy, Apprise, stravalib and python-garminconnect for Strava and Garmin Connect integration, gpxpy, tcxreader and fitdecode for .gpx, .tcx and .fit file import respectively </li> <li>Database: PostgreSQL for efficient data management</li> <li>Observability: Jaeger for basic tracing and monitoring</li> </ul> <p>To deploy Endurain, a Docker image is available, and a comprehensive example can be found in the \"docker-compose.yml.example\" file provided in the project repository. Configuration is facilitated through environment variables, ensuring flexibility and ease of customization.</p>"},{"location":"#developers-note","title":"Developer's Note","text":"<p>As a non-professional developer, my journey with Endurain involved learning and implementing new technologies and concepts, with invaluable assistance from GitHub Copilot and ChatGPT. The primary motivation behind this project was to gain hands-on experience and expand my understanding of modern development practices. Second motivation is that I'm an amateur triathlete and I want to keep track of my gear and gear components usage.</p> <p>If you have any recommendations or insights on improving any aspect of Endurain, whether related to technology choices, user experience, or any other relevant area, I would greatly appreciate your input. The goal is to create a reliable and user-friendly fitness tracking solution that caters to the needs of individuals who prefer self-hosted applications. Your constructive feedback will undoubtedly contribute to the refinement of Endurain.</p>"},{"location":"#features","title":"Features","text":"<p>Endurain currently supports:</p> <ul> <li>Multi-user functionality with admin and user profiles adaptable interfaces</li> <li>Activity import via manual or bulk upload (.gpx, .tcx and .fit files. .fit files are preferred)</li> <li>Strava integration for syncing activities and gear</li> <li>Garmin Connect integration for syncing activities, gear and body composition</li> <li>Activity feeds and statistics (week/month)</li> <li>Basic activity privacy settings</li> <li>Gear tracking (wetsuits, bicycles, shoes, racquets, skis, snowboards)</li> <li>Gear component tracking (e.g., track when components like bike chains need replacing)</li> <li>Default gear for activity types</li> <li>User pages with stats and activity histories</li> <li>Follower features (view activities)</li> <li>Multi-language support</li> <li>Imperial and metric units support</li> <li>Dark/light theme switcher</li> <li>Third-party app support</li> <li>Weight, steps and sleep logging</li> <li>Notification system</li> <li>Define and track goals</li> <li>MFA TOTP support</li> <li>Password reset through email link. Uses Apprise for email notifications</li> <li>Sign-up with configurable email verification and admin approva</li> <li>SSO support (OIDC/SAML)</li> </ul>"},{"location":"#planned-features","title":"Planned Features","text":"<p>Please visit the ROADMAP.md file on GitHub.</p>"},{"location":"#sponsors","title":"Sponsors","text":"<p>A huge thank you to the project sponsors! Your support helps keep this project going.</p> <p>Consider sponsoring Endurain on GitHub to ensure continuous development.</p>"},{"location":"#contributing","title":"Contributing","text":"<p>Contributions are welcomed! Please open an issue to discuss any changes or improvements before submitting a PR. Check out the Contributing Guidelines for more details.</p>"},{"location":"#license","title":"License","text":"<p>This project is licensed under the AGPL-3.0 or later License.</p>"},{"location":"#help-translate","title":"Help Translate","text":"<p>Endurain has multi-language support, and you can help translate it into more languages via Crowdin. </p> <p>Currently supported in:</p> <ul> <li>Catalan by @rubenixnagios</li> <li>Chinese Simplified</li> <li>Chinese Traditional</li> <li>German by @ThreeCO</li> <li>French (FR) @gwenvador</li> <li>GALICIAN (GL)</li> <li>Dutch (NL) @woutvanderaa</li> <li>Portuguese (PT)</li> <li>Slovenian (SL) @thehijacker</li> <li>Spanish (ES) @rgmelkor and @tinchodin</li> <li>English (US)</li> </ul>"},{"location":"#star-history","title":"Star History","text":""},{"location":"#trademark-notice","title":"Trademark Notice","text":"<p>Endurain\u00ae is a trademark of Jo\u00e3o Vit\u00f3ria Silva. </p> <p>You are welcome to self-host Endurain and use the name and logo, including for personal, educational, research, or community (non-commercial) use. Commercial use of the Endurain name or logos (such as offering paid hosting, products, or services) is not permitted without prior written permission.</p> <p>See <code>TRADEMARK.md</code> for full details.</p> <sub>Built with \u2764\ufe0f from Portugal | Part of the Endurain ecosystem</sub>"},{"location":"gallery/","title":"Gallery","text":""},{"location":"gallery/#login-and-sign-up-page","title":"Login and sign-up page","text":"<p>Image can be changed. Image with a size of 1000x1000 pixels is expected. </p>"},{"location":"gallery/#home-page","title":"Home page","text":""},{"location":"gallery/#search-page","title":"Search page","text":""},{"location":"gallery/#activity-page","title":"Activity page","text":""},{"location":"gallery/#activities-pages","title":"Activities pages","text":""},{"location":"gallery/#gears-pages","title":"Gears pages","text":""},{"location":"gallery/#health-pages","title":"Health pages","text":""},{"location":"gallery/#summary-page","title":"Summary page","text":""},{"location":"gallery/#profile-page","title":"Profile page","text":""},{"location":"gallery/#settings-pages","title":"Settings pages","text":"<p> Users administrator, server settings and identity providers pages only visible to admin users </p>"},{"location":"developer-guide/authentication/","title":"Handling authentication","text":"<p>Endurain supports integration with other apps through a comprehensive OAuth 2.1 compliant authentication system that includes standard username/password authentication, Multi-Factor Authentication (MFA), OAuth/SSO integration, and JWT-based session management with refresh token rotation.</p>"},{"location":"developer-guide/authentication/#api-requirements","title":"API Requirements","text":"<ul> <li>Client Type Header: Every request must include an <code>X-Client-Type</code> header with either <code>web</code> or <code>mobile</code> as the value. Requests with other values will receive a <code>403</code> error.</li> <li>Authorization: Every request must include an <code>Authorization: Bearer <access token></code> header with a valid access token.</li> <li>CSRF Protection (Web Only): State-changing requests (<code>POST</code>, <code>PUT</code>, <code>DELETE</code>, <code>PATCH</code>) from web clients must include an <code>X-CSRF-Token</code> header.</li> </ul>"},{"location":"developer-guide/authentication/#token-handling","title":"Token Handling","text":""},{"location":"developer-guide/authentication/#token-lifecycle","title":"Token Lifecycle","text":"<ul> <li>The backend generates an <code>access_token</code> valid for 15 minutes (default) and a <code>refresh_token</code> valid for 7 days (default).</li> <li>The <code>access_token</code> is used for authorization; the <code>refresh_token</code> is used to obtain new access tokens.</li> <li>A <code>csrf_token</code> is generated for CSRF protection on state-changing requests.</li> <li>Token expiration times can be customized via environment variables (see Configuration section below).</li> </ul>"},{"location":"developer-guide/authentication/#oauth-21-token-storage-model-hybrid-approach","title":"OAuth 2.1 Token Storage Model (Hybrid Approach)","text":"<p>Endurain implements an OAuth 2.1 compliant hybrid token storage model that provides both security and usability:</p> Token Storage Location Lifetime Security Purpose Access Token In-memory (JavaScript) 15 minutes Short-lived, XSS-resistant (not persisted) Refresh Token httpOnly cookie 7 days CSRF-protected, auto-sent by browser CSRF Token In-memory (JavaScript) Session Prevents CSRF attacks on state-changing requests <p>Security Properties:</p> <ul> <li>XSS Protection: Access tokens stored in memory cannot be exfiltrated via XSS attacks</li> <li>CSRF Protection: Refresh token in httpOnly cookie + CSRF token header prevents CSRF attacks</li> <li>Session Persistence: Page reload triggers <code>/auth/refresh</code> with httpOnly cookie to restore tokens</li> <li>Multi-tab Support: httpOnly cookie shared across browser tabs</li> </ul>"},{"location":"developer-guide/authentication/#token-delivery-by-client-type","title":"Token Delivery by Client Type","text":"<ul> <li> <p>For web apps: </p> <ul> <li>Access token and CSRF token returned in JSON response body (stored in-memory)</li> <li>Refresh token set as httpOnly cookie (<code>endurain_refresh_token</code>)</li> <li>On page reload, call <code>/auth/refresh</code> to restore in-memory tokens</li> </ul> </li> <li> <p>For mobile apps: </p> <ul> <li>All tokens (access, refresh, CSRF) returned in JSON response body</li> <li>Store tokens in secure platform storage (iOS Keychain, Android EncryptedSharedPreferences)</li> </ul> </li> </ul>"},{"location":"developer-guide/authentication/#authentication-flows","title":"Authentication Flows","text":""},{"location":"developer-guide/authentication/#standard-login-flow-usernamepassword","title":"Standard Login Flow (Username/Password)","text":"<ol> <li>Client sends credentials to <code>/auth/login</code> endpoint</li> <li>Backend validates credentials and checks for account lockout</li> <li>If MFA is enabled, backend returns MFA-required response</li> <li>If MFA is disabled or verified, backend generates tokens</li> <li>Tokens are delivered based on client type:<ul> <li>Web: Access token + CSRF token in response body, refresh token as httpOnly cookie</li> <li>Mobile: All tokens in response body</li> </ul> </li> </ol>"},{"location":"developer-guide/authentication/#oauthsso-flow","title":"OAuth/SSO Flow","text":"<ol> <li>Client requests list of enabled providers from <code>/public/idp</code></li> <li>Client initiates OAuth by redirecting to <code>/public/idp/login/{idp_slug}</code> with PKCE challenge</li> <li>User authenticates with the OAuth provider</li> <li>Provider redirects back to <code>/public/idp/callback/{idp_slug}</code> with authorization code</li> <li>Backend exchanges code for provider tokens and user info</li> <li>Backend creates or links user account and generates session tokens based on client type:<ul> <li>Web clients: Redirected to app with tokens set automatically</li> <li>Mobile clients: Exchange session for tokens via PKCE token exchange endpoint <code>/public/idp/session/{session_id}/tokens</code></li> </ul> </li> </ol>"},{"location":"developer-guide/authentication/#token-refresh-flow","title":"Token Refresh Flow","text":"<p>The token refresh flow implements OAuth 2.1 compliant refresh token rotation:</p> <ol> <li>When access token expires, client calls <code>POST /auth/refresh</code>:<ul> <li>Web clients: Include <code>X-CSRF-Token</code> header with current CSRF token</li> <li>Mobile clients: Include refresh token in request</li> </ul> </li> <li>Backend validates refresh token and session, checks for token reuse<ul> <li>If token reuse detected: Entire token family is invalidated (security breach response)</li> </ul> </li> <li>New tokens are generated (access, refresh, CSRF) with refresh token rotation</li> <li>Old refresh token is stored for reuse detection (grace period: 30 seconds)</li> <li>Response includes new tokens; web clients receive new httpOnly cookie</li> </ol> <p>Token Refresh Request (Web):</p> <pre><code>POST /api/v1/auth/refresh\nX-Client-Type: web\nX-CSRF-Token: {current_csrf_token}\nCookie: endurain_refresh_token={refresh_token}\n</code></pre> <p>Token Refresh Response:</p> <pre><code>{\n \"session_id\": \"uuid\",\n \"access_token\": \"eyJ...\",\n \"csrf_token\": \"new_csrf_token\",\n \"token_type\": \"bearer\",\n \"expires_in\": 1734567890\n}\n</code></pre>"},{"location":"developer-guide/authentication/#refresh-token-rotation-reuse-detection","title":"Refresh Token Rotation & Reuse Detection","text":"<p>Endurain implements automatic refresh token rotation with reuse detection to prevent token theft:</p> Security Feature Description Automatic Rotation New refresh token issued on every <code>/auth/refresh</code> call Token Family Tracking All tokens in a session share a <code>token_family_id</code> Reuse Detection Old tokens are stored and monitored for reuse Grace Period 30-second window allows for network retry scenarios Family Invalidation If reuse detected, ALL tokens in family are invalidated Rotation Count Tracks number of rotations for audit purposes"},{"location":"developer-guide/authentication/#api-endpoints","title":"API Endpoints","text":"<p>The API is reachable under <code>/api/v1</code>. Below are the authentication-related endpoints. Complete API documentation is available on the backend docs (<code>http://localhost:98/api/v1/docs</code> or <code>http://ip_address:98/api/v1/docs</code> or <code>https://domain/api/v1/docs</code>):</p>"},{"location":"developer-guide/authentication/#core-authentication-endpoints","title":"Core Authentication Endpoints","text":"What Url Expected Information Rate Limit Authorize <code>/auth/login</code> <code>FORM</code> with the fields <code>username</code> and <code>password</code>. HTTPS highly recommended 3 requests/min per IP Refresh Token <code>/auth/refresh</code> Cookie: <code>endurain_refresh_token</code>, Header: <code>X-CSRF-Token</code> (web only) - Verify MFA <code>/auth/mfa/verify</code> JSON <code>{'username': <username>, 'mfa_code': '123456'}</code> 5 requests/min per IP Logout <code>/auth/logout</code> Header: <code>Authorization: Bearer <Access Token></code> -"},{"location":"developer-guide/authentication/#oauthsso-endpoints","title":"OAuth/SSO Endpoints","text":"What Url Expected Information Rate Limit Get Enabled Providers <code>/public/idp</code> None (public endpoint) - Initiate OAuth Login <code>/public/idp/login/{idp_slug}</code> Query params: <code>redirect</code>, <code>code_challenge</code>, <code>code_challenge_method</code> 10 requests/min per IP OAuth Callback <code>/public/idp/callback/{idp_slug}</code> Query params: <code>code=<code></code>, <code>state=<state></code> 10 requests/min per IP Token Exchange (PKCE) <code>/public/idp/session/{session_id}/tokens</code> JSON: <code>{\"code_verifier\": \"<verifier>\"}</code> 10 requests/min per IP Link IdP to Account <code>/profile/idp/{idp_id}/link</code> Requires authenticated session 10 requests/min per IP"},{"location":"developer-guide/authentication/#session-management-endpoints","title":"Session Management Endpoints","text":"What Url Expected Information Get User Sessions <code>/sessions/user/{user_id}</code> Header: <code>Authorization: Bearer <Access Token></code> Delete Session <code>/sessions/{session_id}/user/{user_id}</code> Header: <code>Authorization: Bearer <Access Token></code>"},{"location":"developer-guide/authentication/#example-resource-endpoints","title":"Example Resource Endpoints","text":"What Url Expected Information Activity Upload <code>/activities/create/upload</code> .gpx, .tcx, .gz or .fit file Set Weight <code>/health/weight</code> JSON <code>{'weight': <number>, 'created_at': 'yyyy-MM-dd'}</code>"},{"location":"developer-guide/authentication/#progressive-account-lockout","title":"Progressive Account Lockout","text":"<p>Endurain implements progressive brute-force protection to prevent credential stuffing attacks:</p> Failed Attempts Lockout Duration 5 failures 5 minutes 10 failures 30 minutes 20 failures 24 hours <p>Features:</p> <ul> <li>Per-username tracking prevents targeted attacks</li> <li>Lockout persists through MFA flow (prevents bypass)</li> <li>Counter resets on successful authentication</li> <li>Graceful error messages with remaining lockout time</li> </ul>"},{"location":"developer-guide/authentication/#mfa-authentication-flow","title":"MFA Authentication Flow","text":"<p>When Multi-Factor Authentication (MFA) is enabled for a user, the authentication process requires two steps:</p>"},{"location":"developer-guide/authentication/#step-1-initial-login-request","title":"Step 1: Initial Login Request","text":"<p>Make a standard login request to <code>/auth/login</code>:</p> <p>Request: <pre><code>POST /api/v1/auth/login\nContent-Type: application/x-www-form-urlencoded\nX-Client-Type: web|mobile\n\nusername=user@example.com&password=userpassword\n</code></pre></p> <p>Response (when MFA is enabled):</p> <ul> <li>Web clients: HTTP 202 Accepted</li> </ul> <pre><code>{\n \"mfa_required\": true,\n \"username\": \"example\",\n \"message\": \"MFA verification required\"\n}\n</code></pre> <ul> <li>Mobile clients: HTTP 200 OK</li> </ul> <pre><code>{\n \"mfa_required\": true,\n \"username\": \"example\",\n \"message\": \"MFA verification required\"\n}\n</code></pre>"},{"location":"developer-guide/authentication/#step-2-mfa-verification","title":"Step 2: MFA Verification","text":"<p>Complete the login by providing the MFA code (TOTP or backup code) to <code>/auth/mfa/verify</code>:</p> <p>Request: <pre><code>POST /api/v1/auth/mfa/verify\nContent-Type: application/json\nX-Client-Type: web|mobile\n\n{\n \"username\": \"user@example.com\",\n \"mfa_code\": \"123456\"\n}\n</code></pre></p> <p>Backup Code Format</p> <p>Users can also use a backup code instead of a TOTP code. Backup codes are in <code>XXXX-XXXX</code> format (e.g., <code>A3K9-7BDF</code>). See MFA Backup Codes for details.</p> <p>Response (successful verification):</p> <ul> <li>Web clients: Access token and CSRF token in response body, refresh token as httpOnly cookie</li> </ul> <pre><code>{\n \"session_id\": \"unique_session_id\",\n \"access_token\": \"eyJ...\",\n \"csrf_token\": \"abc123...\",\n \"token_type\": \"bearer\",\n \"expires_in\": 1734567890\n}\n</code></pre> <ul> <li>Mobile clients: All tokens returned in response body</li> </ul> <pre><code>{\n \"session_id\": \"unique_session_id\",\n \"access_token\": \"eyJ...\",\n \"refresh_token\": \"eyJ...\",\n \"csrf_token\": \"abc123...\",\n \"token_type\": \"bearer\",\n \"expires_in\": 1734567890\n}\n</code></pre>"},{"location":"developer-guide/authentication/#error-handling","title":"Error Handling","text":"<ul> <li>No pending MFA login: HTTP 400 Bad Request</li> </ul> <pre><code>{\n \"detail\": \"No pending MFA login found for this username\"\n}\n</code></pre> <ul> <li>Invalid MFA code: HTTP 400 Bad Request</li> </ul> <pre><code>{\n \"detail\": \"Invalid MFA code. Failed attempts: 1\"\n}\n</code></pre> <ul> <li>Account locked out (too many failures): HTTP 429 Too Many Requests</li> </ul> <pre><code>{\n \"detail\": \"Too many failed MFA attempts. Account locked for 300 seconds.\"\n}\n</code></pre>"},{"location":"developer-guide/authentication/#important-notes","title":"Important Notes","text":"<ul> <li>The pending MFA login session is temporary and will expire if not completed within a reasonable time</li> <li>After successful MFA verification, the pending login is automatically cleaned up</li> <li>The user must still be active at the time of MFA verification</li> <li>If no MFA is enabled for the user, the standard single-step authentication flow applies</li> </ul>"},{"location":"developer-guide/authentication/#mfa-backup-codes","title":"MFA Backup Codes","text":"<p>Backup codes provide a recovery mechanism when users lose access to their authenticator app. When MFA is enabled, users receive 10 one-time backup codes that can be used instead of TOTP codes.</p>"},{"location":"developer-guide/authentication/#backup-code-format","title":"Backup Code Format","text":"<ul> <li>Format: <code>XXXX-XXXX</code> (8 alphanumeric characters with hyphen)</li> <li>Example: <code>A3K9-7BDF</code></li> <li>Characters: Uppercase letters and digits (excluding ambiguous: 0, O, 1, I)</li> <li>One-time use: Each code can only be used once</li> </ul>"},{"location":"developer-guide/authentication/#when-backup-codes-are-generated","title":"When Backup Codes Are Generated","text":"<ol> <li>Automatically on MFA Enable: When a user enables MFA, 10 backup codes are generated and returned in the response</li> <li>Manual Regeneration: Users can regenerate all backup codes via <code>POST /profile/mfa/backup-codes</code> (invalidates all previous codes)</li> </ol>"},{"location":"developer-guide/authentication/#api-endpoints_1","title":"API Endpoints","text":"What URL Method Description Get Backup Code Status <code>/profile/mfa/backup-codes/status</code> GET Returns count of unused/used codes Regenerate Backup Codes <code>/profile/mfa/backup-codes</code> POST Generates new codes (invalidates old)"},{"location":"developer-guide/authentication/#backup-code-status-response","title":"Backup Code Status Response","text":"<pre><code>{\n \"has_codes\": true,\n \"total\": 10,\n \"unused\": 8,\n \"used\": 2,\n \"created_at\": \"2025-12-21T10:30:00Z\"\n}\n</code></pre>"},{"location":"developer-guide/authentication/#regenerate-backup-codes-response","title":"Regenerate Backup Codes Response","text":"<pre><code>{\n \"codes\": [\n \"A3K9-7BDF\",\n \"X2M5-9NPQ\",\n \"...\"\n ],\n \"created_at\": \"2025-12-21T10:30:00Z\"\n}\n</code></pre>"},{"location":"developer-guide/authentication/#using-backup-codes-for-login","title":"Using Backup Codes for Login","text":"<p>Backup codes can be used in the MFA verification step instead of TOTP codes:</p> <pre><code>POST /api/v1/auth/mfa/verify\nContent-Type: application/json\nX-Client-Type: web|mobile\n\n{\n \"username\": \"user@example.com\",\n \"mfa_code\": \"A3K9-7BDF\"\n}\n</code></pre> <p>Important</p> <ul> <li>Backup codes are shown only once when generated - users must save them securely</li> <li>Each backup code can only be used once</li> <li>Regenerating codes invalidates ALL previous backup codes</li> <li>Store backup codes in a secure location separate from your authenticator device</li> </ul>"},{"location":"developer-guide/authentication/#oauthsso-integration","title":"OAuth/SSO Integration","text":""},{"location":"developer-guide/authentication/#supported-identity-providers","title":"Supported Identity Providers","text":"<p>Endurain supports OAuth/SSO integration with various identity providers out of the box:</p> <ul> <li>Authelia</li> <li>Authentik</li> <li>Casdoor</li> <li>Keycloak</li> <li>Pocket ID</li> </ul> <p>The system is extensible and can be configured to work with:</p> <ul> <li>Google</li> <li>GitHub</li> <li>Microsoft Entra ID</li> <li>Others/custom OIDC providers</li> </ul>"},{"location":"developer-guide/authentication/#oauth-configuration","title":"OAuth Configuration","text":"<p>Identity providers must be configured with the following parameters:</p> <ul> <li><code>client_id</code>: OAuth client identifier</li> <li><code>client_secret</code>: OAuth client secret</li> <li><code>authorization_endpoint</code>: Provider's authorization URL</li> <li><code>token_endpoint</code>: Provider's token exchange URL</li> <li><code>userinfo_endpoint</code>: Provider's user information URL</li> <li><code>redirect_uri</code>: Callback URL (typically <code>/public/idp/callback/{idp_slug}</code>)</li> </ul>"},{"location":"developer-guide/authentication/#linking-accounts","title":"Linking Accounts","text":"<p>Users can link their Endurain account to an OAuth provider:</p> <ol> <li>User must be authenticated with a valid session</li> <li>Navigate to <code>/profile/idp/{idp_id}/link</code></li> <li>Authenticate with the identity provider</li> <li>Provider is linked to the existing account</li> </ol>"},{"location":"developer-guide/authentication/#oauth-token-response","title":"OAuth Token Response","text":"<p>When authenticating via OAuth, the response format matches the standard authentication:</p> <ul> <li>Web clients: Tokens set as HTTP-only cookies, redirected to app</li> <li>Mobile clients: Must use PKCE flow (see Mobile SSO with PKCE below)</li> </ul> <p>Mobile OAuth/SSO</p> <p>Mobile apps must use the PKCE flow for OAuth/SSO authentication. This provides enhanced security and a cleaner separation between the WebView and native app.</p>"},{"location":"developer-guide/authentication/#mobile-sso-with-pkce","title":"Mobile SSO with PKCE","text":""},{"location":"developer-guide/authentication/#overview","title":"Overview","text":"<p>PKCE (Proof Key for Code Exchange, RFC 7636) is required for mobile OAuth/SSO authentication. It provides enhanced security by eliminating the need to extract tokens from WebView cookies, preventing authorization code interception attacks, and enabling a cleaner separation between the WebView and native app.</p>"},{"location":"developer-guide/authentication/#why-use-pkce","title":"Why Use PKCE?","text":"Traditional WebView Flow PKCE Flow Extract tokens from cookies Tokens delivered via secure API Cookies may leak across contexts No cookie extraction needed Complex WebView cookie management Simple token exchange Potential timing issues Atomic token exchange"},{"location":"developer-guide/authentication/#pkce-flow-overview","title":"PKCE Flow Overview","text":"<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Mobile App \u2502 \u2502 Backend \u2502 \u2502 WebView \u2502 \u2502 IdP \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502 \u2502 \u2502\n \u2502 Generate verifier \u2502 \u2502 \u2502\n \u2502 & challenge \u2502 \u2502 \u2502\n \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500>\u2502 \u2502 \u2502\n \u2502 \u2502 \u2502 \u2502\n \u2502 Open WebView with challenge \u2502 \u2502\n \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500>\u2502 \u2502\n \u2502 \u2502 \u2502 \u2502\n \u2502 \u2502 Redirect to IdP \u2502\n \u2502 \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500>\u2502\n \u2502 \u2502 \u2502 \u2502\n \u2502 \u2502 \u2502 User logs in \u2502\n \u2502 \u2502 \u2502<\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500>\u2502\n \u2502 \u2502 \u2502 \u2502\n \u2502 \u2502 Callback with code & state \u2502\n \u2502 \u2502<\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\n \u2502 \u2502 \u2502 \u2502\n \u2502 Redirect with session_id \u2502 \u2502\n \u2502<\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502 \u2502\n \u2502 \u2502 \u2502 \u2502\n \u2502 POST token exchange with verifier \u2502 \u2502\n \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500>\u2502 \u2502 \u2502\n \u2502 \u2502 \u2502 \u2502\n \u2502 Return tokens \u2502 \u2502 \u2502\n \u2502<\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502 \u2502 \u2502\n \u2502 \u2502 \u2502 \u2502\n</code></pre>"},{"location":"developer-guide/authentication/#step-by-step-pkce-implementation","title":"Step-by-Step PKCE Implementation","text":""},{"location":"developer-guide/authentication/#step-1-generate-pkce-code-verifier-and-challenge","title":"Step 1: Generate PKCE Code Verifier and Challenge","text":"<p>Before initiating the OAuth flow, generate a cryptographically random code verifier and compute its SHA256 challenge:</p> <p>Code Verifier Requirements (RFC 7636):</p> <ul> <li>Length: 43-128 characters</li> <li>Characters: <code>A-Z</code>, <code>a-z</code>, <code>0-9</code>, <code>-</code>, <code>.</code>, <code>_</code>, <code>~</code></li> <li>Cryptographically random</li> </ul> <p>Code Challenge Computation:</p> <pre><code>code_challenge = BASE64URL(SHA256(code_verifier))\n</code></pre>"},{"location":"developer-guide/authentication/#step-2-initiate-oauth-with-pkce-challenge","title":"Step 2: Initiate OAuth with PKCE Challenge","text":"<p>Open a WebView with the SSO URL including PKCE parameters:</p> <p>URL to Load:</p> <pre><code>https://your-endurain-instance.com/api/v1/public/idp/login/{idp_slug}?code_challenge={challenge}&code_challenge_method=S256&redirect=/dashboard\n</code></pre> <p>Query Parameters:</p> Parameter Required Description <code>code_challenge</code> Yes (PKCE) Base64url-encoded SHA256 hash of code_verifier <code>code_challenge_method</code> Yes (PKCE) Must be <code>S256</code> <code>redirect</code> No Frontend path after successful login"},{"location":"developer-guide/authentication/#step-3-monitor-webview-for-callback","title":"Step 3: Monitor WebView for Callback","text":"<p>The OAuth flow proceeds as normal. Monitor the WebView URL for the success redirect:</p> <p>Success URL Pattern:</p> <pre><code>https://your-endurain-instance.com/login?sso=success&session_id={uuid}&redirect=/dashboard\n</code></pre> <p>Extract the <code>session_id</code> from the URL - this is needed for token exchange.</p>"},{"location":"developer-guide/authentication/#step-4-exchange-session-for-tokens-pkce-verification","title":"Step 4: Exchange Session for Tokens (PKCE Verification)","text":"<p>After obtaining the <code>session_id</code>, close the WebView and exchange it for tokens using the code verifier:</p> <p>Token Exchange Request:</p> <pre><code>POST /api/v1/public/idp/session/{session_id}/tokens\nContent-Type: application/json\nX-Client-Type: mobile\n\n{\n \"code_verifier\": \"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk\"\n}\n</code></pre> <p>Successful Response (HTTP 200):</p> <pre><code>{\n \"access_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n \"refresh_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n \"csrf_token\": \"abc123def456...\",\n \"expires_in\": 900,\n \"token_type\": \"Bearer\"\n}\n</code></pre> <p>Error Responses:</p> Status Error Description 400 Invalid code_verifier Verifier doesn't match the challenge 404 Session not found Invalid session_id or not a PKCE flow 409 Tokens already exchanged Replay attack prevention 429 Rate limit exceeded Max 10 requests/minute per IP"},{"location":"developer-guide/authentication/#step-5-store-tokens-securely","title":"Step 5: Store Tokens Securely","text":"<p>Store the received tokens in secure platform storage:</p> <ul> <li>iOS: Keychain Services</li> <li>Android: EncryptedSharedPreferences or Android Keystore</li> </ul>"},{"location":"developer-guide/authentication/#step-6-use-tokens-for-api-requests","title":"Step 6: Use Tokens for API Requests","text":"<p>Use the tokens for authenticated API calls:</p> <pre><code>GET /api/v1/activities\nAuthorization: Bearer {access_token}\nX-Client-Type: mobile\nX-CSRF-Token: {csrf_token}\n</code></pre>"},{"location":"developer-guide/authentication/#security-features","title":"Security Features","text":"Feature Description PKCE S256 SHA256 challenge prevents code interception One-time exchange Tokens can only be exchanged once per session 10-minute expiry OAuth state expires after 10 minutes Rate limiting 10 token exchange requests per minute Session linking Session is cryptographically bound to OAuth state"},{"location":"developer-guide/authentication/#configuration","title":"Configuration","text":""},{"location":"developer-guide/authentication/#environment-variables","title":"Environment Variables","text":"<p>The following environment variables control authentication behavior:</p>"},{"location":"developer-guide/authentication/#token-configuration","title":"Token Configuration","text":"Variable Description Default Required <code>SECRET_KEY</code> Secret key for JWT signing (min 32 characters recommended) - Yes <code>ALGORITHM</code> JWT signing algorithm <code>HS256</code> No <code>ACCESS_TOKEN_EXPIRE_MINUTES</code> Access token lifetime in minutes <code>15</code> No <code>REFRESH_TOKEN_EXPIRE_DAYS</code> Refresh token lifetime in days <code>7</code> No"},{"location":"developer-guide/authentication/#session-configuration","title":"Session Configuration","text":"Variable Description Default Required <code>SESSION_IDLE_TIMEOUT_ENABLED</code> Enable session idle timeout <code>false</code> No <code>SESSION_IDLE_TIMEOUT_HOURS</code> Hours of inactivity before session expires <code>1</code> No <code>SESSION_ABSOLUTE_TIMEOUT_HOURS</code> Maximum session lifetime in hours <code>24</code> No"},{"location":"developer-guide/authentication/#security-configuration","title":"Security Configuration","text":"Variable Description Default Required <code>BACKEND_CORS_ORIGINS</code> Allowed CORS origins (JSON array) <code>[]</code> No <code>FRONTEND_PROTOCOL</code> Protocol for cookie security (<code>http</code> or <code>https</code>) <code>http</code> No"},{"location":"developer-guide/authentication/#cookie-configuration","title":"Cookie Configuration","text":"<p>For web clients, the refresh token cookie is configured with:</p> Attribute Value Purpose HttpOnly <code>true</code> Prevents JavaScript access (XSS protection) Secure <code>true</code> (in production) Only sent over HTTPS SameSite <code>Strict</code> Prevents CSRF attacks Path <code>/</code> Application-wide access Expires 7 days (default) Matches refresh token lifetime"},{"location":"developer-guide/authentication/#security-scopes","title":"Security Scopes","text":"<p>Endurain uses OAuth-style scopes to control API access. Each scope controls access to specific resource groups:</p>"},{"location":"developer-guide/authentication/#available-scopes","title":"Available Scopes","text":"Scope Description Access Level <code>profile</code> User profile information Read/Write <code>users:read</code> Read user data Read-only <code>users:write</code> Modify user data Write <code>gears:read</code> Read gear/equipment data Read-only <code>gears:write</code> Modify gear/equipment data Write <code>activities:read</code> Read activity data Read-only <code>activities:write</code> Create/modify activities Write <code>health:read</code> Read health metrics (weight, sleep, steps) Read-only <code>health:write</code> Record health metrics Write <code>health_targets:read</code> Read health targets Read-only <code>health_targets:write</code> Modify health targets Write <code>sessions:read</code> View active sessions Read-only <code>sessions:write</code> Manage sessions Write <code>server_settings:read</code> View server configuration Read-only <code>server_settings:write</code> Modify server settings Write (Admin) <code>identity_providers:read</code> View OAuth providers Read-only <code>identity_providers:write</code> Configure OAuth providers Write (Admin)"},{"location":"developer-guide/authentication/#scope-usage","title":"Scope Usage","text":"<p>Scopes are automatically assigned based on user permissions and are embedded in JWT tokens. API endpoints validate required scopes before processing requests.</p>"},{"location":"developer-guide/authentication/#common-error-responses","title":"Common Error Responses","text":""},{"location":"developer-guide/authentication/#http-status-codes","title":"HTTP Status Codes","text":"Status Code Description Common Causes <code>400 Bad Request</code> Invalid request format Missing required fields, invalid JSON, no pending MFA login <code>401 Unauthorized</code> Authentication failed Invalid credentials, expired token, invalid MFA code <code>403 Forbidden</code> Access denied Invalid client type, insufficient permissions, missing required scope <code>404 Not Found</code> Resource not found Invalid session ID, user not found, endpoint doesn't exist <code>429 Too Many Requests</code> Rate limit exceeded Too many login attempts, OAuth requests exceeded limit <code>500 Internal Server Error</code> Server error Database connection issues, configuration errors"},{"location":"developer-guide/authentication/#example-error-responses","title":"Example Error Responses","text":"<p>Invalid Client Type:</p> <pre><code>{\n \"detail\": \"Invalid client type. Must be 'web' or 'mobile'\"\n}\n</code></pre> <p>Expired Token:</p> <pre><code>{\n \"detail\": \"Token has expired\"\n}\n</code></pre> <p>Invalid Credentials:</p> <pre><code>{\n \"detail\": \"Incorrect username or password\"\n}\n</code></pre> <p>Rate Limit Exceeded:</p> <pre><code>{\n \"detail\": \"Rate limit exceeded. Please try again later.\"\n}\n</code></pre> <p>Missing Required Scope:</p> <pre><code>{\n \"detail\": \"Insufficient permissions. Required scope: activities:write\"\n}\n</code></pre>"},{"location":"developer-guide/authentication/#best-practices","title":"Best Practices","text":""},{"location":"developer-guide/authentication/#for-web-client-applications","title":"For Web Client Applications","text":"<ol> <li>Store access and CSRF tokens in memory - Never persist in localStorage or sessionStorage</li> <li>Implement automatic token refresh - Refresh before access token expires (e.g., at 80% of lifetime)</li> <li>Handle concurrent refresh requests - Use a refresh lock pattern to prevent race conditions</li> <li>Always include required headers:<ul> <li><code>Authorization: Bearer {access_token}</code> for all authenticated requests</li> <li><code>X-Client-Type: web</code> for all requests</li> <li><code>X-CSRF-Token: {csrf_token}</code> for POST/PUT/DELETE/PATCH requests</li> </ul> </li> <li>Handle page reload gracefully - Call <code>/auth/refresh</code> on app initialization to restore in-memory tokens</li> <li>Clear tokens on logout - The httpOnly cookie is cleared by the backend</li> </ol>"},{"location":"developer-guide/authentication/#for-mobile-client-applications","title":"For Mobile Client Applications","text":"<ol> <li>Store tokens securely:<ul> <li>iOS: Keychain Services</li> <li>Android: EncryptedSharedPreferences or Android Keystore</li> </ul> </li> <li>Use PKCE for OAuth/SSO - Required for mobile OAuth flows</li> <li>Include required headers:<ul> <li><code>Authorization: Bearer {access_token}</code> for all authenticated requests</li> <li><code>X-Client-Type: mobile</code> for all requests</li> <li><code>X-CSRF-Token: {csrf_token}</code> for state-changing requests</li> </ul> </li> <li>Handle token refresh proactively - Refresh before expiration</li> <li>Implement secure token deletion on logout</li> </ol>"},{"location":"developer-guide/authentication/#for-security","title":"For Security","text":"<ol> <li>Never expose <code>SECRET_KEY</code> in client code or version control</li> <li>Use strong, randomly generated secrets (minimum 32 characters)</li> <li>Always use HTTPS in production environments</li> <li>Enable MFA for enhanced account security</li> <li>Monitor for token reuse - Indicates potential token theft</li> <li>Enable session idle timeout for sensitive applications</li> <li>Use appropriate scopes - Request only necessary permissions</li> </ol>"},{"location":"developer-guide/authentication/#for-oauthsso-integration","title":"For OAuth/SSO Integration","text":"<ol> <li>Always use PKCE - Required for mobile, recommended for web</li> <li>Validate state parameter - Prevents CSRF attacks on OAuth flow</li> <li>Implement proper redirect URL validation - Prevents open redirects</li> <li>Handle provider errors gracefully with user-friendly messages</li> <li>Support account linking - Allow users to connect multiple providers</li> <li>Respect token expiry - OAuth state expires after 10 minutes</li> </ol>"},{"location":"developer-guide/setup-dev-env/","title":"Setup a dev environment","text":"<p>Bellow are the steps to create a dev environment. Examples bellow will use Endurain repo, but you should adapt those for your scenario (forked repo, etc).</p> <ul> <li>Clone the repo to your dev machine:</li> </ul> <pre><code>cd <folder_to_store_code>\ngit clone https://github.com/endurain-project/endurain.git # this will clone the repo structure to the previous folder inside a folder called endurain\n</code></pre>"},{"location":"developer-guide/setup-dev-env/#docker-image-and-backend-logic","title":"Docker image and backend logic","text":"<p>Make sure Docker is installed, more info here.</p> <ul> <li>On the project root folder, create a new Docker image, the example bellow uses <code>unified-image</code> as the image name:</li> </ul> <pre><code>docker build -f docker/Dockerfile -t unified-image .\n</code></pre> <ul> <li>Go to the project root folder and create a file called <code>docker-compose.yml</code> and adapt it to your needs. Example bellow:</li> </ul> <pre><code>services:\n endurain:\n container_name: endurain\n image: unified-image # based on image that will be created above\n environment:\n - TZ=Europe/Lisbon # change if needed. Default is UTC\n - DB_HOST=postgres\n - DB_PORT=5432\n - DB_PASSWORD=changeme\n - SECRET_KEY=changeme # openssl rand -hex 32\n - FERNET_KEY=changeme # https://fernetkeygen.com or python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"\n - ENDURAIN_HOST=http://localhost:8080 # change if needed\n - BEHIND_PROXY=false\n - ENVIRONMENT=development\n volumes:\n - <folder_to_store_code>/backend/app:/app/backend # this will replace the backend code logic with yours. Any changes in the code need a container reboot for them to apply\n ports:\n - \"8080:8080\" # change if needed\n depends_on:\n postgres:\n condition: service_healthy\n restart: unless-stopped\n\n postgres:\n image: postgres:latest\n container_name: postgres\n environment:\n - POSTGRES_PASSWORD=changeme\n - POSTGRES_DB=endurain\n - POSTGRES_USER=endurain\n - PGDATA=/var/lib/postgresql/data/pgdata\n ports:\n - \"5432:5432\"\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U endurain\"]\n interval: 5s\n timeout: 5s\n retries: 5\n volumes:\n - <path_to_container_folders>/postgres:/var/lib/postgresql/data\n restart: unless-stopped\n\n adminer:\n container_name: adminer\n image: adminer\n ports:\n - 8081:8080\n restart: unless-stopped\n</code></pre> <ul> <li>Start your project based on the docker compose file created before:</li> </ul> <pre><code>docker compose up -d\n</code></pre> <ul> <li>To stop the project:</li> </ul> <pre><code>docker compose down\n</code></pre> <ul> <li>To remove the create <code>unified-image</code> Docker image:</li> </ul> <pre><code>docker image remove unified-image\n</code></pre> <ul> <li>Backend uses Poetry for dependency management. You may need to install Python and Poetry if dependency management is necessary.</li> </ul>"},{"location":"developer-guide/setup-dev-env/#frontend","title":"Frontend","text":"<p>Make sure you have an up-to-date version of Node.js installed.</p> <ul> <li>Go to the root of the project and move to frontend/app folder and install the dependencies:</li> </ul> <pre><code>cd frontend/app\nnpm install\n</code></pre> <ul> <li>Create a file called <code>.env.local</code> inside frontend/app and add the following to it:</li> </ul> <pre><code>VITE_ENDURAIN_HOST=http://localhost:8080 # Adapt this based on the docker compose of your dev environment\n</code></pre> <ul> <li>After the dependencies are installed run the frontend:</li> </ul> <pre><code>npm run dev\n</code></pre> <ul> <li>After the frontend starts running, it should be available in the port <code>5173</code>. You should now be able to access the dev environment at <code>http://localhost:5173</code>. Screenshot bellow shows the output from the <code>npm run dev</code>. Adapt the port based on the command output.</li> </ul> <p></p> <ul> <li>Some processes, like token refresh may redirect your dev env from port <code>5173</code> to <code>8080</code> (or other, depending on your compose file). If this happens simply navigate again to <code>5173</code>.</li> </ul>"},{"location":"developer-guide/supported-types/","title":"Supported types within Endurain","text":""},{"location":"developer-guide/supported-types/#supported-activity-types","title":"Supported activity types","text":"<p>The table bellow details the activity types supported by Endurain.</p> Name Value Run 1 Trail run 2 Track run 34 Treadmill run 40 Virtual run 3 Road cycling 4 Gravel cycling 5 MTB cycling 6 Commuting cycling 27 Mixed surface cycling 29 Virtual cycling 7 Indoor cycling 28 E-Bike cycling 35 E-Bike mountain cycling 36 Indoor swimming 8 Open water swimming 9 General workout 10 Walk 11 Indoor walk 31 Hike 12 Rowing 13 Yoga 14 Alpine ski 15 Nordic Ski 16 Snowboard 17 Ice Skate 37 Transition 18 Strength Training 19 Crossfit 20 Tennis 21 Table Tennis 22 Badminton 23 Squash 24 Racquetball 25 Pickleball 26 Padel 39 Windsurf 30 Stand up paddling 32 Surf 33 Soccer 38 Cardio training 41 Kayaking 42 Sailing 43 Snow shoeing 44 Inline skating 45"},{"location":"developer-guide/supported-types/#supported-gear-types","title":"Supported gear types","text":"<p>The table bellow details the gear types supported by Endurain.</p> Name Value Notes bike 1 N/A shoes 2 N/A wetsuit 3 N/A racquet 4 N/A ski 5 N/A snowboard 6 N/A windsurf 7 N/A water_sports_board 8 Example: stand up paddle and surf board"},{"location":"developer-guide/supported-types/#supported-bike-component-gear-types","title":"Supported bike component gear types","text":"<p>The table bellow details the bike gear component types supported by Endurain:</p> Value back_break_oil back_break_pads back_break_rotor back_tire back_tube back_tubeless_sealant back_tubeless_rim_tape back_wheel back_wheel_valve bottom_bracket bottle_cage cassette chain computer_mount crank_left_power_meter crank_right_power_meter crankset crankset_power_meter fork frame front_break_oil front_break_pads front_break_rotor front_derailleur front_shifter front_tire front_tube front_tubeless_sealant front_tubeless_rim_tape front_wheel front_wheel_valve grips handlebar handlebar_tape headset pedals pedals_left_power_meter pedals_power_meter pedals_right_power_meter rear_derailleur rear_shifter saddle seatpost stem"},{"location":"developer-guide/supported-types/#supported-shoes-component-gear-types","title":"Supported shoes component gear types","text":"<p>The table bellow details the shoes component gear types supported by Endurain:</p> Value cleats insoles laces"},{"location":"developer-guide/supported-types/#supported-racquet-component-gear-types","title":"Supported racquet component gear types","text":"<p>The table bellow details the racquet component gear types supported by Endurain:</p> Value basegrip bumpers grommets overgrip strings"},{"location":"developer-guide/supported-types/#supported-windsurf-component-gear-types","title":"Supported windsurf component gear types","text":"<p>The table bellow details the windsurf component gear types supported by Endurain:</p> Value sail board mast boom mast_extension mast_base mast_universal_joint fin footstraps harness_lines rigging_lines footpad impact_vest lifeguard_vest helmet wing front_foil stabilizer fuselage"},{"location":"features/single-sign-on/","title":"Single Sign-On (SSO) Configuration","text":"<p>Endurain supports Single Sign-On (SSO) integration through OAuth 2.0 and OpenID Connect (OIDC) protocols. This allows users to authenticate using their existing identity provider accounts.</p>"},{"location":"features/single-sign-on/#important-notes","title":"Important Notes","text":"<p>Email Address Matching</p> <p>If you already have an existing Endurain account, the email address in your SSO provider must match your Endurain account email. If the email addresses don't match, Endurain will create a new user account with the SSO email address.</p> <p>Requirements</p> <p>You'll need:</p> <ul> <li>The fully qualified domain name (FQDN) of your OIDC provider</li> <li>The FQDN of your Endurain installation</li> <li>Administrator access to both your identity provider and Endurain</li> </ul>"},{"location":"features/single-sign-on/#supported-identity-providers","title":"Supported Identity Providers","text":"<p>Endurain provides built-in support for the following identity providers:</p> <ul> <li>Authelia</li> <li>Authentik</li> <li>Casdoor</li> <li>Keycloak</li> <li>Pocket ID</li> </ul> <p>The system also supports custom OIDC providers including:</p> <ul> <li>Google</li> <li>GitHub</li> <li>Microsoft Entra ID</li> <li>Any other OIDC-compliant provider</li> </ul>"},{"location":"features/single-sign-on/#configuration-examples","title":"Configuration Examples","text":""},{"location":"features/single-sign-on/#pocket-id","title":"Pocket ID","text":"<p>Pocket ID is a lightweight, self-hosted identity provider that works seamlessly with Endurain.</p>"},{"location":"features/single-sign-on/#step-1-configure-pocket-id","title":"Step 1: Configure Pocket ID","text":"<ol> <li>Log into your Pocket ID instance</li> <li>Navigate to Administration \u2192 OIDC Clients</li> <li>Select Add OIDC client</li> <li>Configure the following settings:<ul> <li>Name: <code>Endurain</code></li> <li>Client Launch URL: Your Endurain FQDN (e.g., <code>https://endurain.mydomain.com</code>)</li> <li>Callback URLs: <code>https://endurain.mydomain.com/api/v1/public/idp/callback/pocket-id</code></li> </ul> </li> <li>Click Save</li> <li>Important: Make a note of your Client ID and Client Secret</li> </ol>"},{"location":"features/single-sign-on/#step-2-configure-endurain","title":"Step 2: Configure Endurain","text":"<ol> <li>Log into your Endurain instance</li> <li>Navigate to Settings \u2192 Identity Providers</li> <li>Select Add Identity Provider \u2192 Select Pocket ID</li> <li>Configure the following settings:<ul> <li>Provider Name: <code>Pocket ID</code></li> <li>Slug: <code>pocket-id</code></li> <li>Provider Type: <code>OIDC</code></li> <li>Issuer URL: Your Pocket ID FQDN (e.g., <code>https://pocketid.mydomain.com</code>) - no trailing slash</li> <li>Client ID: The Client ID from Step 1</li> <li>Client Secret: The Client Secret from Step 1</li> <li>Scopes: <code>openid profile email</code></li> </ul> </li> <li>Click Save</li> </ol>"},{"location":"features/single-sign-on/#step-3-test-the-integration","title":"Step 3: Test the Integration","text":"<ol> <li>Log out of Endurain</li> <li>On the login page, you should see a Sign in with Pocket ID button</li> <li>Click the button to test the SSO flow</li> </ol>"},{"location":"features/single-sign-on/#tailscale-tsidp","title":"Tailscale TSIDP","text":"<p>Tailscale's identity provider (TSIDP) can be used for secure authentication within your Tailscale network.</p>"},{"location":"features/single-sign-on/#step-1-configure-tsidp","title":"Step 1: Configure TSIDP","text":"<ol> <li>Log into your TSIDP instance</li> <li>Select Add new client</li> <li>Configure the following settings:<ul> <li>Client Name: <code>Endurain</code></li> <li>Redirect URIs: <code>https://endurain.mydomain.com/api/v1/public/idp/callback/tsidp</code></li> </ul> </li> <li>Click Create client</li> <li>Important: Make a note of your Client ID and Client Secret</li> </ol>"},{"location":"features/single-sign-on/#step-2-configure-endurain_1","title":"Step 2: Configure Endurain","text":"<ol> <li>Log into your Endurain instance</li> <li>Navigate to Settings \u2192 Identity Providers</li> <li>Select Add Identity Provider \u2192 Custom</li> <li>Configure the following settings:<ul> <li>Provider Name: <code>TSIDP</code></li> <li>Slug: <code>tsidp</code></li> <li>Provider Type: <code>OIDC</code></li> <li>Issuer URL: Your TSIDP FQDN (e.g., <code>https://tsidp.mydomain.com</code>) - no trailing slash</li> <li>Client ID: The Client ID from Step 1</li> <li>Client Secret: The Client Secret from Step 1</li> <li>Scopes: <code>openid profile email</code></li> </ul> </li> <li>Click Save</li> </ol>"},{"location":"features/single-sign-on/#step-3-test-the-integration_1","title":"Step 3: Test the Integration","text":"<ol> <li>Log out of Endurain</li> <li>On the login page, you should see a Sign in with TSIDP button</li> <li>Click the button to test the SSO flow</li> </ol>"},{"location":"features/single-sign-on/#general-configuration-steps","title":"General Configuration Steps","text":"<p>For any OIDC-compliant identity provider, follow these general steps:</p>"},{"location":"features/single-sign-on/#1-configure-your-identity-provider","title":"1. Configure Your Identity Provider","text":"<p>Create an OAuth 2.0/OIDC client application with the following settings:</p> <ul> <li>Application Name: <code>Endurain</code></li> <li>Redirect/Callback URI: <code>https://<your-endurain-domain>/api/v1/public/idp/callback/<slug></code></li> <li>Grant Type: <code>Authorization Code</code></li> <li>Scopes: At minimum <code>openid profile email</code></li> </ul> <p>Save the generated Client ID and Client Secret.</p>"},{"location":"features/single-sign-on/#2-configure-endurain","title":"2. Configure Endurain","text":"<ol> <li>Log into Endurain as an administrator</li> <li>Navigate to Settings \u2192 Identity Providers</li> <li>Click Add Identity Provider</li> <li>Select your provider type or choose Custom for OIDC providers</li> <li>Fill in the required fields (see table below)</li> <li>Click Save</li> </ol> Field Description Example Provider Name Display name shown on login button <code>Google</code>, <code>GitHub</code>, etc. Slug URL-safe identifier (lowercase, hyphens) <code>google</code>, <code>github</code> Provider Type Protocol type <code>OIDC</code> or <code>OAuth2</code> Issuer URL Provider's base URL (no trailing slash) <code>https://accounts.google.com</code> Client ID OAuth client identifier From Step 1 Client Secret OAuth client secret From Step 1 Scopes Space-separated OAuth scopes <code>openid profile email</code>"},{"location":"features/single-sign-on/#3-verify-the-configuration","title":"3. Verify the Configuration","text":"<ol> <li>Log out of Endurain</li> <li>Visit the login page</li> <li>Verify that a Sign in with <code>Provider Name</code> button appears</li> <li>Test the authentication flow</li> </ol>"},{"location":"features/single-sign-on/#troubleshooting","title":"Troubleshooting","text":""},{"location":"features/single-sign-on/#common-issues","title":"Common Issues","text":"<p>Problem: \"Invalid redirect URI\" error</p> <ul> <li>Solution: Ensure the callback URL in your identity provider matches exactly: <code>https://<your-domain>/api/v1/public/idp/callback/<slug></code></li> </ul> <p>Problem: \"Email address mismatch\" creates duplicate account</p> <ul> <li>Solution: Update your existing Endurain account email to match your SSO provider email, or link the identity provider to your existing account</li> </ul> <p>Problem: SSO button doesn't appear on login page</p> <ul> <li>Solution: <ul> <li>Verify the identity provider is enabled in Endurain settings</li> <li>Check if external authentication is enabled on server settings</li> <li>Check that the provider configuration is saved correctly</li> <li>Clear your browser cache and refresh the page</li> </ul> </li> </ul> <p>Problem: \"Invalid issuer URL\" error</p> <ul> <li>Solution: Ensure the Issuer URL does not have a trailing slash and is the correct base URL for your identity provider</li> </ul>"},{"location":"features/single-sign-on/#logs","title":"Logs","text":"<p>For detailed troubleshooting, check the Endurain backend logs:</p> <pre><code>docker logs endurain-backend\n# and/or\ntail -n 100 logs/app.log\n</code></pre> <p>Look for authentication-related errors that can help identify configuration issues.</p>"},{"location":"features/single-sign-on/#security-considerations","title":"Security Considerations","text":"<p>Security Best Practices</p> <ul> <li>Always use HTTPS for both Endurain and your identity provider</li> <li>Keep your Client Secret confidential and never commit it to version control</li> <li>Regularly rotate client secrets</li> <li>Use strong, randomly generated secrets</li> <li>Limit OAuth scopes to only what's necessary (<code>openid profile email</code> is typically sufficient)</li> <li>Monitor authentication logs for suspicious activity</li> </ul>"},{"location":"features/single-sign-on/#additional-resources","title":"Additional Resources","text":"<ul> <li>Authentication Developer Guide - Technical details about Endurain's authentication system</li> <li>Getting Started Guide - General setup instructions</li> <li>OAuth 2.0 Specification - Official OAuth 2.0 documentation</li> <li>OpenID Connect Specification - Official OIDC documentation</li> </ul>"},{"location":"features/sleep-scoring/","title":"Sleep Scoring Guide","text":"<p>Endurain uses a sleep scoring system to help you understand the quality of your sleep. This guide explains how your sleep is evaluated and what the scores mean.</p>"},{"location":"features/sleep-scoring/#what-is-sleep-scoring","title":"What is Sleep Scoring?","text":"<p>Sleep scoring is like a report card for your sleep. The system analyzes your sleep data and gives you:</p> <ul> <li>An overall sleep score (0-100 points)</li> <li>Individual component scores with quality labels</li> <li>Easy-to-understand ratings: Excellent, Good, Fair, or Poor</li> </ul>"},{"location":"features/sleep-scoring/#how-are-scores-calculated","title":"How Are Scores Calculated?","text":""},{"location":"features/sleep-scoring/#overall-sleep-score-0-100","title":"Overall Sleep Score (0-100)","text":"<p>Your overall sleep score combines several important factors:</p> <ul> <li>30% - How long you slept (Duration)</li> <li>40% - How well you slept (Quality)</li> <li>10% - How many times you woke up</li> <li>20% - Your stress levels during sleep</li> </ul> <p>What the scores mean:</p> <ul> <li>90-100: Excellent sleep! You're well-rested and recharged</li> <li>70-89: Good sleep. You got decent rest with room for improvement</li> <li>50-69: Fair sleep. You might feel somewhat rested but could do better</li> <li>0-49: Poor sleep. You likely feel tired and need better rest</li> </ul>"},{"location":"features/sleep-scoring/#understanding-each-score-component","title":"Understanding Each Score Component","text":""},{"location":"features/sleep-scoring/#1-sleep-duration-score","title":"1. Sleep Duration Score","text":"<p>What it measures: How many hours you slept</p> <p>Why it matters: Your body needs enough time to go through all sleep stages and recover properly.</p> <p>Scoring guidelines:</p> Hours of Sleep Score Rating 7-9 hours 90-100 EXCELLENT - Perfect amount! 6-7 hours 70-89 GOOD - A bit short but okay 9-10 hours 70-89 GOOD - A bit long but okay 5-6 hours 50-69 FAIR - Not enough rest 10-11 hours 50-69 FAIR - Possibly oversleeping Less than 5 hours 0-49 POOR - Seriously insufficient More than 11 hours 0-49 POOR - Too much sleep <p>The sweet spot: 7-9 hours, with 8 hours being ideal for most adults.</p>"},{"location":"features/sleep-scoring/#2-sleep-quality-score","title":"2. Sleep Quality Score","text":"<p>What it measures: How your sleep was distributed across different sleep stages</p> <p>Why it matters: Quality sleep isn't just about quantity - your brain and body need the right mix of sleep stages to fully recover.</p> <p>The four sleep stages:</p> <ol> <li>Light Sleep - Transition period, your body starts to relax</li> <li>Deep Sleep - Body repairs muscles and tissues, strengthens immune system</li> <li>REM Sleep - Brain processes memories and emotions, vivid dreams occur</li> <li>Awake - Brief wakeful moments (some are normal!)</li> </ol> <p>Optimal percentages:</p> Sleep Stage Ideal Range Why It Matters Deep Sleep 13-23% (peak: 18%) Physical recovery and healing REM Sleep 20-25% (peak: 22.5%) Memory and emotional processing Light Sleep 45-55% (peak: 50%) Transition between stages Awake Time Less than 5% Normal brief awakenings <p>How it's scored:</p> <ul> <li>Each sleep stage gets its own score based on how close you are to the optimal percentage</li> <li>The scores are combined with these weights:</li> <li>Deep sleep: 25%</li> <li>REM sleep: 30% (most important for mental recovery!)</li> <li>Light sleep: 25%</li> <li>Awake time: 20% (penalty applied)</li> </ul>"},{"location":"features/sleep-scoring/#3-awake-count-score","title":"3. Awake Count Score","text":"<p>What it measures: How many times you woke up during the night</p> <p>Why it matters: Frequent awakenings disrupt your sleep cycles and prevent deep, restorative sleep.</p> <p>Scoring:</p> Awakenings Score Rating What It Means 0-1 times 90-100 EXCELLENT Uninterrupted, restorative sleep 2-3 times 70-89 GOOD Some interruptions but still decent 4-5 times 50-69 FAIR Sleep continuity affected 6+ times 0-49 POOR Very fragmented sleep <p>Note: It's normal to wake up briefly 1-2 times per night. You might not even remember them!</p>"},{"location":"features/sleep-scoring/#4-rem-sleep-percentage-score","title":"4. REM Sleep Percentage Score","text":"<p>What it measures: What percentage of your sleep was REM (Rapid Eye Movement) sleep</p> <p>Why it matters: REM sleep is when your brain consolidates memories, processes emotions, and boosts creativity. It's essential for mental health and learning.</p> <p>Optimal range: 20-25% of total sleep time (about 1.5-2 hours for 8 hours of sleep)</p> <p>What different levels mean:</p> <ul> <li>Too low (<20%): May affect memory, mood, and cognitive function</li> <li>Optimal (20-25%): Brain is fully processing and recovering</li> <li>Too high (>25%): Unusual and may indicate sleep disorders</li> </ul>"},{"location":"features/sleep-scoring/#5-deep-sleep-percentage-score","title":"5. Deep Sleep Percentage Score","text":"<p>What it measures: What percentage of your sleep was deep sleep</p> <p>Why it matters: Deep sleep is when your body does most of its physical repair - healing muscles, strengthening bones, and boosting your immune system.</p> <p>Optimal range: 13-23% of total sleep time (about 1-2 hours for 8 hours of sleep)</p> <p>What different levels mean:</p> <ul> <li>Too low (<13%): Less physical recovery, may feel physically tired</li> <li>Optimal (13-23%): Body is fully recovering and strengthening</li> <li>Too high (>23%): Rare, but may indicate sleep debt recovery</li> </ul> <p>Did you know? Deep sleep decreases naturally as you age, which is normal!</p>"},{"location":"features/sleep-scoring/#6-light-sleep-percentage-score","title":"6. Light Sleep Percentage Score","text":"<p>What it measures: What percentage of your sleep was light sleep</p> <p>Why it matters: Light sleep serves as a transition between sleep stages and makes up the largest portion of your sleep.</p> <p>Optimal range: 45-55% of total sleep time (about 3.5-4.5 hours for 8 hours of sleep)</p> <p>What different levels mean:</p> <ul> <li>Too low (<45%): You may be spending too much time in other stages</li> <li>Optimal (45-55%): Healthy balance of sleep stages</li> <li>Too high (>55%): May not be getting enough deep or REM sleep</li> </ul>"},{"location":"features/sleep-scoring/#7-sleep-stress-score","title":"7. Sleep Stress Score","text":"<p>What it measures: Your average stress level during sleep and how restless you were</p> <p>Why it matters: High stress during sleep indicates your body isn't fully relaxing, which affects recovery quality.</p> <p>Stress levels explained (based on Garmin scale):</p> <ul> <li>0-25: Rest state - fully relaxed</li> <li>26-50: Low stress - mostly relaxed</li> <li>51-75: Medium stress - moderately elevated</li> <li>76-100: High stress - not relaxing properly</li> </ul> <p>Scoring:</p> Stress Level Base Score Rating 0-25 (Rest) 100 EXCELLENT 26-50 (Low) 70-90 GOOD 51-75 (Medium) 50-70 FAIR 76-100 (High) 0-50 POOR <p>Restless moments penalty: Each restless moment during sleep reduces your score by 2-3 points.</p>"},{"location":"features/sleep-scoring/#tips-for-better-sleep-scores","title":"Tips for Better Sleep Scores","text":""},{"location":"features/sleep-scoring/#to-improve-duration-score","title":"To Improve Duration Score:","text":"<ul> <li>Aim for 7-9 hours of sleep per night</li> <li>Keep a consistent sleep schedule, even on weekends. Be consistent on the time you go to bed and awake not only the sleep hours</li> <li>Go to bed when you're tired, not too early or late</li> </ul>"},{"location":"features/sleep-scoring/#to-improve-quality-score","title":"To Improve Quality Score:","text":"<ul> <li>Create a dark, quiet, cool sleeping environment</li> <li>Avoid screens and high intensity brain tasks 1 hour before bedtime (blue light disrupts sleep)</li> <li>Avoid caffeine 6 hours before bed</li> <li>Exercise regularly, but not right before bed</li> </ul>"},{"location":"features/sleep-scoring/#to-reduce-awakenings","title":"To Reduce Awakenings:","text":"<ul> <li>Limit fluid intake 2 hours before bedtime</li> <li>Keep your bedroom cool (60-67\u00b0F / 15-19\u00b0C is ideal)</li> <li>Use white noise or earplugs if needed</li> <li>Address any underlying sleep disorders with a doctor</li> </ul>"},{"location":"features/sleep-scoring/#to-reduce-sleep-stress","title":"To Reduce Sleep Stress:","text":"<ul> <li>Practice relaxation techniques before bed (meditation, deep breathing)</li> <li>Keep a regular exercise routine during the day</li> <li>Avoid stressful activities or conversations before bedtime</li> <li>Consider journaling to clear your mind before sleep</li> </ul>"},{"location":"features/sleep-scoring/#frequently-asked-questions","title":"Frequently Asked Questions","text":""},{"location":"features/sleep-scoring/#q-why-is-my-score-low-even-though-i-slept-8-hours","title":"Q: Why is my score low even though I slept 8 hours?","text":"<p>A: Duration is only 30% of your overall score. You might have had poor quality sleep, many awakenings, or high stress levels. Check your individual component scores to see what needs improvement.</p>"},{"location":"features/sleep-scoring/#q-is-it-bad-if-my-scores-vary-day-to-day","title":"Q: Is it bad if my scores vary day to day?","text":"<p>A: Some variation is normal! Factors like stress, exercise, diet, and life events affect your sleep. Look for trends over weeks rather than individual nights.</p>"},{"location":"features/sleep-scoring/#q-whats-more-important-duration-or-quality","title":"Q: What's more important - duration or quality?","text":"<p>A: Both matter! Quality is weighted slightly higher (40% vs 30%) because you can sleep for 10 hours but still feel tired if the quality is poor. Aim for both good duration AND quality.</p>"},{"location":"features/sleep-scoring/#q-my-remdeep-sleep-percentage-seems-low-is-that-bad","title":"Q: My REM/Deep sleep percentage seems low. Is that bad?","text":"<p>A: Not necessarily. These percentages are averages based on research. Individual needs vary by age, genetics, and lifestyle. If you feel rested and energetic, your sleep is probably fine!</p>"},{"location":"features/sleep-scoring/#q-can-i-compare-my-scores-with-friends","title":"Q: Can I compare my scores with friends?","text":"<p>A: While you can, remember that everyone's sleep needs are different. Focus on improving YOUR scores over time rather than comparing with others.</p>"},{"location":"features/sleep-scoring/#q-what-if-i-have-a-sleep-disorder","title":"Q: What if I have a sleep disorder?","text":"<p>A: These scores are educational tools, not medical diagnoses. If you consistently get poor scores or feel tired despite good scores, consult a healthcare professional or sleep specialist.</p>"},{"location":"features/sleep-scoring/#understanding-your-data","title":"Understanding Your Data","text":""},{"location":"features/sleep-scoring/#when-are-scores-calculated","title":"When are scores calculated?","text":"<p>Scores are automatically calculated when you:</p> <ul> <li>Create a new sleep record</li> <li>Update an existing sleep record</li> <li>Important: If sleep data is imported from Garmin Connect, no calculations are made. Garmin Connect data is used</li> </ul> <p>The system recalculates all scores to ensure they reflect your current data.</p>"},{"location":"features/sleep-scoring/#what-data-is-needed","title":"What data is needed?","text":"<p>For the most accurate scores, provide:</p> <ul> <li>\u2705 Sleep start and end times</li> <li>\u2705 Total sleep duration</li> <li>\u2705 Sleep stage breakdowns (deep, light, REM, awake)</li> <li>\u2705 Number of awakenings</li> <li>\u2705 Stress levels (optional but recommended)</li> </ul> <p>Partial data: Even with incomplete data, the system will calculate scores for the metrics you provide. Missing metrics won't break the scoring system.</p>"},{"location":"features/sleep-scoring/#technical-details","title":"Technical Details","text":"<p>For developers and technically-minded users:</p> <ul> <li>Algorithm: Based on sleep research (provided by Claude Sonnet 4.5 and double checked by me)</li> <li>Scoring method: Weighted average of normalized component scores</li> <li>Data source: Compatible with Garmin Connect and manual entry</li> <li>Score range: All scores normalized to 0-100 scale</li> <li>Labels: Threshold-based categorization (90+=Excellent, 70+=Good, 50+=Fair, <50=Poor)</li> </ul>"},{"location":"features/sleep-scoring/#summary","title":"Summary","text":"<p>Your sleep scores provide valuable insights into your rest quality. By understanding what each score means and following the improvement tips, you can work towards better, more restorative sleep.</p> <p>Remember: - \ud83c\udfaf Focus on trends, not single nights - \ud83d\udcaa Small improvements add up over time - \ud83d\ude34 Consistency is key to good sleep - \ud83e\ude7a Consult a doctor for persistent sleep issues</p> <p>Sweet dreams and happy tracking! \ud83c\udf19\u2728</p>"},{"location":"getting-started/advanced-started/","title":"Getting started advanced","text":""},{"location":"getting-started/advanced-started/#default-credentials","title":"Default Credentials","text":"<ul> <li>Username: admin </li> <li>Password: admin</li> </ul>"},{"location":"getting-started/advanced-started/#docker-deployment","title":"Docker Deployment","text":"<p>Endurain provides a Docker image for simplified deployment. To get started, check out the <code>docker-compose.yml.example</code> file in the project repository and adjust it according to your setup. Supported tags are:</p> <ul> <li>latest: contains the latest released version;</li> <li>version, example \"v0.3.0\": contains the app state available at the time of the version specified;</li> <li>development version, example \"dev_06092024\": contains a development version of the app at the date specified. This is not a stable released and may contain issues and bugs. Please do not open issues if using a version like this unless asked by me.</li> </ul>"},{"location":"getting-started/advanced-started/#supported-environment-variables","title":"Supported Environment Variables","text":"<p>Table below shows supported environment variables. Variables marked with optional \"No\" should be set to avoid errors.</p> Environment variable Default value Optional Notes UID 1000 Yes User ID for mounted volumes. Default is 1000 GID 1000 Yes Group ID for mounted volumes. Default is 1000 TZ UTC Yes Timezone definition. Useful for TZ calculation for activities that do not have coordinates associated, like indoor swim or weight training. If not specified UTC will be used. List of available time zones here. Format <code>Europe/Lisbon</code> expected FRONTEND_DIR <code>/app/frontend/dist</code> Yes You will only need to change this value if installing using bare metal method BACKEND_DIR <code>/app/backend</code> Yes You will only need to change this value if installing using bare metal method DATA_DIR <code>/app/backend/data</code> Yes You will only need to change this value if installing using bare metal method LOGS_DIR <code>/app/backend/logs</code> Yes You will only need to change this value if installing using bare metal method ENDURAIN_HOST No default set <code>No</code> Required for internal communication and Strava. For Strava https must be used. Host or local ip (example: http://192.168.1.10:8080 or https://endurain.com) REVERSE_GEO_PROVIDER nominatim Yes Defines reverse geo provider. Expects geocode, photon or nominatim. photon can be the SaaS by komoot or a self hosted version like a self hosted version. Like photon, Nominatim can be the SaaS or a self hosted version PHOTON_API_HOST photon.komoot.io Yes API host for photon. By default it uses the SaaS by komoot PHOTON_API_USE_HTTPS true Yes Protocol used by photon. By default uses HTTPS to be inline with what SaaS by komoot expects NOMINATIM_API_HOST nominatim.openstreetmap.org Yes API host for Nominatim. By default it uses the SaaS NOMINATIM_API_USE_HTTPS true Yes Protocol used by Nominatim. By default uses HTTPS to be inline with what SaaS expects GEOCODES_MAPS_API changeme Yes Geocode maps offers a free plan consisting of 1 Request/Second. Registration necessary. REVERSE_GEO_RATE_LIMIT 1 Yes Change this if you have a paid Geocode maps tier. Other providers also use this variable. Keep it as is if you use photon or Nominatim to keep 1 request per second DB_HOST postgres Yes postgres DB_PORT 5432 Yes 3306 or 5432 DB_USER endurain Yes N/A DB_PASSWORD No default set <code>No</code> Database password. Alternatively, use <code>DB_PASSWORD_FILE</code> for Docker secrets DB_DATABASE endurain Yes N/A SECRET_KEY No default set <code>No</code> Run <code>openssl rand -hex 32</code> on a terminal to get a secret. Alternatively, use <code>SECRET_KEY_FILE</code> for Docker secrets FERNET_KEY No default set <code>No</code> Run <code>python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"</code> on a terminal to get a secret or go to https://fernetkeygen.com. Example output is <code>7NfMMRSCWcoNDSjqBX8WoYH9nTFk1VdQOdZY13po53Y=</code>. Alternatively, use <code>FERNET_KEY_FILE</code> for Docker secrets ALGORITHM HS256 Yes Currently only HS256 is supported ACCESS_TOKEN_EXPIRE_MINUTES 15 Yes Time in minutes REFRESH_TOKEN_EXPIRE_DAYS 7 Yes Time in days SESSION_IDLE_TIMEOUT_ENABLED false Yes Enforce idle timeouts (supported values are <code>true</code> and <code>false</code>) SESSION_IDLE_TIMEOUT_HOURS 1 Yes Time in hours SESSION_ABSOLUTE_TIMEOUT_HOURS 24 Yes Time in hours JAEGER_ENABLED false Yes N/A JAEGER_PROTOCOL http Yes N/A JAEGER_HOST jaeger Yes N/A JAEGER_PORT 4317 Yes N/A BEHIND_PROXY false Yes Change to true if behind reverse proxy ENVIRONMENT production Yes <code>production</code>, <code>demo</code> and <code>development</code> allowed. <code>development</code> allows connections from localhost:8080 and localhost:5173 at the CORS level. <code>demo</code> equals to <code>production</code> except it does not return user sessions SMTP_HOST No default set Yes The SMTP host of your email provider. Example <code>smtp.protonmail.ch</code> SMTP_PORT 587 Yes The SMTP port of your email provider. Default is 587 SMTP_USERNAME No default set Yes The username of your SMTP email provider, probably your email address SMTP_PASSWORD No default set Yes The password of your SMTP email provider. Some providers allow the use of your account password, others require the creation of an app password. Please refer to your provider documentation. Alternatively, use <code>SMTP_PASSWORD_FILE</code> for Docker secrets SMTP_SECURE true Yes By default it uses secure communications. Accepted values are <code>true</code> and <code>false</code> SMTP_SECURE_TYPE starttls Yes If SMTP_SECURE is set you can set the communication type. Accepted values are <code>starttls</code> and <code>ssl</code> <p>Table below shows the obligatory environment variables for postgres container. You should set them based on what was also set for the Endurain container.</p> Environemnt variable Default value Optional Notes POSTGRES_PASSWORD changeme <code>No</code> N/A POSTGRES_DB endurain <code>No</code> N/A POSTGRES_USER endurain <code>No</code> N/A PGDATA /var/lib/postgresql/data/pgdata <code>No</code> N/A <p>To check Python backend dependencies used, use poetry file (pyproject.toml).</p> <p>Frontend dependencies:</p> <ul> <li>To check npm dependencies used, use npm file (package.json)</li> <li>Logo created on Canva</li> </ul>"},{"location":"getting-started/advanced-started/#session-timeout-configuration-optional","title":"Session Timeout Configuration (Optional)","text":"<p>By default, Endurain sessions last 7 days without enforcing idle timeouts. For enhanced security, you can enable automatic session expiration:</p> <p>Environment Variables:</p> <ul> <li><code>SESSION_IDLE_TIMEOUT_ENABLED</code>: Enable timeout enforcement (default: <code>false</code>)</li> <li><code>SESSION_IDLE_TIMEOUT_HOURS</code>: Logout after inactivity (default: <code>1</code>)</li> <li><code>SESSION_ABSOLUTE_TIMEOUT_HOURS</code>: Force re-login after duration (default: <code>24</code>)</li> </ul> <p>Example:</p> <pre><code>environment:\n SESSION_IDLE_TIMEOUT_ENABLED: \"true\"\n SESSION_IDLE_TIMEOUT_HOURS: \"2\"\n SESSION_ABSOLUTE_TIMEOUT_HOURS: \"48\"\n</code></pre>"},{"location":"getting-started/advanced-started/#docker-secrets-support","title":"Docker Secrets Support","text":"<p>Endurain supports Docker secrets for securely managing sensitive environment variables. For the following environment variables, you can use <code>_FILE</code> variants that read the secret from a file instead of storing it directly in environment variables:</p> <ul> <li><code>DB_PASSWORD</code> \u2192 <code>DB_PASSWORD_FILE</code></li> <li><code>SECRET_KEY</code> \u2192 <code>SECRET_KEY_FILE</code></li> <li><code>FERNET_KEY</code> \u2192 <code>FERNET_KEY_FILE</code></li> <li><code>SMTP_PASSWORD</code> \u2192 <code>SMTP_PASSWORD_FILE</code></li> </ul>"},{"location":"getting-started/advanced-started/#using-file-based-secrets","title":"Using File-Based Secrets","text":"<p>Use file-based secrets to securely manage sensitive environment variables:</p> <ol> <li>Create a secrets directory with proper permissions:</li> </ol> <pre><code>mkdir -p secrets\nchmod 700 secrets\n</code></pre> <ol> <li>Create secret files with strong passwords:</li> </ol> <pre><code># Use randomly generated passwords, not hardcoded ones\necho \"$(openssl rand -base64 32)\" > secrets/db_password.txt\necho \"$(openssl rand -hex 32)\" > secrets/secret_key.txt\necho \"$(python3 -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\")\" > secrets/fernet_key.txt\n\n# Set secure file permissions\nchmod 600 secrets/*.txt\nchown $(id -u):$(id -g) secrets/*.txt\n</code></pre> <ol> <li>Configure docker-compose.yml:</li> </ol> <pre><code>services:\n endurain:\n environment:\n - DB_PASSWORD_FILE=/run/secrets/db_password\n - SECRET_KEY_FILE=/run/secrets/secret_key\n - FERNET_KEY_FILE=/run/secrets/fernet_key\n secrets:\n - db_password\n - secret_key\n - fernet_key\n\nsecrets:\n db_password:\n file: ./secrets/db_password.txt\n secret_key:\n file: ./secrets/secret_key.txt\n fernet_key:\n file: ./secrets/fernet_key.txt\n</code></pre> <p>Note: When using <code>_FILE</code> variants, the original environment variables (e.g., <code>DB_PASSWORD</code>) are not needed. The application will automatically read from the file specified by the <code>_FILE</code> environment variable.</p>"},{"location":"getting-started/advanced-started/#volumes","title":"Volumes","text":"<p>Docker image uses a non-root user, so ensure target folders are not owned by root. Non-root user should use UID and GID 1000. It is recommended to configure the following volumes for data persistence:</p> Volume Notes <code><local_path>/endurain/backend/logs:/app/backend/logs</code> Log files for the backend <code><local_path>/endurain/backend/data:/app/backend/data</code> Necessary for image and activity files persistence on docker image update"},{"location":"getting-started/advanced-started/#bulk-import-and-file-upload","title":"Bulk import and file upload","text":"<p>To perform a bulk import: - Place .fit, .tcx, .gz and/or .gpx files into the data/activity_files/bulk_import folder. Create the folder if needed. - In the \"Settings\" menu select \"Import\". - Click \"Import\" next to \"Bulk Import\".</p> <p>.fit files are preferred. I noticed that Strava/Garmin Connect process of converting .fit to .gpx introduces additional data to the activity file leading to minor variances in the data, like for example additional meters in distance and elevation gain. Some notes:</p> <ul> <li>After the files are processed, the files are moved to the processed folder</li> <li>GEOCODES API has a limit of 1 Request/Second on the free plan, so if you have a large number of files, it might not be possible to import all in the same action</li> <li>The bulk import currently only imports data present in the .fit, .tcx or .gpx files - no metadata or other media are imported.</li> </ul>"},{"location":"getting-started/advanced-started/#importing-information-from-a-strava-bulk-export-beta","title":"Importing information from a Strava bulk export (BETA)","text":"<p>Strava allows users to create a bulk export of their historical activity on the site. This information is stored in a zip file, primarily as .csv files, GPS recording files (e.g., .gpx, .fit), and media files (e.g., .jpg, .png).</p>"},{"location":"getting-started/advanced-started/#importing-gear-from-a-strava-bulk-export","title":"Importing gear from a Strava bulk export","text":""},{"location":"getting-started/advanced-started/#bike-import","title":"Bike import","text":"<p>At the present time, importing bikes from a Strava bulk export is implemented as a beta feature - use with caution. Components of bikes are not imported - just the bikes themselves. There is no mechanism present to undo an import.</p> <p>To perform an import of bikes: - Place the bikes.csv file from a Strava bulk export into the data/activity_files/bulk_import folder. Create the folder if needed; - In the <code>Settings</code> menu select <code>Import</code>; - Click <code>Import Strava Bikes</code> next to <code>Strava gear import</code>; - Upon successful import, the bikes.csv file is moved to /data/activity_files/processed folder; - Status messages about the import, including why any gear was not imported, can be found in the logs.</p> <p>Ensure the file is named <code>bikes.csv</code> and has a header row with at least the fields 'Bike Name', 'Bike Brand', and 'Bike Model'.</p>"},{"location":"getting-started/advanced-started/#shoe-import","title":"Shoe import","text":"<p>At the present time, importing shoes from a Strava bulk export is implemented as a beta feature - use with caution. Components of shooes are not imported - just the shoes themselves. </p> <p>To perform an import of shoes: - Place the shoes.csv file from a Strava bulk export into the data/activity_files/bulk_import folder. Create the folder if needed; - In the <code>Settings</code> menu select <code>Import</code>; - Click <code>Shoes import</code> next to <code>Strava gear import</code>; - Upon successful import, the shoes.csv file is moved to /data/activity_files/processed folder; - Status messages about the import, including why any gear was not imported, can be found in the logs.</p> <p>Ensure the file is named <code>shoes.csv</code> and has a header row with at least the fields 'Shoe Name', 'Shoe Brand', and 'Shoe Model'.</p> <p>Note that Strava allows blank shoe names, but Endurain does not. Shoes with a blank name will thus be given a default name of <code>Unnamed Shoe #</code> on import.</p>"},{"location":"getting-started/advanced-started/#notes-on-importing-gear","title":"Notes on importing gear","text":"<p>NOTE: There is currently no mechanism to undo a gear import.</p> <p>All gear will be imported as active, as Strava does not export the active/inactive status of the gear.</p> <p>Note that Endurain does not allow the <code>+</code> character in gear field names, and thus +'s will removed from all fields and replaced with spaces (\" \") on import. All beginning and ending space characters (\" \") will be removed on import as well.</p> <p>Endurain does not allow duplicate gear nicknames, case insensitively (e.g., <code>Ilves</code> and <code>ilves</code> would not be allowed) and regardless of gear type (e.g., <code>Ilves</code> the bike and <code>ilves</code> the shoe would not be allowed). Gear with duplicate nicknames will not be imported (i.e., only the first item with a given nickname will be imported).</p> <p>The import routine checks for duplicate items, and should not import duplicates. Thus it should be safe to re-import the same file mulitple times. However, due to the renaming of un-named shoes, repeated imports of the same shoe file will create duplicate entries of any unnamed shoes present. </p> <p>Gear that is already present in Endurain due to having an active link with Strava will not be imported via the manual import process.</p>"},{"location":"getting-started/advanced-started/#importing-other-items-from-a-strava-bulk-import","title":"Importing other items from a Strava bulk import","text":"<p>Importing activity metadata and media is under development in October 2025.</p>"},{"location":"getting-started/advanced-started/#image-personalization","title":"Image personalization","text":"<p>It is possible (v0.10.0 or higher) to personalize the login image in the login page. To do that, map the data/server_images directory for image persistence on container updates and: - Set the image in the server settings zone of the settings page - A square image is expected. Default one uses 1000px vs 1000px</p>"},{"location":"getting-started/bare-metal/","title":"Bare-Metal Installation Guide","text":"<p>This guide explains how to install Endurain bare-metal on Debian without Docker.</p>"},{"location":"getting-started/bare-metal/#1-install-required-dependencies","title":"1. Install Required Dependencies","text":"<pre><code>apt install -y \\\n build-essential \\\n git \\\n curl \\\n python3-dev\n</code></pre>"},{"location":"getting-started/bare-metal/#2-install-required-runtime-tools","title":"2. Install Required Runtime Tools","text":"<p>Install the required tools from their official sources:</p> <ul> <li>uv (Python) \u2192 https://docs.astral.sh/uv/</li> <li>Node.js 22 \u2192 https://nodejs.org/en/download</li> <li>PostgreSQL 17 \u2192 https://www.postgresql.org/download/</li> </ul>"},{"location":"getting-started/bare-metal/#3-download-endurain-release","title":"3. Download Endurain Release","text":"<p>Run the following command to download and unpack the latest release.</p> <pre><code>mkdir -p /path/to/endurain\ncd /path/to/endurain\n\nTAG=$(curl -s https://api.github.com/repos/endurain-project/endurain/releases/latest \\\n | grep -oP '\"tag_name\": \"\\K(.*)(?=\")')\ncurl -L \"https://github.com/endurain-project/endurain/archive/refs/tags/$TAG.tar.gz\" \\\n | tar xz\nEXTRACTED=$(ls -d endurain-*)\nshopt -s dotglob\nmv \"$EXTRACTED\"/* .\nshopt -u dotglob\nrm -rf \"$EXTRACTED\"\n</code></pre>"},{"location":"getting-started/bare-metal/#4-create-environment-configuration","title":"4. Create Environment Configuration","text":"<p>Prepare data storage.</p> <pre><code>mkdir -p /path/to/endurain_data/{data,logs}\n</code></pre> <p>Copy the provided example.</p> <pre><code>cp /path/to/endurain/.env.example /path/to/endurain/.env\n</code></pre> <p>Generate your <code>SECRET_KEY</code> and <code>FERNET_KEY</code>. These keys are required for Endurain to work, so be sure to paste them into your <code>.env</code> file.</p> <pre><code>openssl rand -hex 32 # SECRET_KEY\nopenssl rand -base64 32 # FERNET_KEY\n</code></pre> <p>Edit <code>.env</code> file.</p> <pre><code>nano /path/to/endurain/.env\n</code></pre> <p>Adjust the environment variables and set keys. You definitely have to adjust <code>FRONTEND_DIR</code>, <code>BACKEND_DIR</code> and <code>DB_HOST</code>. Environment variables are explained in the Environment Variables Guide.</p> <pre><code>DB_HOST=localhost\nBACKEND_DIR=\"/path/to/endurain/backend/app\"\nFRONTEND_DIR=\"/path/to/endurain/frontend/app/dist\"\nDATA_DIR=\"/path/to/endurain_data/data\"\nLOGS_DIR=\"/path/to/endurain_data/logs\"\n</code></pre>"},{"location":"getting-started/bare-metal/#5-build-the-frontend","title":"5. Build the Frontend","text":"<pre><code>cd /path/to/endurain/frontend/app\nnpm ci\nnpm run build\n</code></pre> <p>Create <code>env.js</code>. Edit the URL if you use a reverse proxy.</p> <pre><code>cat << 'EOF' > /path/to/endurain/frontend/app/dist/env.js\nwindow.env = {\n ENDURAIN_HOST: \"http://YOUR_SERVER_IP:8080\",\n};\nEOF\n</code></pre>"},{"location":"getting-started/bare-metal/#6-set-up-the-backend","title":"6. Set Up the Backend","text":"<pre><code>cd /path/to/endurain/backend\n\nuv tool install poetry\nuv tool update-shell\nexport PATH=\"/root/.local/bin:$PATH\"\n\npoetry self add poetry-plugin-export\npoetry export -f requirements.txt --output requirements.txt --without-hashes\n\nuv venv\nuv pip install -r requirements.txt\n</code></pre>"},{"location":"getting-started/bare-metal/#7-setup-postgres-database","title":"7. Setup Postgres Database","text":"<p>Run the following commands to create a PostgreSQL user and database for Endurain:</p> <pre><code>sudo -u postgres createuser -P endurain\nsudo -u postgres createdb -O endurain endurain\n</code></pre> <p>Check that the PostgreSQL client and server encodings are set to UTF-8.</p> <pre><code>sudo -u postgres psql -c \"SHOW client_encoding;\"\nsudo -u postgres psql -c \"SHOW server_encoding;\"\n</code></pre> <p>If either value is SQL_ASCII, set UTF-8 explicitly for the user and the database.</p> <pre><code>sudo -u postgres psql -c \"ALTER ROLE endurain SET client_encoding = 'UTF8';\"\nsudo -u postgres psql -c \"ALTER DATABASE endurain SET client_encoding = 'UTF8';\"\n</code></pre> <p>This ensures that all connections to the endurain database default to proper UTF-8 encoding.</p>"},{"location":"getting-started/bare-metal/#8-systemd-service","title":"8. Systemd Service","text":"<p>This is an example how you could set up your systemd service.</p> <pre><code>cat << 'EOF' > /etc/systemd/system/endurain.service\n[Unit]\nDescription=Endurain FastAPI Backend\nAfter=network.target postgresql.service\n\n[Service]\nWorkingDirectory=/path/to/endurain/backend/app\nEnvironmentFile=/path/to/endurain/.env\nExecStart=/path/to/endurain/backend/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8080\nRestart=always\nRestartSec=5\nUser=root\nStandardOutput=journal\nStandardError=journal\n\n[Install]\nWantedBy=multi-user.target\nEOF\n</code></pre> <p>Enable and start the service.</p> <pre><code>systemctl daemon-reload\nsystemctl enable endurain\nsystemctl start endurain\n</code></pre>"},{"location":"getting-started/bare-metal/#9-update-to-a-new-version-of-endurain","title":"9. Update to a new version of Endurain.","text":"<p>Remove old version and get the latest.</p> <pre><code>systemctl stop endurain\nrm -rf /path/to/endurain/*\ncd /path/to/endurain\n\nTAG=$(curl -s https://api.github.com/repos/endurain-project/endurain/releases/latest \\\n | grep -oP '\"tag_name\": \"\\K(.*)(?=\")')\ncurl -L \"https://github.com/endurain-project/endurain/archive/refs/tags/$TAG.tar.gz\" \\\n | tar xz\nEXTRACTED=$(ls -d endurain-*)\nshopt -s dotglob\nmv \"$EXTRACTED\"/* .\nshopt -u dotglob\nrm -rf \"$EXTRACTED\"\n</code></pre> <p>Build the Frontend.</p> <pre><code>cd /path/to/endurain/frontend/app\nnpm ci\nnpm run build\n</code></pre> <p>Set Up the Backend.</p> <pre><code>cd /path/to/endurain/backend\npoetry export -f requirements.txt --output requirements.txt --without-hashes\n\nuv venv\nuv pip install -r requirements.txt\n</code></pre> <p>start the service.</p> <pre><code>systemctl start endurain\n</code></pre>"},{"location":"getting-started/getting-started/","title":"Getting started","text":"<p>Welcome to the guide for getting started on hosting your own production instance of Endurain. Like many other services, Endurain is easiest to get up and running trough Docker compose. It is possible to get Endurain up and running without a domain and reverse proxy, but this guide assumes you want to use a reverse proxy and your domain. Endurain can run on any computer that support OCI containers, but in this guide we are using Debian 13 (should also work with 12).</p>"},{"location":"getting-started/getting-started/#prerequisites","title":"Prerequisites","text":"<ul> <li>Domain name pointed to your external IP address.</li> <li>Open FW rules to your server on port 443 and 80. (trough NAT if you are running ipv4)</li> <li>A computer/server with enough disk space for your activity files.</li> <li>A Linux distro that has <code>docker compose</code> cli, and <code>caddy</code> in the repositories.</li> </ul>"},{"location":"getting-started/getting-started/#installing-docker-and-caddy-reverse-proxy","title":"Installing Docker and Caddy reverse proxy","text":"<p>Note: If you have a old-ish distro (Ubuntu 22.04 and older) you need to add the repo for Docker. Read how to do it on Docker documentation. For newer distroes (Debian 13 and Ubuntu 24.04 it is not expected for you to have to do this step).</p> <p>Install Docker:</p> <pre><code>sudo apt update -y\nsudo apt install docker.io docker-compose -y\n</code></pre> <p>Confirm your user has the id 1000:</p> <pre><code>id\n</code></pre> <p>If you are not the user 1000, you need to set the <code>UID</code> and <code>GID</code> to your id in the .env file. But to keep this guide as easy to follow as possible, we will assume that you are user 1000.</p>"},{"location":"getting-started/getting-started/#installing-caddy-reverse-proxy","title":"Installing Caddy reverse proxy","text":"<p>Note: If you have a old-ish distro (Ubuntu 22.04 and older) you need to add the repo for Caddy. Read how to do it on Caddy documentation. For newer distroes (Debian 13 and Ubuntu 24.04 it is not expected for you to have to do this step).</p> <pre><code>sudo apt update -y\nsudo apt install caddy -y\n</code></pre>"},{"location":"getting-started/getting-started/#installing-nginx-proxy-manager-reverse-proxy","title":"Installing Nginx Proxy Manager reverse proxy","text":"<p>Nginx Proxy Manager comes as a pre-built Docker image. Please refer to the docs for details on how to install it.</p>"},{"location":"getting-started/getting-started/#create-directory-structure","title":"Create directory structure","text":"<p>Lets use <code>/opt/endurain/</code> as the root directory for our project.</p> <pre><code>sudo mkdir /opt/endurain\nsudo chown 1000:1000 /opt/endurain\nmkdir -p \\\n /opt/endurain/backend/{data,logs} \\\n /opt/endurain/postgres\n</code></pre>"},{"location":"getting-started/getting-started/#docker-compose-deployment","title":"Docker compose Deployment","text":"<p>In this example of setting up Endurain, we will need two files. One <code>docker-compose.yml</code> and <code>.env</code>.</p> <ul> <li>docker-compose.yml tells your system how to set up the container, network and storage.</li> <li>.env holds our secrets and environment variables.</li> </ul> <p>Splitting up the setup like this make it easy to handle updates to the containers, without touching the secrets and other variables.</p>"},{"location":"getting-started/getting-started/#creating-the-docker-compose-and-env-file","title":"Creating the docker-compose and .env file","text":"<p>To make it as easy as possible for selfhoster to get up and running examples of docker-compose.yml and .env is on the git repo. Here are links to the files on the repo:</p> <ul> <li>docker-compose.yml.example</li> <li>.env.example</li> </ul> <pre><code>cd /opt/endurain\nwget https://raw.githubusercontent.com/endurain-project/endurain/refs/heads/master/docker-compose.yml.example\nwget https://raw.githubusercontent.com/endurain-project/endurain/refs/heads/master/.env.example\n\nmv docker-compose.yml.example docker-compose.yml\nmv .env.example .env\n</code></pre> <p>Now we need to make changes to the files to reflect your environment. Inside docker-compose.yml there is not much we need to do. If you want to store the files another place then <code>/opt/endurain</code> this is the file you need to change.</p> <p>Here is an explaination on what you can set in the <code>.env</code>:</p> Environment variable How to set it DB_PASSWORD Run <code>openssl rand -hex 32</code> on a terminal to get a secret POSTGRES_PASSWORD Set the same value as DB_PASSWORD. SECRET_KEY Run <code>openssl rand -hex 32</code> on a terminal to get a secret FERNET_KEY Run <code>python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"</code> on a terminal to get a secret or go to https://fernetkeygen.com. Example output is <code>7NfMMRSCWcoNDSjqBX8WoYH9nTFk1VdQOdZY13po53Y=</code> TZ Timezone definition. Insert your timezone. List of available time zones here. Format <code>Europe/Lisbon</code> expected ENDURAIN_HOST https://endurain.yourdomain.com BEHIND_PROXY Change to true if behind reverse proxy POSTGRES_DB Postgres name for the database. POSTGRES_USER Postgres user for the database. <p>Please note:</p> <p><code>POSTGRES_DB</code> and <code>POSTGRES_USER</code> are values for the database. If you change it from endurain, you also need to set the environment variables for the app image. Please leave them as <code>endurain</code> if you are unsure.</p>"},{"location":"getting-started/getting-started/#start-the-stack","title":"Start the stack","text":"<p>It is finally time to start the stack!</p> <pre><code>cd /opt/endurain\nsudo docker compose up -d\n</code></pre> <p>Check the log output:</p> <pre><code>docker compose logs -f\n</code></pre> <p>If you do not get any errors, continue to next step.</p>"},{"location":"getting-started/getting-started/#visit-the-site","title":"Visit the site","text":"<ul> <li>Visit the site insecurly on <code>http://<IP-OF-YOUR-SERVER>:8080</code></li> <li>We still can not login to the site, because the <code>ENDURAIN_HOST</code> doesn't match our local URL.</li> </ul>"},{"location":"getting-started/getting-started/#configure-a-reverse-proxy","title":"Configure a reverse proxy","text":"<ul> <li>Before we configure a reverse proxy you need to set your DNS provider to point your domain to your external IP.</li> <li>You also need to open your firewall on port 443 and 80 to the server.</li> </ul>"},{"location":"getting-started/getting-started/#configure-caddy-as-reverse-proxy-and-get-ssl-cert-from-letsencrypt","title":"Configure Caddy as reverse proxy and get SSL cert from letsencrypt","text":"<p>We use Caddy outside docker. This way Debian handles the updates (you just need to run <code>sudo apt update -y</code> and <code>sudo apt upgrade -y</code>)</p> <p>Caddy is configured in the file <code>/etc/caddy/Caddyfile</code></p> <p>Open the file in your favourite editor, delete the default text, and paste in this:</p> <pre><code>endurain.yourdomain.com {\n reverse_proxy localhost:8080\n}\n</code></pre> <p>Restart Caddy</p> <pre><code>sudo systemctl restart caddy\n</code></pre> <p>Check the ouput of Caddy with:</p> <pre><code>sudo journalctl -u caddy\n</code></pre>"},{"location":"getting-started/getting-started/#configure-nginx-proxy-manager-as-reverse-proxy-and-get-ssl-cert-from-letsencrypt","title":"Configure Nginx Proxy Manager as reverse proxy and get SSL cert from letsencrypt","text":"<p>Bellow is an example config file for Endurain: <pre><code>------------------------------------------------------------\nendurain.yourdomain.com\n------------------------------------------------------------\n\nmap $scheme $hsts_header {\n https \"max-age=63072000; preload\";\n}\n\nserver {\n set $forward_scheme http;\n set $server \"your_server_ip\";\n set $port 8884;\n\n listen 80;\n listen [::]:80;\n\n listen 443 ssl;\n listen [::]:443 ssl;\n\n server_name endurain.yourdomain.com;\n\n http2 on;\n Let's Encrypt SSL\n\n include conf.d/include/letsencrypt-acme-challenge.conf;\n include conf.d/include/ssl-cache.conf;\n include conf.d/include/ssl-ciphers.conf;\n ssl_certificate /etc/letsencrypt/live/npm-21/fullchain.pem;\n ssl_certificate_key /etc/letsencrypt/live/npm-21/privkey.pem;\n Asset Caching\n\n include conf.d/include/assets.conf;\n Block Exploits\n\n include conf.d/include/block-exploits.conf;\n HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years)\n\n add_header Strict-Transport-Security $hsts_header always;\n Force SSL\n\n include conf.d/include/force-ssl.conf;\n\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection $http_connection;\n proxy_http_version 1.1;\n\n access_log /data/logs/proxy-host-18_access.log proxy;\n error_log /data/logs/proxy-host-18_error.log warn;\n\n location / {\n HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years)\n\n add_header Strict-Transport-Security $hsts_header always;\n\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection $http_connection;\n proxy_http_version 1.1;\n Proxy!\n\n include conf.d/include/proxy.conf;\n }\n Custom\n\n include /data/nginx/custom/server_proxy[.]conf;\n}\n</code></pre></p>"},{"location":"getting-started/getting-started/#access-your-endurain-instance","title":"Access your Endurain instance","text":"<p>You should now be able to access your site on endurain.yourdomain.com</p> <p>Log in with username: admin password: admin, and remember to change the password</p> <p>\ud83c\udf89 Weee \ud83c\udf89 You now have your own instance of Endurain up and running!</p>"},{"location":"getting-started/getting-started/#how-to-update","title":"How to update","text":"<ul> <li>Take a backup of your files and db.</li> <li>Check for new releases of the container image here. Read release notes carefully for breaking changes.</li> <li>Log on your server and run:</li> <li>Inside <code>/opt/endurain/docker-compose.yml</code>, change out the version tag (the version after <code>:</code>). If you are running <code>:latest</code> tag on the docker image, you do not have to edit anything in the docker-compose.yml file. </li> </ul> <pre><code>cd /opt/endurain\nsudo docker compose pull\nsudo docker compose up -d\n</code></pre> <p>The same is the case for Postgres. Check for breaking changes in release notes on Postgres Website.</p> <p>** It is generally pretty safe to upgrade postgres minor version f.eks 17.4 to 17.5, but major is often breaking change (example 17.2 to 18.1 )</p>"},{"location":"getting-started/getting-started/#things-to-think-about","title":"Things to think about","text":"<p>You should implement backup strategy for the following directories:</p> <pre><code>/opt/endurain/app/data\n/opt/endurain/app/logs\n</code></pre> <p>You also need to backup your postgres database. It is not good practice to just backup the volume <code>/opt/endurain/postgres</code> this might be corrupted if the database is in the middle of a wright when the database goes down.</p>"},{"location":"getting-started/getting-started/#default-credentials","title":"Default Credentials","text":"<ul> <li>Username: admin </li> <li>Password: admin</li> </ul>"},{"location":"getting-started/maria-to-postgres-migration/","title":"MariaDB to Postgres migration guide","text":"<p>This will guide you on how to migrate from MariaDB to Postgres. Endurain will drop support for MariaDB on v0.16.0, so you'll need to perform this migration prior to upgrade to v0.16.0 or higher.</p> <p>This guide uses Endurain's built-in export/import functionality to migrate your data.</p>"},{"location":"getting-started/maria-to-postgres-migration/#prerequisites","title":"Prerequisites","text":"<ul> <li>Endurain instance running with MariaDB</li> <li>PostgreSQL database set up and accessible</li> <li>Admin access to your Endurain instance</li> </ul> <p>\u26a0\ufe0f Important Notes:</p> <ul> <li>The export/import process will migrate all user data except user password. Each user will have to do this process</li> <li>You will need to use default credentials (admin/admin) on new setup</li> <li>Keep your existing MariaDB database running for rollback if needed</li> <li>The import process can take time for large databases with many activities</li> <li>Server settings are not migrated</li> </ul>"},{"location":"getting-started/maria-to-postgres-migration/#migration-steps","title":"Migration Steps","text":""},{"location":"getting-started/maria-to-postgres-migration/#step-1-export-data-from-mariadb-instance","title":"Step 1: Export Data from MariaDB Instance","text":"<ol> <li>Instruct each user to log in to Endurain instance (currently running with MariaDB)</li> <li>Each user should navigate to Settings \u2192 My Profile \u2192 Export/Import</li> <li>Each user should lick Export to download a <code>.zip</code> file containing the user data</li> <li>Each user should save this file in a safe location</li> </ol> <p>\u26a0\ufe0f Do NOT delete your existing MariaDB database - keep it for rollback if needed.</p>"},{"location":"getting-started/maria-to-postgres-migration/#step-2-stop-current-endurain-instance","title":"Step 2: Stop Current Endurain Instance","text":"<p>Stop your current Endurain container:</p> <pre><code>docker compose down\n</code></pre>"},{"location":"getting-started/maria-to-postgres-migration/#step-3-update-environment-variables","title":"Step 3: Update Environment Variables","text":"<p>Update your environment variables to point to PostgreSQL (adapt to your environment):</p> <pre><code>DB_TYPE=postgres\nDB_HOST=postgres\nDB_PORT=5432\nDB_USER=endurain\nDB_PASSWORD=your_postgres_password\nDB_NAME=endurain\n</code></pre> <p>Ensure your PostgreSQL database exists and is accessible with these credentials.</p>"},{"location":"getting-started/maria-to-postgres-migration/#step-4-start-fresh-endurain-with-postgresql","title":"Step 4: Start Fresh Endurain with PostgreSQL","text":"<p>Start Endurain with the new PostgreSQL configuration:</p> <pre><code>docker compose up -d\n</code></pre> <p>This will start a fresh Endurain instance with: - Empty PostgreSQL database - Default admin credentials: admin/admin</p>"},{"location":"getting-started/maria-to-postgres-migration/#step-5-import-data","title":"Step 5: Import Data","text":"<ol> <li>Log in with default credentials: admin/admin</li> <li>Create a new user for each of your instance users if applicable</li> <li>Each user should navigate to Settings \u2192 My Profile \u2192 Export/Import</li> <li>Each user should click Import and select the <code>.zip</code> file exported</li> <li>Wait for the import to complete (this may take several minutes for large databases)</li> </ol> <p>\u26a0\ufe0f Note: User passwords are NOT imported for security reasons. All users will need to reset their passwords.</p>"},{"location":"getting-started/maria-to-postgres-migration/#step-6-verify-migration","title":"Step 6: Verify Migration","text":"<p>Verify the migration was successful by checking:</p> <ul> <li>All activities are present</li> <li>Activity streams display correctly</li> <li>Activity media files load</li> <li>Gear information is correct</li> <li>Integrations (Strava, Garmin) are configured</li> <li>Health data is present</li> </ul>"},{"location":"getting-started/maria-to-postgres-migration/#troubleshooting","title":"Troubleshooting","text":""},{"location":"getting-started/maria-to-postgres-migration/#if-import-fails","title":"If Import Fails","text":"<p>If the import process fails:</p> <ol> <li>Check the application logs in the container</li> <li>Check the <code>app.log</code> file</li> <li>Paste both outputs (container logs and app.log contents) when seeking help</li> </ol>"},{"location":"getting-started/maria-to-postgres-migration/#rolling-back-to-mariadb","title":"Rolling Back to MariaDB","text":"<p>If you need to rollback:</p> <ol> <li>Stop the PostgreSQL instance:</li> </ol> <pre><code>docker compose down\n</code></pre> <ol> <li>Restore your original environment variables (MariaDB settings)</li> <li>Start your original MariaDB instance:</li> </ol> <pre><code>docker compose up -d\n</code></pre>"},{"location":"integrations/3rd-party-apps/","title":"3rd party apps","text":""},{"location":"integrations/3rd-party-apps/#runnerup-integration","title":"RunnerUp Integration","text":"<p>RunnerUp an app for tracking your sport activities with your Android phone, can automatically sync your activities recorded with it to your Endurain instance.</p> <p>RunnerUp is supported until version v0.5.3 of Endurain. An issue is opened to get support for v0.6.0+.</p>"},{"location":"integrations/3rd-party-services/","title":"3rd party services","text":""},{"location":"integrations/3rd-party-services/#garmin-connect-integration","title":"Garmin Connect Integration","text":"<p>To enable Garmin Connect integration, Endurain will ask for your Garmin Connect credentials. These credentials are not stored, but the authentication tokens (access and refresh tokens) are stored in the DB, similar to the Strava integration. The credentials are sent from the frontend to the backend in plain text, so the use of HTTPS is highly recommended.</p> <p>Once the integration with Garmin Connect is configured, on startup, every one and four hours the backend will check if there is new unimported activities and new body composition entries respectively. If yes, the new data is automatically imported.</p> <p>For Garmin Connect integration python-garminconnect Python module is used.</p>"},{"location":"integrations/3rd-party-services/#strava-integration","title":"Strava Integration","text":"<p>\u26a0\ufe0f Warning Due to recent Strava API changes, expect changes in the Strava integration in a following release.</p> <p>To enable Strava integration, ensure your Endurain instance is accessible from the internet and follow Strava's API setup guide. After the integration is successful the access and refresh tokens are stored in the DB. Each user will have his/hers own pair.</p> <p>Once the integration with Strava is configured, on startup and every hour the backend will check if there is new unimported activities. If yes, the new activity is automatically imported.</p> <p>On link, user will need to provide his/her API client ID and secret. Pair will be temporary stored in the DB until the process finishes. Info is sent on a JSON payload and HTTPS end2end is encouraged.</p> <p>On Strava unlink action every data imported from Strava, i.e. activities and gears, will be deleted according to Strava API Agreement.</p> <p>For Strava integration stravalib Python module is used.</p>"}]} |