Case Study
OpenFloor

Overview
People do not ask the real question in a company meeting when their name is attached to it. The awkward one about restructuring, or the strategy nobody actually understands, simply stays unasked. I built OpenFloor so anyone can join with a six-character code, no account and no name, and ask it anyway. Anonymity for questions is built into the structure rather than promised: a question row stores no author, account or device identifier, so there is nothing for a host to look up, even with database access. Around that sit live voting, polls, word clouds, ratings, surveys, timed quizzes, a moderation queue and a projector display, all updating in real time.
The Problem
Every all-hands meeting has a question nobody asks. It is usually the most useful one in the room. The moment a name is attached, the calculation changes: is this worth being the person who said it in front of the leadership team? So people stay quiet, the meeting ends on polite silence, and the same anxieties resurface later in private channels where nobody can answer them.
Tools exist to fix this, and the good ones work well, but they are built and priced for enterprise events. For a fifty-person company running a monthly all-hands, or a lecturer with a seminar group, the price and the setup weight are both wrong. You end up with a shared spreadsheet, a form nobody fills in, or a brave volunteer.
I also did not fully trust the promise. Plenty of products describe submissions as anonymous while quietly storing a user identifier next to the text. Anonymity that depends on a host choosing not to look is not anonymity, it is etiquette.

Constraints
I was building alone, so every feature had to earn its place against the maintenance it would create.
Anonymity for questions had to be real rather than claimed. My test was simple: if I opened the production database with full access, could I work out who asked something? If the answer was yes, the feature was not done.
Joining had to be frictionless. An account, an app install, or even a name field would kill it. A code on a slide, typed once, and you are in.
Moderation had to exist without becoming surveillance. Hosts genuinely need a way to stop abuse being projected onto a wall, but the tools for that must never require knowing who submitted what.
Cost mattered on both sides. It had to be cheap enough to run on a hobby budget, and priced so a small team could just buy it.

Approach
- I started with the smallest honest product: sessions, questions and votes. Three tables, one real-time channel, no polls, no accounts for the audience.
- Before writing the UI, I settled the anonymity boundary at the schema level and left the author column out of the questions table entirely. Every later decision had to work around that absence, which is exactly what I wanted.
- I then solved the one problem anonymity creates, stopping the same person voting fifty times, without reintroducing identity on questions. A random per-browser token, hashed and stored client-side, recorded only against votes and never against question text.
- Moderation came next as an optional workflow, then split into separate presenter and moderator roles once I realised the person running the slides cannot also triage a queue.
- I pushed abuse prevention down into the database as security definer functions, so the rules hold regardless of what a modified client sends.
- Only then did I broaden into polls, word clouds, ratings, rankings, surveys and quizzes, reusing the same participant model for each.
- Throughout, I ran repeated security and code audits and turned every finding into a numbered ticket with acceptance criteria, then added a CI job that replays all migrations against an empty database so the schema cannot drift away from what is committed.
- Finally I built the marketing site, pricing, billing and legal pages so the thing could actually be used by someone other than me.
Solution
- Audience members join with a six-character code or a QR scan, with no account, no email and no name field. The submission box says "Ask a question anonymously", and the schema backs that up.
- Questions carry no author. The questions table has never had a user or device column, and the submission function inserts only the session, the text, the status and the category. There is no column linking a question to a person, so there is nothing for a host, a moderator or me to join on.
- Vote deduplication uses a non-reversible hash mixing browser characteristics with cryptographic randomness, stored in IndexedDB with a cookie nonce so browser profile sync cannot make two devices look like one. It is written only to the votes table, never alongside question text.
- Rate limiting lives in Postgres: ten questions and thirty votes per minute per device, with generous per-IP complements sized for conference Wi-Fi, reading the Cloudflare-verified client IP after I proved by probing the live edge that the more obvious header could be spoofed.
- Moderation runs on content, not people: a pending queue with approve, reject, hide, pin and bulk actions, plus server-side blocked-word filtering that can flag or reject a submission outright.
- Roles are separated. Presenters get a projector display with spotlight, feed, poll results and word cloud modes, a QR overlay and themes. Moderators get their own dashboard and claim the role with a PIN the owner shares verbally, held as a hash in a table that no client role can read.
- Beyond Q&A: multiple-choice polls, word clouds, star, NPS and emoji ratings, ranked choice with Borda scoring, open text, multi-page surveys, and timed quizzes with leaderboards. These features are not held to the same standard as questions; see Reflections.
- Sessions expire. Ended sessions are hard-deleted after the plan's retention window, taking questions, votes and stored tokens with them.
- Exports to CSV, PDF and JSON contain the questions and their votes, and nothing about who sent them.


Outcomes
I don't have usage numbers to share yet. What the build proved is different and, to me, more interesting.
It proved that structural anonymity for questions is achievable without making the product worse. Hosts get a genuine moderation queue, live voting, analytics and sentiment, and none of it needs to know who asked what. The absence of an author column cost me nothing in features and removed an entire category of risk.
It also proved that assumptions about security are usually wrong until tested. My per-IP rate limiting was going to use the obvious header until I probed the live edge and found a client could poison it, which would have handed an attacker a way to lock a whole venue out of asking questions.
Finally, it shipped and stayed shipped: a real domain, a strict content security policy, Stripe billing, a public privacy policy backed by an actual retention job, and continuous integration that replays every migration against an empty database before anything merges.
Reflections
The honest tension is between moderation and anonymity, and I did not resolve it evenly. Q&A is clean. Everything else is not. The same per-browser token identifies a participant across polls, ratings, word clouds and quizzes, and quizzes deliberately ask for a nickname so a leaderboard can exist. If someone types their real name into a quiz, that name now sits beside an identifier that also keys their poll votes. Nobody can reach their questions, but I introduced a linkage that the Q&A design was specifically built to avoid. I would scope the identifier per session and per feature, or rotate it, so those rows cannot be joined at all.
I would also tighten reads. Vote rows are visible to anyone while a session is live, and presence broadcasts the token to every connected client. Neither exposes a person, but both give away more than they need to.
Two smaller things. The privacy policy describes the device token honestly but says nothing about IP-based rate limiting, which it should. And moderation decisions leave no audit trail beyond a status field, which is fine for anonymity and poor for accountability.
Given the time again, I would ship the Q&A alone and let it be a small, sharp product rather than racing an established suite on breadth.