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.

- 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
- ClientsWeb app and React Native app
- Express APITypeScript, Joi validation, per-IP rate limit; EC2, Nginx, PM2
- Socket.IOChat namespace and a media-signaling namespace
- DataPrisma and MySQL, Redis for sessions and rankings
- mediasoup SFUOne router per study group
- Next.jsGuest mode runs in the browser on IndexedDB
- SupabaseAuth, Realtime, and Postgres RPCs
- 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.