Skip to content

Repository files navigation

PacePact

Social training platform for groups of friends preparing for the same endurance event. PacePact generates an AI-powered training plan for the group, automatically marks sessions complete via Strava webhooks, and keeps everyone accountable on a shared live leaderboard.

Features

  • Group training plans — Claude generates a structured plan in the background; the group is created immediately and the user is notified via the bell when the plan is ready
  • Strava auto-sync — sessions are marked complete when you log a matching Strava activity; matched by activity type and weekly schedule, not exact date
  • Brick session support — any two complementary activities (ride + run) on the same day auto-complete a brick session; a 50% progress bar shows while the first leg is pending, with a manual assign fallback
  • Live leaderboard — Supabase Realtime keeps scores updated for everyone in the group simultaneously
  • Group chat — per-group message board with real-time updates via Supabase Realtime
  • In-app notifications — real-time bell in the nav; activity confirmations are always on, message notifications are opt-in per user
  • Invite links — 8-character invite codes let friends join a group in one click
  • Group admin controls — group creator can kick + ban members, rotate the invite link, lock invites to freeze membership, and transfer admin rights to another member; all surfaced via a ••• dropdown on the group page
  • Leave group — non-creator members can leave a group at any time; creators must transfer admin rights first
  • Points system — base points per session, plus bonuses for completing early, exceeding targets, and maintaining a streak
  • Week view — training weeks show date ranges, past weeks are visually locked (green = complete, grey = missed), active week sorted to top
  • Rest day handling — rest sessions are excluded from the session grid; a note shows how many rest days are recommended for the week
  • Magic link auth — passwordless sign-in via email OTP using Resend; styled email template matches the app
  • Strava connect / disconnect — connect Strava on the profile page; disconnect deauthorizes the Strava token and clears stored credentials
  • Payments — Stripe Embedded Checkout for group creation and member cap increases; gated by NEXT_PUBLIC_PAYMENTS_ENABLED feature flag (off by default)

Tech stack

Layer Choice
Framework Next.js 15 (App Router)
Database Supabase (Postgres + Auth + Realtime)
Hosting Railway
AI Anthropic Claude (claude-sonnet-4-20250514)
External API Strava API v3 (OAuth + Webhooks)
Styling Tailwind CSS
Email Resend
Payments Stripe (Embedded Checkout)

Quick start

# 1. Clone and install
git clone https://github.com/your-org/pacepact.git
cd pacepact
npm install

# 2. Configure environment
cp .env.local.example .env.local
# Fill in all values — see docs/setup.md

# 3. Apply database schema
# Run migrations in supabase/migrations/ in your Supabase SQL editor

# 4. Start dev server
npm run dev

See docs/setup.md for the full setup walkthrough including Supabase, Strava API, and Railway deployment.

Project structure

pacepact/
├── app/
│   ├── (auth)/               # Login + signup pages (magic link / OTP)
│   ├── (app)/                # Authenticated app routes
│   │   ├── groups/           # All groups overview
│   │   ├── group/
│   │   │   ├── new/          # Create group + generate plan
│   │   │   └── [groupId]/    # Group home, full plan, members
│   │   ├── profile/          # Strava connect/disconnect, account info
│   │   └── join/[inviteCode]/# Invite link landing page
│   ├── api/
│   │   ├── auth/otp/send/    # Send magic link email via Resend
│   │   ├── strava/
│   │   │   ├── callback/     # OAuth token exchange
│   │   │   ├── disconnect/   # Deauthorize Strava + clear tokens
│   │   │   └── webhook/      # Strava webhook receiver
│   │   ├── groups/
│   │   │   ├── generate-plan/# Claude plan generation
│   │   │   └── [groupId]/    # PATCH edit/rotate invite/lock; DELETE group
│   │   │       └── members/
│   │   │           ├── me/          # DELETE leave group (non-creator members)
│   │   │           └── [userId]/    # DELETE kick+ban; PATCH transfer creator
│   │   ├── stripe/
│   │   │   ├── checkout/     # Create Stripe Checkout Session (create_group / update_members_cap)
│   │   │   └── webhook/      # Stripe webhook receiver (checkout.session.completed)
│   │   ├── activities/
│   │   │   └── assign/       # Assign parked brick leg to standalone session
│   │   ├── notifications/
│   │   │   └── read-all/     # Mark all notifications as read
│   │   └── user/
│   │       ├── profile/      # PATCH update display name + notification prefs
│   │       └── delete/       # Delete authenticated user account
│   └── auth/callback/        # Auth callback (handles PKCE + implicit flow)
├── components/
│   ├── leaderboard/          # Real-time leaderboard table
│   ├── training/             # Week view + session cards
│   ├── groups/               # Group cards, create form, actions menu, message board, plan generating banner
│   ├── notifications/        # NotificationBell — real-time bell icon with dropdown
│   ├── profile/              # Delete account, disconnect Strava, notification settings
│   └── ui/                   # Shared primitives (nav, theme toggle)
├── lib/
│   ├── supabase/             # Browser + server clients; `requireAuth()` shared auth helper
│   ├── strava/               # OAuth, webhook processing, activity matching
│   ├── claude/               # Plan generation prompt + parsing
│   ├── resend/               # Magic link email template
│   ├── stripe/               # Lazy Stripe client initialisation
│   ├── payments/             # Price calculation (members × weeks × rate)
│   ├── groups/               # Session fan-out helper, createGroup() shared fn
│   ├── points/               # Points calculation
│   └── utils/                # Week status, date formatting helpers
├── types/index.ts            # Shared TypeScript types
├── supabase/
│   ├── schema.sql            # Full DB schema + RLS policies
│   └── migrations/           # Incremental migrations (e.g. messages table)
├── docs/
│   ├── setup.md              # Full setup guide
│   └── stripe-setup.md       # Stripe keys, webhook registration, pricing, test cards

