BikeControl (SwiftControl)
Use Zwift Click, Zwift Play, Shimano Di2, or any Bluetooth controller with MyWhoosh, Rouvy, Zwift, and more. Free to try on iOS, Android, Windows, and macOS.
A disciplined training log for jiu-jitsu.
Translation pending
Sports & fitnessTypeScript · PLpgSQLGitHub @christianshields100OnlineOwner 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
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A BJJ training log. Log what you rolled, watch your mat time and submissions trend, get feedback from an AI coach, follow training partners and see what they're doing.
Built for one user first, but multi-user from day one.
avatars bucket for profile photos)@supabase/ssr for browser / server / middleware clientsnpm test) — see lib/stats.test.tsnpm install
cp .env.local.example .env.local # then fill in your Supabase URL + anon key
npm run dev
Then open http://localhost:3000.
NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your publishable / anon key>
NEXT_PUBLIC_SITE_URL=http://localhost:3000 # used by magic-link + WHOOP redirects
ANTHROPIC_API_KEY=sk-ant-... # server-only — powers the Coach chatbot
# Optional integrations (each feature no-ops when its vars are unset):
WHOOP_CLIENT_ID=... # WHOOP OAuth app (developer-dashboard.whoop.com)
WHOOP_CLIENT_SECRET=... # also verifies webhook signatures
NEXT_PUBLIC_SENTRY_DSN=... # error monitoring
YOUTUBE_API_KEY=... # YouTube Data API v3 — study video shelves + Coach find_videos
Create an app at developer-dashboard.whoop.com with scopes
read:recovery read:cycles read:workout read:sleep read:profile offline, set
the redirect URL to https://<your-domain>/api/whoop/callback, and (optionally)
the webhook URL to /api/whoop/webhook. Put the client id/secret in the env
vars above. Users then hit Connect WHOOP in Settings. What it powers:
strain/HR/calories on your own session cards; a recovery-vs-feel and
strain-vs-mat-time chart on the dashboard; a Coach get_whoop_data tool
("should I train today?"); auto-draft nudges when WHOOP logs a jiu-jitsu
workout you haven't; and recovery context in the weekly recap. Health data is
owner-only — followers never see it.
(Gym autocomplete uses a free OpenStreetMap geocoder — no key required.)
Docs live at /developers. Users mint
bearer keys (frk_…, shown once, sha256-stored) in Settings → Developer,
optionally with write scope. Endpoints under /api/v1: me, sessions
(GET list / GET by id / POST), stats. Auth, scopes, and a 1,000 req/day
quota are enforced inside SECURITY DEFINER RPCs (api_authenticate +
api_get_*/api_create_session), so every query is scoped to the key's
owner — the route handlers only ever hold the anon client. CORS is open
(*) since keys are the credential.
Set YOUTUBE_API_KEY (Google Cloud → enable YouTube Data API v3 → create
an API key — the default free quota needs no billing account). The dashboard
then grows a Study section: escape videos for the submission that catches
you most, detail videos for your best weapon. Coach also gains a
find_videos tool. Searches are cached in study_cache for 7 days (shared
across users) to stay far under the 100-searches/day free quota.
supabase_schema.sql —
it's idempotent, you can re-run it./log — log a session: date, duration, rounds, gym, what you drilled,
submissions hit / caught in, training partners, a 1-5 "how it felt", a
free-text note. Submissions and partners use a chip autocomplete seeded from
a canonical list plus your own past entries, so names stay normalized. The
gym uses a map-search autocomplete (GymPicker, backed by the free
OpenStreetMap/Photon geocoder via /api/gyms/search) and stores the place's
OSM id so gyms are standardized for cross-gym analytics. It asks for your
location (best-effort) to rank nearby academies first, and there's always a
"Use '…'" option so gyms not on the map can be saved as free text. Sessions
can carry up to 6 photos/videos (uploaded straight to the public
session-media bucket, RLS-scoped to your own folder) that render on the
session card in the feed and on profiles. /log?edit=<id> reuses the same
form./dashboard — streak, lifetime totals, a Daily / Weekly / Monthly toggle
over the mat-time, rounds, and feel-vs-volume charts (last 14 days / 8 weeks /
6 months), a submission report (Hit / Caught-in / Net toggle — net ranks
your sharpest finishes vs. the submissions that keep catching you, with a
sharpest-finish & nemesis caption), top training partners,
searchable notes with inline edit / delete, and a Coach-generated weekly recap./feed — find training partners by name, follow / unfollow, see their
recent sessions. Instagram-style privacy: toggle your account public or
private. Public = anyone can follow you instantly. Private = follows become
requests you Accept / Decline; existing followers keep access; you can Remove
followers. A pending request grants no visibility. Profiles stay discoverable
in search (so people can request), but a private account's sessions are
hidden from everyone who isn't an accepted follower — enforced in Postgres
RLS, not app code. Every session — in the feed and on profile pages — has
reactions (a small mat-flavoured palette) and a comment thread; who
can react/comment is gated by the same visibility rule via a
can_view_session() SECURITY DEFINER helper reused in the reaction/comment
RLS policies. The shared lib/social.ts fetchSessionSocial() helper loads
and shapes that data for both surfaces./u/[id] — profile pages. Avatar (upload your own photo to Supabase
Storage, or a generated monogram until you do), full name (first + last)
as the heading with @username as the handle underneath, belt rank, clickable
follower / following counts that open and
list pages, session/mat-time/rounds stats, and recent sessions. Reach your
own profile from the header avatar. A private account viewed by a non-follower
shows a locked state and locked lists — enforced by RLS + SECURITY DEFINER
functions, not page code.These are out of scope so we can ship. Tracked for later:
/dashboard, /log, /feed,
/chat, /u, /settings refreshes the session and gates unauthenticated
users back to /login. The same middleware runs the onboarding gate: a
signed-in user whose onboarded flag is false is redirected to /welcome,
and an already-onboarded user who lands on /welcome is bounced to the
dashboard.follows.status = 'accepted' row). Profiles are readable by all
authenticated users (needed for discovery/search). A DB trigger sets a new
follow's status from the target's is_private flag — pending for private,
accepted for public — so a follower can't smuggle in status='accepted'
against a private account. The followee can accept a request or remove a
follower via their own update/delete policies. Inserts/deletes are
ownership-scoped.auth.users creates a public.profiles row on signup,
pulling display_name from OAuth metadata or the email local-part.logSession, updateSession,
deleteSession, follow, unfollow, clearConversation) so we get the
cookie-bound Supabase client and automatic revalidation.lib/stats.ts as pure functions with an injectable
"today", so they're unit-tested in lib/stats.test.ts and the dashboard page
stays a thin server component.chat_messages and prompt-caches the system +
training-log blocks; weekly recaps are cached per (user, week) in
weekly_recaps and only regenerated when a new session lands.app/
auth/callback/ OAuth & magic-link return handler
auth/signout/ POST /auth/signout → clears session
api/chat/ Coach chatbot API (auth + log context + Claude stream)
api/recap/ Weekly recap generation (cached per user/week)
chat/ Coach chat UI + persistence
dashboard/ Charts, feel trend, ledger, partners, recap, notes
feed/ Follows + timeline
log/ Session form (create + edit) + server actions
login/ Magic-link & Google sign-in
welcome/ First-time onboarding wizard (name, DoB, belt, photo)
settings/ Edit profile (name, belt, DoB, photo, home gym)
manifest.ts PWA manifest
components/TagInput.tsx Chip autocomplete (submissions, partners)
components/BeltStripePicker.tsx Belt + stripes selector (settings & onboarding)
lib/profile.ts displayName()/hasFullName() name helpers
lib/submissions.ts Canonical submission names for autocomplete
error.tsx Global error boundary
not-found.tsx 404
loading.tsx Route-transition skeleton
components/AppShell.tsx Header + nav for signed-in routes
lib/supabase/ Browser / server client factories
lib/stats.ts Pure stat helpers (streak, weekly buckets, totals)
middleware.ts Auth refresh + route gating
supabase_schema.sql Tables, RLS, trigger
Push to GitHub, import in Vercel, set the three env vars above (with
NEXT_PUBLIC_SITE_URL pointing at the Vercel URL), and add that URL to
Supabase's allowed redirect list and to the Google OAuth client's authorized
redirect URIs.
sentry.*.config.ts,
instrumentation.ts, app/global-error.tsx) but dormant until the
NEXT_PUBLIC_SENTRY_DSN env var is set in Vercel. No DSN, no overhead..github/workflows/uptime.yml probes /, /login, and
/api/keepalive (a real DB round-trip) every 15 minutes and opens/closes a
GitHub issue on failure/recovery, which emails the repo owner.flowroll-ops repo dumps the database (schema +
data) every Monday and keeps 12 weeks of history. Needs the
SUPABASE_DB_URL secret set there once; restore instructions in its README.Free-tier Supabase projects auto-pause after ~7 days without activity, which
takes the whole site down (auth runs through Supabase). A Vercel cron
(vercel.json) pings /api/keepalive twice a week (Mon & Thu 09:00 UTC); the
endpoint runs one trivial RLS-scoped query so the project always registers
activity. Optionally set a CRON_SECRET env var in Vercel to lock the endpoint
to cron invocations.
/u/[id]/followers/following/welcome — first-time setup wizard. New sign-ups are routed here by
middleware (the onboarding gate) until they finish: a four-step flow
collecting name + date of birth, belt + stripes, home gym (feeds the
suggested-follows below), and an optional profile photo. DoB is stored but
never shown anywhere. Setting onboarded = true lets them into the rest
of the app. To beat the empty-feed cold start, the feed shows "From your
gym" follow suggestions (same standardized home-gym id) and a "log your
first session" nudge until they've logged one./settings — edit profile: first / last name, belt + stripes (the
BeltStripePicker), private date of birth, profile photo, and home gym. Names
flow through lib/profile.ts's displayName() helper, which shows the full
name everywhere a person appears (profiles, feed, follower / following lists,
the header) and falls back to @username when no name is set. Search matches
on username, first name, or last name./chat — "Coach", an AI chatbot (Claude) that answers questions about
your notes and full log history. Scope-locked to BJJ and your own data — it
declines anything off-topic. Coach is agentic: its prompt carries only a
summary (lifetime totals, all-time submission scorecard, the last few
sessions) and it pulls the rest on demand through retrieval tools —
query_sessions (date range / keyword over the full log) and
get_submission_stats (per-submission finished/caught/net/finish-rate for
any period) — via a server-side tool loop in /api/chat (lib/coach-tools.ts),
so answers like "compare my May vs June" come from real queries, not guesses.
Coach can also log a session for you: describe your roll in plain English
and it parses the fields, shows you exactly what it will save, and calls its
log_session write tool only after you confirm.
The weekly recap gets the same scorecard. Conversations persist across reloads, render
markdown, and the training-log context is prompt-cached across turns. Coach
can also web-search for instructional videos/articles (server-side
web_search tool) — locked to BJJ topics, returns real cited links. Capped at
50 messages/user/day (tamper-proof check_and_bump_chat_quota RPC) so one
account can't run up the API bill.