Inspiration
We wanted to create a digital canvas where the internet can leave permanent marks—a collaborative grid where every contributor's identity and submission are verified and immutable. The inspiration came from exploring how decentralized identity (World ID) combined with a persistent, global database could enable trustless human coordination on a shared creative surface.
What it does
Proof of Attrition is an interactive grid-based drawing application where users can claim an empty cell, draw, and permanently submit their artwork to a global canvas. Each user can only submit once per verified identity (backed by World ID), and submissions persist forever on the blockchain-adjacent backend. The app features:
- A zoomable, interactive grid of cells (with hundreds of potential slots)
- World ID integration for identity verification with guest fallback
- Drawing canvas powered by
react-sketch-canvas - Grid snapshot export to PNG via
html2canvas - Real-time submission status tracking and decay countdown
- Backend uniqueness constraints ensuring no duplicate submissions per identity
- Persistent MongoDB storage with CORS-protected API
How we built it
We architected a full-stack monorepo with a React + Vite frontend (SPA) and Express + Mongoose backend (REST API):
Frontend (client/):
- Built with React (Vite) + CSS Modules for component isolation
- Centralized state management in
App.jsxorchestrating popups (login→draw→confirm) useGridhook manages cell fetching, zoom, and local grid stateuseSessionhook handles World ID auth flow and nullifier tracking- Drawing UI powered by
react-sketch-canvaswith export capability - Background audio startup for immersive UX
Backend (server/):
- Express.js server with JSON body handling (15MB limit)
- Mongoose models enforcing
nullifierHash+gridIndexunique constraints /healthendpoint for status checksPOST /api/cellsvalidates payload, prevents duplicates, and writes to MongoDB- CORS allowlist for production deployments
- Error normalization with meaningful conflict taxonomy (
already_submitted,cell_taken,invalid_payload)
Integration:
- World ID widget (
@worldcoin/idkit) root-mounted and programmatically opened - API contract synchronized between client and server
- Environment variables templated for easy local/production setup
Challenges we ran into
- Modal layering complexity: Managing World ID's IDKit modal alongside local login/draw popups required stateful coordination (
isWorldIdPendingguard) to prevent race conditions. - Uniqueness enforcement: Implementing global deduplication across a distributed grid meant carefully designing composite unique indexes (
nullifierHash+gridIndex). - Drawing fidelity in export: Ensuring
react-sketch-canvasdrawing data correctly translated to PNG snapshots viahtml2canvasrequired temporary transform resets. - Identity fallback flows: Gracefully handling World ID dismissal and falling back to guest mode without breaking session state.
- CORS and origin management: Coordinating localhost development with production GitHub Pages deployment required flexible origin whitelisting.
Accomplishments that we're proud of
- Seamless identity verification: We created a smooth World ID + guest fallback flow that respects user choice without sacrificing security.
- Robust conflict resolution: Our error taxonomy (
409for duplicates/occupied cells,400for invalid payloads) provides meaningful feedback and prevents edge-case confusion. - Collaborative UX: The grid UI disables filled cells in real-time, giving users instant visual feedback on what's available.
- Full-stack polish: From typewriter intro overlays to background audio startup to decay countdown in the status bar, every interaction feels intentional.
- Scalability foundation: The monorepo structure, API contract, and database design support future growth to thousands of cells and submissions.
What we learned
- State orchestration at scale: Centralizing all UI state in
App.jsxmade debugging and coordinating async flows (World ID, API calls, grid updates) much easier. - Frontend/backend contracts matter: Synchronizing changes in
server/routes/cells.jsandclient/src/api/client.jsearly prevented nasty integration surprises. - MDN/mongoose uniqueness nuances: Composite unique indexes require careful schema design and test coverage; we learned this the hard way with nullifier deduplication.
- UX as product: Details like disabling filled cells, showing submission counts, and countdown timers made the experience feel "real" to users.
- Environment templating saves time: Using
.env.examplefiles reduced onboarding friction and mistakes significantly.
What's next for Reef
- Persistent art storage: Integrate IPFS or Arweave for permanent, decentralized image hosting.
- Grid expansion: Dynamically grow the grid based on submission density; allow nested/fractal grids.
- Galleries and curation: Add timeline views, trending submissions, and user profiles to explore the creative output.
- Cross-chain identity: Support other decentralized identity providers (ENS, Lens, etc.) alongside World ID.
- Real-time collaboration: Add WebSocket support for live grid updates without polling.
- Mobile-friendly drawing: Optimize drawing canvas and UI for touch devices.
- Decay mechanics: Implement optional time-based fading or replacement rules to refresh the grid over time.
Built with
- Frontend: React, Vite, JavaScript, CSS Modules,
react-sketch-canvas,html2canvas,@worldcoin/idkit - Backend: Node.js, Express, Mongoose, MongoDB Atlas
- Infrastructure: GitHub Pages (frontend), Railway or similar (backend)
- Tools: World ID (identity), npm, Git
Built With
- css-modules
- express.js
- html2canvas
- javascript
- mongodb
- mongoose
- node.js
- react
- react-sketch-canvas
- vite
- worldcoin
- worldid
Log in or sign up for Devpost to join the conversation.