Development

npm run dev        # Start dev server on http://localhost:3000
npm run build      # Production build
npm run lint       # ESLint
npm test           # Jest unit + integration tests
npm run test:watch # Watch mode

Testing

Unit tests cover core business logic (points calculator, activity matcher, plan parser, week status, date formatting, webhook notification insertion, join-gate evaluation) plus the background plan generation route (group created immediately with plan_status: 'generating', after() scheduled, background success and failure paths). Integration tests cover API routes with Supabase mocked — Strava webhook, auth OTP, notification read-all, profile preference updates, brick leg assignment, and group admin actions (kick, transfer creator, rotate invite, lock invites).

npm test                                       # Run all tests
npm test -- --testPathPatterns=calculator      # Run specific file
npm test -- --coverage                         # With coverage report

Deployment

PacePact is designed to deploy on Railway. See docs/setup.md for the full deployment guide including Strava webhook registration.

Points system

Action Points
Complete any scheduled session +10
Session scheduled on Monday or Tuesday +2
Activity exceeds target by >10% +3
7-day streak (session every day) +5

Auth flow

PacePact uses passwordless magic link authentication:

  1. User enters their email on /login
  2. The server calls supabase.auth.admin.generateLink() and sends the link via Resend
  3. User clicks the link → /auth/callback (client component)
  4. The callback page handles both flows:
    • PKCE (?code= query param): exchanges code for session
    • Implicit (#access_token= in URL hash): calls setSession() directly — the hash is never sent to the server, so this must be handled client-side
  5. Profile is upserted, user is redirected to /groups

Strava activity matching

When a Strava webhook arrives, PacePact matches the activity to a planned session by:

  1. Finding incomplete sessions for that user scheduled in the same calendar week as the activity
  2. Filtering by activity type (run, ride, swim, etc.)
  3. Checking the activity meets ≥ 85% of any distance or duration target
  4. Matching to the earliest qualifying session in the week

Multiple activities in the same week can each match a different session — completing two runs on the same day will mark off two run sessions if both are scheduled that week.

A duplicate webhook delivery for the same activity (Strava retries on non-200, and occasionally re-emits) is treated as idempotent: groups already credited or already holding a parked leg with that strava_activity_id are skipped, so a single Strava activity can never mark off a second session.

Brick sessions

In any week that contains a pending brick session, every incoming run or ride is held in brick_activity_parts and shown as a 50% progress bar on the brick session card. There are two resolution paths:

  • Auto-complete: when a complementary leg (opposite type — ride + run) arrives on the same calendar date, stats from both legs are combined and validated against the brick target (85% threshold). The brick is marked complete automatically, and any other activities parked earlier that week are released and matched to remaining standalone sessions.
  • Manual assign: the user selects which standalone session to credit the parked leg to. If only one matching session exists in the week, a single button appears; if multiple exist, a labelled list lets the user pick the right one.

Environment variables

See .env.local.example for all required variables.

About

AI-powered group training plans for endurance events

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages