Back to Work
[ CASE STUDY ]

kizu

A private "group taste space" for small friend groups. Live at kizu.app. Friends drop the movies, music, and places they love into one bounded feed, queue what to do tonight, and get an AI "vibe read" of the group's taste. Built solo on Next.js 16, TypeScript, Supabase, and Vercel for $0.

Role

Solo Founder & Full-Stack Engineer

Duration

4 mo

Type

project

Location

Remote

~200ms

Page loads, was 1–2.5s

region co-location + request-memoized auth

18 users

Across 5 real groups

invite-only, at launch

$0

Total spend

free tiers only, custom domain

3

Pivots, one stack

12 migrations, 10-table final schema

Overview

kizu (kizu.app) is a private "group taste space" for small friend groups. Friends drop the movies, music, and places they love into one shared feed, save what they want to get to, decide what to do tonight, and mark when a recommendation actually landed. An AI "vibe read" reflects the group's collective taste back as an animated card. Designed, built, and shipped solo on Next.js 16, TypeScript, Supabase, and Vercel, on a $0 budget and non-commercial by design. The bet: in a world where everything is AI-generated, the recommendations you actually trust still come from a few people who get you.

The
Challenge

A taste app is worthless on day one, because an empty feed is an empty room. The usual fix, seeding fake activity, is exactly the lie a trust circle cannot survive. A feed built to maximize engagement would betray the premise, so it had to be finite even though finite feeds are worse for retention on paper. And every layer had to fit inside a free tier.

01

Cold start: a group taste feed is empty on day one, and faking friend activity destroys the trust the product is built on

02

An engagement-maximizing feed contradicts the premise; the feed had to end in "you're all caught up"

03

$0 budget: database, AI, hosting, email, and push all had to fit inside a free tier

04

Low frequency: nobody drops a movie every day, so the product needed a recurring appointment mechanic

05

Solo build, with no team to catch regressions in auth, RLS, or cross-region latency

My Approach

Phase 01

One North-Star Metric, Ruthlessly

Anchored the whole product on recs that landed per active group per week. A "landed" event fires when someone marks a thing you dropped as loved or liked. Features that did not move that number got cut, which is the only reason the surface area stayed small.

Phase 02

Anti-Doomscroll Feed

The group feed is bounded and finite. It ends in "you're all caught up," and you close the app. Infinite scroll exists in exactly one place, below that line, in the human-curated Curate river, where it is labelled as such.

Phase 03

Honest Seeding with Kizu Curate

Solved cold start by hand-curating drops attributed to real people: their name, their photo, where we met, their own words, and an explicit consent flag. Never fake friend activity inside a trust circle. The house curator is visibly the house.

Phase 04

Free-Tier Architecture

Server-components-first App Router on Vercel Hobby, Supabase free Postgres with RLS, and Gemini 2.5 Flash for the vibe read behind a provider-swappable interface so the model can be upgraded without touching call sites. No state library, no UI kit, no ORM.

Phase 05

Rec-as-Invite as the Growth Loop

Drop something for a specific person. If they are not a member yet, a no-wall public page at /r/<token> lets them preview that one item and react before signing up, and the rec attaches to their queue once they join.

Key Decisions

Decision 01

A finite feed, against every engagement instinct

Context

Infinite scroll is the default because it works. But kizu's whole claim is that recommendations from a few people who get you beat an algorithm, and an endless feed quietly turns those friends into content.

Outcome

The group feed ends in "you're all caught up." Infinite scroll survives only in the clearly-labelled, human-curated river beneath it.

Decision 02

Hand-curated real people over synthetic seed data

Context

The cold-start fix everyone reaches for is fake activity. In a product whose only asset is trust between five friends, that is the one lie that cannot be walked back.

Outcome

Kizu Curate attributes every seeded drop to a real, consenting person, in their own words. Seeding became a feature with a voice instead of a secret.

Decision 03

Authorize with the user client, write with the service-role client

Context

Supabase writes from Next.js route handlers failed with "new row violates row-level security" because auth.uid() is null on writes there. Loosening the RLS policy would have been the fast fix, and the wrong one.

Outcome

A project-wide pattern: verify identity and group membership with getUser(), then write through a service-role admin client with created_by set from the verified user. Secure, and immune to the token edge cases.

Decision 04

Pin the functions to the database's region before touching a single query

Context

Navigation felt slow, and the instinct was to blame the queries. The real cause was geography: Vercel functions defaulted to iad1 in Virginia while Supabase Postgres lived in us-west-2 in Oregon, so every query paid a 300 to 600 ms cross-country round trip.

Outcome

Co-locating functions in pdx1, memoizing auth per request with React cache(), and parallelizing a 7-query waterfall into about 2 stages took page loads from 1 to 2.5 seconds down to roughly 200ms.

Results & Impact

Live at kizu.app on a custom domain, used by 18 people across 5 real friend groups at launch. 13 app pages, 21 API route handlers, 12 SQL migrations, and a 10-table final schema, built by one engineer for $0 across three product pivots. Page loads sit at roughly 200ms. The AI vibe read, the weekly ritual, multi-group management, rec-as-invite, and PWA push all run on free tiers.

Learnings

What did this teach me?

Latency is usually geography, not SQL. The slowest thing in the app was a map, not a query plan. Discipline also scales better than features: when someone suggested something, asking a few more people before building it is the only reason the product stayed coherent through three pivots. And a whole-branch review caught a production-breaking bug that a per-file pass had missed, a leave-group query ordering by a created_at column the table does not have, which would have silently dumped users back into onboarding.

What would I do differently?

Watch cold starts sooner. A keep-warm job pinged the API lambda but not the pages lambda, and because nothing was watching TTFB, 3-second cold opens ran for a full day before anyone noticed. Beyond that, validate the mechanic with five friends before building it. The first two versions of kizu were fully designed, and one was deployed, before it was clear nobody wanted them. The plumbing survived all three pivots, which is the only reason that was survivable.

Skills Used

Solo Product Ownership 0→1Next.jsTypeScriptSupabasePostgreSQL & RLSGoogle GeminiPerformance DebuggingBrand & Design System