Case study · Full-stack platform · Mar 2023 – Present

Flozable

A social study platform with a web app, a React Native app, and a real-time backend. I co-founded it in 2023, wrote most of the original backend, rewrote it alone in TypeScript, and rebuilt the product in 2026 with one collaborator. It had 160+ registered users by July 2025.

The Flozable home page, with a focus timer preview under the headline “A little focus. A lot of possibility.”
Users
163 registered accounts, excluding bots (July 2025 snapshot)
TypeScript API
~10K lines, 19 Prisma models, written alone
2026 rebuild (shared)
25 Postgres tables, all under row-level security
Rebuild tests (shared)
300+ passing Vitest tests

Overview

Flozable started in 2023 as a social study platform: study plans, friends and groups, real-time chat, leaderboards, and live audio and video rooms, on the web and on mobile. In May 2025 I rewrote the JavaScript backend in TypeScript. In 2026 we rebuilt the product as a calmer focus-study app on a much smaller stack, retiring the separate servers to cut infrastructure and hosting overhead.

My role

I’m a co-founder. I wrote most of the original JavaScript backend in a small team, then rewrote it alone in TypeScript. I built the React Native app and deployed the API on AWS EC2 with Ubuntu, Nginx, and PM2. The 2026 rebuild was shared with one collaborator: I wrote the shared-timer functions, hook, and API route, and my collaborator wrote most of the schema, row-level security policies, and social Realtime channels.

How it works

Original architecture: separate API, cache, and media servers
  1. ClientsWeb app and React Native app
  2. Express APITypeScript, Joi validation, per-IP rate limit; EC2, Nginx, PM2
  3. Socket.IOChat namespace and a media-signaling namespace
  4. DataPrisma and MySQL, Redis for sessions and rankings
  5. mediasoup SFUOne router per study group
2026 rebuild: one app on managed services
  1. Next.jsGuest mode runs in the browser on IndexedDB
  2. SupabaseAuth, Realtime, and Postgres RPCs
  3. PostgresRow-level security on every table

Engineering decisions

Leaderboards that reset at each user’s midnight

Problem
Daily, weekly, and monthly rankings have to roll over at local midnight in every timezone, and reading them has to stay fast.
Approach
When a study session stops, the API increments a Redis sorted set for each of 24 UTC offsets and 3 time windows. Reads use pipelined ZSCORE and ZREVRANK. An hourly cron finds the timezone that just reached midnight, snapshots its boards to MySQL under a unique (date, timezone, mode) constraint, and deletes the live key.
Trade-off
Each stop costs 72 Redis writes in exchange for cheap reads. The design covers whole-hour offsets only, and the live boards can’t be rebuilt from MySQL if Redis is lost. An append-only session log would fix the second problem.

One session model for HTTP and sockets

Problem
REST calls, the chat socket, and the media socket all need the same identity, and a session has to be revocable.
Approach
Login issues an opaque token stored in Redis with a 30-day expiry instead of a JWT, sent in an httpOnly, secure, SameSite=Strict cookie. Active sessions are extended so they keep at least 10 days. Express middleware and both Socket.IO namespaces resolve the token through the same lookup.
Trade-off
Every request pays for a Redis lookup. In return, deleting a session takes effect immediately, which stateless tokens can’t do without a revocation list.

Live study rooms on a media server

Problem
Group audio and video gets expensive quickly over peer-to-peer connections.
Approach
I ran a mediasoup SFU with one router per study group, Opus and VP8, and signaling over a dedicated Socket.IO namespace. Joining a room checks both the session token and group membership.
Trade-off
Room state lives in process memory, so the service runs as a single process. That was fine at our size, but it wouldn’t scale horizontally without shared state.

Moving real data to a new schema

Problem
Adopting Prisma meant carrying accounts and study history across without forcing anyone to reset a password.
Approach
I wrote a migration service that streams the MariaDB dump, rewrites rows to fit the new schema, and bulk-inserts with INSERT IGNORE. Redis keys are renamed with SCAN and pipelines. Old PBKDF2 hashes upgrade to bcrypt the next time each user logs in.
Trade-off
Lazy rehashing means accounts that never log in keep the old hash. I accepted that because the alternative was forcing a reset on everyone.

One timer across devices (2026 rebuild)

Problem
A timer can be open on two devices, and both can try to save the same study session.
Approach
Postgres RPCs lock the timer row with SELECT … FOR UPDATE, compare a revision number, and save the session in the same transaction, so stale or duplicate stops are rejected.
Trade-off
Some business logic now lives in SQL, where it’s harder to test from the app. A dedicated SQL test script checks stale stops, duplicate stops, and cross-account isolation.

Testing

v1 had no automated tests, which made every change to it risky. The rebuild tests as it goes, with 300+ passing Vitest tests across components, hooks, and data logic, plus SQL test scripts for the database functions.

What I’d do next

  • Run the test suite in CI on every push, which the project doesn’t do yet.
  • Instrument signups and retention so product decisions have numbers behind them.