尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Modern personal finance management app for tracking income, expenses, budgets, savings, and financial goals. Built with Next.js, TypeScript, Tailwind CSS, and Supabase
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
A personal financial operating system — Track → Analyze → Plan → Earn →
Grow — Thai-first, mobile-first, built on Next.js and Supabase.
This file covers setup, local development, and deployment only. For
what's actually implemented right now, current build status, known
limitations, and next steps, see PROJECT_STATUS.md — that file is the
only implementation-status source of truth (see CLAUDE.md's "Document
Ownership" section for the full map of which doc owns what).檢視完整 README ↗A personal financial operating system — Track → Analyze → Plan → Earn → Grow — Thai-first, mobile-first, built on Next.js and Supabase.
This file covers setup, local development, and deployment only. For
what's actually implemented right now, current build status, known
limitations, and next steps, see PROJECT_STATUS.md — that file is the
only implementation-status source of truth (see CLAUDE.md's "Document
Ownership" section for the full map of which doc owns what).
useActionState Server Actionsshadcn/ui + Base UI note: the version of
shadcnused to scaffold this project generates components on top of@base-ui/reactrather than Radix UI. Base UI has noasChildprop — it uses arender={<Element />}prop instead. A small helper,src/lib/as-trigger.ts, adapts a normal-looking<Button>Label</Button>into the{ render, children }shape Base UI trigger components expect, so most of the app code reads like the familiarasChildpattern.
.nvmrc; package.json engines.node >=22)npm install
cp .env.example .env.local
# fill in .env.local — see Environment setup below
Copy .env.example to .env.local and fill in:
| Variable | Client-safe? | Where to find it |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Yes | Supabase project → Settings → API |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Yes | Supabase project → Settings → API |
NEXT_PUBLIC_APP_URL | Yes | http://localhost:3000 in development |
SUPABASE_SERVICE_ROLE_KEY | No — server only | Supabase project → Settings → API. Required for billing (checkout customer bootstrap, webhook writes) — see below |
AI_API_KEY / AI_MODEL | No — server only | AI Money Coach (Anthropic). Optional — the chat UI shows a clean "not configured" state without it |
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET / STRIPE_PRICE_ID_PLUS / STRIPE_PRICE_ID_PRO | No — server only | Billing (Stripe). Optional in development; if any one is set in production, all four must be, and SUPABASE_SERVICE_ROLE_KEY becomes required too — src/config/env.ts throws a clear error otherwise |
CRON_SECRET | No — server only | Proactive AI check-in cron (/api/cron/ai-checkin, see vercel.json). Optional in development; without it, the route always rejects rather than skipping its auth check. Vercel attaches it as the request's Authorization: Bearer header automatically once set as a project env var — generate any random value, e.g. openssl rand -hex 32 |
src/config/env.ts validates these with Zod at startup and fails with a
clear error message (naming the missing variable) rather than an obscure
runtime crash. Server-only variables are never prefixed with NEXT_PUBLIC_,
and src/lib/supabase/admin.ts additionally guards itself with the
server-only package so an accidental client-side import fails at build
time, not silently at runtime.
Run npm run check:env against a loaded environment (e.g. after
vercel env pull) to see which variables are present by name only — it
never prints a value.
supabase start, from
the Supabase CLI)..env.local.<your-app-url>/auth/callback. The sign-in code
(src/features/auth/actions.ts, signInWithGoogle) is already wired up —
it just needs a configured provider to work. Until then the "Sign in with
Google" button will redirect to /login?error=oauth.Using the Supabase CLI, from the project root:
supabase link --project-ref <your-project-ref>
supabase db push
This applies every file in supabase/migrations/, in filename order (each
migration's own header comment explains what it adds — see that directory
for the current list rather than relying on a count here, since new
migrations are added as features ship; PROJECT_STATUS.md's "Migrations
Added" section tracks what each one was for).
For local development, supabase start (or supabase db reset) applies
all migrations and then runs supabase/seed.sql automatically.
supabase/seed.sql runs automatically on supabase db reset /
supabase start against your local Supabase instance only — never run
it against a hosted project, since it creates a demo auth user with a
publicly-known password directly via auth.users (only possible against a
local Postgres you fully control).
Demo login: [email protected] / wealthos-demo-password
The demo user gets two accounts (Cash ฿5,000, SCB Bank ฿75,000), a month of income/expense transactions matching the Day-1 spec example, and one transfer between the two accounts (to exercise transfer handling end to end).
npm run dev
Visit http://localhost:3000.
npm test
Runs the Vitest suite (tests/) covering the financial calculation
utilities: income/expense/cash-flow/savings-rate math, refund netting,
transfer exclusion, account balance reconstruction, currency formatting, and
edge cases (zero income, negative cash flow, large values, decimal amounts).
npm run typecheck # next typegen && tsc --noEmit (generates route types first, works on a fresh clone)
npm run lint # eslint
npm test # vitest run
npm run build # next build
All four currently pass cleanly against this codebase.
Windows / Turbopack note:
npm run devandnpm run buildare pinned to--webpackinpackage.json. On this development machine, an Application Control policy blocks the native@next/swc-win32-x64-msvcbinary, and Turbopack (Next 16's default) requires native bindings — only the WASM fallback loads, which Turbopack can't use. Webpack mode still uses the WASM SWC fallback for transforms and builds successfully. If your environment doesn't have this restriction,next build(Turbopack) should also work; feel free to drop--webpackthere.
vercel.json pins
server functions to icn1 (Seoul), next to the production Supabase
project (ap-northeast-2) — every page render makes several database
round trips, so keep the two in the same region if either ever moves..env.example in the host's project
settings — SUPABASE_SERVICE_ROLE_KEY, AI_*, and STRIPE_* as
server-only secrets, the NEXT_PUBLIC_* ones as build-and-runtime
variables. Run npm run check:env against the pulled environment first.NEXT_PUBLIC_APP_URL at your production URL, and add
<production-url>/auth/callback as a redirect URL in Supabase
Authentication → URL Configuration.supabase db push) — do not run supabase/seed.sql against
production.STRIPE_PRICE_ID_PLUS/
STRIPE_PRICE_ID_PRO to those price ids, add a webhook endpoint at
<production-url>/api/billing/webhook subscribed to at least
checkout.session.completed, customer.subscription.created,
customer.subscription.updated, customer.subscription.deleted,
invoice.payment_failed, and invoice.payment_succeeded, and set
STRIPE_WEBHOOK_SECRET to that endpoint's signing secret. Switching from
Stripe test mode to live mode later is just swapping all four STRIPE_*
values for their live-mode equivalents and updating the webhook endpoint
to point at the live-mode secret — no code change.The shape below is the pattern every feature follows — it's deliberately
described as a pattern, not an exhaustive current list, since the exact
set of src/features/* domains grows as the product does (see
PROJECT_STATUS.md for what's currently implemented):
src/
app/ Routes (App Router)
(auth)/ Public auth pages: login, signup, forgot/reset password
(app)/ Protected shell: dashboard, money/*, plan/*, earn/*, ai, profile, billing
auth/callback/ OAuth + email-confirmation callback route handler
onboarding/ Onboarding (outside the app shell, own layout)
components/
ui/ shadcn/ui primitives (Base UI-backed)
layout/ Sidebar, bottom nav, header, nav/tab config
shared/ Cross-feature UI (empty states, ...)
illustrations/ Flat SVG illustration components (see GRAPHICS_PLAN.md)
features/<domain>/ One folder per product domain (accounts, transactions,
budget, goals, income-sources, ai, billing, engagement, ...) —
each with queries.ts (reads), actions.ts (Server Action writes),
and components/
lib/
supabase/ Browser / server / admin Supabase clients + middleware session refresh
validation/ Zod schemas
financial/ Pure, deterministic calculation functions (see CLAUDE.md's "Financial Logic")
as-trigger.ts Base UI render-prop trigger helper
utils.ts cn() class merging
types/database.ts Hand-written schema types (see note in the file)
config/ Validated env access, feature flags
i18n/ th/en dictionaries, server locale resolution, client provider/hook
supabase/
migrations/ See that directory for the current, authoritative list
seed.sql Local-dev-only demo data
tests/ Vitest unit tests
RLS is mandatory on every table (see CLAUDE.md's "Database Rules") — the
general shape: user-owned tables are scoped to user_id = auth.uid(),
shared read-only reference data (like system categories) is readable by
any authenticated user but not writable by them, and write policies verify
every foreign key a client submits actually belongs to the caller rather
than trusting user_id from the request. Application code never trusts
ownership from the client; see src/features/*/actions.ts.
Each migration documents its own tables' policies and reasoning inline —
see supabase/migrations/. For current verified cross-user-isolation test
results (which tables, how many checks, when last verified), see
PROJECT_STATUS.md rather than this file, since that verification is
implementation-status, not setup documentation.
NUMERIC(18,2), never floating point, both in Postgres and
in application code (src/lib/financial/money.ts does all arithmetic in
integer cents). This avoids the classic 0.1 + 0.2 !== 0.3 class of bug
silently corrupting balances over many transactions.from_account_id /
to_account_id (not two linked rows), which makes transfer creation
atomic by construction and makes it structurally impossible to
double-count a transfer as income or an expense. See
create_transfer() in the migration and createTransfer in
src/features/transactions/actions.ts.current_balance is
maintained by a trigger that recalculates the full balance from
opening_balance + a fresh aggregate over transactions on every
relevant write, rather than nudging it up/down in place. This makes drift
structurally impossible at the cost of a cheap aggregate query per write.calculateExpenses() nets refunds against gross expenses — rather than as
income. This keeps Cash Flow = Income − Expenses true by construction.
See the doc comment on calculateExpenses in
src/lib/financial/calculations.ts.(Income − Expenses) / Income × 100, returning 0
(not NaN/Infinity) when income is 0. This is a cash-flow proxy, not
a measure of money actually set aside.TEXT + CHECK, not native Postgres enums — easier to
extend later (ALTER TABLE ... ADD CONSTRAINT) than ALTER TYPE ... ADD VALUE.Tracked in PROJECT_STATUS.md, not here — this file is setup/run/deploy
only (see CLAUDE.md's "Document Ownership" section for why). That file
has the current, verified state of every product area (auth, accounts,
transactions, budget, goals, net worth, AI coach, income engine,
engagement, billing, production hardening) plus known limitations and the
recommended next task.