{"id":31295753,"url":"https://github.com/nothing9537/financer","last_synced_at":"2026-04-13T14:32:01.551Z","repository":{"id":313563434,"uuid":"1043825538","full_name":"nothing9537/financer","owner":"nothing9537","description":"Financer is a production-minded finance dashboard (Next.js 15, TS) with Plaid (sandbox) bank linking, secure server exchange, accounts/transactions sync (PFC mapping), CSV import/export with TF-IDF auto-categorization, analytics charts, URL-synced filters, and a subscription paywall via Lemon Squeezy (Checkout/Portal + webhook)","archived":false,"fork":false,"pushed_at":"2025-09-22T03:49:13.000Z","size":351,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-22T05:38:38.522Z","etag":null,"topics":["csv-import","drizzle-orm","finance","fintech","honojs","lemonsqueezy","nextjs","plaid","react","react-query","recharts","rpc","sentry","shadcn-ui","tailwind-css","type-safety","typescript","zustand"],"latest_commit_sha":null,"homepage":"https://financer-beta.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nothing9537.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-24T17:42:02.000Z","updated_at":"2025-09-22T03:49:17.000Z","dependencies_parsed_at":"2025-09-06T23:32:52.575Z","dependency_job_id":"998e6c86-9c6c-4aa5-8dc1-9d037754f53c","html_url":"https://github.com/nothing9537/financer","commit_stats":null,"previous_names":["nothing9537/financer"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nothing9537/financer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nothing9537%2Ffinancer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nothing9537%2Ffinancer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nothing9537%2Ffinancer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nothing9537%2Ffinancer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nothing9537","download_url":"https://codeload.github.com/nothing9537/financer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nothing9537%2Ffinancer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31757477,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-13T13:27:56.013Z","status":"ssl_error","status_checked_at":"2026-04-13T13:21:23.512Z","response_time":93,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csv-import","drizzle-orm","finance","fintech","honojs","lemonsqueezy","nextjs","plaid","react","react-query","recharts","rpc","sentry","shadcn-ui","tailwind-css","type-safety","typescript","zustand"],"created_at":"2025-09-24T20:05:30.997Z","updated_at":"2026-04-13T14:32:01.540Z","avatar_url":"https://github.com/nothing9537.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Financer — Personal Finance Dashboard (Next.js 15, Plaid, Lemon Squeezy)\n\n**Stack highlights:** Next.js (App Router, RSC), TypeScript, Clerk (auth), Hono (typed API), Drizzle ORM + Neon, TanStack Query/Table, Recharts, Tailwind v4 + shadcn/ui, Sentry.  \n**Fintech:** Plaid **Sandbox** integration (link, exchange, accounts, transactions), subscriptions via **Lemon Squeezy** (Checkout + Customer Portal + webhook, DB status).\n\n\u003e ⚠️ **Educational/demo project.** Plaid runs in **Sandbox** only. Do **not** connect real bank accounts.\n\n---\n\n## TL;DR\n\n- Link a bank via **Plaid (sandbox)** → import **accounts** \u0026 **transactions**  \n- Explore data with **filters/search/sorting/pagination**, inline edits, CSV **export**  \n- **CSV import** with **TF-IDF auto-categorization** + merchant rules  \n- Premium flows are **paywalled** via **Lemon Squeezy** subscriptions  \n- Production touches: **Clerk auth**, **typed Hono API**, **Drizzle migrations**, **Sentry**, typed payload validation (Zod)\n\n---\n\n## ✨ Key Features\n\n### Fintech \u0026 Data\n- **Plaid Sandbox**: Link flow, server-side public_token exchange, persisted **accounts** and **transactions** (with Plaid Personal Finance Category mapping).\n- **CSV Import**: Client-side parsing (PapaParse) with **TF-IDF**-based auto-categorization and merchant rules; **CSV Export** for the active selection.\n- **Overview Analytics**: Daily income/expense time series (gap-filled), category share (pie), and deltas vs previous period.\n\n### Subscriptions \u0026 Access\n- **Lemon Squeezy**:  \n  - `POST /api/subscriptions/checkout` → URL to **Checkout** or **Customer Portal** (if already subscribed)  \n  - `GET /api/subscriptions/current` → current status  \n  - `POST /api/subscriptions/webhook` → HMAC-verified upsert in DB\n  - `Test credentials` → Card Number: 4242 4242 4242 4242, Expiration date: Any in the future, CVV: any, Rest - any valid credentials to bypass LemonSqueezy validation.\n- **Paywall gating**: Plaid linking and CSV import require an active subscription.\n\n### App UX\n- **Transactions Table** (TanStack Table): sorting, selection, inline edits (sheet modals).  \n- **URL-synced filters**: account \u0026 date range filters survive refresh and are shareable (deep-links).  \n- **Accounts \u0026 Categories**: CRUD with modals; first-run import of the Plaid PFC taxonomy.\n\n### Observability \u0026 Safety\n- **Sentry** (server/edge) with prod-only enablement and `/monitoring` tunnel.  \n- **Hono + Zod** validation on API routes; **Clerk**-backed auth guard.  \n- **Drizzle ORM + Neon** with migrations committed.\n\n---\n\n## 🧭 Project Status\n\n- ✅ Next.js 15 (App Router, RSC) + TypeScript + Tailwind v4/shadcn  \n- ✅ Auth (Clerk) + middleware guard  \n- ✅ Hono API (`/api`) + Zod validation  \n- ✅ Drizzle ORM + Neon driver; migrations via `drizzle-kit`  \n- ✅ **Plaid Sandbox**: link → exchange → accounts + transactions → category mapping  \n- ✅ **Lemon Squeezy**: checkout, customer portal, webhook; DB holds subscription status  \n- ✅ CSV import (TF-IDF categorization) and CSV export  \n- ✅ Overview charts \u0026 summary  \n- ✅ Sentry wiring (prod-only), TanStack Query provider  \n- ⏳ CI (GitHub Actions)\n\n---\n\n## 🏗️ Architecture\n\n**Frontend-first, App Router**\n\n- **RSC** pages for first render and streaming; interactive “islands” as client components.  \n- **Feature-Sliced layers**: `shared → entities → features → widgets → app pages`.\n\n**API / Server**\n\n- `src/app/api/[[...route]]/route.ts` — **Hono** router mounted at `/api`; subroutes:\n  - `/plaid`, `/summary`, `/accounts`, `/categories`, `/transactions`, `/subscriptions`\n- Request validation via `@hono/zod-validator`; auth via `@hono/clerk-auth`.\n- **DB (Drizzle ORM / Postgres, Neon)** — core tables:\n  - `accounts` (id, name, userId, plaid_id)  \n  - `categories` (id, name, userId, plaid_id, unique(userId, plaid_id))  \n  - `transactions` (id, amount **milliunits**, payee, notes, date, accountId, categoryId)  \n  - `connected_banks` (id, userId, accessToken, cursor, itemId)  \n  - `subscriptions` (id, userId **unique**, subscriptionId **unique**, status)\n\n**Data \u0026 State**\n\n- TanStack Query hooks under `src/entities/**/api` for server data.  \n- Small local/Zustand stores where needed (e.g., CSV flow).  \n- Utilities for money math (milliunits), Plaid taxonomy, formatting, and URL filters.\n\n**UI**\n\n- shadcn/ui components, Tailwind v4, lucide icons.\n\n---\n\n## 🔌 API Surface (selected)\n\n`/api/plaid`  \n- **POST** `/create-link-token` → `{ token }`  \n- **POST** `/exchange-public-token` `{ public_token }` → persist bank + accounts + transactions  \n- **GET** `/connected-bank` → current connection  \n- **DELETE** `/connected-bank` → remove connection  \n- **POST** `/webhook` (Plaid) → sync by cursor\n\n`/api/subscriptions`  \n- **GET** `/current` → subscription status  \n- **POST** `/checkout` → `{ url }` (Checkout or Customer Portal)  \n- **POST** `/webhook` (Lemon Squeezy) → HMAC verify + DB upsert\n\n`/api/summary`  \n- **GET** `/` with `?from=YYYY-MM-DD\u0026to=YYYY-MM-DD\u0026accountId=…` → time series + category share + deltas\n\n`/api/accounts`, `/api/categories`, `/api/transactions` — CRUD, bulk ops, search \u0026 pagination.\n\n---\n\n## 🧰 Tech Stack\n\n**Frontend:** Next.js 15 · React 19 · TypeScript · Tailwind v4 · shadcn/ui · TanStack Query/Table · Recharts  \n**API:** Hono · Zod · `@hono/clerk-auth` · Route Handlers (Node runtime)  \n**Auth:** Clerk  \n**DB:** Drizzle ORM + `@neondatabase/serverless`  \n**Fintech:** Plaid SDK (Sandbox) · **Lemon Squeezy** SDK (checkout/portal/webhooks)  \n**Observability:** Sentry (server/edge)  \n**Data tooling:** PapaParse, uuid, date-fns, lodash, zustand\n\n---\n\n## 🔐 Security \u0026 Privacy\n\n- **Sandbox only** for Plaid. Never use real banking credentials.  \n- Store tokens server-side; in production, **encrypt at rest** and scrub from logs.  \n- HttpOnly cookies via Clerk; Lemon webhooks verified with HMAC.  \n- Middleware protects the app (public routes are limited to sign-in/up and the subscription webhook).\n\n---\n\n## Quick Start Guide\n\n1) **Create accounts \u0026 collect tokens (fill `.env`)**\n   - **Clerk**: `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, `CLERK_SECRET_KEY`, `CLERK_PUBLISHABLE_KEY - same as NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`\n   - **Database (Neon Postgres)**: `DEVELOPMENT_DB_URL`, `PRODUCTION_DB_URL`\n   - **Plaid (Sandbox)**: `PLAID_CLIENT_TOKEN`, `PLAID_SECRET_TOKEN` *(use sandbox keys only)*\n   - **Lemon Squeezy**: `LEMONSQUEEZY_STORE_ID`, `LEMONSQUEEZY_PRODUCT_ID`, `LEMONSQUEEZY_API_KEY`, `LEMONSQUEEZY_WEBHOOK_SECRET`\n   - **App URL**: `NEXT_PUBLIC_APP_URL` (e.g., `http://localhost:3000` and later your vercel app URL)\n   - **Sentry (optional, prod/preview only)**: `NEXT_PUBLIC_SENTRY_DSN`, `SENTRY_AUTH_TOKEN`\n\n2) **Set up the project locally**\n   ```bash\n   git clone \u003cyour-repo-url\u003e \u0026\u0026 cd \u003cproject-folder\u003e\n   cp .env.example .env     # paste all tokens from step 1\n   pnpm i                   # or: bun i\n   pnpm db:generate \u0026\u0026 pnpm db:migrate\n   pnpm dev                 # app on http://localhost:3000\n   ```\n\n3) **Connect Lemon Squeezy webhooks (recommended via ngrok)**\n   - Install **ngrok** locally, then expose your dev server:\n     ```bash\n     ngrok http 3000\n     ```\n   - Copy the **HTTPS Forwarding URL** from ngrok and set it as your Lemon Squeezy webhook endpoint:\n     ```\n     https://\u003cyour-ngrok-subdomain\u003e.ngrok.io/api/subscriptions/webhook\n     ```\n   - Save the webhook in Lemon Squeezy and use your `LEMONSQUEEZY_WEBHOOK_SECRET` in `.env`.  \n\n4) **Sign in \u0026 try the flows**\n   - Go to `/sign-in` (Clerk) → log in.\n   - Test **subscription** (Checkout/Portal) and **Plaid (sandbox)** linking.\n   - Use **filters/analytics** and **CSV import/export**.\n\n### Environment variables (`.env.example`)\n\n- **Clerk:** `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, `CLERK_SECRET_KEY`, `CLERK_PUBLISHABLE_KEY - same as NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`\n- **DB:** `PRODUCTION_DB_URL`, `DEVELOPMENT_DB_URL` (Neon)  \n- **Sentry:** `NEXT_PUBLIC_SENTRY_DSN` (prod/preview), `SENTRY_AUTH_TOKEN` (CI)  \n- **Plaid (sandbox):** `PLAID_CLIENT_TOKEN`, `PLAID_SECRET_TOKEN`  \n- **App URL:** `NEXT_PUBLIC_APP_URL`  \n- **Lemon Squeezy:** `LEMONSQUEEZY_STORE_ID`, `LEMONSQUEEZY_PRODUCT_ID`, `LEMONSQUEEZY_API_KEY`, `LEMONSQUEEZY_WEBHOOK_SECRET`\n\n\u003e **Sentry in development**: enabled only in production/preview when a DSN is present. Locally, keep `NEXT_PUBLIC_SENTRY_DSN` empty.\n\n---\n\n## 📊 Metrics \u0026 Charts\n\n- `/summary` aggregates daily series, computes **income/expenses/remaining** and deltas vs the previous window.  \n- `fillMissingDays` for continuous time series; category share pie for composition.  \n- Filters are URL-synced for deep links.\n\n---\n\n## ✅ Code Quality \u0026 Best Practices — Review\n\n**What’s strong**\n- **Typed, modular design:** App Router with a clean split into entities/features/widgets; typed API via Hono + Zod; Drizzle schemas checked into repo.  \n- **Clear domain boundaries:** finance entities (accounts, categories, transactions) and subscription state kept separate and explicit.  \n- **Security posture:** Clerk for auth, HMAC verification on Lemon webhooks, Plaid confined to sandbox; tokens kept server-side.  \n- **Developer ergonomics:** Tailwind v4 + shadcn/ui, TanStack Query/Table; URL-synced filters improve UX and shareability.  \n- **Observability hooks:** Sentry wiring (server/edge), production-only enablement, monitoring endpoint.\n\n**What to improve next (high-impact, short effort)**\n1. **Encrypt tokens at rest** (Plaid access tokens) and add a scrubber middleware for logs.  \n2. **Rate limiting \u0026 idempotency**: apply per-IP/user limits on `/api/plaid/*`; idempotent handling for webhooks (keyed by `event.id`).  \n3. **Performance budget proof**: add a `/metrics` page (Web Vitals ingest) and include a Lighthouse (mobile) screenshot in README.  \n4. **Accessibility basics**: keyboard focus, ARIA labels for table controls, color-contrast ≥ 4.5:1, live regions for async updates.  \n5. **Error boundaries \u0026 friendly fallbacks**: empty/error/skeleton states are in place—add a global error boundary for unexpected crashes.  \n6. **Backups \u0026 migrations policy**: document Neon backup cadence and a simple rollback procedure (Drizzle migrations).  \n7. **Internationalization / currency**: centralize currency/locale formatting and (optionally) add an i18n stub for future expansion.\n\n---\n\n## 🗺️ Roadmap\n\n- Budgets \u0026 alerts (per category), monthly/periodic reports (CSV/PDF)  \n- i18n \u0026 multi-currency  \n- Scheduled sync (cron)  \n- At-rest encryption for tokens/secrets  \n- CI (GitHub Actions) pipeline (lint → typecheck → build → deploy)\n\n---\n\n## 📂 Notable Source Layout\n\n- `src/app/(dashboard)/*` — Overview, Transactions, Accounts, Categories, Settings  \n- `src/app/api/[[...route]]/*` — Hono subroutes (plaid, subscriptions, summary, …)  \n- `db/schemas/*` — Drizzle tables \u0026 relations  \n- `src/entities/*` — typed API hooks, types, small stores (per domain)  \n- `src/features/*` — user flows (plaid-connect, csv-import, subscribe-button, filters, …)  \n- `src/widgets/*` — composite UI blocks (tables, sheets, header, summary)  \n- `src/shared/*` — UI kit, utils, hooks, constants\n\n---\n\n## 📄 License\n\nMIT\n\n---\n\n**Notes**  \nThis README reflects the current codebase: Next.js 15 + Clerk + Hono API + Drizzle/Neon; Plaid (Sandbox) and Lemon Squeezy are integrated at the API level with paywall gating; CSV import + TF-IDF categorization; Sentry wiring in prod/preview. If you change env keys or route names, update the sections above accordingly.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnothing9537%2Ffinancer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnothing9537%2Ffinancer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnothing9537%2Ffinancer/lists"}