[{"data":1,"prerenderedAt":410},["ShallowReactive",2],{"project-\u002Fprojects\u002Ffinixa-en":3,"related-\u002Fprojects\u002Ffinixa-en":389,"legal-link-en":408},{"id":4,"title":5,"body":6,"description":361,"extension":362,"git":363,"git_soon":366,"meta":367,"navigation":368,"order":369,"path":370,"seo":371,"stack":372,"stem":382,"type":383,"updated":384,"web":385,"__hash__":388},"projects_en\u002Fprojects\u002Ffinixa.md","Finixa",{"type":7,"value":8,"toc":335},"minimark",[9,17,22,29,33,122,126,131,138,142,157,161,172,176,187,192,196,214,218,221,225,242,246,249,252,256,260,266,268,272,277,279,283,288,290,294,300,302,306,312,314,318,323,325,329],[10,11,12,16],"p",{},[13,14,15],"strong",{},"Logging an expense in under five seconds from your phone"," — the one constraint that drove every decision in Finixa, from the PostgreSQL schema down to the input gesture.",[18,19,21],"h2",{"id":20},"whats-the-context-behind-finixa","What's the context behind Finixa?",[10,23,24,25,28],{},"Finixa is a personal finance SaaS MVP built around one hard UX constraint: ",[13,26,27],{},"add an expense as fast as possible from a mobile device",". It covers expense and income tracking, per-category and per-subcategory budgeting, envelope budgeting (50\u002F30\u002F20), virtual recurring transactions, and monthly statistics backed by PostgreSQL RPCs. The app is packaged for iOS\u002FAndroid via Capacitor in addition to the web deployment.",[18,30,32],{"id":31},"whats-the-tech-stack","What's the tech stack?",[34,35,36,48,66,76,82,88,102,112],"ul",{},[37,38,39,42,43,47],"li",{},[13,40,41],{},"Nuxt 4 + Vue 3 (SPA, hash routing)"," — SSR is intentionally disabled: hash-based routing is required for Capacitor compatibility, as the ",[44,45,46],"code",{},"capacitor:\u002F\u002F"," scheme does not support push-state routing.",[37,49,50,53,54,57,58,61,62,65],{},[13,51,52],{},"Strict TypeScript"," — Amounts are signed at the database level (",[44,55,56],{},"-50"," = expense, ",[44,59,60],{},"+1000"," = income). The central discriminant is ",[44,63,64],{},"TransactionType ('depense' | 'revenu' | 'epargne')",", used consistently from the database schema up to UI components.",[37,67,68,71,72,75],{},[13,69,70],{},"Pinia (15 stores)"," — Three-layer architecture: ",[44,73,74],{},"app\u002Fservices\u002F"," isolates all Supabase calls, stores hold state and expose mutations, pages and components consume stores. No direct Supabase calls in components.",[37,77,78,81],{},[13,79,80],{},"Supabase (PostgreSQL + Auth + RLS)"," — Row Level Security on every table. Computed views (stats, virtual recurrences) are PostgreSQL RPCs called directly from the service layer.",[37,83,84,87],{},[13,85,86],{},"Edge Functions (Deno)"," — Four serverless functions for external integrations: quick transaction insertion via API key, daily digest, push notifications, category listing endpoint.",[37,89,90,93,94,97,98,101],{},[13,91,92],{},"Custom SCSS (token-based design system)"," — No Tailwind. Design tokens are auto-injected into every component via ",[44,95,96],{},"additionalData"," in ",[44,99,100],{},"nuxt.config",". Components use only semantic CSS custom properties.",[37,103,104,107,108,111],{},[13,105,106],{},"Capacitor 8"," — iOS\u002FAndroid packaging, ",[44,109,110],{},"@capacitor\u002Fpreferences"," for secure local storage (PIN, theme, onboarding state).",[37,113,114,117,118,121],{},[13,115,116],{},"@nuxtjs\u002Fi18n"," — Full FR\u002FEN localization, ",[44,119,120],{},"no_prefix"," strategy compatible with hash routing.",[18,123,125],{"id":124},"notable-technical-points","Notable Technical Points",[127,128,130],"h3",{"id":129},"_1-transaction-add-flow","1 · Transaction add flow",[10,132,133,134,137],{},"The full sequence from button tap to reactive UI update — the layer separation in action. ",[44,135,136],{},"add.vue"," only knows the store, the store only knows the service, the service only knows Supabase.",[127,139,141],{"id":140},"_2-operation-date-vs-accounting-date","2 · Operation date vs accounting date",[10,143,144,145,148,149,152,153,156],{},"Each transaction has a ",[44,146,147],{},"date"," (when the operation occurs) and a nullable ",[44,150,151],{},"accounting_date"," (when it is attributed to the budget). For recurring transactions with ",[44,154,155],{},"accounting_offset: 'next_month'",", the accounting date is set to the 1st of the following month at materialization time. Concrete use case: a subscription debited on December 28th but counted against January's budget.",[127,158,160],{"id":159},"_3-subcategory-budget-versioning","3 · Subcategory budget versioning",[10,162,163,164,167,168,171],{},"Editing a budget does not rewrite history: a new row is created with ",[44,165,166],{},"valid_from",", the old one is closed with ",[44,169,170],{},"valid_to",". Stats queries use the date to find the budget that was in effect at that point in time — January statistics remain accurate even if the budget changed in May.",[127,173,175],{"id":174},"_4-virtual-recurrences-and-lazy-materialization","4 · Virtual recurrences and lazy materialization",[10,177,178,179,182,183,186],{},"Recurring transactions are not persisted in the database by default. On every load, ",[44,180,181],{},"getDatesInRange()"," computes occurrences for the current period and ",[44,184,185],{},"getOccurrenceKey()"," generates a stable identifier. Only unmatched occurrences are shown as \"virtual\" rows. Persistence only happens when the user checks off an occurrence.",[10,188,189,191],{},[44,190,181],{}," handles day-of-month clamping (e.g., monthly on the 31st → Feb 28\u002F29) and all edge cases for weekly, quarterly, and yearly frequencies.",[127,193,195],{"id":194},"_5-postgresql-rpcs-for-all-aggregations","5 · PostgreSQL RPCs for all aggregations",[10,197,198,199,202,203,202,206,209,210,213],{},"No stats logic on the frontend. All computed views are SQL functions: ",[44,200,201],{},"get_subcategory_stats",", ",[44,204,205],{},"get_variable_daily_remaining",[44,207,208],{},"recurring_virtual_for_month",", etc. Called via ",[44,211,212],{},"supabase.rpc()"," from the service layer.",[127,215,217],{"id":216},"_6-api-key-system-for-external-integrations","6 · API key system for external integrations",[10,219,220],{},"Deno Edge Functions expose endpoints authenticated via a SHA-256 hash of a user-generated key. This allows adding a transaction from an iOS Shortcuts automation without exposing Supabase credentials.",[18,222,224],{"id":223},"scss-design-system","SCSS Design System",[10,226,227,228,231,232,235,236,97,238,241],{},"Tokens are defined in two layers: a primitive palette (never referenced directly in components) and semantic CSS custom properties (the only layer components may use). The token file is ",[13,229,230],{},"auto-injected"," into every ",[44,233,234],{},"\u003Cstyle lang=\"scss\">"," block via ",[44,237,96],{},[44,239,240],{},"nuxt.config.ts"," — no manual imports needed.",[18,243,245],{"id":244},"what-i-learned-contributed","What I Learned \u002F Contributed",[10,247,248],{},"The most challenging part was the recurring transactions engine: computing stable virtual occurrences over any arbitrary time window, with day-of-month clamping (e.g., monthly on the 31st → Feb 28\u002F29), while cleanly handling the split between operation date and accounting date. Budget versioning required rethinking the schema so that stats queries are deterministic regardless of which historical period is queried.",[250,251],"hr",{},[18,253,255],{"id":254},"diagrams","Diagrams",[127,257,259],{"id":258},"global-architecture","Global architecture",[10,261,262],{},[263,264],"img",{"alt":259,"src":265},"\u002Fdiagrams\u002Ffinixa\u002F01-architecture.svg",[250,267],{},[127,269,271],{"id":270},"transaction-add-flow","Transaction add flow",[10,273,274],{},[263,275],{"alt":271,"src":276},"\u002Fdiagrams\u002Ffinixa\u002F02-add-transaction-flow.svg",[250,278],{},[127,280,282],{"id":281},"operation-date-vs-accounting-date","Operation date vs accounting date",[10,284,285],{},[263,286],{"alt":282,"src":287},"\u002Fdiagrams\u002Ffinixa\u002F03-accounting-date.svg",[250,289],{},[127,291,293],{"id":292},"budget-versioning","Budget versioning",[10,295,296],{},[263,297],{"alt":298,"src":299},"Subcategory budget versioning","\u002Fdiagrams\u002Ffinixa\u002F04-budget-versioning.svg",[250,301],{},[127,303,305],{"id":304},"virtual-recurrences","Virtual recurrences",[10,307,308],{},[263,309],{"alt":310,"src":311},"Virtual recurrences and lazy materialization","\u002Fdiagrams\u002Ffinixa\u002F05-virtual-recurring.svg",[250,313],{},[127,315,317],{"id":316},"api-key-authentication-flow","API key authentication flow",[10,319,320],{},[263,321],{"alt":317,"src":322},"\u002Fdiagrams\u002Ffinixa\u002F06-api-key-flow.svg",[250,324],{},[127,326,328],{"id":327},"scss-design-system-1","SCSS design system",[10,330,331],{},[263,332],{"alt":333,"src":334},"SCSS design system — tokens and injection","\u002Fdiagrams\u002Ffinixa\u002F07-scss-design-system.svg",{"title":336,"searchDepth":337,"depth":337,"links":338},"",2,[339,340,341,350,351,352],{"id":20,"depth":337,"text":21},{"id":31,"depth":337,"text":32},{"id":124,"depth":337,"text":125,"children":342},[343,345,346,347,348,349],{"id":129,"depth":344,"text":130},3,{"id":140,"depth":344,"text":141},{"id":159,"depth":344,"text":160},{"id":174,"depth":344,"text":175},{"id":194,"depth":344,"text":195},{"id":216,"depth":344,"text":217},{"id":223,"depth":337,"text":224},{"id":244,"depth":337,"text":245},{"id":254,"depth":337,"text":255,"children":353},[354,355,356,357,358,359,360],{"id":258,"depth":344,"text":259},{"id":270,"depth":344,"text":271},{"id":281,"depth":344,"text":282},{"id":292,"depth":344,"text":293},{"id":304,"depth":344,"text":305},{"id":316,"depth":344,"text":317},{"id":327,"depth":344,"text":328},"Mobile-first personal finance web app — expense tracking, envelope budgeting, and recurring transactions.","md",[364,365],"Git Repository","https:\u002F\u002Fgithub.com\u002FOwenLB\u002Ffinixa",false,{},true,1,"\u002Fprojects\u002Ffinixa",{"title":5,"description":361},[373,374,375,376,377,378,379,380,381],"Nuxt 4","Vue 3","TypeScript","Pinia","Supabase","PostgreSQL","SCSS","Capacitor","Deno (Edge Functions)","projects\u002Ffinixa","Web & mobile app","2026-07-17",[386,387],"Visit app","https:\u002F\u002Ffinixa.net\u002F","n1pi5Num-z_xp1omvaF9F7WPKfzcE_bHHhFyBlo-7qM",[390,394,398,403],{"title":391,"type":392,"path":393,"order":337},"Peliculle","iOS app","\u002Fprojects\u002Fpeliculle",{"title":395,"type":396,"path":397,"order":344},"ScanAuto","Web app","\u002Fprojects\u002Fscanauto",{"title":399,"type":400,"path":401,"order":402},"Developer Portfolio","Website","\u002Fprojects\u002Fportfolio-dev",4,{"title":404,"type":405,"path":406,"order":407},"Photo Portfolio","Full-stack site","\u002Fprojects\u002Fportfolio-photo",5,{"title":409},"Legal notices",1784470460443]