Inspiration
Most music apps introduce people by what's in their library: same songs, same artists, same genre tags. That's a weak signal. Two people can have completely different playlists and still use music for the exact same reason. One person puts on something quiet because they need space; another reaches for something loud for the same need, just dressed differently. Their taste doesn't overlap at all, but the role the song plays in their life does.
That's the whole idea behind Nova: different songs, same reason. Instead of matching on what you listen to, we match on why.
What it does
You pick a handful of songs that mean something to you. For each one, you drop a pin on a mood circle (how positive and how energetic it makes you feel) and pick one to three feelings from a shared vocabulary, like aching, defiant, weightless, and homesick, grouped by the kind of moment you reach for a song in. That's it. No essay, no 50-question quiz.
From there, Nova builds you a listening portrait (an AI-written read on what your songs say about you) and drops you into an interactive galaxy. It isn't a dump of every user on the platform but a bounded neighborhood: you, your strongest matches, a few far-off strangers, and newcomers still finding their place. Tap into someone else's star and you can hop into their neighborhood, exploring outward one connection at a time instead of scrolling a directory.
When Nova finds a real match, it doesn't just show you a percentage. It generates a Connection Card with:
- the specific thread connecting you two, not "you both like sad songs"
- the songs from both of your profiles that prove it
- one real difference worth bringing up
- a suggested first line, so you're not staring at a blank text box
There's also Wander: tap a song and see who else picked the exact same track but felt the complete opposite about it. Same song, opposite reason, which is its own kind of interesting. And if a Connection Card isn't enough, you can send someone a song with a one-line reason, even clipping out the 5 to 12 seconds you want them to hear first.
The goal was never a compatibility score. It's a reason to say something.
How we built it
Next.js, React, TypeScript, and Tailwind on the frontend; Supabase (Postgres + pgvector) on the backend; Three.js, React Three Fiber, and d3-force-3d for the galaxy itself.
Song identity is the boring-but-necessary part: a typed title and artist get resolved through MusicBrainz into a canonical song, with Gemini as a fallback when MusicBrainz can't confidently place it. Once a song is identified, it gets exactly one embedding, shared by everyone who picks it, so we never pay for the same song a hundred times.
Matching happens per pick, not per person. Each pick (one song plus one person's mood and feelings about it) gets its own vector: the song's embedding plus that person's mood, weighted so the song's meaning leads and the mood position is just a nudge. We deliberately don't average someone's picks into one profile vector, because that would let five unrelated, cheerful songs quietly bury the one song that's really about their grandmother. pgvector does the first pass with cosine similarity. It's cheap, fast, and doesn't call an LLM yet.
Then Gemini steps in and makes a real judgment instead of just narrating a number. It reads both people's full public profiles and either finds a specific, evidenced connection or throws the pair out. It never scores the match itself. Instead, it answers categorical questions (how specific is the evidence, how much of it is there, how much is there to talk about), and we compute the final score from those answers in code.
The galaxy's clusters aren't a fixed list we hand-picked either. An unsupervised layer runs underneath: HDBSCAN clustering over everyone's picks discovers listening-motivation groups on its own, and Gemini writes a name and description for whatever it finds. So the map of "kinds of listeners" isn't something we guessed at up front; it's something the data produces.
Challenges we ran into
The most embarrassing one: we originally asked Gemini for a 0 to 100 match score directly. It gave almost every pair an 85. Asking a language model to invent a precise number is asking it to guess, not judge. We ripped that out and replaced it with the categorical-questions-plus-computed-score approach above, and suddenly scores spread out and meant something.
The second was sneakier and took longer to catch. Our match vectors combine a song's meaning (768 numbers) with a person's mood (2 numbers), but we hadn't normalized either side. The mood numbers turned out to be geometrically far "louder" than the song embedding, so matching was quietly about 95% "did you put the dot in the same spot" and barely about the song at all. Two people who had never heard each other's songs but happened to feel similarly energetic would score a near-perfect match. The fix was normalizing the song part and deliberately shrinking the mood part's weight so the song leads.
Speed was its own fight. Every connection judgment is a Gemini call reading two whole profiles, and the model started "thinking" by default before answering, taking 5 to 18 seconds per pair. A cold load of your Connections page could take 20+ seconds. We capped how much the model was allowed to think, raced a faster model against the main one as a backup, and started judging a few pairs in parallel instead of one at a time. That took a ~21-second cold load down to about 4 seconds.
We also found a real security bug before it mattered. Our post-login redirect only checked that a ?next= URL started with /, which a crafted address like /\evil.com can pass while a browser still treats it as evil.com. Anyone could have sent a teammate a link that looked like our own site and quietly forwarded them somewhere else after they signed in. We wrote a proper same-origin check and routed every redirect in the app through it.
What we learned
Similarity and connection are not the same problem. Vector search is great at finding candidates; it's terrible at deciding whether an introduction is worth making. You need something after retrieval that's willing to say "no." Nova rejects plenty of vector-similar pairs because there's no real evidence behind them, and that's a feature, not a failure rate.
We also learned that making AI output trustworthy is mostly not about the prompt. It's the boring stuff around it: forcing structured output instead of parsing free text, checking that every song a card references exists in that person's picks before it's ever shown, caching so you're not re-asking the same question, and being deliberate about what data is and isn't allowed to reach the model or the other person.
The most useful thing Nova produces was never a score. It's a specific, true reason two strangers might understand each other, and that turned out to be a much harder (and more interesting) engineering problem than "find similar vectors."
Built With
- animejs
- d3.js
- deezer-api
- gemini-embeddings
- google-gemini
- hdbscan
- itunes-api
- musicbrainz
- nextjs
- node.js
- oauth
- pgvector
- postgresql
- react
- react-three-fiber
- row-level-security
- shadcn-ui
- spotify
- supabase
- swr
- tailwindcss
- three.js
- typescript
- vercel
- webgl

Log in or sign up for Devpost to join the conversation.