A self-hosted personal daily operating system. Cadence puts your habits, to-dos, calendar, training, weather, and goals into one dense, glanceable dashboard — with a Discord bot so you can log and check things from your phone without opening the app.
It's built to run on a wall-mounted tablet (designed around a Samsung Galaxy Tab A7 Lite in landscape), but it works in any browser and installs as a PWA on phones.
Single-user by design. You self-host it, it's yours, your data lives in your own Supabase project.
Cadence is a small monorepo with three independently-deployed pieces:
| Directory | What it is | Runs on |
|---|---|---|
/ (root) |
The dashboard — the Next.js web app + Discord bot + API. This is the core; the two services below are optional add-ons. | Vercel |
services/boostcamp-sync/ |
Training sync — a Python service that logs into Boostcamp, classifies each workout (Push/Pull/Legs + muscles worked), and writes it to your Supabase. Feeds the Training view. | Railway |
services/voice-scheduler/ |
Voice scheduler — a Python Discord bot: drop a voice memo in a channel → Whisper transcribes it → it becomes a calendar event via the dashboard's /api/schedule. |
Railway |
Each service has its own README and .env.example. Set up the dashboard first — the services just read/write the same Supabase project and call the dashboard's API, so they're only useful once it's running.
- What's in this repo
- Features
- How it works
- Prerequisites
- Setup
- Customize it for you
- Discord command reference
- Project structure
- Troubleshooting
- License
Dashboard — a fixed 2-column grid of auto-flipping cards, each cycling through related faces:
- Now / Weather — big clock + current conditions.
- Next Event / Next Todo / Tomorrow / This Week — your next calendar event, next due to-do, and upcoming days.
- Training — recent workout + weight trend.
- Habits — today's checklist + streaks.
- Goals / Quote — big-goal progress.
Habits — AM/PM checklists with streaks and 7-day completion, soft-delete, per-period tracking.
To-dos — custom categories you add / rename / delete right in the app (starts with a "Personal" tab), high/low priority, drag-to-reorder, and due dates that badge in the list, appear on the calendar, and escalate through your daily check-ins.
Calendar — a week strip plus a 30-day scrollable agenda from your Google Calendar, with timed to-dos overlaid on their due day.
Training — 30-day weight chart, per-day workout history, a muscle heatmap (a body map showing which muscle groups you've hit recently), PRs, and an editable weekly split target. Optionally auto-synced from Boostcamp via the boostcamp-sync service.
Weather — current conditions + a 7-day forecast (US National Weather Service, no API key needed).
Weekly snapshot — habits, training, weight delta, and tasks completed, at a glance.
Notifications — an in-app inbox. Notes you send with /note (or by voice) land here, with an unread badge in the sidebar.
Screen guard — burn-in protection for wall-mounted tablets: night dimming, idle dim, and hold-to-peek.
Discord bot — the app is fully drivable from Discord:
- Slash commands:
/habit,/todo,/weight,/note,/calendar,/help. - Tap-to-log habit reminders (AM/PM) with buttons — no typing.
- Daily check-ins (morning summary, midday, evening wrap) with a "due soon" block.
- Calendar reminders (1 h / 30 / 15 / 5 min before) with a one-tap snooze link.
- Optional voice-to-action: send a spoken/typed sentence, an LLM turns it into the right event/to-do/habit/weight/note.
┌─────────────────────────────────────────┐
Browser / PWA ───► │ Next.js 14 app (Vercel) │
(tablet, phone) │ • React dashboard + views │
│ • /api/* route handlers │
Discord ──────────►│ • /api/discord/interactions (bot) │
(commands, buttons) │ • middleware.ts (PIN gate) │
└───────────────┬─────────────────────────┘
Vercel Cron ──────────────────────►│ (scheduled /api/cron/* jobs)
▼
┌────────────────────────┐
│ Supabase (Postgres) │ ◄── your data
└────────────────────────┘
external reads: Google Calendar (iCal + API), weather.gov
- Framework: Next.js 14 (App Router), React 18, TypeScript. Some controls use Ionic React; styling is Tailwind + inline styles.
- Data: Supabase (Postgres). All DB access is server-side via route handlers using the service-role key, so keys never reach the browser and RLS locks out the public anon role.
- Auth: a single PIN (
DASHBOARD_PIN).middleware.tsgates every page/route behind a signed cookie; cron and Discord routes authenticate with secrets/signatures instead. - Scheduling: Vercel Cron hits
/api/cron/*routes on a schedule (seevercel.json). - No build step for your data: everything is read/written live from Supabase; the app polls every ~10 s.
You'll need free accounts for:
| Service | Required? | For |
|---|---|---|
| Supabase | Yes | Database |
| Vercel | Yes (to host the dashboard) | Deployment + cron |
| Discord | Recommended | Bot commands, reminders, check-ins |
| Google Cloud | Recommended | Calendar read/write |
| Anthropic | Optional | Voice-to-action parsing (Claude) — used by /api/schedule |
| OpenAI | Optional | Voice-memo transcription (Whisper) — used by the voice-scheduler service |
| Boostcamp | Optional | Source of workout data — used by the boostcamp-sync service |
| Railway | Optional | Hosts the two Python services in services/ |
Local tooling: Node.js 18+ and git — plus Python 3.11+ only if you run the services in services/.
git clone https://github.com/<you>/cadence.git
cd cadence
npm install- Create a new project at supabase.com.
- Open SQL Editor → New query, paste the entire contents of
db/schema.sql, and run it. This creates every table and enables Row Level Security. - Go to Project Settings → API and copy three values for the next step:
- Project URL →
NEXT_PUBLIC_SUPABASE_URL anonpublickey →NEXT_PUBLIC_SUPABASE_ANON_KEYservice_rolesecretkey →SUPABASE_SERVICE_ROLE_KEY(server-only — keep it secret)
- Project URL →
cp .env.example .env.localFill in at minimum the Supabase keys, a DASHBOARD_PIN, and a CRON_SECRET. Generate the secret with:
node -e "console.log(require('crypto').randomBytes(24).toString('hex'))"Every variable is documented in .env.example. You can add the Google/Discord/optional values now or come back to them.
npm run devOpen http://localhost:3000. (Locally, if DASHBOARD_PIN is unset the app stays open; once set you'll see the PIN screen.)
You should be able to add habits, to-dos, log weight, etc. — that confirms Supabase is wired up. The rest (calendar, Discord) is optional and layered on next.
Cadence uses Google Calendar two ways. You can set up either or both.
Read (show events on the dashboard + reminders):
- Google Calendar → Settings → click your calendar → Integrate calendar.
- Copy the Secret address in iCal format →
GOOGLE_CALENDAR_ICAL_URL.
Write (create/edit events from Discord /calendar and voice): this needs OAuth.
- Google Cloud Console → create a project → enable the Google Calendar API.
- APIs & Services → Credentials → Create OAuth client ID (type: Web application). Copy the client ID/secret →
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET. - Use the OAuth Playground (gear → Use your own OAuth credentials) to authorize the
https://www.googleapis.com/auth/calendarscope and exchange for a refresh token →GOOGLE_REFRESH_TOKEN. - Set
GOOGLE_CALENDAR_IDto the calendar you want to write to (your email, or a specific calendar's ID).
- Discord Developer Portal → New Application.
- General Information: copy Application ID →
DISCORD_APP_ID, and Public Key →DISCORD_PUBLIC_KEY. - Bot → Reset Token → copy →
DISCORD_BOT_TOKEN. - OAuth2 → URL Generator: scopes
bot+applications.commands, then open the generated URL to invite the bot to your server. - Create a channel webhook (Channel → Edit → Integrations → Webhooks → New) → copy URL →
DISCORD_ALERTS_WEBHOOK_URL. (Optionally a second one →DISCORD_LOGS_WEBHOOK_URL.) - After you've deployed (step 7), set the Interactions Endpoint URL (General Information) to:
https://<your-app>/api/discord/interactions— Discord will verify it live. - Register the slash commands by visiting once in your browser (logged into Vercel, or after you've turned off Vercel deployment protection):
https://<your-app>/api/discord/register?secret=<CRON_SECRET or REGISTER_SECRET>A{ "ok": true, "scope": "guild (instant)" }response means it worked. Reload Discord (Ctrl+R) to see the commands.
- Push your repo to GitHub and Import it in Vercel.
- Add every variable from your
.env.localunder Settings → Environment Variables (Production). - Deploy. Your app is live at
https://<project>.vercel.app— set that asNEXT_PUBLIC_APP_URLand redeploy.
Deployment protection: if you enable Vercel Authentication/Deployment Protection, external callers (Discord, cron pings from other services) get blocked. Cadence has its own PIN + secret auth, so you can safely leave Vercel's protection off.
The schedules live in vercel.json and register automatically on deploy. Vercel sends CRON_SECRET as the auth bearer, so no extra config is needed.
| Route | What it does |
|---|---|
/api/cron/summary |
Morning daily summary to Discord |
/api/cron/checkin |
Midday + evening check-ins (with "due soon" tasks) |
/api/cron/habits-am / habits-pm |
Tap-to-log habit reminders |
/api/cron/calendar-alert |
Fires event reminders (runs every 5 min) |
/api/cron/boostcamp-pull |
Triggers the optional training sync |
Note: sub-hourly crons (the every-5-minute
calendar-alert) require the Vercel Pro plan. On Hobby, change it to an hourly (or coarser) schedule invercel.json. Times are in UTC — adjust for your timezone.
- Voice-to-action (
/api/schedule): setANTHROPIC_API_KEYandSCHEDULE_SECRET. POST{ "text": "dentist tuesday at 2pm" }with headerx-schedule-secret; an LLM routes it to the right action. Wire it to any transcription source — or deploy the ready-madeservices/voice-scheduler/Discord bot: drop a voice memo in a channel and it transcribes and schedules it for you. - Boostcamp training sync: the companion service in
services/boostcamp-sync/logs into Boostcamp, classifies workouts, and writes them to Supabase. SetBOOSTCAMP_SYNC_URL+BOOSTCAMP_SYNC_SECREThere, then deploy that service (see step 10).
Two small Python services live in services/. Both are optional, run on Railway (each with its own Root Directory), and share your Supabase project. Set up the dashboard first — the services are only useful once it's live. Each folder has its own README and .env.example; the essentials are below.
a) Boostcamp training sync — services/boostcamp-sync/
Logs into Boostcamp, classifies each workout (Push/Pull/Legs + muscles worked), and writes it to Supabase to feed the Training view and muscle heatmap.
- Database: the required tables (
boostcamp_workouts,boostcamp_summary) are already created if you randb/schema.sql. Running the sync against a separate database instead? Useservices/boostcamp-sync/migration.sql. - Deploy on Railway from this repo with Root Directory =
services/boostcamp-sync. It bootspython server.py(seerailway.toml); enable App Sleeping and Generate Domain. - Service env vars (on Railway):
BOOSTCAMP_EMAIL,BOOSTCAMP_PASSWORD,SUPABASE_URL,SUPABASE_KEY(the service_role key),BOOSTCAMP_SYNC_SECRET(any long random string). Optional:DISCORD_WEBHOOK_URL(workout/crash alerts),TZ_OFFSET_MINUTES(default-420= US Pacific). - Wire it to the dashboard: set
BOOSTCAMP_SYNC_URL(the Railway domain) and the matchingBOOSTCAMP_SYNC_SECRETin Vercel. Theboostcamp-pullcron then triggers it on schedule.
Full walkthrough: services/boostcamp-sync/DEPLOY.md.
b) Voice scheduler — services/voice-scheduler/
A Discord bot that turns a voice memo into a scheduled item. Pipeline: voice memo → OpenAI Whisper transcribes → POST to the dashboard's /api/schedule → Claude turns it into an event / to-do / habit / weight log / note.
- Enable voice-to-action on the dashboard first (step 9): set
ANTHROPIC_API_KEYandSCHEDULE_SECRETin Vercel. - Discord: create (or reuse) a bot application; enable Message Content Intent; invite it with View Channels + Read Message History; with Developer Mode on, right-click the watched channel → Copy ID.
- Deploy on Railway with Root Directory =
services/voice-scheduler. It runs as aworker(seeProcfile) — no public port. - Service env vars (on Railway):
DISCORD_BOT_TOKEN,OPENAI_API_KEY,CHANNEL_ID,SCHEDULE_URL(https://<your-app>/api/schedule),SCHEDULE_SECRET(same value you set in Vercel).
Full walkthrough: services/voice-scheduler/README.md.
Cadence ships with the original author's defaults. Change these before (or after) going live:
| What | Where | Change to |
|---|---|---|
| Weather coordinates | set WEATHER_LAT / WEATHER_LON env vars (US only — NWS) |
your lat/lon |
| Location label | components/HomeClient.tsx, components/MobileClient.tsx, components/WeatherView.tsx |
your city name |
| Timezone | America/Los_Angeles appears throughout (lib/, app/api/cron/*, components) |
search-and-replace with your IANA timezone |
| To-do categories | managed in-app — use the + tab to add, ✎ to rename/delete. Starts seeded with "Personal" (see db/schema.sql). |
no code changes needed |
| Weekly training split | default Push/Pull/Push/Pull/Legs |
edit inline on the Training page, or the default in app/api/boostcamp/recent-body-map/route.ts |
| Cron times | vercel.json (UTC) |
your schedule |
Your habits, to-dos, goals, and weight all live in the database — add/edit them in the app, not the code.
| Command | Does |
|---|---|
/help |
List all commands |
/habit [name] [period] |
Mark a habit done; blank → list all habits with tap-to-log buttons |
/todo [text] [priority] [category] [due] |
Add a task; blank → list open tasks in two columns |
/weight <lbs> |
Log body weight |
/note <text> |
Send a note to the dashboard |
/calendar create|modify|delete |
Manage Google Calendar events |
Habit reminders and check-ins are pushed automatically by the cron jobs.
app/ Next.js routes
api/ server route handlers (habits, todos, calendar, discord, cron, …)
page.tsx app shell
components/ dashboard cards, views, flip faces
lib/ supabase, discord, google calendar, ical parser, cron/auth helpers
types/ shared TypeScript types
db/schema.sql full database schema (run once in Supabase)
middleware.ts PIN auth gate
vercel.json cron schedules
services/ optional companion services (deployed separately on Railway)
boostcamp-sync/ Python — Boostcamp → Supabase training sync
voice-scheduler/ Python — Discord voice-memo → calendar-event bot
- Dashboard is empty / everything 401s — check
SUPABASE_SERVICE_ROLE_KEYis set correctly in Vercel; that's the key the server reads with. - PIN screen won't accept my PIN — env changes need a redeploy on Vercel; make sure
DASHBOARD_PINis set for Production. - Discord commands don't appear — re-hit
/api/discord/register?secret=…, then reload the Discord client (Ctrl+R). - Discord buttons/commands 401 or time out — turn off Vercel Deployment Protection; it blocks Discord's requests.
- Calendar is blank — confirm
GOOGLE_CALENDAR_ICAL_URLis the secret iCal URL; recurring events only show on matching days. - Layout looks cramped — the dashboard is tuned for a landscape tablet (~893×533). Full-page views adapt better to phones/portrait.
MIT. This is a personal project shared as-is — no warranty, no support guarantees. PRs welcome.