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
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.
- presence:joined ×4Sessional — Presence rows, alive only while inside the fence.
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.
Web, native iOS, HTTP API, realtime socket server, and two shared packages.
One ThemeColors contract, filled five ways — light, dark, and three venue skins that take precedence over mode.
Durable, sessional and volatile — encoded in the schema, not in a cleanup script.
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.
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.
Attendees are the users. The venue is the one who has to say yes.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
{
"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.
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.
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.
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.
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.
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.
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.



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.
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.
// 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;
}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.
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.
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