Skip to main content
강홍재/ James
← Work
EMBA2026 · Solo Builder· Started(First Commit date)

SPARK Live

A seminar Q&A where the audience follows the slides on their phones, asks by name on the slide they are looking at, and the presenter answers per slide starting with the most-upvoted - built from four documents in three days and 252 commits, run at a real event, and the result was five questions.

  • Next.js
  • TypeScript
  • Supabase Realtime
  • Cloudflare Workers
  • Self-hosted
  • Playwright
  • QA

Setup

Problem

Questions do not come at seminars. The room makes raising a hand awkward, so people let it go, and when a question does come, the presenter struggles to answer it without knowing which slide it was about. When the event ends, slides and questions scatter and nothing stays with the club. Those were the three problems written in the BRD. Live Q&A services already exist. There were two reasons to build one anyway. One was to fit SPARK's event flow. The other was that the seminar's theme was "if you can write it, you can build it." The goal was to show, at the seminar, that a non-developer can build a service from four documents - BRD, PRD, IA, CLAUDE - and to hand those documents to participants as samples. This app was built as the evidence for that claim.

Context

Built for the AI seminar of SPARK, the startup club at SKKU EMBA (2026-09-28, 19:30, Korea Conference Center). The first commit was on 26 September at 21:41 and the event was the evening of the 28th, so there were a little over two days. Of 252 commits, 238 landed on the 27th alone; by type they split into docs 128, feat 64, fix 30, test 27, refactor 3. More documentation commits than code is the character of this project. The four documents came first, 70 PRD acceptance criteria (ACs) came out of them, and each AC got one Playwright test. Production is a Cloudflare Worker in front of a self-hosted Supabase on a Mac mini, exposed through a temporary tunnel. I knew the temporary tunnel changes its address on every restart and caps concurrent requests at 200, and still decided the first event would run on it, with a note to switch to a named tunnel if the audience looked likely to pass 100.

Users

Three kinds of users. The audience are EMBA classmates, non-developers, on smartphones, entering with one QR scan and no login or signup. The presenter stands at a laptop and projector and only advances slides during the talk. Staff upload slides and check records before and after a session. The roster for the actual event held 46 people. To be honest about it, far fewer actually used it. Eight devices left a trace of participation, and four of them asked a question. Devices that only entered were not recorded, so I do not know how many people came in through the QR code.

Hypothesis

If questions go unasked not for lack of courage or interest but for lack of a path - asking on the slide you are looking at, without raising a hand, leaving only your name - then letting people follow the slides on their phones and ask right there should yield ten questions per session. The BRD's success criteria were: works through the end with 50 concurrent users (B-1), ten or more questions per session (B-2), and 70% or more of attendees joining via QR (B-3). All three carried a "needs confirmation" mark.

Build

What I did
  • Wrote the four documents (BRD, PRD, IA, CLAUDE) first, then turned PRD acceptance criteria AC-01 to AC-70 into 69 Playwright tests. Every test name starts with its AC number ("AC-05 when the presenter advances, an audience screen on the current slide shows the same number within 2 seconds"), and the 2-second bar is a single constant in tests/helpers.ts
  • Audience screen - enter by QR, follow the presenter within 2 seconds, ask by name on the slide you are viewing, three reactions. Presenter screen - opened by a secret link, advance, new-question alerts, a phone remote tab. Staff - slide upload, hide, reorder, video, roster entry, handouts, a satisfaction survey
  • Q&A review - questions grouped by slide number and, within each group, sorted by empathy count (the sum of the three reactions), with written answers. The flow record of which slide was advanced when is written by a DB trigger, so the record cannot be skipped even if the advance function or the status API changes
  • Realtime - Supabase Realtime private broadcast channel plus presence. On disconnect it reconnects at 1.5s × 2^n (max 10s) and falls back to 3-second HTTP polling in between. Notifications are sent from DB triggers via realtime.send
  • Slides - PDFs are converted in the browser with pdf.js to 1600px-wide JPEGs (quality 0.85). Korean CMaps, standard fonts, and wasm are copied by a postinstall script. The PPTX converter (Gotenberg) only turns on when GOTENBERG_URL is set and is absent in production, so PPTX files are exported to PDF in PowerPoint before upload
  • Deployment - moved from Vercel to Cloudflare Workers (vinext build), with spark-live.pages.dev as a Pages proxy that forwards requests to the Worker. The backend is a self-hosted Supabase (v0.8.2, Envoy gateway) on a Mac mini exposed through a cloudflared temporary tunnel. A script swaps the Worker secrets when the tunnel address changes, with no rebuild
  • Load - the AC-32 test has 50 audience members enter at once, three advances all delivered with images within 2 seconds, ten simultaneous questions within 2 seconds, and an advance right after 150 reactions within 2 seconds. I confirmed that the Realtime default (100 events per second) overflows when 50 people mash reactions and silently drops advances, questions, and alerts for 5 to 60 seconds, and wrote a script that raises it to 5000/1000/500
  • Operations record - STATUS.md keeps, for each of 24 deployed versions, the rollback command and a post-deploy check (home, /events, /new return 200, a nonexistent session returns 404), whether the DB changed, the paths of 7 backups taken before migrations and deletions, three rehearsals, the pre-event checklist, and the day-of rollback plan
Product decisions
  • No anonymous questions; a name is required, with a nudge toward real names. Anonymity might have produced more questions, but I judged that what a club seminar keeps is worth something only if it says who asked what. The "anonymous" wording on slide v6 was replaced too
  • No audience signup, login, or registration, and no accounts for staff or presenters either - secret links instead. The cost, written plainly in the docs: the device ID is chosen by the browser, so a new ID gets around the 10-second interval and the one-reaction limit
  • The presenter's secret key is hidden from the address bar, so a projector or a screen share cannot leak presenter rights. Only a sha256 hash of the key is stored, and no referrer is sent
  • Deletion is blocked by privilege, not by code convention - a migration revokes DELETE and TRUNCATE from service_role, and there are no DELETE statements or cascading deletes in the code. Since rehearsal questions cannot be deleted either, rehearsals run in a separate session
  • No PPTX conversion server in production - conversion is one step the presenter does in PowerPoint, exporting to PDF. Two days before the event, one line of guidance was cheaper than running one more server
  • A change freeze declared at 17:28 the day before the event - code, DB, and settings all left as they were. Each later deploy is marked in STATUS as "an exception to the freeze"
  • A rehearsal finding built the same day - when the presenter went to slide 5 on the console and back to 2, the audience could only see slides 1 and 2, so the audience was allowed to view up to the furthest slide advanced
  • Nothing missing from the documents was guessed; it was marked "needs confirmation" and asked. STATUS lists 16 items implemented exactly as the documents' "needs confirmation" notes said and 11 decided because the documents were silent. Sixteen "needs confirmation" comments remain in the code
QA considerations
  • Are the acceptance criteria 1:1 with tests - 68 of the PRD's 70 ACs are named in the 69 Playwright tests. The two missing, AC-54 and AC-55, belong to F-14 (question pre-approval), which was not built, so the test list is the implementation scope. There is no CI, though; tests only run locally with pnpm test
  • Does a slide advance still get through when 50 people press reactions at once - the most dangerous property was that Realtime drops anything over its quota without an error. I raised the quota and recorded the numbers: with 50 simulated users locally, advance (with image) 0.5 to 1s and question arrival 0.2s; on real devices, 60 and 100 devices at about 1.1s with zero failures, partial 429/500 failures from 150 devices up
  • Were flaky tests written up as flaky - two full runs with 4 parallel workers gave 65/69, a different four failing each time, all passing when run alone. The causes were timing thresholds under load and a long-running dev server rendering stale code (confirmed from the trace). I did not write 69/69
  • Can the event day be rolled back - every one of the 24 deploys has its wrangler rollback command and the previous version hash recorded, and DB functions have a separate plan to restore from backup SQL via psql. Seven dumps before migrations and deletions
  • Does rehearsal data contaminate production - rehearsals run in a separate session, and 26 test and rehearsal sessions (95 questions, 597 reactions, 308 slide rows, 76 flow records) were deleted before the event. Because that breaks the no-delete rule, a one-line exception was added to PRD 5.6 first
  • Is the pre-event state free of rehearsal leftovers - the current slide number was once left at 19, so "confirm the current number is 1 before pressing Start" went on the checklist. Every slide checked (1600×900, no broken or clipped text), macOS auto-update off, sleep off, and a 401 confirming the production password is not the development value
  • Was the failure seen before the fix - STATUS records several times (AC-42 among them) that the test was confirmed failing on the pre-fix code before the change. A passing test can prove nothing
  • Input boundaries - PPTX files are checked by magic bytes to protect the converter, request bodies are capped at 16KB, presenter requests at 6 seconds. Known holes are written down too: the staff password attempt limit does not work on Workers because requests spread across isolates (13 wrong tries in a minute, no 429), and a question list past 1000 is cut by max_rows

