[{"data":1,"prerenderedAt":549},["ShallowReactive",2],{"navigation_docs_en":3,"-en-reference-validation-strategy":191,"-en-reference-validation-strategy-surround":544},[4,61,127,171],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":60},"Guide","i-lucide-book-open","\u002Fen\u002Fguide","en\u002F1.guide",[10,15,20,25,30,35,40,45,50,55],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fguide\u002Fintroduction","en\u002F1.guide\u002F01.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Install the App","\u002Fen\u002Fguide\u002Finstallation","en\u002F1.guide\u002F02.installation","i-lucide-smartphone",{"title":21,"path":22,"stem":23,"icon":24},"Authentication","\u002Fen\u002Fguide\u002Fauth","en\u002F1.guide\u002F03.auth","i-lucide-lock",{"title":26,"path":27,"stem":28,"icon":29},"Wallets","\u002Fen\u002Fguide\u002Fwallets","en\u002F1.guide\u002F04.wallets","i-lucide-wallet",{"title":31,"path":32,"stem":33,"icon":34},"Categories","\u002Fen\u002Fguide\u002Fcategories","en\u002F1.guide\u002F05.categories","i-lucide-tags",{"title":36,"path":37,"stem":38,"icon":39},"Transactions","\u002Fen\u002Fguide\u002Ftransactions","en\u002F1.guide\u002F06.transactions","i-lucide-receipt",{"title":41,"path":42,"stem":43,"icon":44},"Transfers","\u002Fen\u002Fguide\u002Ftransfers","en\u002F1.guide\u002F07.transfers","i-lucide-arrow-left-right",{"title":46,"path":47,"stem":48,"icon":49},"Statistics","\u002Fen\u002Fguide\u002Fstatistics","en\u002F1.guide\u002F08.statistics","i-lucide-bar-chart-3",{"title":51,"path":52,"stem":53,"icon":54},"Theme","\u002Fen\u002Fguide\u002Ftheme","en\u002F1.guide\u002F09.theme","i-lucide-palette",{"title":56,"path":57,"stem":58,"icon":59},"Settings","\u002Fen\u002Fguide\u002Fsettings","en\u002F1.guide\u002F10.settings","i-lucide-settings",false,{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":60},"Development","i-lucide-code","\u002Fen\u002Fdevelopment","en\u002F2.development",[67,72,77,82,87,92,97,102,122],{"title":68,"path":69,"stem":70,"icon":71},"Installation","\u002Fen\u002Fdevelopment\u002Finstallation","en\u002F2.development\u002F01.installation","i-lucide-download",{"title":73,"path":74,"stem":75,"icon":76},"Codebase Graph","\u002Fen\u002Fdevelopment\u002Funderstand-anything","en\u002F2.development\u002F02.understand-anything","i-lucide-network",{"title":78,"path":79,"stem":80,"icon":81},"Offline & PWA","\u002Fen\u002Fdevelopment\u002Foffline","en\u002F2.development\u002F03.offline","i-lucide-wifi-off",{"title":83,"path":84,"stem":85,"icon":86},"Data Migration History","\u002Fen\u002Fdevelopment\u002Fmigration","en\u002F2.development\u002F04.migration","i-lucide-database",{"title":88,"path":89,"stem":90,"icon":91},"Deployment","\u002Fen\u002Fdevelopment\u002Fdeployment","en\u002F2.development\u002F05.deployment","i-lucide-rocket",{"title":93,"path":94,"stem":95,"icon":96},"Testing","\u002Fen\u002Fdevelopment\u002Ftesting","en\u002F2.development\u002F06.testing","i-lucide-flask-conical",{"title":98,"path":99,"stem":100,"icon":101},"Date Utilities","\u002Fen\u002Fdevelopment\u002Fdate-utilities","en\u002F2.development\u002F07.date-utilities","i-lucide-calendar",{"title":103,"path":104,"stem":105,"children":106,"page":60},"Ai Workflow","\u002Fen\u002Fdevelopment\u002Fai-workflow","en\u002F2.development\u002F08.ai-workflow",[107,112,117],{"title":108,"path":109,"stem":110,"icon":111},"Overview","\u002Fen\u002Fdevelopment\u002Fai-workflow\u002Foverview","en\u002F2.development\u002F08.ai-workflow\u002F01.overview","i-lucide-bot",{"title":113,"path":114,"stem":115,"icon":116},"Agents","\u002Fen\u002Fdevelopment\u002Fai-workflow\u002Fagents","en\u002F2.development\u002F08.ai-workflow\u002F02.agents","i-lucide-users",{"title":118,"path":119,"stem":120,"icon":121},"Skills","\u002Fen\u002Fdevelopment\u002Fai-workflow\u002Fskills","en\u002F2.development\u002F08.ai-workflow\u002F03.skills","i-lucide-lightbulb",{"title":123,"path":124,"stem":125,"icon":126},"Troubleshooting","\u002Fen\u002Fdevelopment\u002Ftroubleshooting","en\u002F2.development\u002F09.troubleshooting","i-lucide-life-buoy",{"title":128,"icon":129,"path":130,"stem":131,"children":132,"page":60},"Reference","i-lucide-file-code","\u002Fen\u002Freference","en\u002F3.reference",[133,138,142,147,152,156,161,166],{"title":134,"path":135,"stem":136,"icon":137},"Architecture","\u002Fen\u002Freference\u002Farchitecture","en\u002F3.reference\u002F01.architecture","i-lucide-boxes",{"title":139,"path":140,"stem":141,"icon":44},"Transaction Types","\u002Fen\u002Freference\u002Ftransaction-types","en\u002F3.reference\u002F02.transaction-types",{"title":143,"path":144,"stem":145,"icon":146},"Sync","\u002Fen\u002Freference\u002Fsync","en\u002F3.reference\u002F03.sync","i-lucide-refresh-cw",{"title":148,"path":149,"stem":150,"icon":151},"Offline-first","\u002Fen\u002Freference\u002Foffline-first","en\u002F3.reference\u002F04.offline-first","i-lucide-list-ordered",{"title":153,"path":154,"stem":155,"icon":121},"Technical Decisions","\u002Fen\u002Freference\u002Ftech-decisions","en\u002F3.reference\u002F05.tech-decisions",{"title":157,"path":158,"stem":159,"icon":160},"Validation Strategy","\u002Fen\u002Freference\u002Fvalidation-strategy","en\u002F3.reference\u002F06.validation-strategy","i-lucide-shield-check",{"title":162,"path":163,"stem":164,"icon":165},"What Changed Since Firebase","\u002Fen\u002Freference\u002Ffirebase-migration","en\u002F3.reference\u002F07.firebase-migration","i-lucide-hamburger",{"title":167,"path":168,"stem":169,"icon":170},"Performance","\u002Fen\u002Freference\u002Fperformance","en\u002F3.reference\u002F08.performance","i-lucide-gauge",{"title":172,"icon":173,"path":174,"stem":175,"children":176,"page":60},"Premium","i-lucide-star","\u002Fen\u002Fpremium","en\u002F4.premium",[177,181,186],{"title":108,"path":178,"stem":179,"icon":180},"\u002Fen\u002Fpremium\u002Foverview","en\u002F4.premium\u002F01.overview","i-lucide-layers",{"title":182,"path":183,"stem":184,"icon":185},"Telegram Bot","\u002Fen\u002Fpremium\u002Ftelegram-bot","en\u002F4.premium\u002F02.telegram-bot","i-lucide-send",{"title":187,"path":188,"stem":189,"icon":190},"AI Chat","\u002Fen\u002Fpremium\u002Fai-chat","en\u002F4.premium\u002F03.ai-chat","i-lucide-sparkles",{"id":192,"title":157,"body":193,"description":536,"extension":537,"links":538,"meta":539,"navigation":540,"path":158,"seo":541,"stem":159,"__hash__":543},"docs_en\u002Fen\u002F3.reference\u002F06.validation-strategy.md",{"type":194,"value":195,"toc":525},"minimark",[196,201,210,261,266,369,373,378,421,426,476,483,487,490,493,508,512],[197,198,200],"h2",{"id":199},"current-approach-client-only-validation","Current approach: client-only validation",[202,203,204,205,209],"p",{},"Validation is ",[206,207,208],"strong",{},"client-only",". There are no server-side business-rule validators. The server (Supabase Postgres) enforces only column types, NOT NULL constraints, RLS owner policies, and per-user sync rules.",[211,212,213,229],"table",{},[214,215,216],"thead",{},[217,218,219,223,226],"tr",{},[220,221,222],"th",{},"Layer",[220,224,225],{},"Framework",[220,227,228],{},"Purpose",[230,231,232,247],"tbody",{},[217,233,234,241,244],{},[235,236,237,240],"td",{},[206,238,239],{},"Client"," (browser)",[235,242,243],{},"Zod v4",[235,245,246],{},"Form UX - defaults, trimming, instant error feedback; runtime shape checks",[217,248,249,255,258],{},[235,250,251,254],{},[206,252,253],{},"Server"," (Supabase Postgres)",[235,256,257],{},"Column types + RLS",[235,259,260],{},"Column types, NOT NULL, per-user ownership (RLS policy)",[262,263,265],"h3",{"id":264},"where-validation-lives","Where validation lives",[211,267,268,278],{},[214,269,270],{},[217,271,272,275],{},[220,273,274],{},"Entity \u002F use",[220,276,277],{},"Zod schema",[230,279,280,303,317,330,343,356],{},[217,281,282,285],{},[235,283,284],{},"Transaction form",[235,286,287,291,292,295,296,299,300],{},[288,289,290],"code",{},"app\u002Fapp\u002Fcomponents\u002Ftrns\u002Ftypes.ts"," (",[288,293,294],{},"trnItemSchema",", ",[288,297,298],{},"transactionSchema",") + ",[288,301,302],{},"app\u002Fapp\u002Fcomponents\u002FtrnForm\u002Futils\u002Fvalidate.ts",[217,304,305,308],{},[235,306,307],{},"Stat config",[235,309,310,291,313,316],{},[288,311,312],{},"app\u002Fapp\u002Fcomponents\u002Fstat\u002FuseStatConfig.ts",[288,314,315],{},"ConfigSchema",")",[217,318,319,322],{},[235,320,321],{},"Date \u002F query params",[235,323,324,291,327,316],{},[288,325,326],{},"app\u002Fapp\u002Fcomponents\u002Fdate\u002FstatDateParams.ts",[288,328,329],{},"queryParamsSchema",[217,331,332,335],{},[235,333,334],{},"User settings",[235,336,337,291,340,316],{},[288,338,339],{},"app\u002Fapp\u002Fcomponents\u002Fuser\u002Ftypes.ts",[288,341,342],{},"userSettingsSchema",[217,344,345,348],{},[235,346,347],{},"Currency rates shape",[235,349,350,291,353,316],{},[288,351,352],{},"app\u002Fapp\u002Fcomponents\u002Fcurrencies\u002Ftypes.ts",[288,354,355],{},"ratesSchema",[217,357,358,361],{},[235,359,360],{},"Locale",[235,362,363,291,366,316],{},[288,364,365],{},"app\u002Fapp\u002Fcomponents\u002Flocale\u002Ftypes.ts",[288,367,368],{},"localeSchema",[262,370,372],{"id":371},"what-each-side-validates","What each side validates",[202,374,375],{},[206,376,377],{},"Zod (client):",[379,380,381,388,394,400,406,415],"ul",{},[382,383,384,387],"li",{},[288,385,386],{},".trim().min(1)"," - prevents empty strings after trim",[382,389,390,393],{},[288,391,392],{},".positive()"," on amounts",[382,395,396,399],{},[288,397,398],{},".default()"," for form initial values",[382,401,402,405],{},[288,403,404],{},".transform()"," for query param parsing (string to number \u002F boolean)",[382,407,408,409,411,412,316],{},"Discriminated union for transaction type (",[288,410,294],{}," = ",[288,413,414],{},"transactionSchema | transferSchema",[382,416,417,418],{},"Transfer same-wallet check: ",[288,419,420],{},"incomeWalletId !== expenseWalletId",[202,422,423],{},[206,424,425],{},"Supabase Postgres (server):",[379,427,428,446,452,459,469],{},[382,429,430,431,295,434,295,437,295,440,295,443],{},"Column types: ",[288,432,433],{},"text",[288,435,436],{},"numeric",[288,438,439],{},"integer",[288,441,442],{},"boolean",[288,444,445],{},"jsonb",[382,447,448,451],{},[288,449,450],{},"NOT NULL"," on required columns",[382,453,454,455,458],{},"RLS: every per-user table has ",[288,456,457],{},"(select auth.uid())::text = \"userId\""," - rows from other users are invisible and writes are rejected",[382,460,461,464,465,468],{},[288,462,463],{},"rates"," table is read-only for authenticated users (no client writes); rows are written server-side by the ",[288,466,467],{},"fetch-rates"," edge function",[382,470,471,472,475],{},"A trigger auto-creates ",[288,473,474],{},"user_settings"," on signup (no client-side bootstrap needed)",[202,477,478,479,482],{},"There are ",[206,480,481],{},"no server-side business-rule validators"," (no string length limits enforced server-side, no numeric range checks, no cross-field validation). Those rules live only in the Zod schemas on the client.",[262,484,486],{"id":485},"trade-off","Trade-off",[202,488,489],{},"A determined client that bypasses the form can write arbitrary data to local SQLite, which PowerSync will upload to Supabase. Postgres will accept it as long as it passes column types and RLS. This is an accepted trade-off for a single-user personal finance app - the overhead of duplicating every business rule as Postgres constraints or Row-Level-Security policies was not justified.",[202,491,492],{},"If stricter enforcement is needed in the future, the options are:",[379,494,495,502,505],{},[382,496,497,498,501],{},"Postgres ",[288,499,500],{},"CHECK"," constraints for ranges and lengths",[382,503,504],{},"Supabase Edge Function as an upload proxy with server-side Zod validation",[382,506,507],{},"Re-evaluating whether Supabase's built-in validation (pg_jsonschema, check constraints) covers the critical rules",[197,509,511],{"id":510},"next-steps","Next Steps",[379,513,514,520],{},[382,515,516,519],{},[517,518,153],"a",{"href":154}," - related architectural decisions and rationale",[382,521,522,524],{},[517,523,134],{"href":135}," - project structure and store pattern",{"title":526,"searchDepth":527,"depth":527,"links":528},"",2,[529,535],{"id":199,"depth":527,"text":200,"children":530},[531,533,534],{"id":264,"depth":532,"text":265},3,{"id":371,"depth":532,"text":372},{"id":485,"depth":532,"text":486},{"id":510,"depth":527,"text":511},"Client-only validation approach and trade-offs.","md",null,{},{"icon":160},{"title":157,"description":542},"Finapp uses client-only validation with Zod v4. Explains where schemas live, what each one validates, and what the server enforces.","LIKa1qcL8-vHkbWIZOxBGXbfh-VTStBlD-9Ehc9fqIE",[545,547],{"title":153,"path":154,"stem":155,"description":546,"icon":121,"children":-1},"Rationale behind key design choices.",{"title":162,"path":163,"stem":164,"description":548,"icon":165,"children":-1},"What changed between v6 (Firebase) and the current Supabase + PowerSync app - backend, performance, security, tests, and architecture.",1782114344818]