HomeBlog › Supabase Realtime mit Claude Code
Real-time & Supabase

Supabase Realtime mit Claude Code:
Live-Daten in React 2026

📅 5. Mai 2026 ⏱ 11 min Lesezeit 🔨 Claude Code & Supabase
Live-Daten ohne Polling, Multiplayer-Features ohne WebSocket-Boilerplate — Supabase Realtime macht es moeglich. Mit Claude Code als KI-Pair-Programmer setzt du Postgres Changes, Broadcast und Presence in wenigen Minuten auf. Dieser Guide zeigt den kompletten Stack: von der Channel-Konfiguration bis zur produktionsreifen React-Integration inklusive Row-Level Security und Scale-Tipps fuer 2026.

1. Supabase Realtime Setup

Setup Client, Channel, Subscribe — der Dreiklang

Supabase Realtime basiert auf Phoenix Channels (Elixir) und laeuft ueber eine persistente WebSocket-Verbindung. Jede Verbindung besteht aus einem Client, beliebig vielen Channels (Themen) und Subscriptions auf Events. Claude Code kennt die aktuelle API und generiert den Boilerplate automatisch — du beschreibst, was sich aendern soll, Claude schreibt den Hook.

Das grundlegende Setup besteht aus drei Schritten: Client anlegen, Channel definieren, Events abonnieren. Der Client haelt die WebSocket-Verbindung und reconnectet automatisch.

Installation und Client-Initialisierung

Zuerst das Supabase-SDK und optional die TypeScript-Typen installieren:

bash
# NPM
npm install @supabase/supabase-js

# Oder mit pnpm / bun
pnpm add @supabase/supabase-js
bun add @supabase/supabase-js

# TypeScript-Typen aus Supabase CLI generieren (optional, aber empfohlen)
npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts
lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import type { Database } from '../types/supabase'

const supabaseUrl  = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnon = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

export const supabase = createClient<Database>(supabaseUrl, supabaseAnon, {
  realtime: {
    params: {
      eventsPerSecond: 10,   // Rate-Limit clientseitig
    },
  },
})

Ersten Realtime-Channel anlegen

TypeScript
import { supabase } from '../lib/supabase'

// Channel anlegen (Name ist frei waehlbar, muss im Projekt eindeutig sein)
const channel = supabase.channel('room:global')

// Subscription starten
channel.subscribe((status) => {
  if (status === 'SUBSCRIBED') {
    console.log('Realtime verbunden!')
  }
  if (status === 'CHANNEL_ERROR') {
    console.error('Channel-Fehler — Channel-Name pruefen')
  }
})

// Cleanup (z.B. in useEffect Return)
await supabase.removeChannel(channel)
Claude Code Prompt-Tipp: "Erstelle mir einen wiederverwendbaren Supabase-Client mit Realtime-Konfiguration fuer Next.js 14 App Router, mit automatischem Reconnect und TypeScript-Typen aus meinem Schema." — Claude generiert die korrekte Singleton-Implementierung.

RealtimeChannel

Logische Gruppe fuer Events. Mehrere Subscriptions pro Channel moeglich.

Status-Lifecycle

SUBSCRIBED → CHANNEL_ERROR → TIMED_OUT → CLOSED. Immer auf Fehler reagieren.

Multiplexing

Eine WebSocket-Verbindung, viele Channels — effizient und skalierbar.

Auto-Reconnect

Das SDK reconnectet automatisch bei Verbindungsabbruch — kein manuelles Handling noetig.

2. Postgres Changes: INSERT, UPDATE, DELETE live empfangen

Postgres Changes Datenbank-Events direkt im Browser

Postgres Changes ist das Kernstueck von Supabase Realtime. Es nutzt Postgres Logical Replication, um Zeilen-Aenderungen (INSERT, UPDATE, DELETE, TRUNCATE) in Echtzeit an abonnierte Clients zu pushen. Der Filter laesst sich auf Schema, Tabelle und sogar einzelne Zeilen einschraenken.

Row-Level Security (RLS) greift dabei auf Datenbankebene: Ein Client sieht nur Aenderungen an Zeilen, auf die er auch SELECT-Zugriff hat. Claude Code kann RLS-Policies automatisch mitgenerieren, wenn du die Anforderungen beschreibst.

Alle Events einer Tabelle abonnieren

TypeScript
const channel = supabase
  .channel('schema-db-changes')
  .on(
    'postgres_changes',
    {
      event:  '*',            // INSERT | UPDATE | DELETE | TRUNCATE | *
      schema: 'public',
      table:  'messages',
    },
    (payload) => {
      console.log('Change received!', payload)
      // payload.eventType: 'INSERT' | 'UPDATE' | 'DELETE'
      // payload.new: Neue Zeile (INSERT / UPDATE)
      // payload.old: Alte Zeile (UPDATE / DELETE)
      // payload.table, payload.schema, payload.commit_timestamp
    }
  )
  .subscribe()

