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

QRsocial

A social network scoped to one room. Scan the code at the door, prove you're actually inside, and join a pool that dissolves the moment you leave it.

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

No users yet. Here's what's actually built.

Countable facts, each verifiable by opening the repo. When this ships with real usage these numbers should be replaced by outcomes, not joined by them.

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

Everyone in the room, and no way to say anything.

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 digital community for people physically present at the same venue. That word — temporary — is where the interesting engineering is. Every social product is built to accumulate. This one had to be built to forget, and forgetting is much harder to retrofit than to design in.

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 headcount that can't be faked

GET /venues/:id/stats returns live occupancy broken into active, grace and pending. Not tickets sold and not check-ins — people whose phones are currently inside the fence, each one having passed a rotating code and a live selfie.

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.

Nothing to moderate tomorrow

The room deletes itself. There's no forum accumulating under the venue's name, no history to police, no archive anyone can be sued over — the operational cost of hosting one ends when the night does.

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

Ephemeral is a schema decision, not a marketing word.

When you left the venue in the hero, three different things happened to three kinds of record — because lifetime is a tier every model is assigned to, and a background sweeper in the realtime service is what makes the promise true.

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.

Sessional
While in venue
Presence · SelfieVerification · CompatibilityScore · FollowRequest

Scoped to a visit. A pending follow request deliberately outlives the venue — if you both left before answering, it is still waiting, because the alternative punishes whoever walked out first.

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

Not a prototype. Two screens from the iOS app.

Everything above this line is a demonstration I built for this page. These two are the actual product — the pool that withholds names, and the one screen in the app where a name appears.

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 itNothing here was reachable a moment ago. The name, the pronouns, the bio and the interests all arrive with the Connection record — and the button underneath states the terms plainly: private, unlocked only, closes 24h after the night.
It knows where it came from, and when it ends
The threadIt knows where it came from, and when it ends“Met at Khajaguda Hills Rooftop · private” is provenance in the header, not buried in a settings screen. The afterglow banner is the expiry stating itself — a disappearing thread that mentions it once during onboarding has told you on the day you cared least.

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