Household is a self-hosted, installable web app that turns the chores of running a home — shopping lists, recipes, loyalty cards, to-dos — into a lightly gamified shared space. A Laravel JSON API backs a Vue 3 SPA, with real-time sync over WebSockets, an integration into a separate Croatian grocery-price API, and a zero-knowledge encrypted vault for loyalty cards.
Household is a full home-management product rather than a demo: a Laravel 13 JSON API serving a Vue 3 + TypeScript single-page app that installs as a PWA. It covers shopping lists, a grocery catalog, recipes, loyalty cards, and to-dos, with real-time sync over WebSockets, Web Push notifications, and Google OAuth — all running on a single self-managed Ubuntu VPS behind Nginx, with Supervisor-managed workers and a GitHub Actions deploy.
Two pieces stand out technically. The loyalty-card vault is zero-knowledge: card numbers are encrypted in the browser with a PBKDF2-derived AES-GCM key, and the backend stores only a salt and an opaque authenticator — it can neither read nor decrypt the cards. And price comparison reaches into a completely separate Laravel + Postgres service (the prices project) over an authenticated HTTP API, so a large Croatian grocery dataset powers basket comparisons without ever being coupled into this database.
It’s built to feel rewarding without being gimmicky: an XP/achievement system turns chores into shared household progress, with guardrails so the gamification never distorts the real data or gates real features. It’s designed, integrated, and shipped end-to-end as a single coherent app — and the counts and claims above are taken directly from the source, not estimated.
List changes broadcast over WebSockets so every member's screen updates as items get checked off — no refresh, no polling. Web Push notifications reach members even when the app is closed.
Loyalty-card numbers are encrypted in the browser with a household PIN; the server never sees plaintext or the PIN. It stores only a salt and an opaque authenticator token, so a database leak reveals nothing usable.
Shopping-list items can be linked to real Croatian grocery products and compared across retail chains, pulling live data from a standalone prices service over an authenticated HTTP API.
Completing lists, building the catalog, and inviting members award XP and unlock achievements — including secret and meta achievements — turning routine chores into shared progress.
New households are set up through an onboarding wizard, can theme themselves, and grow through signed invite and referral links — with Google OAuth as an alternative to email signup.
The app installs to the home screen as a PWA with auto-updating service worker assets, and ships a complete English/Croatian localization across the whole interface.
Loyalty-card numbers are sensitive and shared within a household, but a self-hosted app on a single VPS is one database dump away from leaking them. Server-side encryption still leaves keys on the same box as the data.
Encryption happens entirely in the browser: PBKDF2 (100k iterations) derives an AES-GCM key from a household PIN, and only a random salt plus an encrypted sentinel 'authenticator' are persisted. The server can verify nothing and decrypt nothing — a leak yields salts and ciphertext, not card numbers.
Multiple household members shop and edit lists at the same time; stale screens cause double-buys and confusion. Polling an API would be wasteful and laggy for a family-sized app on modest hardware.
List mutations broadcast ShoppingListUpdated over Laravel Reverb, consumed client-side through Laravel Echo, so UIs reconcile in real time. Web Push (VAPID) covers the closed-app case, with both broadcast and queue workers kept alive by Supervisor and gracefully restarted on each deploy.
Price comparison needs a large, frequently-updated Croatian grocery dataset that doesn't belong inside a home-management app. Coupling the two databases would entangle two unrelated projects.
Prices live in a standalone Laravel + Postgres service; Household talks to it only through a thin PricesService over an authenticated HTTP API (product search, basket, chains). Grocery items hold a link to a remote product, and basket comparison returns per-chain totals — keeping the two apps independently deployable.
Gamification can easily become noise or gate real functionality behind cosmetic 'classes'. The goal was motivation, not a paywall or a grind that distorts the underlying data.
An XpService and AchievementService award XP and unlock achievements off real activity (completed lists, catalog size, members, referrals), with abuse guardrails at the counter level so stats stay honest. Secret and meta achievements reward collecting sets — all cosmetic, with no feature gated behind progression.