Einzelne Event-Typen filtern

TypeScript
// Nur neue Nachrichten empfangen
channel.on(
  'postgres_changes',
  { event: 'INSERT', schema: 'public', table: 'messages' },
  (payload) => appendMessage(payload.new)
)

// Nur Updates (z.B. Gelesen-Status)
channel.on(
  'postgres_changes',
  { event: 'UPDATE', schema: 'public', table: 'messages' },
  (payload) => markRead(payload.new.id)
)

// Zeilenfilter: Nur eigene Nachrichten des aktuellen Nutzers
channel.on(
  'postgres_changes',
  {
    event:  'INSERT',
    schema: 'public',
    table:  'messages',
    filter: `room_id=eq.${roomId}`,   // Serverside-Filter!
  },
  (payload) => appendMessage(payload.new)
)

Row-Level Security fuer Realtime aktivieren

RLS muss explizit fuer Realtime aktiviert werden. Claude Code generiert bei Bedarf die passende SQL-Migration:

SQL / Supabase Dashboard
-- 1. RLS fuer Tabelle aktivieren
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;

-- 2. Realtime fuer diese Tabelle erlauben
ALTER PUBLICATION supabase_realtime ADD TABLE messages;

-- 3. Lese-Policy: Nutzer sieht nur Nachrichten in seinen Raeumen
CREATE POLICY "users_see_own_room_messages"
  ON messages
  FOR SELECT
  USING (
    room_id IN (
      SELECT room_id FROM room_members
      WHERE user_id = auth.uid()
    )
  );

-- 4. Realtime nutzt denselben Nutzer-Kontext wie normale Queries
--    (JWT aus supabase.auth.getSession() wird automatisch mitgesendet)
Achtung: Ohne RLS-Policy empfaengt ein anonymer Client ALLE Zeilen-Aenderungen der Tabelle — auch Daten anderer Nutzer. Immer RLS aktivieren und testen, bevor du Realtime in Produktion bringst. Claude Code kann dir einen RLS-Test-Prompt generieren.
Event payload.new payload.old Typischer Use-Case
INSERT Neue Zeile (vollstaendig) {} Live-Feed, Chat, Notifications
UPDATE Aktualisierte Zeile Alte Werte (Primkey) Status-Aenderungen, Likes
DELETE {} Geloeschte Zeile (Primkey) Item entfernen, Archivieren
TRUNCATE {} {} Cache leeren, Reset

3. Broadcast: Custom Events in Echtzeit senden

Broadcast Ephemere Events ohne Datenbankschreibzugriff

Broadcast ist ideal fuer Events, die nicht persistiert werden muessen: Cursor-Positionen, Tipp-Indikatoren, Reaktionen, Live-Kommentare. Im Gegensatz zu Postgres Changes wird nichts in die Datenbank geschrieben — die Nachricht wird nur an alle aktuell verbundenen Clients im selben Channel weitergeleitet.

Das Latenzfenster ist extrem gering (typisch <100ms weltweit) weil kein Datenbankschreibzugriff im kritischen Pfad liegt. Claude Code verwendet Broadcast standardmaessig fuer alle Cursor-Sharing- und Live-Collaboration-Features.

Events senden und empfangen

TypeScript — Sender
const channel = supabase.channel('room:collab-123')

// Empfaenger registrieren (vor subscribe!)
channel.on(
  'broadcast',
  { event: 'cursor' },
  (payload) => {
    renderCursor(payload.payload.userId, payload.payload.x, payload.payload.y)
  }
)

await channel.subscribe()

// Event senden (nach subscribe)
const sendCursor = (x: number, y: number) => {
  channel.send({
    type:    'broadcast',
    event:   'cursor',
    payload: { userId: currentUser.id, x, y },
  })
}

Use-Case: Echtzeit-Cursor-Sharing (Figma-Style)

hooks/useCursorShare.ts
import { useEffect, useRef, useState } from 'react'
import { supabase } from '../lib/supabase'

interface CursorState {
  userId: string
  x: number
  y: number
  color: string
}

