The deliberately narrow shop-window (I3): venues, venue events, photos and a bare attendee count. No user is ever rendered — not by name, not by alias. The wall doubles as the conversion funnel: "42 going" without who is the sign-up pitch.
ShowUP is a social network built entirely around local events — no event, no post. People use it to physically show up at parties, gigs and gatherings, which makes it different from an ordinary social app in one uncomfortable way: a leak here isn't embarrassing, it's a stranger knowing where you will be on Friday night. This review walks through how that single fact shaped the whole design — the threat model, the layered defenses, the app flow, and the mockups-v2 screens that carry each guarantee into the UI.
The core adversary is a stalker: a stranger trying to track a specific person across events to find them in the real world. Everything else follows from making that impossible. The answer is not invisibility — a hidden person can't be counted, can't chat, can't be checked at the door. The answer is pseudonymity relative to the viewer:
Friends see the real you. Real name, where you're going, what you host. You are not
hiding from your mates.
Strangers see a fresh generated alias per event — DrunkBeaver67
here, SneakyOtter12 there — with no way to link the two.
Countable within one event, unlinkable across events.
Two further adversaries round out the model. The person holding your unlocked phone — a friend "just checking something", a snatch-and-scroll at a party — is answered by a local app-lock PIN with a background blur cover. And the gatecrasher at the venue door is answered by per-guest single-use door codes, verified atomically.
Crucially, every network-facing guarantee must hold in the database itself (Row-Level Security), so a modified client changes nothing. The UI is a convenience; Postgres is the boundary.
Security here is not a module — it's a stack of independent layers, each answering a different attacker, each surviving the failure of the others.
Fig. 1 — The defense stack. L3 (amber) is the load-bearing layer: every read path resolves identity through it, enforced by RLS in the database.
The security spec (doc 18) reduces the whole design to eight invariants. They are merge gates, not aspirations — an adversarial test suite runs them in CI against every read path as ten different actors (guest, stranger, friend, blocked, host, door staff…), and a change that breaks one does not merge. Not behind a flag, not "temporarily".
| # | Invariant |
|---|---|
| I1 | A stranger cannot resolve a private user's real identity through any read path, at any event. |
| I2 | A private user's aliases cannot be linked to each other across events. |
| I3 | A guest sees only venue profiles, venue-hosted approved public events, their posts, and attendee counts. Never a user profile, never a name. |
| I4 | Private users are unfindable by search — the "hidden user" response is byte-identical to "no such user", with no timing difference. A distinguishable response is a probe oracle. |
| I5 | Every guarantee holds in the database. A modified client changes nothing. |
| I6 | The service_role key never reaches a client bundle, a commit, or a log. |
| I7 | Every alias reveal is written to an append-only audit log — including passive reads. |
| I8 | Media is never reachable by a durable unauthenticated URL — signed, expiring links only; EXIF stripped on upload. |
The flow reads left to right: launch, the wall, the core loop, the physical door. Each dashed line is a security boundary — a tier you cannot cross without earning it (registering, joining, being a friend, being the host).
Fig. 2 — App flow with security boundaries. Dashed verticals are tier walls; deep links can never cross one on their own — every state-changing route stops at a confirmation screen.
Live mockups-v2 embeds, grouped by the security story each one carries. Every frame is the real HTML mockup — open any of them full-size.
The deliberately narrow shop-window (I3): venues, venue events, photos and a bare attendee count. No user is ever rendered — not by name, not by alias. The wall doubles as the conversion funnel: "42 going" without who is the sign-up pitch.
Data minimization at the door: email or OAuth, never a phone number. Behind it, layered anti-abuse — disposable-domain blocklist (synced, not hand-curated), ~2 signups per IP per day stored as salted hashes and purged in 48h.
The private/public choice is taught during onboarding, not buried in settings — the alias model is a selling point, and an uninformed default would betray it. Private means unlinkable to strangers, not hidden from friends.
The atomic unit. One attendee list, three renderings: friends resolve real people, strangers see per-event aliases with generated avatars, guests see a number. The resolver lives in the database (I1, I5) — the screen just draws what it's given.
Join = enter the chat. Aliases are stable within the event, so mute/ban still works; realtime subscriptions are authorized separately from table RLS, and a late joiner gets no backfill older than their join watermark.
The one sanctioned path through the alias: a friend request to the alias itself, unmasking only on mutual acceptance. Two pending requests to the same hidden person must be indistinguishable from requests to two different people — even a "duplicate" error would be a correlation oracle.
A private profile is unfindable by search (I4) and shows a stranger nothing — with a response identical to "no such user". Public profiles are findable by design: protection is opt-in.
The network can't help when the attacker holds your unlocked phone at the party. PIN or biometric on launch and on foregrounding; a blur cover hides the app-switcher snapshot. Progressive backoff on failures, no wipe.
The PIN is stored as a salted hash in the OS secure enclave and never leaves the device — so there is deliberately no "reset via email" backdoor. Forgot it? Sign out and re-authenticate; the account credential is the real key.
Every guest gets their own 6-char alphanumeric code — a personal ticket, not a shared password. Single-use by design, which kills pass-back at the door.
The host — or delegated door staff with strictly door-scoped rights — verifies the code. Check-in is atomic (FOR UPDATE): a code marks used exactly once even under simultaneous scans. ~10 wrong attempts throttles but never hard-locks — a queue at a club door can't be bricked. A private guest shows as their alias, even to staff.
The everyday screen, quietly shaped by the model: city-scoped (a region setting, not live GPS), forward-looking, friend-nudged. Location privacy by never collecting location.
A security-centered design is defined as much by its stated limits as by its guarantees — each one is written down so it's never mistaken for a gap, and so the Privacy Policy can describe it honestly.
| Content identifies people. A face in a photo, a distinctive writing style | Cannot be solved technically — the alias protects metadata, not content. |
| Small events shrink the crowd an alias hides in | Accepted — the alias still doesn't reveal identity. |
| A stolen backup reveals who exists and who attended what | Only the alias mapping is encrypted; said plainly in the policy. The key lives outside the database precisely so backups don't carry it. |
| The app PIN is not forensics-grade | It stops the person at the party, not a lab. Full-disk encryption is the OS's job. |
| No root detection, cert pinning, screenshot blocking | Defeatable theater — cost exceeds benefit when the database is the boundary. |
| Offline check-in can double-enter | Detected on sync and reported, never silently prevented. |
Distilled from nineteen internal design documents — concept, guest tier & RLS amendments, anti-abuse registration, app-lock PIN, door-staff delegation, the security requirements & adversarial test spec, and alias friend requests — plus the full mockups-v2 screen set. Every frame above embeds the real HTML mockup; diagrams are hand-drawn inline SVG.
A work sample by David Mkrtchian — product design, threat modelling, and system architecture for ShowUP, 2026.