WAYPOINT
One dashboard. Every goal.
- Role
- Solo build
- Type
- Personal · Live
- Systems
- React · Supabase · IndexedDB · Google Drive · Capacitor
- Scale
- 3 interchangeable storage backends
Objective
Most note apps hand you a blank page and a thousand templates. Waypoint has three things. Tracks are subjects. Topics are units inside a track. Tasks are the next actions, with a priority and a due date. Everything rolls up into one Focus Now list, ranked by what is overdue, what is due soon and what matters most, across every active track.
There is no backend Waypoint requires. Open it as a guest and the data stays in IndexedDB. Connect your own Supabase project for an account that works across devices. Or sign in with Google and the data becomes one JSON file in a Waypoint folder in your Drive, under the drive.file scope, so the app cannot see anything else there. A guest's data moves into a real account in one click.
Around that core: a rich-text notebook with images, audio and link previews, and [[links]] between notes, tasks, topics and tracks, with backlinks. Flashcards written as "question :: answer" lines. Repeating tasks. Spaced revisit that books reviews when a topic is finished. A review page with streaks and an activity grid. A focus timer that logs time against a task. Full-text search. A quick-add box that reads dates, repeats and priority out of a sentence and drops the rest in an Inbox. Tasks can be moved between topics and tracks.
Tracks can be written as Markdown and imported in one go, or exported back out. A JSON backup downloads from Settings and restores into any account. It is an installable web app, and there is an Android app (Capacitor) available as a direct download.
Why it is hard: three genuinely different storage models, a relational database with row-level security, a browser store, and a file in someone else's Drive, have to look identical to every page. None of them may leak its shape into the UI.
Decisions
One Backend interface across three storage models
Guest (IndexedDB), Supabase (Postgres with row-level security) and Google Drive (one JSON file) all implement the same Backend interface. No page or hook branches on which one is active. Moving from guest to a real account copies the data across and leaves the original alone.
Features written once, above the interface
Search, the review page, streaks, export and link resolution are pure functions over one snapshot() call, not a query per backend. Adding one is a client-side change. The price is that Supabase reads the whole account to answer them, which is fine at personal scale and is capped at 50,000 rows.
Cards come from notes, not a card editor
A line written as "question :: answer" in any note becomes a flashcard when the note saves. Making cards by hand is the step people quit on. Cards are matched on the question, so rewording an answer keeps its schedule. A session holds at most 20 cards, so a backlog waits instead of becoming a debt.
Spaced revisit that follows how well I know it
Finishing every task in a topic books review tasks at 3, 7 and 21 days. Rate the topic shaky and they come at 1, 3 and 7. Rate it solid and they come at 7 and 30. Ticking everything off says I did it, not that it stuck. At most 12 reviews stay open per track, so finishing a big roadmap does not bury me.
Focus Now as one ranked list, not per-track lists
Overdue first, then due within two days, then by priority, by due date inside each tier. "What should I do right now" is a better question than reading a dozen to-do lists.
Markdown import as a first-class input
A whole track can be written as `# Track / ## Topic / - [ ] Task #priority:high #due:2026-09-10 #repeat:weekly` and imported in one go, with a preview and non-fatal warnings for anything it cannot parse. Export writes the same format back out, so a track round-trips.
A stateless token relay, not a stored session
Google Drive sign-in needs a client secret exchanged server-side, and Drive-only users have no database of mine. A Supabase Edge Function relays the code and refresh exchange and forgets it. The refresh token goes straight back to the browser. Nothing is stored that could leak.
Architecture
Backend abstraction
Page / hook → Backend interface → Guest (IndexedDB) → Supabase (Postgres + RLS) → Google Drive (one JSON file)
- Every Supabase row is scoped to auth.uid() by row-level security, so a user only ever sees their own data.
- The trigger and counter functions are not callable through the API, so one user cannot touch another's progress counters.
- The Drive backend cannot touch anything outside its own folder. The drive.file scope enforces that, not application code.
Whole-account features
Search / Review / Export / Restore → snapshot() → Pure functions → Rendered result
- Backups are validated field by field on the way in and restored as new rows under fresh ids, so a restore can never overwrite or collide with existing data.
Notes to links and cards
Note saved → [[links]] resolved by name → question :: answer lines parsed → Cards synced to the note → Daily session, capped at 20
- Deleting a note deletes the cards that came from it. A link to nothing becomes a note of that name when opened.
Result
A live app that covers the whole loop: pick a storage backend at no cost, track real work across several subjects, take notes that link to each other, turn them into flashcards, and look back at what actually got done. Guest, Supabase and Drive all work through the same interface. The live database was migrated in place from its first schema with the existing data intact.
By the numbers
Stack
React, TypeScript, Vite, Tailwind CSS, Supabase, TanStack Query, TipTap, Capacitor
Lessons learned
- I built email and web-push reminders and then removed them. Only one of the three backends could ever send one, and a toggle that works for a third of users is worse than no toggle. I'd ask what every storage option can honestly support before building a feature that only one can.
- Google Drive sign-in needed its own OAuth setup and a relay to keep the secret off the browser. I'd settle who holds the secret before writing any client-side auth code. Retrofitting a relay is more disruptive than starting with the constraint.
- The live database was still on the first schema when I came back to it. The migration was safe to re-run, but I read what it would drop before running it. A setup script that is idempotent still deserves a look at its destructive lines.
- The UI had a deliberate quieting pass after the features were built. I'd schedule that pass as normal work, not as a reaction to clutter.
