Skip to content

hao-phamdev/Verdant

Repository files navigation

Verdant

A personal finance web app built around the once-weekly desktop session, not daily engagement.

Live: verdant-finance.com · Demo: click "Try the demo" on the landing page (no signup, read-only sandbox).


What it is

Most finance apps assume you'll check them every day. Verdant assumes you won't. The whole app is built around sitting down once a week on payday: log last week's transactions, plan next week's envelopes, move on.

The paycheck cycle is the unit of time everywhere — net worth, spending, budget allocation. Past weeks are read-only by design. Future weeks don't exist until you log the paycheck that starts them.

It's invite-only (about ten users projected). I built it as a portfolio piece and as a tool I actually use.


Screenshots

Captures from the live app. Demo path lets you click through without signing up.

Dashboard Budget
Dashboard Budget
Investments Spending Log
Investments Spending Log

Stack

Frontend: React 18 · Vite · bespoke CSS (no Tailwind for layout) · motion for orchestrated UI sequences · hand-rolled SVG charts (no Recharts)

Backend: Supabase (PostgreSQL + Row-Level Security + Edge Functions) · pgcrypto for bcrypt PIN hashing

Auth: Clerk (Google OAuth) bridged to Supabase via third-party auth · custom-minted Supabase JWT for the demo path · bcrypt 4-digit PIN as a second factor

Infra: Vercel (auto-deploy from main) · Cloudflare DNS + Email Routing · custom domain

Other: papaparse for CSV transaction imports · Python tools for seeding the demo user

Total operating cost: ~$12/year (domain only — everything else on free tiers).


Notable engineering moves

A few things that took real thought, lifted from the architectural notes:

  • Row-Level Security on every table, with Clerk as the IdP. Policies filter by (select auth.jwt() ->> 'sub') = user_id. users.id is TEXT (Clerk user ID directly) instead of UUID, so every policy is a direct equality check instead of a JOIN.

  • Custom-minted JWT for the public demo. "Try the demo" hits an Edge Function that signs a Supabase JWT with sub='demo'. The frontend treats it like any other session; RLS isolates demo data automatically. No service-role key ever touches the browser.

  • Restrictive RLS policies for the read-only demo. Permissive policies pass for demo SELECTs, but restrictive policies block all INSERT/UPDATE/DELETE when auth.jwt() ->> 'sub' = 'demo'. Read-only enforcement lives in the database, not the UI.

  • Hand-rolled SVG charts. Interactive Catmull-Rom spline for net worth (pointer drag, range tabs reseed the series). stroke-dasharray donut for investment allocation. CSS-flex daily bars for spending. No chart library — smaller bundle, exactly the interactions I wanted.

  • Week-as-period model derived from data. Period boundaries are computed from the paychecks table at query time. No separate periods table, so no sync bugs and biweekly/weekly schedule changes just work.

  • Per-IP rate limiting without a separate rate-limit table. The waitlist Edge Function counts existing rows in the waitlist table itself, filtered by ip = <cf-connecting-ip> and created_at > NOW() - INTERVAL '1 hour'. Reuses data that's already being written.

  • Email-existence oracle defense. users.email is deliberately not UNIQUE — that would let an attacker probe registration by reading the unique-violation error. Email uniqueness is owned by Clerk, which has proper abuse handling.


Run locally

git clone https://github.com/nhat-phamdev/Verdant.git
cd Verdant
npm install
cp .env.example .env  # fill in the keys below
npm run dev           # http://localhost:5173

.env needs:

VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
VITE_CLERK_PUBLISHABLE_KEY=
VITE_ALLOWLIST=you@gmail.com   # comma-separated Google emails

Supabase Edge Functions live in supabase/functions/ and deploy via the Supabase CLI. The schema (supabase/schema.sql) is annotated with the threat model and inline rationale — reading it teaches you the security model alongside the data model.


Project status

Shipped to production and in personal use. Scope is intentionally bounded:

  • Desktop-only (1440px). The marketing surface is responsive; the app itself is not.
  • Invite-only via app-side allowlist.
  • No tests yet — manual smoke testing on the production URL. Tests deferred.
  • No CI beyond Vercel auto-deploy from main.

This isn't a SaaS — it's a personal-use app I happen to be proud of. If that framing is useful for context: I'm a junior CS major, this is my first real shipped project, and getting it to production solo at this scale was the point.


License

MIT — see LICENSE.

About

Personal finance web app built around the once-weekly desktop session. React + Supabase + Clerk OAuth. Hand-rolled SVG charts, row-level security, custom-minted JWT demo path.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors