# Minimal MVP Plan: P2P Poll App ## Recommendation Build the MVP with: - Vanilla JS or TypeScript - Vite for local dev/build - Yjs for shared state - `y-webrtc` for browser-to-browser sync - `y-indexeddb` for local persistence/offline recovery This is the best fit for a small poll app because the data model is tiny, concurrent edits are possible, and Yjs already solves merge/conflict handling while `y-webrtc` gives a direct P2P transport with only signaling infrastructure. ## Why This Stack ### Recommended: Yjs + y-webrtc - Best balance of simplicity and correctness - Clients connect directly over WebRTC - No custom conflict-resolution logic needed - Easy to add offline persistence - Good fit for a single shared document/room ### Not recommended for the first MVP #### PeerJS - Very simple transport layer - But you must design and maintain your own replication and merge rules - Fine for demos with a host-authoritative peer, weaker for true collaborative editing #### GUN - Fast to prototype realtime shared state - But official tutorials commonly use a GUN peer/relay server for shared data - For this app, its data model is less explicit than a CRDT and gives less control over vote semantics #### Automerge - Very capable CRDT and local-first model - But heavier than needed for a single small poll - Better choice if the project is expected to evolve into a richer collaborative app ## MVP Scope Ship exactly one shared poll per room. Included: - Join a poll by opening a shared URL - Add a new option - Vote for one option - Change your vote - Realtime updates across peers - Local persistence in the browser - Basic connection status UI Excluded: - User accounts - Multiple polls per room - Option deletion/editing - Authentication/authorization - Rich presence - Advanced discovery - Production-grade TURN/signaling deployment ## MVP UX Keep the interface intentionally small: - Poll title at the top - Option list with vote counts - One button per option to cast vote - Small form to add an option - Status line showing `connecting`, `connected`, or `offline` - Small label showing which option you voted for Join model: - Room ID in the URL, e.g. `/?room=poll-demo` - Users share the URL manually Identity model: - Generate a local `userId` once and store it in `localStorage` - Optional local display name, also stored locally ## Shared Data Model Use a single Yjs document per room. Suggested structure: - `poll` as a `Y.Map` - `poll.title` as a string - `poll.options` as a `Y.Map` - `poll.votes` as a `Y.Map` Each option record: - `id` - `label` - `createdBy` - `createdAt` Important design choice: Do not store vote counters as mutable shared numbers for the MVP. Instead, derive counts from `votes`. Why: - A user changing vote becomes a single write: `votes[userId] = optionId` - No double-counting logic - Concurrent writes are easier to reason about - Rendering counts from `votes` is trivial at this scale ## Sync Model ### Networking - Use `WebrtcProvider(roomId, ydoc)` - Start with the default public signaling servers for local development - If needed, swap to a self-hosted signaling server later without changing the app model ### Persistence - Use `IndexeddbPersistence(roomId, ydoc)` - This preserves state across reloads and helps when reconnecting after temporary disconnects ### Conflict behavior - Concurrent option additions merge naturally - Concurrent vote changes resolve at the per-user key level - Tally is recalculated from the merged vote map ## Suggested Project Structure ```text src/ main.ts app.ts state.ts sync.ts render.ts identity.ts styles.css index.html ``` Responsibilities: - `sync.ts`: create Yjs doc, WebRTC provider, IndexedDB provider - `state.ts`: shared structures, add option, cast vote, selectors - `identity.ts`: local `userId` and optional name - `render.ts`: DOM updates - `app.ts`: wire events to state and rendering ## Implementation Plan ### Phase 1: App shell - Create Vite app with vanilla TS or JS - Add a minimal single-page UI - Parse `room` from the URL - Generate and persist `userId` Success check: - App loads - Room ID appears in UI - User can type in the form ### Phase 2: Shared state - Add Yjs document - Create root shared maps - Seed default poll title if missing - Observe document updates and re-render UI Success check: - State exists in one browser tab - Refresh keeps local state when IndexedDB is enabled ### Phase 3: P2P sync - Add `y-webrtc` - Join room based on URL room ID - Show connection status from provider events Success check: - Two browsers on different tabs/devices see the same options - New options appear in near real time ### Phase 4: Voting logic - Implement `castVote(optionId)` as `votes.set(userId, optionId)` - Derive tallies from `votes` - Highlight the local user’s current vote Success check: - Votes update live across peers - Changing vote updates counts correctly ### Phase 5: Basic hardening - Trim/validate option labels - Prevent empty options - Ignore duplicate labels case-insensitively for MVP - Show simple offline/connecting text Success check: - Basic misuse does not break the UI - Reconnect restores updates ## Minimal API Surface These functions are enough for the first build: - `getRoomId(): string` - `getUserId(): string` - `initSync(roomId): AppSync` - `ensurePollInitialized()` - `addOption(label: string)` - `castVote(optionId: string)` - `getViewModel(): PollViewModel` - `render(viewModel)` ## Risks And MVP Mitigations ### NAT / WebRTC connectivity Risk: - Some peer pairs may fail to connect in restrictive networks Mitigation: - Accept this for MVP - Keep signaling configurable - Add TURN only if testing shows frequent failures ### Small public signaling dependency Risk: - Public signaling is fine for demos, not ideal for production Mitigation: - Treat it as replaceable infrastructure - Self-host later if the prototype is kept ### Duplicate or messy options Risk: - Users may add near-duplicate entries Mitigation: - Normalize labels - Prevent exact duplicates for MVP ### No trust/auth model Risk: - Any participant in the room can add options and vote Mitigation: - Accept for MVP - Frame rooms as small trusted groups ## Estimated MVP Size For one developer: - Initial prototype: 0.5 to 1 day - Polished MVP with basic resilience: 1 to 2 days Rough code size: - 250 to 500 lines plus styles ## Nice Next Steps After MVP - Copy-link button - Participant list / presence - Vote limit modes: single-choice or multi-choice - Option editing/deletion - QR code for room join - Self-hosted signaling server - PWA packaging for better offline behavior ## Sources - PeerJS getting started: https://peerjs.com/client/getting-started - Yjs collaborative editor guide: https://docs.yjs.dev/getting-started/a-collaborative-editor - Yjs offline support: https://docs.yjs.dev/getting-started/allowing-offline-editing - y-webrtc README: https://github.com/yjs/y-webrtc - GUN tutorial showing shared data via a GUN peer: https://gun.eco/converse - Automerge network sync: https://automerge.org/docs/tutorial/network-sync/