My personal freelance ledger. A single-user Next.js 16 dashboard for tracking clients, projects, payments, expenses, and invoices across 5 currencies. ~10k LOC of source, all auth gated behind one password against a hidden Supabase user. The repo is here for inspection (and as part of my portfolio) — not as a product. There's no signup, no public deployment URL in this repo, and no roadmap for general use.
┌──────────────────────────────────────────────────────────────────┐
│ Dashboard (the part you open every morning) │
│ ├ Outstanding hero (every unpaid balance, converted to base FX) │
│ ├ Earned this month + MoM delta │
│ ├ Overdue · Due in 30 days │
│ ├ Insights strip: avg days-to-payment · biggest debtor · YTD │
│ ├ 6-month revenue chart · top-client pie · recent payments │
│ └ Reminders widget for stale invoices │
├──────────────────────────────────────────────────────────────────┤
│ Pipeline (kanban) │ Invoices │
│ ├ 3 columns: unpaid / │ ├ Auto-numbered (YYYY-NNN) │
│ │ partially paid / paid │ ├ Snapshots issuer + client │
│ ├ Drag a card → status flips │ │ at issue time │
│ ├ "Days open" badge on each │ ├ React-PDF renderer (fr/en) │
│ └ Per-project payment log │ └ "Send reminder" with throttle │
├──────────────────────────────────────────────────────────────────┤
│ Expenses (new in 2.0) │ Settings │
│ ├ Date · vendor · category │ ├ Issuer profile │
│ ├ Multi-currency │ ├ FX rates (manual or auto from │
│ ├ Subtotals + monthly │ │ frankfurter.app — free, ECB) │
│ └ Subtracted from MTD on the │ ├ Invoice defaults (TVA note, │
│ dashboard │ │ accent, footer, reminder days)│
│ │ ├ Backup download (JSON dump) │
│ │ └ Appearance (theme, locale) │
└──────────────────────────────────────────────────────────────────┘
1.x focused on annual income as the hero metric. As a freelancer that's the wrong cadence — cashflow happens monthly. 2.0 promotes monthly earned to the top, adds a month-over-month delta (rendered green/red), and demotes YTD to a smaller insights strip below.
Also new:
- Average days to payment — quote-to-first-payment latency, paid projects only. Single most useful cashflow signal.
- Biggest debtor — the one client to chase first.
- Auto-FX refresh — pulls mid-market rates from frankfurter.app (free, ECB, no API key) instead of relying on hand-edited numbers that go stale.
- Expenses tracking — the tagline says "every freelance coin" but 1.x only tracked income. Now it tracks both.
- Mobile drawer — sidebar was desktop-only; added a hamburger + Sheet-based mobile nav.
Next.js 16 · React 19 · TypeScript
Tailwind 4 · motion · @number-flow/react
Supabase (Postgres + Auth + RLS) · @supabase/ssr
recharts · react-hook-form · zod
@dnd-kit · @react-pdf/renderer
shadcn/ui · cmdk · sonner
Hosted on Vercel. Single-user auth via a hidden Supabase user; the password gate on / calls signInWithPassword with a fixed email constant (HIDDEN_OWNER_EMAIL).
src/
├── app/
│ ├── (auth)/login/ password gate (single-user)
│ └── (app)/ authenticated routes
│ ├── dashboard/ hero stats + charts + insights
│ ├── projects/ kanban + project dialog
│ ├── payments/ payments hub
│ ├── expenses/ expenses page (new in 2.0)
│ ├── invoices/ invoice list + editor + PDF
│ ├── clients/ clients CRUD
│ ├── activity/ event log
│ └── settings/ issuer, currencies, defaults
├── components/
│ ├── app/ sidebar, top-bar, mobile-nav, command palette
│ └── ui/ shadcn primitives
├── lib/
│ ├── data/{queries,actions,events}.ts server-side data layer
│ ├── supabase/{server,client,types}.ts ssr clients + TS types
│ ├── money.ts format / convert / toBase
│ ├── invoice-number.ts auto-numbering
│ ├── constants.ts app-wide constants
│ └── auth.ts cached getAuthUser
└── proxy.ts middleware for auth refresh
supabase/migrations/ 0001 .. 0009 (numbered, append-only)
If you actually want to run this — it's source-available, but you'll be setting up your own everything.
- Supabase: create a new project. Run the migrations in
supabase/migrations/0001..0009in order via the SQL editor. Edit0003_seed_owner.sqlfirst to set:'owner_uuid'to your Supabase auth user UUID. - Create the hidden user in Supabase Auth: email
owner@freelane.local, password is whatever you'll type on the unlock screen. - Env:
cp .env.example .env.local # fill in NEXT_PUBLIC_SUPABASE_URL + NEXT_PUBLIC_SUPABASE_ANON_KEY - Run:
pnpm install pnpm dev
- Single-user by design. The login is a password gate against a hardcoded email constant. No signups, no multi-tenancy, no team support. Adding any of that is a non-trivial rewrite.
- No tests. This is a tool I open every morning. I notice when it breaks. That's "the test plan." For a real product you'd want at minimum Playwright on the critical flows.
- No issues being accepted. If you find a bug while reading the code, by all means open a discussion — but feature requests will be closed.
- Pure personal opinions baked in: invoice template is French-first, base currency defaults to PHP, exchange rates assume you're somewhere with multiple income currencies. Configurable but unapologetically defaulted to my situation.
/// OPEN FOR NEW WORK /// CONTRACT & FREELANCE /// REMOTE WORLDWIDE ///