Skip to main content

6 min read #react #supabase #pwa #typescript #chartjs

Mood Tracker: one slider per day, Supabase RLS, and the discipline of not shipping features

A tracking app that asks you a single 1–10 question. Notes on Supabase Row-Level Security, PWA install prompts, and why streaks are honest tools or engagement bait depending on how you build them.

Mood Tracker is the smallest useful daily-log app I could justify building. One slider, one to ten, once a day. The rest of the surface area exists to keep that one input honest: Supabase for auth and storage, Chart.js for trends, a streak counter, CSV export, dark mode, and a PWA install path so it feels like the icons next to it on a phone home screen. Repo: github.com/aelmufti/mood-tracker. Live demo: mood-tracker-ac8d8.web.app.

Why a single-number rating

Multi-axis mood scales — separate sliders for energy, anxiety, focus, sleep — are more scientifically defensible. They are also the reason every mood-tracking app I have tried got abandoned by week three. Each extra input is a chance for the user to skip the day entirely. A single slider has no answer you can refuse to give.

The number is imprecise, but it is consistently imprecise. Comparing today to last Tuesday is meaningful because both days went through the same one-slider funnel. That consistency is worth more than per-axis fidelity for the actual use case, which is "look back over a quarter and ask whether something was off".

Picking Supabase over Firebase

Boîte à Livre, an earlier project of mine, used Firebase Realtime Database and that worked fine. For Mood Tracker I wanted a relational shape (one row per user per day, indexed by date) and proper SQL. Supabase is Postgres-as-a-service with auth and an auto-generated REST/RPC layer on top. The Postgres part is the part I actually wanted; the auth and API are bonuses.

The single feature that paid for the migration was Row-Level Security. The policy is one line per table: using (auth.uid() = user_id) with check (auth.uid() = user_id). That clause is the actual authorization boundary. The client SDK can ask Postgres for "all moods", and the database will only return the caller's rows. You cannot accidentally leak another user's data by writing the wrong filter in the client, because the database refuses to return it.

Compare that to Firebase rules, which are a separate JSON DSL you maintain out-of-band from your data model. RLS lives next to the table it protects and uses the same language you query with. For a one-developer project that is a meaningful reduction in places-where-things-can-go-wrong.

The PWA install path, and when not to nag

PWAs can prompt their own install via the beforeinstallprompt event. Most apps that catch this event immediately show a banner on the user's first visit. That is the wrong instinct. The user has not yet decided whether your app is worth a home-screen slot.

Mood Tracker waits until the user has logged at least three days before surfacing the install affordance, and then it is a quiet line in the settings panel, not a modal. The conversion rate is lower than a banner would deliver, but the ones who do install are people who have actually used the thing. That feels like the honest number.

Streaks: honest tool or engagement bait?

A consecutive-days streak is one of the oldest patterns in habit apps and one of the most criticized. The criticism is usually that streaks turn a low-stakes habit into a high- stakes commitment, and that the fear of breaking a long streak becomes the reason for the entry rather than the actual introspection.

I kept the streak counter, but I made two choices that I think keep it on the right side of that line:

  • The streak does not appear on the input screen. You see it on the stats page. The act of logging your day should not be coloured by "do not break the streak".
  • There is no penalty for missing a day. The streak resets to one, not to zero, and there is no paywall/notification/"keep your streak" pressure. It is a piece of information, not a leash.

CSV export, because lock-in is not a feature

The export button dumps every row the current user owns into a flat CSV. No date filters, no premium tier, no API key. If you decide you would rather use Notion or a spreadsheet, you can leave and take your data with you. This is the cheapest possible respect-the-user feature and most apps still do not do it.

What is missing on purpose

No notifications, no notes field, no tags, no multi-axis ratings, no social sharing, no AI summaries. I have an opinion about each of those and the opinion is "this app is not better for adding it." A 12-second daily interaction does not need a roadmap. The discipline of not shipping the next feature is, more than anything, what kept the app usable for me over months instead of weeks.