Skip to content
Work

Jiuup

  • Web app
  • Full-stack development
  • UI/UX design
  • Realtime chat

Meet & connect with friends everywhere.

Role
Founder & lead developer
Timeline
March 2026 to present
Team
  • Brian Chan, Founder, lead developer, UI/UX design
  • Brian Wu, Algorithm tuning
  • Jeannie Tsai, Colour & UI design, print collateral, character design
  • Carlos Lu, Planning & on-campus promotion
  • Emily Chang, Marketing, promotion & planning
  • Flora Huang, Branding, marketing, back-office design & data analysis
Built with
Next.js, TypeScript, Supabase, Capacitor, Tailwind

It all came from a conversation with a classmate.

“There are so many people at this school,” he said, “so why does everyone still seem lonely? And the things I’m into always feel too niche. I can never find anyone to do them with.”

Jiuup’s feed on a phone and on a laptop. Around them are posts written by students, in Chinese, such as asking who wants to drive back to Taipei together, who wants to play badminton at 7:30, and who wants to go to the new café.

The problem

Most people on a campus want to go out more than they do. A small survey we ran with 25 students pointed this way, and research says the same. People expect others to be less interested in connecting than they really are (Epley and Schroeder, 2014), and they underestimate how much a message from them would be appreciated (Liu et al., 2022). What stops them is the cost of being the one who asks. People often don’t want to be the one to pick the people, send the message, and carry the risk of being rejected.

How to let people find each other?

A thought: wanna do stuff. It branches into asking somebody — ask who? can't find anyone; Brian might be interested, but he is not available today — and into not worth the hassle. Every branch arrives at the same answer: maybe next time.

Our solution

We built a way to take the cost out of asking. Asking one person means choosing who to ask and then hearing their answer. A post is open to anyone who is interested, so nobody has to be picked and nobody has to say no. People who don’t feel like going simply scroll past. Anyone can post an activity, tag it, and say what it needs. You can also join one that somebody else has posted. After that the hangout has its own room, where you meet the people in it, talk, work out the details, and then go and do the thing in person.

Jiu is the Mandarin reading of 揪, which means to get people together. Up expresses a feeling of speed and success. So we named it Jiuup, pronounced jiu-up.

Jiuup is a campus platform where students post activities, meet people who like the same things, and keep talking until they meet in person.

Get inFind or startTogetherIn personalready set upInvite link or QR codeor1BrowseNo account neededPublic activities only2Sign inOnly when you actGoogle, Apple or LINE3Set upName, school, interestsFirst time only4The feedPublic and your campusFilter, search, sort5Post an activityType, time, place, seatsPublic or campus only5Join oneOpen now, seats leftInvite-only needs a code6The roomRealtime chat and notesPolls and lucky draws7Meet in real lifeGo and do the thingIn person, togetheralready set upInvite link or QR code1BrowseNo account neededPublic activities only2Sign inOnly when you actGoogle, Apple or LINE3Set upName, school, interestsFirst time only4The feedPublic and your campusFilter, search, sort6The roomRealtime chat and notesPolls and lucky draws7Meet in real lifeGo and do the thingIn person, together5Post an activityType, time, place, seatsPublic or campus only5Join oneOpen now, seats leftInvite-only needs a code
From wanting to go out to actually meeting up.

Why it is shaped like this. You can browse the feed without an account, and we only ask you to sign in when you want to join or post. This way a post can be shared as a normal link and anyone can read it. Posting and joining both lead to the same chat room, so we only had to build one room and make it good.

UI design

We wanted Jiuup to feel bold and lively. The goal I cared about most was legibility, with strong contrast that still feels soft on the eyes.

We used Figma to brainstorm and to try out layouts and colours with our designer. We started from a plain skeleton of the feed.

An early skeleton of the Jiuup feed. A plain light page with a greeting, a row of category tabs, and two simple post cards with a name, a title, a seat count and a join button.
The first skeleton of the feed before design.

In the middle of the project the design kept getting heavier. We had not tested it with users yet, but we could tell we were drifting away from the problem we wanted to solve. So we decided to simplify. We put more of our effort into how smooth the software feels and into the UX itself, such as the flow and the transitions between screens. For the UI we kept only what was needed and spent our time on colour and on making it easy on the eyes. The categories are a good example. We considered parent and child categories and chose one flat row of fourteen, so one tap filters the feed.

