
Web Application

A parent-controlled web application that supports daily habits of children aged 4–12 through gamification. Kids complete tasks, earn stars and XP, level up their characters, and collect badges. Parents manage tasks, handle approval workflows, and track progress.
To track their children's daily habits, parents either use a paper chart or a task app designed for adults. Both break down at the same point: the child can approve their own reward, so the system loses its credibility. The real problem the app had to solve was not the interface — it was the separation of authority.
Two distinct users in a single product. A child aged 4–12: limited reading and writing, touch targets need to be large, reward feedback has to appear instantly. A parent: defines tasks, approves completed ones or sends them back with a coaching note, follows weekly progress — and wants to be certain the child cannot interfere with that flow.
Sole developer. Product scope, data model, Supabase schema and RLS policies, the Server Action layer, two separate interfaces (child/parent), PWA setup, the KVKK cookie flow, and Vercel deployment.
A single Next.js 16 App Router application. The read path is Server Component → lib/dal/*; the write path is Server Action → Zod validation → Supabase. Authorization is three-layered: session checks in proxy.ts, Postgres RLS policies, and an additional guard inside the Server Action. The child context (activeChild) is kept in Zustand; server data is never copied into the store. Streak reconciliation runs as a scheduled job via /api/cron/reconcile-streaks. The system's second client lives in a separate repository: an iOS application written in SwiftUI. That client carries no backend of its own — it connects to the same Supabase schema, so the authorisation rules and RLS policies are identical to the web. Its maturity today is at skeleton level: parent sign-in, child selection and the flow of the panel screens are in place, and the Supabase client configuration sits as an optional loader — without configuration the app does not crash, it runs unconnected and the screens fill with sample data. So the architectural decision is made and verified; the data binding work is not finished.
If a parent approved the same task in two tabs, or a child tapped the button twice, the reward could be written twice. The update is now performed against the expected current value; the second write is rejected.
Trade-offOn a collision the user has to be shown a "try again" message; there is no silent merge. Compared to a plain UPDATE, that means more code and more failure paths.
If the child profile PIN reached the browser, a sibling or the child themselves could easily get past it. The verify_child_pin RPC performs the hash comparison on the server and cuts off brute-force attempts by counting consecutive failures.
Trade-offEvery profile switch costs a network round trip, and profiles cannot be switched while offline.
Device clocks and time zones are not trustworthy; a child could earn a streak by changing the device date. The streak is recalculated server-side by a scheduled job.
Trade-offThe streak can update late, only once the cron has run; the user does not see the change immediately.
Supabase was chosen so that the critical part of authorization — that a child can only see their own tasks — could be expressed as a database policy rather than in application code: even if the application layer has a bug, no data leaks. Zod serves as a single schema, used both on Server Action input and on the form side. Zustand holds only short-lived client state; the source of truth for server data is always the server.
Two independent authorization layers: Postgres RLS and an in-Server-Action guard — if one is bypassed, the other stops it. The PIN is hashed and brute-force counted. Cloudflare Turnstile is active on registration, login, and the child→parent switch. The parent action lock is temporarily released through password verification. Security headers and a Report-Only CSP are defined in next.config.ts. Google Analytics loads only after cookie consent is given and only in production (KVKK).
Installable as a PWA, with a service worker and an /offline fallback page. 26 test files with Vitest, weighted toward the concurrency behaviour of the approval and reward flows. Lighthouse mobile (my own measurement): 100/100 — September 2026. In the same measurement the median LCP is 1666 ms, total blocking time 1 ms and CLS 0.
Today I would build the concurrency guarantee as a database constraint rather than with CAS in the application layer. On Ustura I solved appointment overlap with a Postgres exclusion constraint; that approach requires less code and moves the guarantee out of the application and into the database itself. Second, instead of leaving the CSP in Report-Only I would switch it to enforce mode before going live — collecting reports does not stop a violation.
Tell me what you want to build; I'll tell you up front how long it takes and where to start.