From localStorage to Supabase

May 24, 2026 · blog

The first version of the app stored everything in localStorage. Challenges, tasks, progress entries — all in one JSON blob under a single key. It worked surprisingly well for solo use.

Then someone asked: "Can I check my streak from my phone?"

No. No you cannot. Because your data lives in one browser on one device.


Why localStorage Worked Initially

For prototyping, it's unbeatable:
- No backend setup
- Instant reads/writes
- No auth required (data is implicitly scoped to the browser)
- Zero latency

I had the full app running locally in a day. Create challenge, add tasks, check them off, see heatmap. All client-side. Fast iteration.


Why It Had to Go

The moment you want shared challenges — the entire point of this app — localStorage is dead. User A checks off a task on their machine. User B has no way to see that. There's no sync, no server, no shared state.

Also: clear your browser data and everything's gone. Switch browsers and you start fresh. It's fine for preferences, terrible for data you care about.


The Migration

I moved to Supabase (PostgreSQL). Six tables:

The schema uses a few tricks:
- handle_new_user() trigger auto-creates a user row when someone signs up
- handle_new_challenge() trigger auto-adds the creator as owner/member
- Unique constraints on (task_id, user_id, date) for progress — upsert instead of insert-or-check


Row Level Security

Supabase's RLS lets you define access rules at the database level:

This means even if someone crafts a malicious API call, the database rejects it. Security isn't in my app code — it's in the schema. One less thing to get wrong.


What Stayed in localStorage

Two things:
- Starred challenge ID (which challenge shows on dashboard — UI preference)
- Legacy seed data (unused but harmless, left in place)

Everything else hits Supabase directly. No local cache, no optimistic updates yet. Simple first.


The Diff

624 insertions, 355 deletions. Mostly replacing in-memory store operations with async Supabase calls. The UI barely changed — just swapped the data source underneath.

The app is now multi-device, multi-user, and persistent. Which is what it needed to be from the start — I just prototyped faster with localStorage and migrated when the real requirements kicked in.


← back to blog