forked from quic-issues/427e7578-d7bf-49c8-aee9-2dd999e25316
311 lines
7.0 KiB
Markdown
311 lines
7.0 KiB
Markdown
# 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<optionId, Y.Map>`
|
||
- `poll.votes` as a `Y.Map<userId, optionId>`
|
||
|
||
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/
|