Infrastructure

Self-host everything.
Own your data.

Cadiv runs entirely on your own hardware. No SaaS subscription, no data harvesting, no third-party analytics. Pull the images, add your keys, and docker compose up.

Containers

2

Setup Time

~10 min

Min RAM

512 MB

Prerequisites

What you need

A Server

Any Linux box, VPS, Raspberry Pi, NAS, or even your Mac. 512 MB RAM minimum, 1 GB recommended.

Docker & Compose

Docker Engine 20+ and Docker Compose v2. That's the only system dependency.

Strava API App

A free Strava API application for activity import. Takes 2 minutes to create. Optional — connect after login from Settings.

Whoop API App (optional)

Optional Whoop developer app for recovery and strain data. Free to create.

Guide

Step-by-step setup

From zero to riding in about 10 minutes.

1

Create a Strava API Application

Strava is used for activity import and sync. Create a free API app to connect your rides after logging in.

a

Go to strava.com/settings/api and log in with your Strava account.

b

Create a new application with these settings:

Application Name Cadiv (or anything you like)
Category Training
Club (leave blank)
Website Your domain or http://localhost:4321
Authorization Callback Domain localhost or your domain
c

Note your Client ID and Client Secret — you'll need them in Step 3.

2

Create a Whoop Developer App

Optional

If you use a Whoop band and want recovery, strain, and workout data, set up a developer app. Skip this if you only use Strava.

a

Go to developer-dashboard.whoop.com and sign in.

b

Create a new application. Set the redirect URI to:

http://localhost:4000/api/auth/whoop/callback

Replace localhost with your domain if deploying remotely.

c

Enable these scopes:

offline read:recovery read:cycles read:workout read:sleep read:profile read:body_measurement
d

Note your Client ID and Client Secret.

3

Create your config files

Create a new directory and two files. No source code needed — Docker pulls the pre-built images.

Create a directory

$ mkdir cadiv && cd cadiv

Create docker-compose.yml

Paste this into docker-compose.yml:

docker-compose.yml

version: "3.9"

services:

postgres:

image: postgres:16-alpine

environment:

POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

volumes: [ cadiv-pgdata:/var/lib/postgresql/data ]

healthcheck:

test: [ "CMD-SHELL", "pg_isready -U cadiv" ]

cadiv:

image: lukevinskywynn/cadiv:latest

depends_on: { postgres: { condition: service_healthy } }

env_file: .env

ports: [ "4000:4000", "4321:4321" ]

volumes:

cadiv-pgdata:

The full docker-compose.yml with all environment variables is available on Docker Hub.

Create your .env file

Create a .env file in the same directory with your secrets:

.env

# Your login credentials

CADIV_USERNAME=admin

CADIV_PASSWORD=your-strong-password

# PostgreSQL

POSTGRES_PASSWORD=your-strong-database-password

# Session secret (generate: openssl rand -hex 32)

SESSION_SECRET=your-64-char-hex-string

# Strava (optional, from Step 1)

STRAVA_CLIENT_ID=12345

STRAVA_CLIENT_SECRET=abc123def456...

# Whoop (optional, from Step 2)

WHOOP_CLIENT_ID=your-whoop-id

WHOOP_CLIENT_SECRET=your-whoop-secret

Tip: Generate a secure session secret

$ openssl rand -hex 32
4

Pull & Start

One command to pull the image and start everything.

$ docker compose up -d

[+] Pulling 2/2

[+] Running 2/2

✔ Container cadiv-postgres Started

✔ Container cadiv Started

The first run downloads the pre-built image from Docker Hub. Subsequent starts are near-instant.

What happens on startup

1. PostgreSQL starts and waits for healthy
2. Server runs prisma migrate deploy to create/update tables
3. Your user account is provisioned from CADIV_USERNAME / CADIV_PASSWORD
4. Express server starts on port 4000
5. Astro client starts on port 4321
6. Background sync scheduler starts (Strava + Whoop every 5 min)
5

Sign In & Ride

Open your browser and sign in with your credentials.

a

Open http://localhost:4321 in your browser.

b

Sign in with the username and password you set in your .env file.

c

Go to Settings → Connected Accounts → Connect Strava to import your ride history.

d

(Optional) Connect Whoop for recovery and strain data.

Architecture

What's in the box

PostgreSQL 16

cadiv-postgres

All your data lives here — activities, coaching plans, user profiles, notifications, sync cursors. Persistent volume means your data survives container restarts and upgrades.