Outcome

Metrics
  • 252 commits, 2026-09-26 to 09-29 (KST): 7 on the 26th, 238 on the 27th, 7 on the 29th. docs 128 / feat 64 / fix 30 / test 27 / refactor 3
  • 1,183 lines of documentation (PRD 449, STATUS 305, IA 166, README 136, BRD 78, CLAUDE 49), 175 tracked files, 13 migrations
  • 70 PRD acceptance criteria, 69 Playwright tests in 17 spec files, 68 criteria covered. Two full runs at 65/69, every test passing alone. No CI
  • 24 deployed versions, 22 rollback commands recorded, 7 pre-change backups, 3 rehearsals
  • Load: 50 simulated users locally, advance 0.5 to 1s and question 0.2s; on real devices 60 and 100 at about 1.1s with zero failures, partial 429/500 from 150
  • Event (2026-09-28, session XV357Y): roster 46, 8 devices with a trace of participation, 5 questions (4 devices, 1 answered), 5 reactions (4 devices), 1 survey response. Started 19:35, last advance 20:42 (slide 29 of 50)
  • Against the success criteria: B-2 (ten or more questions) missed at five. B-1 peak concurrency and B-3 QR join rate cannot be judged because connected-device counts were never stored (at least 17% by participating devices)
Result / Learning

It went up at the event, ran to the end, and the result was five questions. The target was ten. The session started at 19:35 and stopped at 20:42 on slide 29 of 50, and eight of the 46 people on the roster left a trace on a device. The satisfaction survey got one response, largely because the survey card was built to appear after the session ended and the session was ended at 14:20 the next day, so the survey only opened after the event. Of the three success criteria, B-2 was missed and B-1 and B-3 cannot be judged: device counts were only visible live and never stored, so there is no peak, and devices that only entered were not recorded. The building side went as the documents said. Four documents produced 70 ACs, 68 of them were pinned as tests, and after load tests, three rehearsals, and 24 deploys, changes stopped at 17:28 the day before. There is no record of an app failure during the event. As evidence for the seminar's theme - "if you can write it, you can build it" - it did its job. Put those two paragraphs side by side and the real conclusion of this project appears. Building something and getting it used were different problems. The behaviour was verified; participation was never designed. The 50-user load test passed, eight devices actually came in, and there is no data to explain why eight out of 46.

Retrospective
  • I wrote B-1 and B-3 as success criteria and built no way to measure them. Presence was only visible live, the peak was never stored, and devices that only entered were not recorded. The same person who pinned 70 acceptance criteria as tests left two of the three success criteria unmeasurable. Feature ACs and business KPIs did not get the same rigour.
  • I tied the satisfaction survey to session end and ended the session the next day. The one window for collecting responses in the room passed with the survey closed, and there is one response. Across three rehearsals I never once hit the fact that the presenter stopping and staff pressing End are different moments. The rehearsals verified features, not the running order of the event.
  • There is no CI. Sixty-nine tests run only locally, and nothing guarantees that any of the 24 deploys ran them first. Writing the post-deploy 200/404 checks by hand every time was the cost of not having CI. At 238 commits in a day, setting up CI looked like time I could not spare, and that call is wrong even in hindsight.
  • I never designed the audience's path from entering to asking. The QR join rate was at least 17%, and there is no record of when or where the QR was shown or how many times joining was encouraged. The pre-event checklist had server, slide, and password items but no step for inviting the audience in. The app moved a slide in under two seconds; nobody designed the part that gets people into the app.
Tech stack
  • Next.js 16.3.6 (vinext 1.0.0-beta.12)
  • React 19.2.8
  • TypeScript
  • Cloudflare Workers + Pages 프록시
  • Supabase 셀프호스팅 v0.8.2 (Postgres · Realtime · Storage)
  • cloudflared 임시 Tunnel
  • pdfjs-dist 6.3
  • Playwright 1.63
  • wrangler 4.140
  • Mac mini