A

Mark, colour, a post

Jiuup
  • Ground#202830
  • Blue#4D6F9A
  • Orange#CC7456
  • Glass#CED0D1
  • Online#7BDC74
YYin ChenNTHU MaterialsEatAnyoneHotpot after the midterm?Four seats, near the south gate.2h ago3/5Hsinchu, EastJoinr 2420202032121620 in 44 target44r 12, pad 1620

Why these numbers. The card is frosted glass on a dark background, so we calculated the contrast on the real colour of the glass. Every button is at least 44 high, the smallest size a thumb can hit reliably. The blur is 12 because 24 made Chrome drop frames when scrolling.

B

The feed and the room

The feed323216161fr1fr1fr144044avatar 48bell 44tags 36gap 16200416image 2 to 1create 56inset 24
The roomnotes 320conversation, fluiddetails 320144060max 65%controls 44bubble r16pad 12 × 8send 44field 44

Why the layout. A post with a picture is exactly as tall as two normal posts plus the gap, so the columns always line up. In the chat room only the conversation stretches, and on narrow screens the side panels turn into sheets.

Architecture design

For the backend we first considered services from Cloudflare and Google. At that time we wanted to validate the product quickly and iterate fast, so we chose Supabase. It was quick to build with, its database is standard SQL that we can migrate later, and its realtime feature is well optimised for chat.

ClientsFrontend (Next.js)Server (Vercel)Backend (Supabase)OutsideHTTPSrequestssessionservice roledailyqueries and RPC, directWebSocketchangeswebhookOAuthpush notificationBrowserAny phone or desktopInstalled PWAService workerPagesServer-rendered feedFeed rankingScored on the deviceState and cacheSWR and localStorageShield layerCache, breaker, retryRealtime providerSwappable transportMiddlewareSession and roleAPI routesRate-limited (Redis)Scheduled jobsClean-up, remindersAuthPKCE sign-inPostgres29 tablesRow-level securityFunctions, triggersFile storageRealtimeChanges, presencePush functionOn each notificationSign-inGoogle, Apple, LINEPush serviceWeb Push
ClientsOne codebase, in the browser or installed
  • BrowserAny phone or desktop
  • Installed PWAService worker with a cached shell and Web Push
FrontendNext.js 15, React 19, TypeScript, Tailwind
  • PagesThe first screen of the feed is rendered on the server and the rest hydrates
  • State and cacheUser context, SWR and localStorage, with optimistic updates
  • Feed rankingEvery post is scored on the device, per person
  • Shield layerEvery request goes through cache, breaker, dedupe, queue and retry
  • Realtime providerOne interface for live data, so the transport can be swapped
ServerVercel
  • MiddlewareChecks the session on protected routes and the role on admin routes
  • API routesInvite codes, reminders, push sign-up, HEIC conversion, share images
  • Scheduled jobsDaily clean-up of old posts, metric snapshots, reminders and re-engagement
  • Rate limiterUpstash Redis, with a memory fallback
  • Admin and adsRetention and click-through for the team, and a results link for each advertiser
BackendSupabase
  • AuthGoogle, Apple and LINE, over PKCE
  • Postgres29 tables, functions for polls and invite codes, triggers for seat counts
  • RealtimeMessages, members, read state, presence and typing
  • StorageAvatars, post pictures, chat images and files
  • Push functionA database webhook on each notification, sent out as a push

Why it is wired this way. The app reads and writes to the database directly, so most requests do not pass through our server. This makes every query faster and gives us live updates without running our own socket server. We only use the server for things that need a secret key, such as scheduled jobs and push notifications. Realtime is wrapped in one interface so we can replace the provider later without changing any screen.

