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:
- Fractional Indexing in Peer-to-Peer Networks - This article explores how to implement fractional indexing in distributed systems without a central authority.
- 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:
- fractional-indexing - A solid implementation inspired by Evan's work. The Observable notebook explanation provides an excellent deep dive into the implementation details.
- 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:
- Update the in-memory state
- Save to IndexedDB via Dexie
- 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
- Fractional indexing is the right solution for collaborative ordered lists - It elegantly solves the concurrent reordering problem.
- Jitter matters - The random jitter in jittered-fractional-indexing significantly reduces collision probability in high-concurrency scenarios.
- Local-first architecture improves UX - Using IndexedDB with Dexie makes the app feel instant, even with network latency.
- Supabase makes real-time easy - The built-in real-time functionality handles the complexity of broadcasting changes to all clients.
- 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
Comments
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:
useAutoRefreshhook polls/api/versionevery 60 seconds
- Compares local build ID with server build ID
UpdateBannercomponent displays when new version detected
- Prompts user to refresh for latest version
User Tracking
All mutations include user attribution:
authorfield: Set on item/comment creation
updatedbyfield: 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.jsruns beforenext build
- Generates
public/build-id.jsonwith timestamp-based identifier
- Served via
/api/versionendpoint 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.
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:
Diagram: Complete Drag-and-Drop Operation Flow
This sequence demonstrates:
- Fractional Indexing:
generateKeyBetween()calculates the new order without touching other items
- Optimistic UI:
useLiveQuerytriggers re-render immediately after local write
- 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: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.
.png%3Ftable%3Dblock%26id%3D2c835a1d-210b-80bb-90b3-d03b89c472d1%26cache%3Dv2&w=3840&q=75)