export function useCursorShare(roomId: string, userId: string) {
  const [cursors, setCursors] = useState<Record<string, CursorState>>({})
  const channelRef = useRef<any>(null)
  const throttleRef = useRef<number>(0)

  useEffect(() => {
    const channel = supabase.channel(`cursors:${roomId}`)
    channelRef.current = channel

    channel
      .on('broadcast', { event: 'cursor-move' }, ({ payload }) => {
        if (payload.userId === userId) return   // eigene ignorieren
        setCursors((prev) => ({ ...prev, [payload.userId]: payload }))
      })
      .subscribe()

    return () => { supabase.removeChannel(channel) }
  }, [roomId, userId])

  const trackCursor = (x: number, y: number) => {
    const now = Date.now()
    if (now - throttleRef.current < 50) return   // Max 20 Events/s
    throttleRef.current = now
    channelRef.current?.send({
      type: 'broadcast', event: 'cursor-move',
      payload: { userId, x, y, color: '#7c3aed' },
    })
  }

  return { cursors, trackCursor }
}
Claude Code Workflow: Beschreibe den Use-Case ("Cursor-Sharing wie in Figma, max. 20 Events pro Sekunde, eigene Cursor ignorieren") — Claude generiert den kompletten Hook inkl. Throttle und Cleanup. Peer-Review dauert dann Minuten statt Stunden.

Broadcast-Optionen: self und ack

TypeScript
// self: true → eigene Broadcasts auch lokal empfangen (Standard: false)
const channel = supabase.channel('room:test', {
  config: {
    broadcast: { self: true, ack: false },
    // ack: true → send() gibt Promise zurueck, das resolved wenn Server Event bestätigt
    // ack: false → fire-and-forget (Standard, niedrigere Latenz)
  },
})

4. Presence: Wer ist gerade online?

Presence Verteilter Zustand fuer Nutzer-Online-Status

Presence loest ein klassisches Distributed-Systems-Problem elegant: Alle Clients sehen konsistent, welche Nutzer gerade verbunden sind — inklusive automatischem Cleanup bei Verbindungsabbruch. Supabase implementiert Presence via CRDT (Conflict-free Replicated Data Types), sodass der Zustand bei allen Clients konvergiert.

Typische Anwendungsfaelle: "X Personen sehen dieses Dokument", Tipp-Indikatoren in Chats, Multiplayer-Cursor, Collaborative-Editing-Sessions. Claude Code implementiert Presence-Features standardmaessig mit Join/Leave-Events und State-Sync.

Presence: track, sync, join, leave

TypeScript
interface UserPresence {
  userId:    string
  username:  string
  avatar:    string
  joinedAt:  string
}

const channel = supabase.channel('room:presence-demo', {
  config: { presence: { key: currentUser.id } },  // Eindeutiger Key pro Nutzer
})

// sync: wird bei jeder State-Aenderung gefeuert (initiales Sync + join/leave)
channel.on('presence', { event: 'sync' }, () => {
  const state = channel.presenceState<UserPresence>()
  // state = { [key: string]: UserPresence[] } (Array wegen Multi-Tab)
  const onlineUsers = Object.values(state).flat()
  setOnlineUsers(onlineUsers)
})

// join: neuer Nutzer tritt bei
channel.on('presence', { event: 'join' }, ({ key, newPresences }) => {
  console.log(`${newPresences[0].username} ist beigetreten`)
})

// leave: Nutzer verlässt (auch bei Verbindungsabbruch!)
channel.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
  console.log(`${leftPresences[0].username} hat verlassen`)
})

await channel.subscribe()

// Eigenen Zustand registrieren (nach subscribe!)
await channel.track({
  userId:   currentUser.id,
  username: currentUser.name,
  avatar:   currentUser.avatarUrl,
  joinedAt: new Date().toISOString(),
})

Presence-State mit Claude Code aktualisieren

TypeScript
// Zustand aktualisieren (z.B. Aktivitaet tracken)
await channel.track({
  ...currentPresence,
  lastActivity: new Date().toISOString(),
  currentPage:  window.location.pathname,
})

// Presence beenden (Nutzer meldet sich bewusst ab)
await channel.untrack()

// Alle aktuell verbundenen Nutzer abrufen (synchron nach sync-Event)
const state   = channel.presenceState<UserPresence>()
const online  = Object.values(state).flat()
const count   = online.length

CRDT-Konsistenz

Alle Clients konvergieren zum selben State — auch nach Netzwerkproblemen.

Auto-Cleanup

Bei Verbindungsabbruch wird der Nutzer automatisch aus dem Presence-State entfernt.

Multi-Tab

Gleicher Nutzer in zwei Tabs = zwei Eintraege unter demselben Key.

Beliebige Payload

Jeder serialisierbare JSON-Wert kann als Presence-Zustand getrackt werden.

5. React-Integration: useEffect-Cleanup und Custom Hook

React Saubere Subscriptions mit useEffect und Custom Hooks

