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. The anonymity is structural rather than promised: a question row carries no author column at all, so there is nothing for a host to look up, even with database access. Around that sits 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 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 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
1. I started with the smallest honest product: sessions, questions and votes. Three tables, one real time channel, no polls, no accounts for the audience.
2. 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.
3. I then solved the one problem anonymity creates, stopping the same person voting fifty times, without reintroducing identity. A random per browser token, hashed and stored client side, recorded only against votes and never against question text.
4. 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.
5. I pushed abuse prevention down into the database as security definer functions, so the rules hold regardless of what a modified client sends.
6. Only then did I broaden into polls, word clouds, ratings, rankings, surveys and quizzes, reusing the same anonymous participant model for each.
7. 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.
8. 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 at a six character code or QR scan, with no account, no email and no name field. The submission box says "Ask a question anonymously" and means it.
- Questions carry no author. The 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 join available to a host, a moderator, or me.
- 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, 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.
- 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 have no usage numbers worth quoting, so I will not invent any. What the build proved is different and, to me, more interesting.
It proved that structural anonymity 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 anyone is. 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.