Skip to content
All workIn build
Client work · Venue-scoped social

QRsocial

QRsocial helps people meet others at the venue they are visiting. Scan the code, confirm you are there and join a shared room where you can chat or request a connection. Profile details appear only after both people agree.

11pm. Low light, loud room, and everyone facing the same view.

Client
Kalyan
Role
Product Design · Design System · Full-Stack
Scope
Web · iOS · API · Realtime
Status
In build · 2025
QR Social
Skyline Rooftop
4 here · faces only until you unlock
Faces in the room
92%
78%
85%
66%
01
Under the surface

Every tap up there wrote a record down here.

The prototype is not a video. Each action names the model it touched — Presence while you're inside, FollowRequest when you ask, Connection when they agree, ChatMessage until you leave. Scroll back up, do something, and watch this fill.

What the model didnewest first
  • presence:joined ×4
    Sessional — Presence rows, alive only while inside the fence.
data-venue-themeBAR
--canvaspage ground
--surfacecards, inputs
--inkprimary text
--mutedsecondary text
--linehairline borders
--accentthe one action
--font-display Space Grotesk — rendering live, not substituted.
02
What exists

In development, with no real-user outcomes yet.

The web experience, API and real-time interactions are at different stages from the native iOS app. The figures below describe build scope and should not be read as adoption or business outcomes.

5
Services in the monorepo

Web, native iOS, HTTP API, realtime socket server, and two shared packages.

72
Semantic token slots

One ThemeColors contract, filled five ways — light, dark, and three venue skins that take precedence over mode.

3
Data lifetimes

Durable, sessional and volatile — encoded in the schema, not in a cleanup script.

03
The brief

Help people at the same venue start a conversation.

Kalyan came with a straightforward observation about the venues he works with: a bar on a Friday night holds a hundred people who will never speak to each other, and the apps that claim to fix this are built on the opposite assumption — that you are at home, on the couch, swiping through people who are nowhere near you.

The brief asked for a temporary shared room for people physically present at the same venue. The design therefore had to make entry quick, reveal personal information only with permission and remove the venue's shared roster and chat after a visit.

The design constraint

A social app whose whole value is being in a specific room can never be more convenient than being in that room. Every entry step I added made the product safer and the door slower. That tension is the actual design problem — not the grid of faces.

The other customer

Attendees are the users. The venue is the one who has to say yes.

A more useful live headcount

The venue view counts active, pending and recently disconnected guests who entered with a rotating code, location check and live selfie. These checks increase confidence in the count, but do not make it impossible to manipulate.

The QR is the door, and it re-keys

Minting a new code retires every previous one in the same transaction. A host who wants the room closed rotates it; whoever screenshotted the old one is out. Access control the bar staff already understand.

A room that does not become a permanent forum

The shared venue roster and room chat are temporary, reducing the history a venue would otherwise need to manage. Accepted connections and private conversations have separate lifetimes explained below.

That is the pitch, not a finding. Whether a live room actually moves dwell time or bar spend is exactly the kind of claim this page can't make yet — it needs a venue, a season, and a comparison against nights without it.

04
Entry

Three proofs at the door. Two of them cost nothing.

Presence is asserted three ways, and each closes a hole the others leave open — a QR alone is copyable, GPS alone is spoofable, a selfie alone proves a face and not a place. But three proofs also sounds like three obstacles, so here is what each one actually costs someone half-distracted in a loud room.

010taps — decodes itself

Point at the code

Hold the phone up to the code on the bar. A continuous decode loop fires the moment it reads, so there is no shutter to find and nothing to confirm.

What it closes

Codes carry an expiry, and minting a new one atomically retires every previously ACTIVE code in the same transaction. A photographed code dies the moment the host rotates it.

020taps — passive

Stand where you already are

Nothing. Validation returns the venue's centre and radius, and position reports on its own for the rest of the night.

What it closes

75m fence, re-checked all session. Leaving opens a grace window instead of evicting you — phones lose GPS indoors, and being thrown out for walking past a wall is not acceptable.

032taps — capture, confirm

One photo of your face

Press the shutter, then Use photo. The camera never restarts between the code and your face — one video element is shared across both, so the switch is a cross-fade rather than a reload.

What it closes

Camera only, no library upload. It anchors the entry token and becomes your visible identity — the thing proving you're real is the only thing anyone can see.

Street to room
2deliberate taps
zero keystrokes

Three proofs sounds like three obstacles. Two of them cost nothing — the code reads itself and the fence checks itself — so the only thing asked of someone half-distracted in a loud room is to hold up a phone and press a shutter. Registration happens once, long before the door; nobody creates an account standing in a queue.

That count is the returning path. Your first time also costs one system camera prompt — unavoidable, and deliberately spent at the scanner rather than saved for the selfie, so permission is granted before anyone is pointing a lens at their own face.

05
On the wire

The name isn't hidden. It was never in the payload.

You unlocked a profile in the hero. This is what the server actually sent to make that happen — and what it refused to send before you had permission. Step through and watch displayName simply not exist until a Connection does.

