#supabase#claude#realtime-collaboration#next.js#ai-integration

How to Build a Real-Time Collaborative AI Editor with Supabase Realtime and Claude

March 6, 2026
7 min read

WA

Waleed Ahmed
How to Build a Real-Time Collaborative AI Editor with Supabase Realtime and Claude

How to Build a Real-Time Collaborative AI Editor with Supabase Realtime and Claude

Building a collaborative document editor with AI assistance used to require enterprise-grade infrastructure. Today, a solo founder or small team can ship this in a weekend using Supabase Realtime and Claude. This post walks you through architecture, code, and the exact patterns that make it work.

What You're Building

A real-time collaborative text editor where multiple users can edit simultaneously, with Claude running in the background to provide AI suggestions, summaries, and tone adjustments. Changes sync instantly across all connected clients, and AI operations complete without blocking the editing experience.

This matters because collaborative AI tools are now table stakes for productivity SaaS. Users expect real-time collaboration (think Google Docs) AND AI assistance (think GitHub Copilot). Building both at once no longer requires managing WebSockets yourself — Supabase Realtime handles the hard parts.

Architecture Overview

The pattern is simple:

  1. Supabase Realtime manages document changes and broadcasts them to all clients
  2. Next.js API routes handle Claude integration and heavy lifting
  3. React state manages local edits with optimistic UI updates
  4. Edge functions process AI requests asynchronously without blocking sync

No extra messaging queues. No complex state machines. Just three things talking to each other.

Setting Up Supabase Schema

First, create a documents table with proper structure for collaborative editing:

create table documents (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  content text not null,
  created_by uuid references auth.users(id),
  created_at timestamp default now(),
  updated_at timestamp default now()
);

create table document_members (
  id uuid primary key default gen_random_uuid(),
  document_id uuid references documents(id) on delete cascade,
  user_id uuid references auth.users(id) on delete cascade,
  created_at timestamp default now(),
  unique(document_id, user_id)
);

create table ai_suggestions (
  id uuid primary key default gen_random_uuid(),
  document_id uuid references documents(id) on delete cascade,
  suggestion_type text, -- 'improvement', 'summary', 'tone_change'
  original_text text,
  suggested_text text,
  created_at timestamp default now()
);

Enable realtime on the documents table:

alter publication supabase_realtime add table documents;

React Client Implementation

Here's the collaborative editor component:

'use client';
import { useEffect, useState, useRef } from 'react';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

export default function CollaborativeEditor({ documentId }: { documentId: string }) {
  const [content, setContent] = useState('');
  const [activeUsers, setActiveUsers] = useState(0);
  const [aiSuggestions, setAiSuggestions] = useState<any[]>([]);
  const editorRef = useRef<HTMLTextAreaElement>(null);
  const changeTimeoutRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    // Subscribe to document changes
    const channel = supabase
      .channel(`doc:${documentId}`)
      .on(
        'postgres_changes',
        { event: 'UPDATE', schema: 'public', table: 'documents', filter: `id=eq.${documentId}` },
        (payload) => {
          setContent(payload.new.content);
        }
      )
      .on('presence', { event: 'sync' }, () => {
        const state = channel.presenceState();
        setActiveUsers(Object.keys(state).length);
      })
      .subscribe((status) => {
        if (status === 'SUBSCRIBED') {
          channel.track({ user: 'anonymous', timestamp: new Date() });
        }
      });

    return () => {
      channel.unsubscribe();
    };
  }, [documentId]);

  const handleContentChange = (newContent: string) => {
    setContent(newContent);

    // Debounce saves to Supabase
    clearTimeout(changeTimeoutRef.current);
    changeTimeoutRef.current = setTimeout(() => {
      supabase
        .from('documents')
        .update({ content: newContent, updated_at: new Date().toISOString() })
        .eq('id', documentId)
        .then();
    }, 1000);

    // Trigger AI suggestions asynchronously
    triggerAiSuggestions(newContent);
  };

  const triggerAiSuggestions = async (text: string) => {
    const response = await fetch('/api/ai-suggestions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ documentId, text, type: 'improvement' }),
    });
    const suggestions = await response.json();
    setAiSuggestions(suggestions);
  };

  return (
    <div className="flex flex-col h-screen bg-gray-50">
      <div className="p-4 bg-white border-b flex justify-between">
        <h1 className="text-xl font-bold">Collaborative Editor</h1>
        <span className="text-sm text-gray-600">{activeUsers} users online</span>
      </div>
      
      <div className="flex gap-4 p-4">
        <textarea
          ref={editorRef}
          value={content}
          onChange={(e) => handleContentChange(e.target.value)}
          className="flex-1 p-4 border rounded font-mono text-sm"
          placeholder="Start typing... AI suggestions will appear on the right"
        />
        
        <div className="w-80 bg-white border rounded p-4 overflow-y-auto">
          <h2 className="font-bold mb-4">AI Suggestions</h2>
          {aiSuggestions.map((suggestion) => (
            <div key={suggestion.id} className="mb-3 p-3 bg-blue-50 rounded text-sm">
              <p className="font-semibold text-blue-900">{suggestion.suggestion_type}</p>
              <p className="text-gray-700 mt-1">{suggestion.suggested_text}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Claude Integration via API Route

Create /api/ai-suggestions to handle Claude calls without blocking the editor:

import { Anthropic } from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

export async function POST(request: Request) {
  const { documentId, text, type } = await request.json();

  const systemPrompt =
    type === 'improvement'
      ? 'You are a writing assistant. Suggest 2-3 concrete improvements to make this text clearer, more engaging, or more professional.'
      : 'You are a summary assistant. Provide a 1-2 sentence summary of the key points.';

  const message = await anthropic.messages.create({
    model: 'claude-3-5-sonnet-20241022',
    max_tokens: 1024,
    system: systemPrompt,
    messages: [{ role: 'user', content: text }],
  });

  const suggestions = message.content[0].type === 'text' ? message.content[0].text : '';

  // Save suggestions to database for history
  // (implementation omitted for brevity)

  return Response.json({ suggestions, type, documentId });
}

Key Patterns That Actually Work

1. Debounce Saves, Not Edits
Don't save every keystroke. Debounce saves to Supabase with a 1-second delay. The UI updates instantly locally, then syncs.

2. AI as Side Effect
AI suggestions are never blocking. Fire them off as a separate request after debounce. If Claude is slow, the editor still responds instantly.

3. Presence Over Polling
Use Supabase Presence to track who's online, not periodic polls. It's built-in and real-time.

4. Optimistic Updates
Show changes locally before Supabase confirms. When realtime updates come back, they'll match what you already displayed.

Deployment Considerations

  • Environment variables: Store ANTHROPIC_API_KEY in Vercel secrets or Railway
  • Rate limiting: Claude API calls add up fast with multiple users. Implement request queuing
  • Database indexes: Index document_id and user_id on all foreign key columns
  • Cold starts: Use Supabase Edge Functions for AI calls if you need sub-100ms latency

Scaling Beyond MVP

Once you ship:

  • Add conflict resolution for simultaneous edits using Operational Transformation (OT) or CRDTs
  • Implement version history with Supabase row-level security
  • Cache Claude responses for identical text to reduce API spend
  • Add webhooks to trigger AI analysis on document completion

The Real Advantage

What makes this architecture win is simplicity. You're not managing WebSocket servers, message queues, or distributed state. Supabase Realtime solves sync, Claude solves intelligence, and your Next.js API route connects them. That's it.

Solo founders can ship this to production in a weekend. Teams can iterate on features instead of infrastructure. That's the whole point.