Shield layer and security

  1. Cache
  2. Circuit breaker
  3. Dedupe
  4. Queue
  5. Retry
  6. Supabase
  • The cache uses memory first, then localStorage. Expiry is jittered by 20% so entries do not all lapse at once.
  • The circuit breaker stops asking after repeated failures and serves the stale copy. It tries again after a cooldown that doubles each time.
  • Retry uses exponential backoff with full jitter and never retries a 4xx.
  • The realtime guard reconnects with backoff, pings every 30 seconds, disconnects after five hidden minutes and batches bursts.
  • BrowserCSP, HSTS, frame and referrer policies on every response
  • SessionPKCE sign-in, and middleware blocks requests without a session
  • DataRow-level security in Postgres, over a hundred policies
  • AbuseLimits at two levels, Redis at the API and SQL inside the insert policies
  • InputZod schemas, plus an allow-list for file types and sizes
  • InvitesCodes are checked on the server, ten tries in fifteen minutes
  • AdminRole is read from the database, and every action is logged

Why they exist. There is no backend of our own between the app and the database, so two jobs that a backend normally does had to go somewhere else. The first is protecting the user from a slow or failed query. When many identical requests arrive together the shield layer only sends one. When a service keeps failing it stops calling it for a while and shows the last good data. It also retries at random intervals so that all clients do not reconnect at the same moment. The second is protecting the data. The app talks to Postgres with a public key, so checks in the interface alone are not enough. Each table has its own rules for who can read and write a row, and the limits on posting and messaging are written in the same rules because these inserts never go through our API.

Feed ranking

Popularity
Joins × 3 plus clicks, divided by (hours + 2) to the power 1.5
Freshness
Falls in a straight line over five days
Fit
Activity type 35, distance 25, age 15, familiar place 12.5, time of day 12.5
  • One slot in five is held for posts under six hours old.
  • A join counts 3, a save 2, a click 1, and each fades over thirty days.
  • With fewer than five interactions, fit falls back to distance, age, how full a post is, and chance.

Why it runs on the device. We do not need to run or pay for a ranking server, and changing the sort order is instant. We keep one slot in five for new posts so a hangout posted a minute ago can be seen before anyone has joined it. This matters a lot on a small campus. The small random value is seeded by the post, so the order stays the same when the page renders again.

Business

Jiuup also has an admin console. The teammate in charge of analysis uses it to track retention and click-through rates and shares the findings with the team, so we can decide together what to change next.

We also designed a way to earn revenue. After signing a contract with a local business, we can publish its ad on the platform. During the promotion period the business gets its own link where it can see the performance, the conversion rate and the analysis of its ad.

Challenges

We met many challenges along the way. Two of them shaped the project the most.

1Cold startAds and social postsA few people at a timeClubs, events, flyersA crowd at oncediscussedwhat we didPostJoinChatThe loop only starts when many join at the same time
2Choosing the platformWeb appNothing to downloada fewmostInstalls the PWAStays in the browserGets notificationsMisses messagesComes backLeaves

Cold start

A hangout platform is empty until many people join at the same time. At first we discussed common ways to promote it, such as paid ads and social media posts. Then we realised that if we could not reach many students at once, the cycle of posting, joining and chatting would never start.

So we chose the most direct ways to reach a large group of students. We worked with student clubs, and they used Jiuup for lucky draws and announcements at their events. This also helped the clubs, because they no longer needed to create another group chat just to make announcements. We also handed out flyers ourselves at the busiest places on campus.

Choosing the platform

My first idea was simple. I wanted the fastest way for everyone to access the platform, with no download and no extra steps. That is why we chose a web app, which also made development faster. Web push notifications seemed mature enough at the time, so I thought message notifications would not be a problem. People who wanted an app experience could install the PWA. We wrote an install guide and optimised the installed version for phones.

Later we found that installing a PWA is simple, but it is not the way people are used to getting an app. Many users kept using the browser. Because of the limits of web notifications in the browser, they often did not receive new messages after a chat started. This was the most direct reason the experience got worse. It was also the main reason many users left after the first wave of excitement.

We wanted to build iOS and Android versions, but we had to put that plan on hold because of the development cost and our limited team. We did not overcome this challenge. It taught me to think about how users will come back before I decide how they get in.

Summary

Jiuup started from one conversation with a classmate. We turned it into a product, ran it with real students for about six months, and kept changing it based on what we saw. The design became simpler once we returned to the problem we set out to solve. The architecture stayed small so that we could move fast. Working with clubs helped us get through the cold start. The platform choice is the part we could not fix in time, and it showed me how important it is for users to be able to come back. I will take these lessons into what I build next.