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.jsx orchestrating popups (logindrawconfirm)
  • useGrid hook manages cell fetching, zoom, and local grid state
  • useSession hook handles World ID auth flow and nullifier tracking
  • Drawing UI powered by react-sketch-canvas with export capability
  • Background audio startup for immersive UX

Backend (server/):

  • Express.js server with JSON body handling (15MB limit)
  • Mongoose models enforcing nullifierHash + gridIndex unique constraints
  • /health endpoint for status checks
  • POST /api/cells validates 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 (isWorldIdPending guard) 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-canvas drawing data correctly translated to PNG snapshots via html2canvas required 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 (409 for duplicates/occupied cells, 400 for 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.jsx made debugging and coordinating async flows (World ID, API calls, grid updates) much easier.
  • Frontend/backend contracts matter: Synchronizing changes in server/routes/cells.js and client/src/api/client.js early 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.example files 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

Share this project:

Updates