[{"data":1,"prerenderedAt":266},["ShallowReactive",2],{"project-fitbud":3},{"id":4,"title":5,"body":6,"category":250,"date":251,"description":252,"extension":253,"image":254,"meta":255,"navigation":256,"path":257,"seo":258,"stem":259,"tags":260,"url":264,"__hash__":265},"projects\u002Fprojects\u002Ffitbud.md","Fitbud",{"type":7,"value":8,"toc":241},"minimark",[9,14,18,22,32,39,46,93,100,111,118,128,135,173,180,209,213,224,227,231,234,238],[10,11,13],"h2",{"id":12},"overview","Overview",[15,16,17],"p",{},"An offline-first application, designed as a native mobile app firstly, however, it's supported on the browser as well. Built with Spring Boot to learn it and it's many capabilities, this application has a custom OAuth JWT token system, while also having the Google and Facebook OAuth implementations. It's using REST API for all CRUD operations, while following the HATEOAS principle. For the DB I'm using PostgreSQL with it's JSON column capabilities and things like GIN indexes for faster DB searches.",[10,19,21],{"id":20},"features","Features",[23,24,25],"ul",{},[26,27,28],"li",{},[29,30,31],"strong",{},"Auth",[15,33,34,35,38],{},"First-party JWT issuer. BCrypt-hashed passwords, HS256 access tokens (15m), opaque SHA-256-hashed refresh tokens (30d) with rotation. ",[29,36,37],{},"Plus OAuth social login"," (Google + Facebook) via token-exchange",[23,40,41],{},[26,42,43],{},[29,44,45],{},"Workouts",[15,47,48,49,53,54,57,58,61,62,65,66,69,70,73,74,77,78,81,82,77,85,88,89,92],{},"REST + HATEOAS CRUD. Each workout has a ",[50,51,52],"code",{},"visibility ∈ {PRIVATE, PUBLIC, FRIENDS}",". Conditional links (",[50,55,56],{},"edit","\u002F",[50,59,60],{},"delete",") are emitted only when the viewer can perform the action. Paged listings via Spring Data + ",[50,63,64],{},"PagedResourcesAssembler",". JSON Merge Patch for ",[50,67,68],{},"PATCH",". Also ",[29,71,72],{},"full-text search"," over the workout body, ",[29,75,76],{},"optimistic-locking"," (",[50,79,80],{},"version",") on mutations, ",[29,83,84],{},"client-supplied-id upsert",[50,86,87],{},"PUT","), and ",[29,90,91],{},"soft-delete + delta-sync"," for offline clients.",[23,94,95],{},[26,96,97],{},[29,98,99],{},"Exercises",[15,101,102,103,106,107,110],{},"A global exercise ",[29,104,105],{},"catalog"," (admin-curated) plus per-workout ",[29,108,109],{},"exercise lines"," (sets + reps + note, ordered). Reads are open to any authenticated user; writes are admin-only. Optimistic-locking, soft-delete, trigram name search, and delta-sync — same offline-sync machinery as recipes.",[23,112,113],{},[26,114,115],{},[29,116,117],{},"Workout Plans",[15,119,120,121,124,125,127],{},"A user-owned, ordered ",[29,122,123],{},"collection of workouts"," with its own ",[50,126,52],{},". Membership add\u002Fremove\u002Freorder, per-workout visibility filtering (a workouts own visibility always wins, inaccessible members are hidden and not counted), optimistic-locking, soft-delete, and delta-sync.",[23,129,130],{},[26,131,132],{},[29,133,134],{},"Achievements.",[15,136,137,138,141,142,145,146,149,150,153,154,156,157,160,161,164,165,168,169,172],{},"An ",[29,139,140],{},"offline-first, server-evaluated achievements system"," awarding tiered (Bronze\u002FSilver\u002FGold) badges across three metrics: ",[50,143,144],{},"WORKOUTS_COMPLETED",", ",[50,147,148],{},"DISTINCT_EXERCISES_USED",", and ",[50,151,152],{},"LOGIN_STREAK",". Two metrics are server-derived from existing data; ",[50,155,152],{}," is the single client-reported signal (",[50,158,159],{},"PUT \u002Fme\u002Fstreak","), stored as a monotonic maximum. Unlocks are append-only and never revoked. The sync contract is ",[29,162,163],{},"reduced",": delta cursor + ",[50,166,167],{},"X-Server-Time"," header only — no tombstones, no ",[50,170,171],{},"@Version","\u002F409, and no scheduled pruner.",[23,174,175],{},[26,176,177],{},[29,178,179],{},"Friendships.",[15,181,182,183,186,187,57,190,193,194,197,198,201,202,205,206,208],{},"Mutual request\u002Faccept relationship between two users. Canonical-ordered pair table (",[50,184,185],{},"user_a \u003C user_b",") with ",[50,188,189],{},"PENDING",[50,191,192],{},"ACCEPTED"," states. ",[50,195,196],{},"FRIENDS","-visibility workouts are visible only to accepted friends, checked live on every read. Users can be ",[29,199,200],{},"found by exact email"," and browsed via a ",[29,203,204],{},"profile hub"," that links to the user's workouts, plans, and achievements. Those per-owner listings reuse the same ",[50,207,196],{}," visibility filtering; a friend's achievements are friend-gated (404 for non-friends, since achievements have no PUBLIC tier).",[10,210,212],{"id":211},"flutter","Flutter",[15,214,215,216,219,220,223],{},"The project follows a ",[29,217,218],{},"layered MVVM"," (Model–View–ViewModel) architecture with an explicit data layer. Dependencies always point inward: ",[29,221,222],{},"UI → ViewModels → Repositories → (API clients \u002F DAOs) → plain domain models",". Nothing points back out.",[15,225,226],{},"There is to much to point out here, so I would refer you to the architecture doc on my Github.",[10,228,230],{"id":229},"what-i-did","What I did",[15,232,233],{},"Everything, from the idea, to the documentation, to the backend implementation all the way to the UI\u002FUX, and finally deployment on my own self-host machine. Best way to see the application is to test it yourself. :)",[10,235,237],{"id":236},"stack","Stack",[15,239,240],{},"Built with Java 21, Spring Boot, Flutter, and PostgreSQL",{"title":242,"searchDepth":243,"depth":243,"links":244},"",2,[245,246,247,248,249],{"id":12,"depth":243,"text":13},{"id":20,"depth":243,"text":21},{"id":211,"depth":243,"text":212},{"id":229,"depth":243,"text":230},{"id":236,"depth":243,"text":237},"Personal","2023-06-01","A mobile-first, offline-first application for working-out. Built with Java 21 and Spring Boot, as well as Flutter for the FE","md","\u002Fprojects\u002Ffitbud.jpg",{},true,"\u002Fprojects\u002Ffitbud",{"title":5,"description":252},"projects\u002Ffitbud",[261,262,212,263],"Java 21","Spring Boot","PostgreSQL","#","-Rbz3cw8hkd1YEl-4H7TKQq7d2kxe-h1AMEnZ3zFr1A",1785186536637]