Real-Time Multiplayer To-do list with Fractional Indexing

Real-Time Multiplayer To-do list with Fractional Indexing

Tags
Node.js
React.js
Web Dev
Projects
Next.js
Netlify
Fullstack
TailwindCSS
Supabase
Published
December 13, 2025
Author
Kalen Wallin
api_v1_role
api_v1_category
Websites
api_v1_company
Fractional Indexing
api_v1_type
api_v1_year
2025
api_v2_order
6
Talk

The Problem

While working on a new feature at work that required sort order, one question haunted me: What is the best way to handle sort order for a list of drag and drop items that is accessed by multiple users in real-time?

The Journey

Step 1: Researching Sort Order Management

I started by diving deep into various approaches for managing sort order in drag-and-drop interfaces. The naive approach—simply using integer indices (1, 2, 3, 4...)—requires renumbering all subsequent items when you insert something in the middle. This becomes especially problematic in a collaborative environment where multiple users are making changes simultaneously.

Step 2: Discovering Fractional Indexing

My research led me to an elegant solution: fractional indexing. This technique uses strings that represent fractional values between items, allowing you to insert new items between any two existing items without renumbering anything else.
I found several invaluable resources:
Evan Wallace's Articles (creator of esbuild)
Evan wrote two foundational articles that changed how I think about this problem:
  1. Fractional Indexing in Peer-to-Peer Networks - This article explores how to implement fractional indexing in distributed systems without a central authority.
  1. Real-time Editing of Ordered Sequences - Evan explains how Figma handles real-time collaborative editing of ordered lists using fractional indexing in a centralized architecture.
npm Packages
Two npm packages caught my attention:
  1. fractional-indexing - A solid implementation inspired by Evan's work. The Observable notebook explanation provides an excellent deep dive into the implementation details.
  1. jittered-fractional-indexing - An extension of fractional-indexing that adds random jitter functionality. This is crucial for minimizing index collisions when multiple users generate fractional indices concurrently.
How Fractional Indexing Works
Instead of using integers (1, 2, 3, 4), fractional indexing uses lexicographically ordered strings:
  • Items start with indices like: "a0", "a1", "a2"
  • To insert between "a0" and "a1", you generate: "a0V"
  • To insert between "a0V" and "a1", you generate: "a0h"
The beauty is that you can always generate a string that falls between any two existing strings, and the sorting is done lexicographically. The jitter functionality adds randomness to reduce the probability that two users will generate the exact same index at the same time.

Step 3: Building the Drag-and-Drop Interface

With a solid understanding of fractional indexing, I built a Next.js application with a drag-and-drop interface similar to a product backlog. The key features:
  • Drag items to reorder them
  • Each item gets a fractional index instead of an integer position
  • When an item is dropped, calculate a new fractional index between its neighbors
  • Update the item's position using the generated index

Step 4: Adding Local Sync with Dexie

To provide a seamless experience across browser tabs, I integrated Dexie.js, a wrapper around IndexedDB. This allows:
  • Persisting the list locally in the browser
  • Syncing changes between multiple tabs of the same browser
  • Instant UI updates without waiting for network requests
  • Offline-first functionality
When a user drags an item:
  1. Update the in-memory state
  1. Save to IndexedDB via Dexie
  1. Broadcast the change to other tabs

Step 5: Adding Cloud Sync with Supabase

To enable collaboration across devices and users, I integrated Supabase:
  • Store all items in a Postgres database
  • Each item has its fractional index stored as a string column
  • Sorting is done with a simple ORDER BY fractional_index
The database schema is straightforward:
CREATE TABLE items ( id UUID PRIMARY KEY, content TEXT, fractional_index TEXT NOT NULL, created_at TIMESTAMP, updated_at TIMESTAMP ); CREATE INDEX idx_items_fractional_index ON items(fractional_index);

Step 6: Implementing Real-Time Sync with Supabase Realtime

The final piece of the puzzle was adding real-time collaboration using Supabase Realtime:
  • Subscribe to database changes using Postgres CDC (Change Data Capture)
  • When any user updates an item, all connected clients receive the change instantly
  • Each client updates its local state and UI automatically
  • The fractional indexing ensures that even concurrent reorders don't conflict
The result is a truly multiplayer experience where multiple users can drag and drop items simultaneously, and everyone sees the changes in real-time.

The Result

The combination of these technologies creates a robust, real-time collaborative drag-and-drop experience:
No conflicts - Fractional indexing eliminates position conflicts
Real-time updates - Supabase Realtime keeps everyone in sync
Local-first - Dexie provides instant feedback and offline support
Scalable - No need to renumber items when reordering
Concurrent-safe - Jittered indices minimize collision probability

Key Takeaways

  1. Fractional indexing is the right solution for collaborative ordered lists - It elegantly solves the concurrent reordering problem.
  1. Jitter matters - The random jitter in jittered-fractional-indexing significantly reduces collision probability in high-concurrency scenarios.
  1. Local-first architecture improves UX - Using IndexedDB with Dexie makes the app feel instant, even with network latency.
  1. Supabase makes real-time easy - The built-in real-time functionality handles the complexity of broadcasting changes to all clients.
  1. Order with strings, not integers - Storing sort order as lexicographically ordered strings is more flexible than integer-based approaches.
Building this application taught me that the right data structure (fractional indices) combined with the right tools (Supabase, Dexie, jittered-fractional-indexing) can transform a seemingly complex problem into an elegant solution.

This application can be found at https://fractin.netlify.app/. Go ahead and add some items to the list. Use multiple browser tabs and devices to feel the power of Dexie and Supabase at your fingertips!

Demos

Items

notion image

Comments

notion image

Technical Details

Feature Summary

