7.0 KiB
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-webrtcfor browser-to-browser syncy-indexeddbfor 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, oroffline - 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
userIdonce and store it inlocalStorage - Optional local display name, also stored locally
Shared Data Model
Use a single Yjs document per room.
Suggested structure:
pollas aY.Mappoll.titleas a stringpoll.optionsas aY.Map<optionId, Y.Map>poll.votesas aY.Map<userId, optionId>
Each option record:
idlabelcreatedBycreatedAt
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
votesis 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
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 providerstate.ts: shared structures, add option, cast vote, selectorsidentity.ts: localuserIdand optional namerender.ts: DOM updatesapp.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
roomfrom 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)asvotes.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(): stringgetUserId(): stringinitSync(roomId): AppSyncensurePollInitialized()addOption(label: string)castVote(optionId: string)getViewModel(): PollViewModelrender(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/