Security and reliability fixes ahead of the public launch, a simpler savings flow, and new legal pages.
FixedPlaid access tokens are now excluded from PlaidItem queries by default — routes that need them opt in explicitly, removing the risk of leaking bank credentials through future code paths
FixedCompleting onboarding a second time is now blocked instead of wiping accounts, categories, and savings goals
ImprovedStripe and Plaid webhook routes pin the Node.js runtime so raw request bodies remain byte-exact for signature verification
NewAdded Privacy Policy and Terms of Service pages
ImprovedSavings rows now use a single editable balance — click the balance to edit, hit Enter, and the difference flows to or from Ready to Assign automatically
FixedSavings balance updates now apply atomically, eliminating a drift between savings balance and Ready to Assign when two changes happened in quick succession
ImprovedStripe and Plaid webhooks now record event IDs in a dedup table so retries can't cause duplicate processing
FixedStripe webhook now returns 200 on logic errors to stop the retry loop that could re-trigger destructive Plaid cleanup; failed events are still logged for manual replay
FixedPlaid webhook now returns 500 on processing failures so the provider retries, fixing the silent drop of transaction syncs when a single update failed
FixedPostHog now degrades gracefully when its API key is unset instead of crashing the page on every load
ImprovedRemoved the stub /api/user/settings endpoint — theme is persisted client-side via next-themes
FixedTransaction update API now whitelists the fields a client can change, preventing reassignment of userId, accountId, transferId, or other internal flags
ImprovedBulk transaction endpoints now cap requests at 500 items, reject invalid ObjectIds in the list, and bulk-delete now checks auth at the route level
ImprovedSingle-transaction routes now return 400 for invalid IDs and stop echoing raw Mongoose error text in 500 responses
FixedLogin no longer reveals whether an email is registered or which sign-in method it uses — all credential failures show the same message
FixedLogin query now lowercases the email so signing in with a different case than the one used at signup works
Improved/admin pages are now gated at the edge so non-admins are redirected before any admin UI is rendered
ImprovedAll [id] API routes (accounts, categories, credit promos, loans, transactions) now return 400 for malformed IDs instead of 500
ImprovedPlaid sync, webhook, and exchange-token routes set maxDuration to 60s on Vercel to fit larger transaction batches
ImprovedProduction builds now refuse to start if PLAID_ENV is misconfigured or Stripe price IDs are missing — fail-fast instead of silent sandbox / empty checkout
ImprovedAuth: signed-in sessions now refresh from the database at most every 60 seconds (or on explicit session.update()) instead of on every request
NewStripe webhook handles checkout.session.completed, customer.deleted, and customer.updated for cleaner subscription state recovery
FixedTrial eligibility is now marked at checkout creation so two checkout tabs can't both pass the trial gate before the first webhook arrives
ImprovedMongo connection now uses a tuned pool (max 10 connections, 5s server-selection timeout, 30s socket timeout) and strict query mode
FixedError page no longer prints raw error messages; shows a generic message with an optional reference id
ImprovedMath-expression evaluator in the budget UI now uses a small parser instead of `new Function()` — no possibility of eval-style execution
FixedPlaid auto-transfer detection now requires both sides to be at the same institution, eliminating false positives where coincidentally-matching amounts on different banks would be silently linked
FixedWhen a pending Plaid match's manual transaction is missing, the bank-side transaction is now kept as a normal entry instead of being deleted (previously the bank record was permanently lost because the sync cursor had moved past it)
ImprovedPostHog server client now batches events (flushAt 20 / 10s) instead of flushing on every capture — saves 50-200ms on every API response that records an error or analytics event
ImprovedPlaid sync logs containing transaction descriptions/amounts/account names are now suppressed in production via a dev-only logger
ImprovedPOST /api/transactions, /api/transfers, and /api/credit-promos now validate every field at the route boundary — negative amounts, malformed dates, invalid enums, and oversized descriptions are rejected with a 400 before they touch the database
ImprovedTransfers cannot be created between the same account on both sides
ImprovedCredit promos: principal ≥ 0, post-promo APR 0-100, transfer fee ≥ 0, end date must be a real calendar date
ImprovedGET /api/transactions now caps results at 500 by default (1000 max via ?limit=) instead of returning the entire history in one response
FixedMoney fields now apply atomic increments instead of read-modify-write — two concurrent assigns/withdrawals (e.g., from two tabs) both land instead of one silently overwriting the other
FixedWithdrawing from a savings goal now uses a balance >= amount filter so a concurrent withdrawal cannot push the balance negative
FixedTransaction, BudgetTotal, CategoryEnvelope, and SavingsEnvelope money fields now round at the schema layer, preventing pennies of drift from sneaking in via direct property writes
ImprovedPOST /api/accounts, /api/categories, and /api/onboarding now validate the request body at the route boundary instead of relying on Mongoose to reject malformed input with a generic 500
ImprovedOnboarding draft is capped at 64 KB to prevent stuffing arbitrary JSON into the User document
ImprovedStripe and Plaid webhook routes, admin endpoints, Plaid integration code, and the rest of the verbose-logging surface now route diagnostic logs through a dev-only logger — production logs no longer contain transaction descriptions, amounts, item ids, or institution names
ImprovedAccount deletion now requires confirming your email address in the request body — protects against accidental or CSRF-style deletion
ImprovedPATCH /api/user now caps the name field at 100 characters and rejects non-string values
ImprovedNative browser alerts on the accounts and categories pages replaced with toast notifications
ImprovedRemoved the placeholder Toggle Admin Status button from the admin user detail page
FixedFooter Features / How It Works / FAQ links now navigate back to the home page anchor when clicked from /about, /blog, /privacy, etc., instead of doing nothing
NewNew GET /api/health endpoint for uptime monitors — returns 200 with database state and process uptime when healthy, 503 otherwise