← Selected work

Case Study

Johny Memo — a personal OS

Tasks, notes and expenses in one private place. I designed the interface first, then built the API, the database and the auth underneath it.

Design + full-stackSolo2026Live in production

Johny Memo — the sign-in page and workspace

What it's for

I wanted one place for the three lists I was keeping separately — tasks, notes, and money going in and out — and I wanted it to feel like mine: fast, private, and not a repurposed spreadsheet. Johny Memo is that place.

It's a solo project, in every sense: I drew the interface and the flows first, in a design tool, before writing a line of the backend. Everything it's built with — the sync, the auth, the database policies — exists to serve that interface, not the other way around.

What it had to survive

Four things shaped every decision below more than any single feature request did:

  • One person. No team to review a decision — every trade-off had to be one I could live with alone.
  • Free-tier infrastructure. Supabase and Cloudflare Pages, not a budget for anything heavier.
  • A daily driver, on a phone. It had to stay usable on a slow or dropped connection, because that's when I actually reach for it.
  • Real data. My own tasks, notes and finances — not a demo dataset — so privacy had to be structural, not just promised in a privacy policy nobody reads.

Built three times over

This is a learning project first. Some of it was built with AI-assisted ("vibe") coding, but reviewed and reworked at each stage rather than accepted as-is — each rewrite below was a deliberate step to learn the layer the last version was hiding, not a restart out of confusion.

v1

Static HTML

Proving the interface before anything was dynamic.

  • Pages and layout only — no state, no data, just the design translated into markup.
v2

Vanilla JavaScript

Learning the DOM directly before reaching for a framework.

  • Added interactivity and local state by hand — the parts a framework would normally hide.
v3

Next.js

Moved to a framework to learn its conventions.

  • Traded hand-written wiring for routing, data fetching and a file-based structure — to see what problems a framework actually solves.
v4

React

Simplified back down once the extra machinery stopped paying for itself.

  • Kept the framework, dropped the server-rendering layer a single-user PWA doesn't need.

Four calls, and what they cost

01

Magic-link sign-in, not a password

One less thing to build and one less thing to leak. No password-reset flow, no hashing to get right on day one — Supabase Auth handles the email round-trip.

Trade-offEvery sign-in needs a live inbox. There's no offline-first login, and a slow email provider is a slow login.

02

Row Level Security in Postgres, not an if in the API

Data isolation lives at the database layer, so a bug in my application code can't leak one person's tasks into another's response — the database refuses the query before it gets that far.

Trade-offEvery table needs its policy written and tested up front. A missing policy fails closed — it returns nothing, not an error — which reads exactly like an empty state.

03

Optimistic UI with undo, not a confirm dialog

Checking a task or deleting a note lands immediately, and only rolls back if the server disagrees. A confirm dialog protects against the same rare mistake on every single delete; undo pays that cost once, only when it's actually needed.

Trade-offThe UI can show a state the server hasn't accepted yet. Rolling back a rejected write without it feeling like a glitch took more care than a confirm dialog would have.

04

A PWA, not a native app

One codebase, installable straight from the browser, no app-store review cycle to ship a fix. For a tool only I use, that loop matters more than native APIs I'm not reaching for anyway.

Trade-offNo home-screen presence unless I remember to add it, and background sync is weaker than what a native app gets for free.

Two things that actually broke

A fallback link that didn't

Testing turned this one up myself: after signing in through LINE, the app didn't forward to the dashboard — the fallback redirect pointed at a destination URL the system couldn't find. The fix was as plain as the cause: correcting the link so it pointed at the right place.

Messages that never landed

After wiring up the LINE Messaging API to log incoming messages as notes, the webhook received them but Supabase never saved them — for two reasons, once I dug in. The field names in the payload (line_user_id, for one) didn't match the table's columns, and a data-type mismatch sent text into a column expecting a number. Underneath both was Row Level Security: the webhook wrote from the server using the anon key, and RLS quietly rejected the insert rather than raising an error.

The fix was two-part — align the schema, and write from the server with the service_role key instead of anon, since server-side code is exactly the case RLS's anon-key policies aren't meant to authorize.

Where it landed

Johny Memo is live and in daily use — it's where my own tasks, notes and expense tracking actually live now, not a demo dataset built for a portfolio. It's deployed on Cloudflare Pages with Supabase behind it, and it's the featured project on this site.

Live at johny.siwat.me

What I'd do differently

  • Write the RLS policies and their tests before the first UI screen, not after — a failure that returns nothing instead of an error is a hard shape of bug to notice by eye.
  • Add a lightweight way to see what's still queued for sync — the optimistic UI is honest about local state, but not yet about what's still in flight to the server.
  • Keep notes like this one as I build, instead of reconstructing the reasoning after the fact.