QR Social
Find the code
It's on the bar and at the door
Codes rotate — an old photo won't work
01 / 6ScanPOST /api/v1/entry/validate-qr
{
  "venue": {
    "name": "Skyline Rooftop",
    "venueType": "BAR"
  },
  "geofence": {
    "latitude": 17.4326,
    "longitude": 78.4071,
    "radiusMeters": 75
  }
}

Validation only — no presence row is created yet. A scraped QR gets you a venue name and nothing else.

At the door

Accepting runs as one transaction — the request moves to ACCEPTED and the Connection is materialised together, or neither happens. A half-accepted request that unlocks one direction is the exact bug you cannot ship in a product about consent.

06
Data model

What disappears, and what a guest chooses to keep.

Leaving clears the shared room after a short location grace period. An accepted connection can remain because both people agreed to it, while a pending request may remain long enough for the recipient to respond. The table below shows those differences.

Durable
Persists
User · Venue · VenueQrCode · Connection

Only what survives the night. Connection is here — a connection you consented to is yours to keep, and it is the single exception to the whole ephemeral model.

Visit
Visit + response window
Presence · SelfieVerification · CompatibilityScore · FollowRequest

Presence, the entry selfie and compatibility score belong to a visit. A pending follow request can remain after the visit so the recipient still has time to respond.

Volatile
TTL + on exit
ChatMessage

No history for late joiners, gone when you leave. Someone arriving at 11pm cannot read what was said at 9pm — the conversation belongs to the people who were there.

07
The real build

Current screens from the iOS build.

Everything above this line is an interactive portfolio demonstration. The screens below come from the current product build; some use live services and the room screen still uses sample data, as described in the status section.

Six live, nobody named
Inside the roomSix live, nobody namedWave and Follow are two weights of intent — one drops a line in the room chat, the other is the request that could unlock a profile. Keeping the low-stakes action visually lighter is most of what stops the pool reading as a dating grid.
The first screen with a name on it
After consentThe first screen with a name on itThe name, pronouns, bio and interests appear only after both people accept the connection. The private conversation then remains available for 24 hours after the event.
It knows where it came from, and when it ends
The threadIt knows where it came from, and when it endsThe header reminds both people where they met and that the conversation is private. A visible expiry message explains when this thread will close instead of leaving that detail in onboarding.

One session, one build, in the app's dark default. The venue switch demonstrated above is live on the web client, which sets data-venue-theme from the venue record; on iOS the five token fills exist and the provider supports them, but nothing selects a skin yet. The room screen renders from fixtures — chat, inbox and profile are wired to the live socket.

08
Architecture

Why presence got its own service.

Request/response traffic and long-lived socket connections have almost nothing in common operationally. One scales on CPU per request; the other scales on how many connections a box holds open, and needs sticky sessions to do it. Put both in one process and the cheap thing scales with the expensive one — so you pay for the expensive one.

So the realtime service is separate and stateless. Rooms are venue:<id> and every socket also joins user:<id> on connect — both adapter-safe, so running N replicas behind a Redis adapter is a deployment change rather than a rewrite.

That split creates one problem: the API knows when a follow is accepted, and only the socket server can tell the other person now. Rather than pull Redis into an MVP for one message type, the two talk over an internal HTTP webhook with a constant-time bearer check, fired best-effort so a slow notification can never delay a response. It is explicitly a seam — the publisher is one function, and swapping it for pub/sub touches no callers.

Shared event contractpackages/types
// One package both ends import, so the
// client and server cannot drift apart.
interface ServerToClient {
  "presence:joined":  (a: AttendeePreview) => void;
  "presence:left":    (id: string) => void;
  "presence:updated": (p: PresencePatch) => void;
  "chat:message":     (m: RoomMessage) => void;
  "follow:request":   (r: RequestPreview) => void;
  "follow:accepted":  (c: ConnectionRef) => void;
}
09
Where it stands

What's done, what isn't, and what I got wrong.

The entry handshake, the privacy gate, the follow loop, the ephemeral chat and the presence sweeper are built and wired. The web client runs against the live socket. The native iOS app is further behind — its room screen still renders from fixtures while chat, inbox and profile are connected, and the host-side tooling exists as three API endpoints with no interface on top of them yet.

I am also treating the compatibility score with suspicion. It is computed from shared interests and profile attributes, and a number on someone's face is a strong claim to make about a stranger. A 66% is a judgement rendered by a system that has met neither person.

What I'd revisit

The grace period is the decision I keep returning to. Two to three minutes was specified as an anti-flicker measure for bad indoor GPS, and for that it is right. But it also means someone can leave and stay visible in the pool for three minutes after — which is a safety property, not a UX one, and deserves to be argued as such rather than inherited from a brief.

Screenshot prevention is specified and not yet implemented. Worth being precise in public: on both platforms this is a deterrent, not a guarantee. A second phone pointed at the screen defeats every version of it, and a privacy model that quietly relied on it would be dishonest.
Next — Enterprise Telemetry

QRsocial decides who is in a room. Farm OS decides whether the room is safe to be in.

The same instinct, pointed at hardware — a system that reports its own state, and says so before it fails. Custom ESP32 nodes, printed enclosures, and a dashboard 22,000 birds depend on.

Farm OS