What is Win Stacking, you ask?
Win Stacking is a daily habit tracker idea that I had a few years ago built around one idea: You log your wins every day, build streaks, and earn medals as your consistency grows. Users can log three types of wins: Simple, Great, and Outstanding which are visualised as a stack of coins on the screen. The longer the streak, the bigger the stack.
The goal wasn't to build the next big SaaS product. It was to demonstrate end-to-end full stack capability across a real, working application, with authentication, a database, business logic, and live deployment. I also wanted to build a project I had some passion for, plus something that I would actually use myself in real life.
The Frontend: React, TypeScript and Tailwind
The frontend is built with React 19, TypeScript, and Tailwind CSS v3, scaffolded with Vite. The choice of this stack reflects how I have worked in professional settings at Buzz Interactive.
The UI is component-driven throughout. Key components include:
- Dashboard : the main view, showing stats, week selector, coin stack, and wins list
- CoinStack: an SVG-based visual representation of wins as stacked coins, with different sizes and colours per win type
- CalendarPopover: a custom date picker built from scratch without any library
- MedalPanel: a slide-out drawer showing earned medals, progress bars, and history
- WinForm / WinCard: add, edit and delete wins with a three-tier type selector
All API calls are centralised in a single
client.ts
file using Axios, with a request interceptor that automatically
attaches the JWT token to every authenticated request. Environment
variables via Vite's
import.meta.env
switch the API base URL between local development and production
automatically.
The Backend: ASP.NET Core and C#
The API is built with ASP.NET Core 10 and C#. I chose .NET because it's a production-grade, typed backend framework which is the same stack I used in the commercial projects on my CV. It's also a natural pairing with React frontends in many enterprise environments.
The architecture follows a clean separation of concerns:
- Controllers: handle HTTP routing and return responses. Thin by design.
- Services: contain all business logic (streak calculation, medal awarding, user registration)
- Models: strongly-typed C# classes representing domain entities
- DbContext: Entity Framework Core data access layer
Authentication uses JWT tokens issued on login. Every protected
endpoint uses the
[Authorize]
attribute, and the user's ID is extracted from the token claims on
each request — meaning users can only ever access their own data, by
design.
Passwords are hashed with BCrypt before storage and never stored in plain text. CORS is configured to allow only the known frontend origins, both local and production.
Business Logic: Streak & Medal Calculation
The streak logic lives entirely in the backend
WinService. It works by pulling all distinct win dates for a user, ordering
them descending, and walking backwards from today checking for
consecutive days. A streak is considered active if the user logged a
win today or yesterday.
Medals are awarded automatically when a streak milestone is reached —
7 days for Bronze, 14 for Silver, 30 for Gold. Each award is stored as
a
MedalRecord
in the database, creating a permanent history. The active display
resets if a streak breaks, but the lifetime count and history always
remain.
The Database: SQLite + Entity Framework Core
The app uses SQLite as its database, accessed through Entity Framework Core — Microsoft's official ORM for .NET. SQLite was chosen deliberately for this project: it's a single file, zero configuration, and perfectly suited for a portfolio app. For a production SaaS product I'd reach for PostgreSQL, but SQLite is the right tool here.
Entity Framework Core handles schema management through migrations.
The initial migration creates three tables:
Users, Wins, and
MedalRecords. On startup, the app runs
db.Database.Migrate()
automatically — so the database is always up to date with no manual
steps needed after deployment.
Containerisation: Docker
The backend is containerised using Docker, which was required for deployment to Render (the hosting platform that I chose for the API). Docker solves a fundamental problem: "it works on my machine." By building the app inside a container with a known .NET SDK version, the build and run environment is identical everywhere.
The Dockerfile uses a multi-stage build. A separate build stage with the full .NET SDK, then a slim runtime-only image for the final container. This keeps the deployed image small:
Deployment: Vercel + Render
The frontend and backend are deployed independently, which is standard practice for decoupled full stack apps:
-
Vercel handles the React frontend. It detects the
Vite project automatically, runs
npm run build, and serves the static output globally via CDN. Every push to the main GitHub branch triggers an automatic redeploy. - Render hosts the ASP.NET Core API. It pulls from the GitHub repo, builds the Docker image, and runs the container. Environment variables — including the JWT secret key — are set in Render's dashboard, never committed to source control.
CORS on the backend is configured to allow requests only from the
Vercel domain in production and localhost in development. The frontend
switches API endpoints automatically using environment-specific
.env
files.
Reflections
This project covers the full development lifecycle: architecture decisions, frontend component design, backend API and business logic, database schema and migrations, containerisation, and deployment. Every part of the stack was built intentionally, not just wired together from tutorials.
The things that would come next in a production version: automated tests for the streak and medal logic, a PostgreSQL database for scalability, and a CI/CD pipeline to run tests before every deploy.