Cyber2Tech

From searching alone,to playing together.

Product
Woonzero
Type
Our own product
Duration
Aug 2026 – ongoing
Stack
React · NestJS · Prisma
Briefly

Woonzero was already an active social platform, but finding people to play with wasn't its own layer; there was no game picker when creating a post, so which game a post was even about was mostly a guess. We added a layer on top: game identities, filterable looking-for-group listings, and a group chat that opens automatically once a listing fills.

Problem
No game layer, search scattered
Solution
Game identity + filtered listing + group chat
01Problem

Finding a teammate was a matter of luck.

Someone looking for a teammate on Woonzero had to rely on ordinary posts, comments or direct messages in a feed with no game tagging; without a game picker on the post composer, finding people to play with wasn't a dedicated feature, just something people improvised in the general feed. There was no filter for role, rank or available time, so finding the right person came down to luck more than search.

representative flow
02Constraints

Three constraints shaped the design.

K1

Game identity runs on self-reporting, today

Verifying rank through an outside service like Riot or Steam was its own body of work; rank and role were deliberately left to the player's own word, with a verification badge pushed to the roadmap. A listing owner weighs an applicant's stated rank and decides for themselves.

K2

Has to stay current in four languages at once

The community speaks four languages; listing and guide content has to stay equally current across all of them. A build-time test that catches a missing translation key keeps that discipline in place — the translation itself is still typed by hand.

K3

Listings can't turn into clutter

Unlimited open listings make the list unusable fast; every listing gets an expiry, and its state updates through a lazy transition computed on read rather than a separate timer.

03 — Approach

A listing searches for a role, not a player.

A player first links a game identity, then opens a looking-for-group listing stating the role, rank range and time they need. How you join depends on the mode the listing owner picked: some listings let you drop straight into an open slot, others require you to apply and wait for the owner to approve or reject you. A group chat opens automatically the moment the listing is created, so by the time the roster is full, everyone is already in it.

  1. 01Player profilegame identity linked
  2. 02Listing openedrole + rank range + time
  3. 03Joiningjoin directly, or apply and wait
  4. 04Group chatopens with the listing
  5. 05Playing togetherroster complete
listing.jsonrepresentative · LFG listing response (LfgResponse)
{
  "id": 482,
  "slots": 5,
  "filledSlots": 3,
  "joinMode": 1,
  "state": 0
}
What we tried and left behind

Feed ranking used to keep the most-liked posts of all time at the top; as the platform grew, that one rule turned into a real bug — old posts stayed pinned at the top forever. We also tried a Hacker News-style formula, a Wilson/Bayesian rate estimate, and a single slow-decay formula; none of them held up against years of accumulated history or the data we actually had (no dislikes, no view counts). What shipped is a time-invariant score, an affinity signal, and a small quota reserved for older posts that are explicitly tagged as archive.

04Screens

An open role has to show up immediately.

A team-search request from chat turns into a listing tagged open or full.
Linked game identities get added one by one, past teams sit on the same profile.
05What we learned

What we're carrying into the next project.

  1. 01We don't leave a simple rule in place once we know it will age badly ('most-liked of all time'); for feed ranking we tested several alternatives against real data before settling on a time-invariant formula.
  2. 02We don't pick a storage provider on unit price alone; at this scale, egress and integration cost mattered more than the per-GB difference.
  3. 03Keeping a separate database column per language stops scaling; embedded i18next plus a build-time test that catches missing keys turned out to be the more sustainable way to keep four languages equally current.
Next case
SmartMail Orchestration

Is your community's
search scattered too?