AI-powered custom workouts, compatible with your Tonal
roni.coach
Important
Not affiliated with Tonal Systems, Inc. Roni is an independent, unofficial tool that works with Tonal fitness machines. "Tonal" is a trademark of Tonal Systems, Inc., used here under nominative fair use. This project is not endorsed by, sponsored by, or associated with Tonal Systems, Inc. in any way.
Roni is an AI coaching companion for Tonal fitness machines. Connect your Tonal account, and the app reads your training history, strength scores, and workout data to program custom weekly workout plans. The coach uses Google Gemini models to select exercises, manage periodization, and push approved workouts directly to Tonal with no manual builder work. It is built on Next.js and Convex with real-time sync.
This project is open-source for two reasons: technical users who want to self-host their own copy on free-tier infrastructure, and anyone who wants to audit the code to understand exactly how their Tonal credentials and workout data are handled. The code is the answer to "are you storing my password?"
Self-host. Clone the repo, spin up a Convex deployment, set the required server secrets, and run locally or deploy to Vercel. You control the infrastructure, secrets, and data handling. Instructions are in the Self-Host Setup section below.
Operator-managed deployments. The codebase supports both a shared server-side Gemini key and per-user bring-your-own-key (BYOK) storage. Which mode is enforced is a deployment policy decision, not something the public repo can guarantee for any specific hosted instance.
- AI chat coach powered by Google Gemini with Tonal-specific tools - reads your Tonal history, programs workouts, explains decisions
- Custom weekly training plans with periodization (Building, Deload, and Testing blocks)
- Exercise selection based on your equipment, goals, and injury history
- Progressive overload tracking across sessions
- Injury and mobility constraint management
- One-click workout push directly to your Tonal - no manual entry
- Shared-key and bring-your-own-key (BYOK) support
Active, maintained by one person. This is a personal project, not a startup. Issues triaged on a best-effort basis. PRs welcome but may take time to review.
| Layer | Technology |
|---|---|
| Frontend | Next.js 16 (App Router), React 19, Tailwind CSS v4 |
| UI | shadcn/ui (Base UI), Lucide icons |
| Backend | Convex (queries, mutations, actions, real-time sync) |
| AI Coach | @convex-dev/agent with Google Gemini models |
| Auth | @convex-dev/auth (password + Resend OTP) |
| Monitoring | Sentry (web), Vercel Analytics |
| Deployment | Vercel (web), Convex (backend) |
- Node.js 22 (matches
.nvmrc; Node.js 20+ should also work) - npm
- A Convex account (free tier works)
- A Google AI Studio API key for the server-side Gemini integration
- A Resend account + API key (optional - only needed for password reset OTP emails)
- A Tonal account to test the integration end-to-end
git clone https://github.com/JeffOtano/roni.git roni
cd roni
npm install
npm run setup # interactive: bootstraps Convex, generates secrets, prompts for optional integrations
# In two separate terminals:
npx convex dev # terminal 1
npm run dev # terminal 2
# Open http://localhost:3000npm run setup walks you through Convex deployment creation, generates TOKEN_ENCRYPTION_KEY, EMAIL_CHANGE_CODE_PEPPER, and the JWT keypair, and prompts for optional integrations (Resend, Discord webhooks, PostHog, Sentry). It is safe to re-run - existing values are preserved unless you choose to overwrite them.
By default, self-hosted deployments start with analytics, Sentry, and the public contact form disabled. Those integrations are opt-in and can be enabled during npm run setup or by setting the variables below.
| Variable | Description |
|---|---|
GOOGLE_GENERATIVE_AI_API_KEY |
Google AI Studio API key. Used for the shared Gemini key and embeddings |
AUTH_RESEND_KEY |
Optional Resend API key (re_...). Sends password-reset OTP emails |
TOKEN_ENCRYPTION_KEY |
64-char hex string. Encrypts Tonal OAuth tokens and BYOK Gemini keys |
EMAIL_CHANGE_CODE_PEPPER |
64-char hex string. HMAC pepper for email-change verification codes |
DISCORD_CONTACT_WEBHOOK |
Optional Discord webhook for the public /contact form |
DISCORD_WEBHOOK_URL |
Optional Discord webhook for operator notifications |
POSTHOG_PROJECT_TOKEN |
Optional PostHog project token for server-side product analytics |
PHOENIX_API_KEY |
Optional Phoenix Cloud API key. Enables AI conversation/eval tracing |
PHOENIX_COLLECTOR_ENDPOINT |
Optional Phoenix OTLP collector URL. Defaults to https://app.phoenix.arize.com |
PHOENIX_PROJECT_NAME |
Optional Phoenix project name for trace segmentation. Defaults to roni-coach |
PHOENIX_HOST |
Optional alternate Phoenix host. PHOENIX_COLLECTOR_ENDPOINT takes precedence |
BYOK_DISABLED |
Optional kill switch that forces all users onto the shared Gemini key |
TOKEN_ENCRYPTION_KEY_OLD |
Optional old key used only during encryption-key rotation |
DISABLE_CRONS |
Optional true to silence all cron jobs. Useful on dev deployments |
CONVEX_SITE_URL |
Set automatically by Convex. Do not set manually |
| Variable | Description |
|---|---|
CONVEX_DEPLOYMENT |
Written automatically by npx convex dev. Do not edit |
NEXT_PUBLIC_CONVEX_URL |
Convex deployment URL (https://<name>.convex.cloud). Written automatically |
NEXT_PUBLIC_GITHUB_REPO_URL |
Optional public GitHub repo URL. Enables the OSS banner |
NEXT_PUBLIC_CONTACT_FORM_ENABLED |
Optional true flag that enables the public /contact form UI |
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN |
Optional PostHog token for browser analytics |
NEXT_PUBLIC_POSTHOG_HOST |
Optional PostHog host. Defaults to /ingest |
NEXT_PUBLIC_SENTRY_DSN |
Optional Sentry DSN. Browser/server error reporting is off unless set |
NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE |
Optional trace sample rate from 0 to 1 |
NEXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE |
Optional session replay sample rate from 0 to 1 |
NEXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE |
Optional replay-on-error sample rate from 0 to 1 |
Set these in Vercel project settings (or your deployment platform). The first three are only needed if you want Sentry source-map uploads during production builds. SENTRY_TRACES_SAMPLE_RATE is read at runtime by the Next.js server and edge Sentry instrumentation.
| Variable | Description |
|---|---|
SENTRY_AUTH_TOKEN |
Build-time. Sentry auth token used by the Next.js build plugin |
SENTRY_ORG |
Build-time. Sentry organization slug |
SENTRY_PROJECT |
Build-time. Sentry project slug |
SENTRY_TRACES_SAMPLE_RATE |
Runtime. Optional server/edge trace sample rate from 0 to 1 |
convex/ Backend (Convex)
ai/ AI coach agent, tool definitions, context builder
coach/ Programming engine - exercise selection, periodization, progressive overload
tonal/ Tonal API integration - OAuth, encrypted tokens, proxy with caching
schema.ts Full data model
crons.ts Scheduled jobs (token refresh, cache refresh, data retention)
src/
app/ Next.js pages (App Router)
(app)/ Authenticated routes - dashboard, chat, schedule, stats, progress, strength, profile, settings, activity, check-ins, exercises
connect-tonal/ Tonal OAuth connection flow
login/ Auth pages
onboarding/ New user onboarding (connect, preferences, optional Gemini key step)
workouts/ Public workout library (SEO)
features/ Public marketing routes
components/ Shared React components
lib/ Shared TypeScript types and utilities
scripts/ Build and CI helper scripts
| Command | Description |
|---|---|
npm run dev |
Start Next.js dev server (port 3000) |
npx convex dev |
Start Convex dev backend with hot reload |
npm run typecheck |
Type check with tsc --noEmit |
npm test |
Run all tests once |
npx vitest --project backend |
Backend tests only |
npx vitest --project frontend |
Frontend tests only |
npm run test:watch |
Run tests in watch mode |
npm run test:coverage |
Run tests with coverage report |
npm run test:e2e |
Run Playwright smoke tests |
npm run build |
Production build |
npm run lint |
ESLint |
npm run format |
Prettier (write) |
npm run format:check |
Prettier (check only) |
npm run knip |
Dead code detection |
npm run ai:dataset |
Sync shared eval scenarios to a Phoenix dataset |
npm run ai:eval:smoke |
Run prompt-only smoke evals against Phoenix thresholds |
Full AI traces (user messages, system instructions, training snapshots, tool args/results, assistant outputs, token/latency metadata) stream to Phoenix Cloud via OpenTelemetry. Set PHOENIX_API_KEY in the Convex backend to enable capture. Product analytics continue to flow to PostHog. See docs/trust-model.md for what data leaves the backend and what is scrubbed before it does.
aiRun.runIdmatches the Phoenix trace id for each user turn so dashboards can join on it.aiToolCallsstores per-tool args and a bounded result preview for post-hoc tool analysis.- Eval scenarios live in
convex/ai/evalScenarios.tsand drive both localvitestruns and the Phoenix smoke suite underscripts/evals/phoenix/.
Two workflows exercise the coach and need repo secrets configured under Settings → Secrets and variables → Actions:
ci.yml→AI prompt smoke evalsjob (runs on every PR)ai-evals-nightly.yml→Phoenix agent experimentjob (runs nightly at 04:17 UTC)
| Secret | Required? | Notes |
|---|---|---|
GOOGLE_GENERATIVE_AI_API_KEY |
Required for real runs | Smoke script soft-fails (exit 0) when missing so fork PRs without access to secrets don't block merges. |
PHOENIX_API_KEY |
Required for traces to reach Phoenix Cloud | Nightly experiment fails hard without it. |
PHOENIX_COLLECTOR_ENDPOINT |
Optional | Defaults to https://app.phoenix.arize.com. |
PHOENIX_PROJECT_NAME |
Optional | Defaults to roni-coach; segments traces in the Phoenix UI. |
PHOENIX_HOST |
Optional | Alternate host; PHOENIX_COLLECTOR_ENDPOINT takes precedence when both are set. |
Fork PRs inherit no secrets by design. The smoke job detects a missing GOOGLE_GENERATIVE_AI_API_KEY and skips gracefully; the nightly workflow runs only on the base repo, so fork contributors don't need to worry about it.
Vitest with two projects: backend (Node environment, convex/**/*.test.ts) and frontend (jsdom, src/**/*.test.{ts,tsx}). Test files are co-located next to source files.
npm test # all tests
npx vitest --project backend # backend only
npx vitest --project frontend # frontend only
npx vitest run convex/stats.test.ts # single file
npm run test:e2e # Playwright smoke testsCoverage thresholds are enforced in CI. Lint is also treated as a hard gate: warnings fail CI.
npx convex deploy --cmd 'npm run build'
This is the build-command pattern used for Vercel deployments: deploy the Convex backend first, then run the Next.js production build.
Setup:
- Connect the GitHub repo to a Vercel project
- Set the following environment variables in Vercel project settings:
CONVEX_DEPLOY_KEY- get from Convex dashboard (Settings > Deploy keys)NEXT_PUBLIC_CONVEX_URL- your production Convex URL (https://<name>.convex.cloud)NEXT_PUBLIC_GITHUB_REPO_URL- optional, enables the OSS banner in productionNEXT_PUBLIC_CONTACT_FORM_ENABLED- optional, set totrueonly ifDISCORD_CONTACT_WEBHOOKis configured- PostHog variables if using analytics (
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN,NEXT_PUBLIC_POSTHOG_HOST) - Sentry variables if using error tracking (
NEXT_PUBLIC_SENTRY_DSN,SENTRY_AUTH_TOKEN,SENTRY_ORG,SENTRY_PROJECT)
- Set production secrets in the Convex dashboard (same keys as the env table above, with production values)
- Push to
main- Vercel auto-deploys on every push
npx convex deployTonal API --> [encrypted tokens] --> Convex proxy/cache layer --> Convex DB
|
v
User (chat) --> send message --> AI Coach Agent (Gemini, tool-driven) --> reads context
|
creates workoutPlans (draft)
|
user approves --> push to Tonal API
The coach uses @convex-dev/agent with Google Gemini models. In the current codebase the primary model is gemini-3-flash-preview, the fallback model is gemini-2.5-flash, and embeddings use the server-side Google AI key. Tool-driven capabilities include:
- Read Tonal training history, strength scores, and workout data
- Create and modify weekly workout plans with periodization
- Select exercises based on equipment and training goals
- Manage goals, injuries, and training preferences
- Push approved workouts directly to Tonal
Shared key + BYOK: The Gemini provider is resolved per request. The repo supports both a shared server-side key and encrypted per-user BYOK storage. Whether BYOK is required is controlled by deployment policy in convex/byok.ts; failed BYOK requests error explicitly instead of silently falling back.
- OAuth tokens encrypted with AES-256 at rest
- Cron refreshes expiring tokens every 30 minutes
withTokenRetrypattern: try with current token, on 401 refresh and retry once- Proxy layer with stale-while-revalidate caching to minimize API calls
- Circuit breaker pattern for API health tracking
| Schedule | Job |
|---|---|
| Every 15 minutes | Recover stuck workout pushes |
| Every 15 minutes | Health check (expired tokens, stuck pushes, circuit state) |
| Every 30 minutes | Refresh Tonal tokens |
| Every 30 minutes | Refresh active-user cache |
| Every 1 hour | Activation checks |
| Every 6 hours | Check-in evaluation (missed sessions, milestones) |
| Every 6 hours | Garbage-collect orphaned chat-image storage |
Cron 0 3 * * * |
Sync movement catalog |
Cron 0 4 * * 0 |
Sync Tonal workout catalog |
Cron 0 2 * * 0 |
Data retention cleanup |
This project is free. Hosting and my time are not. If it's saved you work, consider chipping in. No pressure.
- CONTRIBUTING.md for setup, coding standards, and PR expectations
- CODE_OF_CONDUCT.md for community expectations
- SUPPORT.md for where to ask for help or report bugs
- SECURITY.md for private security reporting
- CHANGELOG.md for notable changes
- RELEASING.md for the maintainer release workflow
See SECURITY.md for private reporting and docs/trust-model.md for the data-handling trust model.
MIT. See LICENSE.

