OpenLedger is a private, local-first finance tool. Warm ledger aesthetic, editorial UX. Premium financial report feel. Not a fintech platform — a personal budgeting application with no backend, no accounts, and no cloud dependency.
Metaphor: a financial journal — you record, it reveals.
v0.11.1 (2026-07-03) — Open Product Family Branding Live domain: https://ledger.kovina.org Status: Production — header lockup redesigned, full icon suite generated from master, OpenPalette alignment, branding documentation established, favicon regeneration, README header icon, GitHub social preview image.
Google OAuth was broken due to a Supabase project mismatch. The auth cookie was being set for OpenSprout's project (rbdyrymtgfqqkdemicdo) instead of OpenLedger's (qoxmibmbyjmkntzrckyr). Two contributing factors:
- Missing
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYon Vercel. The callback'screateServerClientreceivedundefinedfor the key →exchangeCodeForSessionfailed silently → no session cookies set → guest mode. The legacyNEXT_PUBLIC_SUPABASE_ANON_KEYwas present but unused after the browser client switched to the publishable key format. - Stale OpenSprout cookies on localhost. Both apps run on
localhost(different ports). Cookies are not scoped by port. OpenSprout'ssb-rbdyrymtgfqqkdemicdo-auth-tokencookie was visible to OpenLedger. TheuseAuth()hook read it as a valid session → user appeared signed-in with a wrong-project session → "Continue with Google" wouldn't initiate (client already had a session).
- Middleware stripped: No Supabase client, no
getUser(), no cookie handling. Only Cache-Control header remains. - Callback (
/auth/callback): Matches OpenSprout's working pattern exactly —request.cookies.getAll()(Next.js API), no manual parsing, no delays, no?code=handling. - Browser client (
client.ts): UsesNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, explicitflowType: "pkce",autoRefreshToken: true,detectSessionInUrl: true. - Server client (
server.ts): Switched from anon key to publishable key. - Auth hook (
auth-hook.ts): Client-sideexchangeCodeForSessionremoved.clearWrongProjectCookies()— clears anysb-*cookie not matchingqoxmibmbyjmkntzrckyr. Logs gated behindlocalStorage.DEBUG_AUTH=true. - Sign-in button: Uses
window.location.assign(data.url)(manual redirect) instead of relying on Supabase auto-redirect. AddedqueryParams: { access_type: "offline", prompt: "select_account" }. - Dev debug button: "Debug Google OAuth" button (dev-only) for isolated OAuth testing.
- Click "Continue with Google" →
signInWithOAuth()returnsdata.url window.location.assign(data.url)→ Google OAuth- Google →
/auth/callback?code=<pkce_code> - Callback:
createServerClient→exchangeCodeForSession(code)→ writesb-qoxmibmbyjmkntzrckyr-auth-tokencookies → redirect/app /app:useAuth()→clearWrongProjectCookies()→getSession()→ user signed in- Sign out:
supabase.auth.signOut()→ redirect/— next sign-in opens Google cleanly
- Vercel env: Added
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY. ExistingNEXT_PUBLIC_SUPABASE_URLconfirmed correct (points toqoxmibmbyjmkntzrckyr). .env: Switched from anon key to publishable key:sb_publishable_FTJ_XShVabxUb_NVrH7Htw_h3w2ponJ.env.example: Updated to publishable key format.
next.config.ts:Cache-Control: private, no-cache, no-store, must-revalidatefor HTML routes. Vercel was applyings-maxage=31536000(1 year CDN cache)./sw.jsexplicit rule: same no-cache headers. SW was excluded from no-cache regex (.jsin exclusion list) → CDN cached it for 1 year → browser never detected SW updates.- SW upgraded:
skipWaiting(), proactive cache refresh on activate,VERSIONconstant.
- Network-first for HTML pages, cache-first for static assets.
- Google avatars bypass:
lh3.googleusercontent.comrequests are not intercepted (noevent.respondWith()) to avoidconnect-srcCSP violations. PwaRegistercomponent checks for updates every 60s, shows "Reload" banner.
'unsafe-eval'inscript-src(React dev mode).https://lh3.googleusercontent.comin bothimg-srcandconnect-src.
- Net worth moved to top summary strip (Income/Spent/Remaining/Net worth).
- Import button on Transactions tab.
- Dark mode fixes: import modal background, Select component, account badges.
- Accounts tab: "Add account" button + kind badges on each card.
- Backup 406:
fetchLatestBackup().single()→.maybeSingle(). - MCP tokens 500: GET handler uses authenticated client (not service role). RLS allows users to SELECT own tokens.
| Path | Purpose |
|---|---|
src/lib/supabase/client.ts |
Browser client (publishable key, PKCE, detectSessionInUrl) |
src/lib/supabase/server.ts |
Server client (publishable key, cookies() API) |
src/lib/supabase/auth-hook.ts |
useAuth() — session detection, project guard, debug logging |
src/lib/supabase/admin.ts |
Admin client (service role, server-only) |
src/app/auth/callback/route.ts |
PKCE code exchange + cookie writing |
src/components/auth-panel.tsx |
Google sign-in button + sign-out |
src/app/account/page.tsx |
Account page sign-in |
src/components/public-header.tsx |
Session-aware nav |
src/middleware.ts |
Cache-Control only (no auth) |
public/sw.js |
Service worker (SW cache, avatar bypass) |
src/app/api/mcp/tokens/route.ts |
MCP token CRUD |
- Summary strip: income / spent / remaining (44px/64px typography, numbers dominate)
- "Where Did My Money Go?" — category breakdown with interactive filtering
- Comparison pills: This Month / Last Week / Last Month / 3 Months / 6 Months / Last Year
- All-months bar chart: 1200×600 viewBox, full-bleed width, income/expense/net bars
- Budget progress inline, net worth section with per-account breakdown
- Custom MonthPicker dropdown replacing native select
- Premium staged sheet: Choose Account → Upload → Auto-Categorized Preview → Accept
- Editorial drop zone, trust info block, step progress indicator, subtle transitions
- Auto-categorization from bank descriptions
- Category learning: corrections persist to localStorage
- Month-scoped:
computeMonthIncome,computeMonthExpenses,computeMonthCashflow - Month-over-month:
computeMonthOverMonth,computeCategoryMonthOverMonth - Average spending:
averageSpendingByCategory - Comparison engine: 6 range types, 12 tests
- Immutable rule: Every displayed number must come from the finance engine
LedgerReport,ImportFlow,AccountsView,AllMonthsBarChartMonthPicker,ComparisonPills,DatePicker,Selectcategories.ts— category hierarchy + keyword mapping
- Local-first. Guest mode is default. No account required.
- No tracking. No analytics, no telemetry, no third-party scripts.
- Finance engine immutable rule. Every displayed number must come from
src/lib/finance/helpers. - Calm UX. Avoid financial gamification, urgency patterns, or manipulative UI.
- Design system. OpenProof Design Playbook — editorial layout, pill buttons, accent color #7A2F00.
- Branch naming:
feat/*,fix/*,docs/*,refactor/*,chore/*. - Auth: Rebuilt matching OpenSprout's pattern. Middleware has zero auth logic. All auth state comes from server callback +
useAuth()hook.
OpenLedger is part of the Open Product Family under the Kovina ecosystem.
Kovina -> OPEN -> OpenLedger (product: "Ledger")
- OPEN is the shared family prefix. "OPEN" in headers is uppercase (tracking 0.06em, opacity-50).
- OpenLedger is the full application name. Header lockup:
[app icon] OPEN / Ledger(stacked, icon 28px, leading-tight). - OpenPalette is the canonical design reference for color tokens, spacing, and component patterns.
- All OPEN products share the same header lockup structure:
[app icon] OPEN / {ProductName} - Icon pipeline: All app icons generated from
openledger_light_mode.png(1024x1024 master) viascripts/generate-icons.jsusing Lanczos resampling. 155 assets produced for Windows ICO, MSIX, Android, iOS, macOS, Web/PWA, social OG, and GitHub. - Favicon: Replaced favicon-16x16.png, favicon-32x32.png, icon-192x192.png, icon-512x512.png with new generated assets.
- See
docs/BRANDING.mdfor full branding documentation and header implementation reference.
All ecosystem repos follow: https://github.com/sparshsam/ecosystem-standards
- Privacy Policy: https://ledger.kovina.org/privacy
- Terms of Service: https://ledger.kovina.org/terms
- Conforms to the Kovina Privacy & Terms Standard (v1.0, 2026-07-02)
- Pages:
src/app/privacy/page.tsx,src/app/terms/page.tsx(both use<LegalLayout>) - Footer links to
/privacyand/termsviapublic-footer.tsx - Contact: sparshsam@gmail.com