Item Management Features

  • Add Operations: Add to top or bottom with pre-calculated fractional order keys
  • Edit Operations: Inline editing with Enter/Escape keyboard shortcuts
  • Delete Operations: Single-click deletion with immediate local feedback
  • Toggle Complete: Checkbox interface for task completion tracking
  • Reorder Operations: Drag-and-drop with automatic fractional index calculation

Comment System Features

The CommentSection component provides per-item commenting:
  • Expandable/collapsible comment sections
  • Comment count badge on items
  • Drag-and-drop reordering within comments
  • Same dual persistence strategy as items

Version Detection System

The application includes automatic version detection:
  • useAutoRefresh hook polls /api/version every 60 seconds
  • Compares local build ID with server build ID
  • UpdateBanner component displays when new version detected
  • Prompts user to refresh for latest version

User Tracking

All mutations include user attribution:
  • author field: Set on item/comment creation
  • updatedby field: Set on item updates
  • Username defaults to "Anonymous" if not provided
  • Displayed in item metadata below task text

Styling and User Interface

The application uses Tailwind CSS 4.x for styling with:
  • Dark mode support via dark: variant classes
  • Responsive design with mobile-first approach
  • Utility-first CSS patterns throughout
  • Custom animations for success messages and pulsing indicators

Deployment and Build System

The build process includes custom build ID generation:
  • scripts/generate-build-id.js runs before next build
  • Generates public/build-id.json with timestamp-based identifier
  • Served via /api/version endpoint for version detection
  • Enables zero-downtime deployments with client-side update prompts

Data Flow

This app uses three foundational architectural patterns that enable the application's collaborative, offline-first functionality: Fractional Indexing for conflict-free list ordering, Dual Persistence Strategy for local-first data management, and Optimistic UI Pattern for responsive user interactions. These concepts are implemented consistently across both the items list and the comment system.
notion image
Fractional Indexing generates order keys without database-wide updates, the Dual Persistence Strategy writes to local IndexedDB first for instant feedback then syncs to Supabase, and the Optimistic UI Pattern uses reactive queries (useLiveQuery) to re-render immediately when local data changes.

Pattern 1: Fractional Indexing

Fractional indexing enables conflict-free list reordering by assigning lexicographically-ordered string keys to items. When reordering, the system generates a new key between two existing keys without modifying any other items in the list.
The application imports generateKeyBetween from the fractional-indexing library and uses it in multiple contexts:
Context
Function Call
Add item to top
generateKeyBetween(null, firstOrder)
Add item to bottom
generateKeyBetween(lastOrder, null)
Drag item down
generateKeyBetween(targetItem.order, nextItem?.order || null)
Drag item up
generateKeyBetween(prevItem?.order || null, targetItem.order)
Default items
generateKeyBetween(null, null)
The order field is stored as a string in both the Dexie Item interface and the Supabase database schema, enabling lexicographic sorting.

Pattern 2: Dual Persistence Strategy

The application maintains two synchronized data stores: a local IndexedDB database accessed via Dexie, and a remote PostgreSQL database provided by Supabase. Every mutation writes to local storage first, then asynchronously syncs to the remote database.

Pattern 3: Optimistic UI

The optimistic UI pattern provides sub-50ms response times by rendering local data changes immediately, before remote synchronization completes. This is implemented using Dexie's reactive query hook useLiveQuery.

Reactive Query Implementation

The Home component uses useLiveQuery to maintain reactive subscriptions to the Dexie database:
const items = useLiveQuery(() => db.items.toArray()) ?? []; const allComments = useLiveQuery(() => db.comments.toArray()) ?? [];
When any component calls db.items.add()db.items.update(), or db.items.delete(), Dexie automatically triggers the useLiveQuery hook, which causes React to re-render with the updated data. This happens synchronously within the same event loop tick.

Write-Through Pattern

All mutation functions follow this pattern:
Operation
Add Item
Edit Item
Delete Item
Toggle Complete
Reorder (Drag)
No manual state management or loading indicators are needed because useLiveQuery handles the reactive updates automatically.

Eventual Consistency

The remote write operations use async/await but do not block the UI. When Supabase completes the write, it broadcasts the change via the realtime channel, which updates all other connected clients' local Dexie databases. The originating client already has the correct data from its optimistic local write.

Pattern Integration in Practice

The three core patterns combine in every user interaction. Here's a concrete example of reordering an item via drag-and-drop:
notion image
Diagram: Complete Drag-and-Drop Operation Flow
This sequence demonstrates:
  1. Fractional IndexinggenerateKeyBetween() calculates the new order without touching other items
  1. Optimistic UIuseLiveQuery triggers re-render immediately after local write
  1. Dual Persistence: Local Dexie update followed by async Supabase sync
The same pattern applies to the comment system in CommentSection component, ensuring consistent behavior across all reorderable lists in the application.

Data Model and Ordering

Both Item and Comment entities share a common structure optimized for fractional indexing and audit tracking:
notion image
Diagram: Core Data Model
The order field is the key to the fractional indexing system. It is:
  • A string type (not numeric) to support lexicographic sorting
  • Indexed in Dexie for efficient queries
  • Sorted using string comparison: a.order < b.order ? -1 : a.order > b.order ? 1 : 0 
The author and updatedby fields provide audit trails, showing who created and last modified each entity. These fields are optional but defaulted to "Anonymous" when no username is provided.

Summary

The three core patterns establish the architectural foundation:
Pattern
Primary Benefit
Implementation
Fractional Indexing
O(1) reordering without re-indexing
generateKeyBetween() from fractional-indexing library
Dual Persistence
Offline capability + multi-user sync
Dexie (local) + Supabase (remote)
Optimistic UI
Instant feedback (sub-50ms)
useLiveQuery reactive queries
These patterns are applied consistently across the items list and the comment system, creating a uniform user experience throughout the application.