Port: 5432 · Volume: cadiv-pgdata

Cadiv

lukevinskywynn/cadiv

A single container running the full stack: Express API server with Prisma ORM (port 4000) and Astro + React web client (port 4321). Handles OAuth, data import, training insights, coaching engine, notifications, and background sync.

Ports: 4000 (API) + 4321 (Web)

Advanced

Custom domain & HTTPS

Deploying to a VPS with a custom domain? Here's how to update the configuration.

1. Update redirect URLs

Replace localhost with your domain in .env:

.env

APP_ORIGIN=https://cadiv.yourdomain.com

STRAVA_REDIRECT_URI=https://cadiv.yourdomain.com/api/auth/strava/callback

STRAVA_SUCCESS_REDIRECT=https://cadiv.yourdomain.com/app/dash

STRAVA_FAILURE_REDIRECT=https://cadiv.yourdomain.com/?auth=error

2. Update Strava callback domain

Go back to strava.com/settings/api and update the Authorization Callback Domain to your domain (e.g., cadiv.yourdomain.com).

3. Add a reverse proxy

Use Nginx, Caddy, or Traefik to terminate TLS and proxy to the containers. Example with Caddy:

Caddyfile

cadiv.yourdomain.com {

handle /api/* {

reverse_proxy localhost:4000

}

handle {

reverse_proxy localhost:4321

}

}

Caddy automatically provisions Let's Encrypt certificates. Zero HTTPS configuration needed.

Operations

Common commands

docker compose up -d

Start all containers in background

docker compose down

Stop all containers

docker compose logs -f cadiv

Follow Cadiv logs

docker compose pull && docker compose up -d

Update to latest version

docker compose exec postgres pg_dump -U cadiv cadiv > backup.sql

Backup database

docker compose exec -T postgres psql -U cadiv cadiv < backup.sql

Restore database

docker compose restart cadiv

Restart Cadiv

Reference

Environment variables

Variable Required Description
DATABASE_URL No Full PostgreSQL connection string. If set, uses this instead of the bundled Postgres container
POSTGRES_PASSWORD Yes Password for the bundled PostgreSQL container. Not needed if using DATABASE_URL
SESSION_SECRET Yes Secret for signing session cookies. Use openssl rand -hex 32
STRAVA_CLIENT_ID Yes Your Strava API application Client ID
STRAVA_CLIENT_SECRET Yes Your Strava API application Client Secret
STRAVA_REDIRECT_URI No OAuth callback URL. Default: http://localhost:4000/api/auth/strava/callback
STRAVA_SUCCESS_REDIRECT No URL after successful auth. Default: http://localhost:4321/app/dash
STRAVA_FAILURE_REDIRECT No URL after failed auth. Default: http://localhost:4321/?auth=error
WHOOP_CLIENT_ID No Whoop developer app Client ID (optional)
WHOOP_CLIENT_SECRET No Whoop developer app Client Secret (optional)
WHOOP_REDIRECT_URI No Whoop OAuth callback URL (optional)
APP_ORIGIN No Frontend URL. Default: http://localhost:4321
CLIENT_PORT No Host port for web UI. Default: 4321
SERVER_PORT No Host port for API. Default: 4000
POSTGRES_PORT No Host port for PostgreSQL. Default: 5432

Mobile

Connecting the mobile app

The Cadiv mobile app (React Native / Expo) can point to your self-hosted server for a fully private mobile experience.

1. Download the Cadiv app

Install the Cadiv mobile app from the App Store or Google Play. The same app works with any Cadiv server — no custom builds required.

2. Connect to your server

When you open the app for the first time, you'll see the Connect to Server screen. Enter your server URL (e.g., https://cadiv.yourdomain.com or http://192.168.1.50:4000).

The app verifies the connection by fetching /api/config from your server. Once verified, it stores the URL securely and proceeds to login — using your server's own Strava app credentials.

Zero configuration

The server URL is configured in-app — not hardcoded. You can change it anytime from Settings → Server.

3. Authenticate via Strava

Tap Connect with Strava. The app opens the Strava OAuth flow against your server. Your server exchanges the auth code, creates a JWT, and redirects back to the app. All authentication is handled by your self-hosted server — nothing touches any external service beyond Strava's OAuth.

Your data.
Your server.
Your edge.

No subscriptions. No data harvesting. Just you, your bike, and your own server.