尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Spend Smarter. Save More.
A personal finance tracker and financial intelligence platform. Tracks income, expenses,
budgets and saving goals, and turns that data into rule-based (non-AI) insights, forecasts
and a financial health score — per the spec in
requirements/Finora — Complete Project Specification & Development Plan.md.
This README is written so a new developer can go from a fresh clone to a fully running app,
end to end, without asking anyone anything. Follow it top to bottom the first time; use the
table of contents to jump back in later.查看完整 README ↗Spend Smarter. Save More.
A personal finance tracker and financial intelligence platform. Tracks income, expenses,
budgets and saving goals, and turns that data into rule-based (non-AI) insights, forecasts
and a financial health score — per the spec in
requirements/Finora — Complete Project Specification & Development Plan.md.
This README is written so a new developer can go from a fresh clone to a fully running app, end to end, without asking anyone anything. Follow it top to bottom the first time; use the table of contents to jump back in later.
@better-auth/expo) and a dashboard summary screen, sharing
the same backend as the web app. See apps/mobile — more screens are being
added incrementally.apps/client and apps/server.@finora/database so the Prisma/Node runtime never ends up in a browser (or Expo) bundle.Install these before touching the repo:
node -v to check).corepack enable
corepack prepare [email protected] --activate
No local Postgres, no Docker, no global CLI installs beyond pnpm — everything else is a workspace dependency.
For someone who's done this before and just wants the commands:
git clone <this-repo-url> finora && cd finora
corepack enable && corepack prepare [email protected] --activate
pnpm install
# create packages/database/.env, apps/server/.env, apps/client/.env.local
# (see "Environment files" below for exact contents)
pnpm --filter @finora/database exec prisma migrate dev
pnpm --filter @finora/database run seed
pnpm dev
Then open http://localhost:3000 and log in as [email protected] / Pa$$w0rd (seeded), or
register your own account. If any of that is unclear, read the full walkthrough below.
git clone <this-repo-url> finora
cd finora
corepack enable
corepack prepare [email protected] --activate
This repo pins packageManager: [email protected] in package.json — Corepack reads that and makes
sure everyone on the team uses the exact same pnpm version, so pnpm install produces the same
lockfile result for everyone.
Create a free Neon project and copy its connection string (Neon shows it
on the project dashboard, looks like postgresql://user:password@host/db?sslmode=require).
This is the only external service you need an account for to run the app locally.
Next.js, Prisma, and Expo each read their own env file — there's no shared root .env
that all three consume automatically. Copy each .env.example below and fill in real values:
cp packages/database/.env.example packages/database/.env
cp apps/server/.env.example apps/server/.env
cp apps/client/.env.example apps/client/.env.local
cp apps/mobile/.env.example apps/mobile/.env # only needed if you're running the mobile app
| File | Variables | Notes |
|---|---|---|
packages/database/.env | DATABASE_URL | Same Neon connection string everywhere. |
apps/server/.env | DATABASE_URL, PORT, WEB_APP_URL, BETTER_AUTH_URL | PORT defaults to 4000 locally. |
apps/client/.env.local | DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, NEXT_PUBLIC_API_URL, GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET | The last two are optional — see step 8. |
apps/mobile/.env | EXPO_PUBLIC_AUTH_URL, EXPO_PUBLIC_API_URL | Only needed to run the mobile app (step 9). |
Generate a BETTER_AUTH_SECRET:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
Paste the result into apps/client/.env.local. Leave GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET
blank for now — the app runs fully on email/password without them, and step 8 covers turning
Google sign-in on later.
pnpm install
This installs every workspace package (apps/*, packages/*) in one pass. If you only just
added apps/mobile to your checkout, this is also the step that would pull in its Expo/React
Native dependencies.
pnpm --filter @finora/database exec prisma migrate dev
pnpm --filter @finora/database run seed
The first command creates every table (User, Session, Transaction, Budget, ... — the
full schema in packages/database/prisma/schema.prisma) in your Neon database. The second
seeds:
Both commands are safe to re-run; the seed script upserts by a fixed ID, so running it twice never duplicates data.
pnpm dev
This runs apps/client (http://localhost:3000) and apps/server (http://localhost:4000)
together via Turborepo, with hot reload on both. The first page load after starting can
take several seconds — Next.js/Turbopack and Nest both compile routes lazily on first request,
not up front — this is expected, not a hang.
Open http://localhost:3000. You should land on /login. Either:
[email protected] / Pa$$w0rd) — you'll see an extra
Admin item in the sidebar (see The admin dashboard), orOnce logged in you should see the Dashboard with empty-state cards ("No transactions yet", etc.) — that's correct for a brand-new database. Add a transaction to confirm the full client → API → Postgres round trip works.
Skip this if email/password is enough for what you're doing.
http://localhost:3000.http://localhost:3000/api/auth/callback/google — note the
port. Better Auth's routes live in apps/client (port 3000), not apps/server (4000);
pointing this at 4000 is the most common setup mistake here.apps/client/.env.local's
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET.pnpm dev. A "Continue with Google" button now appears on /login and /register.The mobile app reuses this same Google client — no separate Google Cloud setup needed for it,
since the OAuth flow still round-trips through apps/client's web callback URL even when
launched from the phone.
The mobile app is in its foundation phase (login/register + a dashboard summary screen) — see apps/mobile for what's there and the Tech stack section above for what's still to come.
cd apps/mobile
pnpm start
This opens Expo's dev tools. Scan the QR code with the Expo Go app on your phone (same
Wi-Fi network as your computer), or press a/i to launch an Android/iOS emulator.
Before it can reach your API, edit apps/mobile/.env: localhost only resolves to "this
computer," which a phone isn't. Replace it with your computer's LAN IP (find it with
ipconfig on Windows / ifconfig or ip addr on macOS/Linux — something like
192.168.1.20):
EXPO_PUBLIC_AUTH_URL="http://192.168.1.20:3000"
EXPO_PUBLIC_API_URL="http://192.168.1.20:4000"
The Android emulator specifically can instead use http://10.0.2.2:3000 /
http://10.0.2.2:4000, which it maps back to your computer's actual localhost.
Running pnpm --filter @finora/database run seed creates one account:
| Password | Role | |
|---|---|---|
[email protected] | Pa$$w0rd | admin |
Use it to see the admin dashboard without having to manually promote a
user. Change or remove this account before a real production deploy — it's meant for local
development and demos, not shipped as-is with a real user base. To promote any other account to
admin instead, either use the admin dashboard's own role toggle (once logged in as this seeded
admin), or set it directly: UPDATE "user" SET role = 'admin' WHERE email = '...'.
apps/
client/ Next.js app — also owns auth (Better Auth mounted at /api/auth/*)
server/ NestJS REST API — one module per domain (accounts, transactions,
budgets, goals, reports, analytics, insights, forecast, health-score,
subscriptions, recurring-transactions, notifications, export, receipts,
gamification, admin)
mobile/ Expo app (React Native + Expo Router) — foundation phase
packages/
database/ Prisma schema + generated client (@finora/database)
types/ Shared enums, mirrored from schema.prisma (not re-exported from
@finora/database — see below)
validation/ Zod schemas shared by web/mobile forms and Nest DTOs
utils/ Currency/date/math helpers, shared by all three apps
config/ Shared tsconfig base
requirements/ The original spec this app is built from
render.yaml Render Blueprint for apps/server (see Deployment)
apps/client/vercel.json Vercel build config for apps/client (see Deployment)
Run from the repo root unless noted. Most are Turborepo wrappers that build workspace dependencies in the right order automatically.
| Command | What it does |
|---|---|
pnpm dev | Runs apps/client + apps/server together, hot-reloading. |
pnpm build | Production build of every app/package, in dependency order. |
pnpm lint | ESLint across every workspace package. |
pnpm typecheck | tsc --noEmit across every workspace package. |
pnpm db:generate | Regenerate the Prisma client after editing schema.prisma. |
pnpm db:migrate | prisma migrate dev — create + apply a new migration locally. |
pnpm db:seed | Re-run the seed script (idempotent). |
pnpm db:studio | Open Prisma Studio (a GUI for browsing/editing your Neon data). |
pnpm exec turbo run build --filter=@finora/client | Build just the web app (+ its dependencies). |
pnpm exec turbo run build --filter=@finora/server | Build just the API (+ its dependencies). |
pnpm --filter @finora/mobile run start | Start the Expo dev server (see step 9). |
Iterating on a shared package (packages/types, packages/validation, packages/utils,
packages/database) while pnpm dev is running: the dev servers don't watch those packages
for you. Rebuild the one you changed by hand — e.g. pnpm --filter @finora/validation run build
— then the running apps/client/apps/server pick up the change.
A few decisions here aren't obvious from reading the code alone:
packages/types doesn't just re-export Prisma's enums. @finora/database's entry point
instantiates PrismaClient, which needs Node's fs and native bindings. If
@finora/types/@finora/validation (used inside "use client" forms, and inside the mobile
app) re-exported enums from @finora/database, bundlers would pull the whole Prisma runtime
into the browser/Expo bundle. packages/types/src/enums.ts hand-mirrors the Prisma schema's
enums as plain as const objects instead — keep them in sync by hand when schema.prisma
changes.
Shared packages are pre-built, not consumed as raw TypeScript. NestJS's CLI only
transpiles apps/server/src; it can't consume a workspace package's raw .ts source the way
Next.js can (via transpilePackages). Each shared package has its own build (tsc → dist/,
CommonJS), and packages/database's build also runs prisma generate first. Turborepo handles
the ordering for you — see Available scripts.
Better Auth (not NestJS) owns login, and lives in apps/client. NestJS only verifies
bearer JWTs against Better Auth's JWKS endpoint (apps/server/src/auth/jwt-verifier.service.ts)
— it never issues them. This is why apps/server has no /api/auth/* routes of its own, and
why enabling Google sign-in (step 8) only ever touches apps/client.
The mobile app reuses the exact same Better Auth backend, via @better-auth/expo (a
SecureStore-backed session + deep-link OAuth handling on top of the same email/password +
Google config apps/client already runs). There is no separate mobile auth system, and
packages/database is deliberately never a mobile dependency — the mobile app only ever talks
HTTP to apps/server's REST API and apps/client's /api/auth/* routes, same as the web app.
Logging in as a user with role: "admin" (the seeded account, or one you promote — see
Seeded accounts) adds an Admin item to the sidebar, at /admin:
/admin) — DB health/latency/uptime, aggregate usage stats (users,
transactions, budgets, ...), and a real API-traffic chart (requests/day, top routes, status
codes), backed by request logging middleware in apps/server./admin/users) — search/list every user./admin/users/:id) — view profile fields, reset that user's password,
and change their role between user/admin.User list/detail/password-reset/role-change all go through Better Auth's own admin plugin
APIs directly from the browser (authClient.admin.*) rather than through apps/server — see
the RolesGuard/@Roles("admin") pattern in apps/server/src/auth/ for how the site
health/traffic endpoints are protected instead.
Three pieces, deployed separately: apps/client → Vercel, apps/server → Render,
Postgres → Neon (already set up from local dev — reuse the same project, or create a
separate one for production). See .env.production.example (root) for the full variable
reference; apps/client/.env.production.example and apps/server/.env.production.example
have the per-app subset.
The one rule that matters most: BETTER_AUTH_URL (both apps) and NEXT_PUBLIC_API_URL must
point at your real deployed URLs, not localhost — Better Auth signs JWTs with
BETTER_AUTH_URL as both issuer and audience, and the API checks incoming tokens against that
exact value via JWKS.
Nothing to deploy — Neon is already a hosted service. Either reuse your dev project's connection string, or create a new Neon project for production and run the migration against it once before first deploy:
DATABASE_URL="<your-prod-connection-string>" pnpm --filter @finora/database exec prisma migrate deploy
DATABASE_URL="<your-prod-connection-string>" pnpm --filter @finora/database run seed
Render's free tier works for this (spins down when idle; first request after idle takes ~30–60s to wake — fine for a personal project, worth knowing before you wonder why the first login is slow).
Fastest path: a render.yaml Blueprint at the repo root already encodes steps 2–5 below (root
directory, build/start commands, health check path). Render → New + → Blueprint → connect
this repo → it reads render.yaml automatically and only prompts you for the three env vars in
step 6. The manual steps are spelled out below in case you'd rather set the service up by hand.
pnpm install && pnpm exec turbo run build --filter=@finora/server
pnpm --filter @finora/server run start
DATABASE_URL, WEB_APP_URL,
BETTER_AUTH_URL — see apps/server/.env.production.example. Don't set PORT; Render
injects it.https://your-api.onrender.com URL — you'll need it for
the client's NEXT_PUBLIC_API_URL.apps/client/vercel.json already encodes the framework preset, build command, and output
directory (steps 3–5 below) — Vercel reads it automatically once Root Directory is set. Root
Directory is the one setting vercel.json can't express (it's not part of its schema), so step 2
below is still a manual, one-time click regardless of the file.
apps/client (Vercel needs this to detect it's a Next.js app and set
the right output conventions; it still installs from the monorepo root automatically once
it detects pnpm-workspace.yaml).next build in isolation):
cd ../.. && pnpm exec turbo run build --filter=@finora/client
.next).DATABASE_URL, BETTER_AUTH_SECRET (generate a fresh one, don't reuse the dev value),
BETTER_AUTH_URL (your Vercel URL — you may need to deploy once first to learn it, then
redeploy after setting this), NEXT_PUBLIC_API_URL (the Render URL from step 2),
SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASS/EMAIL_FROM (powers password-reset
emails — without these the reset link only reaches the Render server logs, which users
can't see; a Gmail address + app password
works fine), and optionally GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET. See
apps/client/.env.production.example.BETTER_AUTH_URL (client) and
WEB_APP_URL/BETTER_AUTH_URL (server, on Render) all match it exactly, then redeploy
both if you had to change anything.https://<your-vercel-domain>/api/auth/callback/google) as an additional Authorized
redirect URI in Google Cloud Console — it doesn't replace the localhost one, both coexist.If Vercel's function size limit complains: Prisma's generated client triggers a Next.js
build warning about "dynamic filesystem access" tracing the whole project into the server
function bundle. It's usually harmless (well under Vercel's 50MB limit for a project this
size), but if you hit the limit, look at outputFileTracingExcludes in apps/client/next.config.ts
or switch Prisma to a driver adapter — not needed for the app as it stands today.
The foundation is built (see Tech stack), but it isn't deployed anywhere yet —
today it only runs via pnpm --filter @finora/mobile run start + Expo Go/an emulator (step 9
above). When it's ready to ship: Expo Application Services (EAS) has a
free tier for builds and OTA updates (the closest mobile equivalent to what Vercel gives the
web app) — apps/mobile/app.json already has android.package/ios.bundleIdentifier set
(com.finora.app) as a starting point for that.
Every phase in the spec's §42 development plan, plus work that came after it:
Still ahead: the rest of the mobile app's screens (transactions, budgets, goals, accounts, subscriptions, recurring, reports, analytics, insights, notifications, settings — tracked incrementally), an Android EAS build, and AI-based intelligence (v1 is deliberately SQL/statistics/rules only, per spec §44).
pnpm dev looks stuck on "Loading…". Expected — Next.js/Turbopack
and Nest both compile routes lazily on first request, not up front. Give it 10–15 seconds
(longer right after a fresh install or a big dependency change) before assuming something's
broken; subsequent loads are fast.EADDRINUSE / port 3000 or 4000 already in use. Something else (an old pnpm dev, an
orphaned node process) is still bound to that port. On Windows:
Get-NetTCPConnection -LocalPort 3000,4000 -State Listen to find the PID, then
Stop-Process -Id <pid>.prisma migrate/generate fails with a file-lock error on Windows. Stop any running dev
server first (it may be holding a lock on generated files), then retry.[email protected]. Make sure you actually ran
pnpm --filter @finora/database run seed after migrating — it's a separate step, not part of
migrate dev.3000,
not 4000.localhost in
apps/mobile/.env only works if you're running in a web browser preview — a physical phone
or the Android emulator needs your computer's real LAN IP (or 10.0.2.2 for the Android
emulator specifically). See step 9.expo-doctor warns about duplicate react/react-native/expo installs. This is a
known pnpm + Expo monorepo interaction (Next.js and Expo pin different, both-correct React
versions) — already worked around via .npmrc's node-linker=hoisted and
apps/mobile/metro.config.js's explicit nodeModulesPaths/disableHierarchicalLookup.
Don't "fix" this by trying to unify the React version across apps; verify instead that
pnpm --filter @finora/mobile exec expo export --platform android actually bundles cleanly
(that's the check that matters — it exercises Metro's real module resolution).