Der haeufigste Fehler bei Realtime + React: Kein Cleanup beim Unmount fuehrt zu Memory Leaks und doppelten Event-Handlern im StrictMode (React 18 mountet zweimal!). Das korrekte Muster ist immer: Channel anlegen, subscriben, im Cleanup-Return entfernen.

Claude Code kennt dieses Anti-Pattern und generiert Cleanup immer automatisch mit. Der Custom Hook useLiveTable kapselt das komplette Lifecycle-Management und kann fuer beliebige Supabase-Tabellen wiederverwendet werden.

useEffect mit Realtime — korrekte Cleanup-Strategie

TypeScript — Basispattern
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase'

function LiveMessages({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<Message[]>([])

  useEffect(() => {
    // 1. Initiale Daten laden
    supabase
      .from('messages')
      .select('*')
      .eq('room_id', roomId)
      .order('created_at')
      .then(({ data }) => setMessages(data ?? []))

    // 2. Realtime-Subscription
    const channel = supabase
      .channel(`messages:${roomId}`)
      .on('postgres_changes', {
        event:  'INSERT',
        schema: 'public',
        table:  'messages',
        filter: `room_id=eq.${roomId}`,
      }, (payload) => {
        setMessages((prev) => [...prev, payload.new as Message])
      })
      .subscribe()

    // 3. PFLICHT: Cleanup beim Unmount / roomId-Wechsel
    return () => {
      supabase.removeChannel(channel)
    }
  }, [roomId])  // roomId in Dependency-Array — bei Wechsel neu subscriben

  return (
    <ul>
      {messages.map((m) => <li key={m.id}>{m.content}</li>)}
    </ul>
  )
}

Custom Hook: useLiveTable (universell wiederverwendbar)

hooks/useLiveTable.ts
import { useEffect, useState, useCallback } from 'react'
import { supabase } from '../lib/supabase'
import type { RealtimePostgresChangesPayload } from '@supabase/supabase-js'

interface UseLiveTableOptions<T> {
  table:      string
  schema?:    string
  filter?:    string
  onInsert?:  (row: T) => void
  onUpdate?:  (row: T) => void
  onDelete?:  (row: Partial<T>) => void
  initialFetch?: () => Promise<T[]>
}

export function useLiveTable<T extends { id: string }>(
  options: UseLiveTableOptions<T>
) {
  const { table, schema = 'public', filter, onInsert, onUpdate, onDelete, initialFetch } = options
  const [rows, setRows]       = useState<T[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError]     = useState<Error | null>(null)

  useEffect(() => {
    let mounted = true

    // Initiale Daten
    if (initialFetch) {
      initialFetch()
        .then((data) => { if (mounted) { setRows(data); setLoading(false) } })
        .catch((e)   => { if (mounted) { setError(e);  setLoading(false) } })
    } else {
      setLoading(false)
    }

    const handleChange = (payload: RealtimePostgresChangesPayload<T>) => {
      if (!mounted) return
      if (payload.eventType === 'INSERT') {
        setRows((p) => [...p, payload.new])
        onInsert?.(payload.new)
      }
      if (payload.eventType === 'UPDATE') {
        setRows((p) => p.map((r) => r.id === payload.new.id ? payload.new : r))
        onUpdate?.(payload.new)
      }
      if (payload.eventType === 'DELETE') {
        setRows((p) => p.filter((r) => r.id !== (payload.old as T).id))
        onDelete?.(payload.old as Partial<T>)
      }
    }

    const channelName = `live-${table}-${filter ?? 'all'}`
    const channel = supabase
      .channel(channelName)
      .on('postgres_changes', { event: '*', schema, table, filter }, handleChange)
      .subscribe()

    return () => {
      mounted = false
      supabase.removeChannel(channel)
    }
  }, [table, schema, filter])  // Callbacks absichtlich nicht in Deps (stabil halten)

  return { rows, loading, error }
}

// Verwendung:
// const { rows, loading } = useLiveTable<Todo>({
//   table: 'todos',
//   filter: `user_id=eq.${userId}`,
//   initialFetch: () => supabase.from('todos').select().eq('user_id', userId).then(r => r.data ?? []),
// })
React 18 StrictMode: Im StrictMode mountet React Komponenten zweimal in der Entwicklung, um Cleanup-Fehler aufzudecken. Das mounted-Flag im Hook verhindert State-Updates nach dem Unmount. Teste immer im StrictMode, bevor du in Produktion gehst.

6. Performance und Scale: Throttling, Channel-Limits, Unsubscribe

Performance Realtime skalierbar betreiben

Supabase Realtime skaliert vertikal mit dem gewählten Plan. Auf dem kostenlosen Plan sind 200 gleichzeitige Connections und 2 Channels pro Connection möglich. Pro- und Business-Pläne heben diese Limits auf bis zu 10.000 concurrent Connections an.

Auf Applikationsebene gibt es mehrere Stellschrauben: Clientseitiges Throttling für hochfrequente Events, gezieltes Filtern statt globaler Subscriptions, sauberes Unsubscribe beim Unmount und batching von State-Updates. Claude Code baut diese Optimierungen auf Anfrage direkt in den generierten Code ein.

Channel-Limits und Verbindungs-Budget

Plan Concurrent Connections Channels / Connection Messages / Sekunde
Free 200 2 10
Pro 500 100 100
Team 1.000 100 500
Enterprise 10.000+ unbegrenzt benutzerdefiniert

Throttle-Util fuer hochfrequente Events

utils/throttle.ts
// Generischer Throttle fuer Realtime-Events (z.B. Cursor, Scroll, Resize)
export function throttle<T extends (...args: any[]) => any>(
  fn: T,
  limitMs: number
): T {
  let last = 0
  return ((...args) => {
    const now = Date.now()
    if (now - last < limitMs) return
    last = now
    return fn(...args)
  }) as T
}

// Verwendung:
const sendCursorThrottled = throttle(sendCursor, 50)   // Max 20 Events/s
document.addEventListener('mousemove', (e) => {
  sendCursorThrottled(e.clientX, e.clientY)
})

Mehrere Channels effizient verwalten

hooks/useChannelManager.ts
import { useRef, useEffect } from 'react'
import { supabase } from '../lib/supabase'
import type { RealtimeChannel } from '@supabase/supabase-js'

// Singleton-Map: verhindert doppelte Channel-Subscriptions (z.B. React StrictMode)
const channelRegistry = new Map<string, RealtimeChannel>()

export function getOrCreateChannel(name: string): RealtimeChannel {
  if (channelRegistry.has(name)) {
    return channelRegistry.get(name)!
  }
  const channel = supabase.channel(name)
  channelRegistry.set(name, channel)
  return channel
}

export async function releaseChannel(name: string): Promise<void> {
  const channel = channelRegistry.get(name)
  if (channel) {
    await supabase.removeChannel(channel)
    channelRegistry.delete(name)
  }
}

// Alle aktiven Channels beim App-Unload aufraeumen
if (typeof window !== 'undefined') {
  window.addEventListener('beforeunload', () => {
    channelRegistry.forEach((ch) => supabase.removeChannel(ch))
  })
}

State-Batching fuer hochfrequente Updates

TypeScript — React 18 Batching
import { flushSync, startTransition } from 'react'

// Fuer zeitkritische Updates (z.B. eigene Nachrichten sofort anzeigen)
const handleInsert = (payload: any) => {
  flushSync(() => setMessages((p) => [...p, payload.new]))
}

// Fuer nicht-dringende Updates (z.B. Background-Feed, Analytics)
const handleBulkUpdate = (payload: any) => {
  startTransition(() => setFeedItems((p) => [...p, payload.new]))
}

// React 18 batcht Updates in Event-Handlern automatisch —
// bei async Callbacks (Realtime-Events) muss man das selbst steuern
Claude Code Scale-Audit Prompt: "Analysiere alle Supabase-Realtime-Subscriptions in meinem Projekt und pruefe: Fehlendes Cleanup, fehlende Server-Filter, ueberfluessige Channels und moegliche Memory Leaks. Erstelle einen Bericht und fixe die Probleme direkt." — Spart Code-Reviews von 2–3 Stunden auf unter 5 Minuten.

Monitoring und Debugging

TypeScript — Debug-Logging aktivieren
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(url, anonKey, {
  realtime: {
    logger: (level, msg, data) => {
      if (process.env.NODE_ENV === 'development') {
        console.log(`[Realtime][${level}] ${msg}`, data)
      }
    },
  },
})

// Aktive Channels inspizieren
const activeChannels = supabase.getChannels()
console.log(`Aktive Channels: ${activeChannels.length}`)
activeChannels.forEach((ch) => {
  console.log(`- ${ch.topic}: ${ch.state}`)
})
Supabase Realtime Postgres Changes Broadcast Presence React Hooks Row-Level Security Claude Code Next.js 2026 WebSocket Live Data

Bereit fuer Live-Daten in deinem Projekt?

Starte jetzt deinen kostenlosen Trial und nutze Claude Code als deinen Supabase-Realtime-Experten. Kein Kreditkarte erforderlich — sofort loslegen.

Kostenlos testen →