Skip to content

Project · 2025

Levelup

A private personal-growth platform with guided journeys, journaling, and goal tracking.

Role: Creator
  • Node.js
  • Express
  • SQLite
  • React
  • Vite

Project snapshot

Status
Private personal app, self-hosted for household use.
Core model
Guided journeys composed of tasks and reflection questions, with cadence-based progress tracking.
Surfaces
Guided journeys (including shared journeys for two people), daily gratitude, journal, goal tracker, photo memories, system-design and interview practice tracks, and admin-controlled feature flags.
Quality
About 105 Jest/Vitest test files across backend and frontend, plus a small Playwright e2e suite.

Technical proof

  • Backend uses ES modules with routes, services, and a BaseRepository data-access layer shared by 14 repositories.
  • Client IP resolution deliberately avoids Express's trust proxy, trusting Cloudflare's connecting-IP header only when the raw socket peer is loopback, to stop rate-limit bypass via a spoofed header.
  • Recent work added OAuth, shared journeys for two people, a system-design practice track, and a rate-limit fix after real usage patterns exhausted the original budget.

What it is

Level Up is a full-stack journaling and self-improvement app I built for personal use. The core concept is “guided journeys”: structured programs a user enrolls in and progresses through at a configurable cadence (daily, weekly, bi-weekly). Journeys ship as data, not code, so adding one is mostly writing content: a couples communication course (“A Year of Conversations”), a behavioral interview prep track, and a system-design interview practice track are all live, with more added as JSON journey definitions and curricula. A journey can also be shared between two people working through it together, which is its own subsystem rather than a flag on the single-user version.

Each journey is composed of tasks and reflection questions. The app tracks per-task completion and surfaces an overall progress percentage. Beyond journeys, there’s a daily gratitude prompt, a free-form journal with autosave, a goal tracker, a photo memories section, and a certifications tracker.

Key features

The journey engine is the part I spent the most time on. Journey content lives as data (backend/journeys/data), and journey progression, reminders, and shared-journey state are each their own service, so adding a new journey type doesn’t touch the reminder logic or vice versa. An admin console covers invites, enabling or disabling signups, promoting users, and issuing scoped, one-time-reveal API credentials for anything that needs to call the API outside the browser.

Architecture tour

Layering and data flow

The backend is Express, entirely ES modules, structured as server.js → 19 route files in backend/routes → roughly 16 service files → 14 repository files, all extending a shared BaseRepository. BaseRepository wraps sqlite3’s callback API in promises and centralizes a transaction mutex, so every repository gets safe transactions without reimplementing them. The frontend is a React 18 SPA on Vite, using React Router and Axios. Data lives in SQLite, and unusually for an app this size, schema evolution mostly went through the seed and init scripts rather than a formal migration pipeline: there’s a single migration file (001-add-journey-config-system.js) on top of the base schema.

A design decision worth explaining

The app sits behind a local reverse-proxy chain in front of the Express process. Rather than turning on Express’s trust proxy and reading X-Forwarded-For (the default advice), the client-IP helper trusts the proxy’s client-IP header only when the raw socket peer is loopback, meaning the request really did come through the local proxy chain and not a direct connection spoofing the header. trust proxy would walk past both loopback hops and land on the leftmost X-Forwarded-For entry, which a client fully controls, letting anyone mint unlimited rate-limit buckets with a fake header. This one took a specific bug to notice, not a design review.

Testing

Backend tests run under Jest (about 64 test files, using node --experimental-vm-modules for ESM support), frontend under Vitest (about 41 test files), plus a small Playwright browser suite for end-to-end flows. Total is around 105 test files.

Learnings

The rate limit is the clearest example of a mistake I had to fix in production, not catch in review. It started at 100 requests per 15 minutes, which sounds generous until you notice the SPA fires three or four API calls per page view: that budget exhausted after about 28 page navigations, and because the limiter keyed on IP address, an entire household sharing one connection shared one budget and failed silently with 429s. I widened the general limit to 100 per minute and gave the auth routes their own, tighter limiter, since login attempts and page navigation shouldn’t compete for the same bucket.

A few UI surfaces also got built, shipped, and then ripped out once real use showed they weren’t the right shape (a legacy “my journeys” view, for one), and a batch of touch-target and accessibility fixes went in well after the fact rather than up front. None of that is unusual for a project built solo in spare time. What’s useful about it in hindsight is that fixing the rate limit and the IP-trust logic both came from actually using the app day to day, not from reading the code and guessing what would go wrong.