OpenAthlete
AGPLv3 endurance training platform: EU-oriented hosting, transparent CTL/ATL/TSB logic in code, self-hosting, and full export. Built in Grenoble.
Israel's leading running community. Redefining running culture since 2022.
Translation pending
Sports & fitnessTypeScript · HTMLGitHub @Ofergr15OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Running-club PWA for the Madregot club. Coaches parse training plans from text or
images with AI and push structured workouts to athletes' Garmin watches; athletes
get a Hebrew-first mobile app with their program, a Strava-style activity feed,
attendance, academy reports and push notifications.Read the full README ↗Running-club PWA for the Madregot club. Coaches parse training plans from text or images with AI and push structured workouts to athletes' Garmin watches; athletes get a Hebrew-first mobile app with their program, a Strava-style activity feed, attendance, academy reports and push notifications.
next-intl (he/en, RTL)garmin-connect packagemaxDuration = 300)Requires Node >= 22.12.
npm install
Create a project at supabase.com, then run the SQL in
this order in the SQL Editor. Both steps are required — schema.sql creates
the base tables (coaches, groups, athletes, weekly_plans,
workout_deliveries, athlete_activities) and the migrations build everything
else on top of them. Running only one of the two leaves the app unable to boot.
supabase/schema.sql — the base tables.supabase/migrations/ in numeric order, from
000_extensions.sql through the highest-numbered file (currently
076_notification_image.sql, 72 files).Migrations are applied by hand in the SQL Editor; there is no supabase db push
wired up in this repo. To concatenate them in order for a single paste:
for f in supabase/migrations/*.sql; do echo "-- $f"; cat "$f"; echo; done > /tmp/all-migrations.sql
Then copy your project URL and API keys from Settings → API.
cp .env.local.example .env.local
.env.local.example documents every variable the app reads, grouped by the
feature it unlocks and annotated with what breaks when it's missing. Only the
first block ("Core") plus ENCRYPTION_KEY is needed to boot; the rest are
per-feature and degrade gracefully when blank.
Two that catch people out:
ENCRYPTION_KEY must be 64 hex characters, and is not rotatable in
place — a new key cannot decrypt existing Garmin/Strava connections. Generate
a fresh one only for an empty database.NEXT_PUBLIC_VAPID_PUBLIC_KEY /
VAPID_PRIVATE_KEY pair (npx web-push generate-vapid-keys). With either
missing, sending silently no-ops rather than erroring.npm run dev
Open http://localhost:3000.
npx vercel
Add the same environment variables in Vercel → Settings → Environment
Variables. The cron schedules in vercel.json are registered automatically on
deploy.
When pulling Vercel env vars locally, always target a temp path —
vercel env pull /tmp/mc-vercel.env --environment=production. The default destination is.env.local, which overwrites your own file.
npm run check # typecheck + tests + lint + build (run before pushing)
npm run typecheck
npm run test # vitest
npm run lint
Bump src/lib/version.ts and package.json together for each change (patch
bumps by default).
This trips up every change that touches a timestamp, so it's documented in
src/lib/utils.ts as well:
athlete_activities.start_time) store Garmin's
startTimeLocal wall-clock in a TIMESTAMPTZ column that Postgres reads as
UTC. The athlete's real local time therefore is the timestamp's UTC
wall-clock — accessors must read UTC parts, and comparing these to Date.now()
silently loses Israel's offset.israelNow() / israelToday() / israelDateAnchor().Do not try to fix this by setting a global TZ — that was tried and
reverted; TZ is deliberately unset outside of individual tests.
Further design notes live in docs/ (academy-feature.md, feed-plan.md,
run-chat-architecture.md, competitor-research.md).