Ein Kunde rief mich an einem Donnerstagnachmittag 2021 an, ein Antiquitätenhändler aus Bath, der seine monatlichen Live-Auktionen online nehmen wollte. Einfach genug, dachte ich. Dann sagte er: „Und die Gebote müssen für alle Zuschauer live aktualisiert werden, ohne Seite neu zu laden." Ach so. Das war der Moment, in dem ein „einfacher WordPress-Job" zu einem zweiwöchigen Architektur-Gespräch wurde.
Wichtigste Erkenntnis: Live-Bieterverfahren auf Next.js plus Supabase hängen von Realtime-Kanälen, zeilenbasierter Sicherheit und serverseitiger Gebotsvalidierung ab; bekomme die State Machine richtig hin, bevor du die UI baust.
Ich habe über 12.000 Websites bei Seahawk Media gebaut, und Echtzeit-Features sind diejenigen, die dich beißen, wenn du sie nicht von Anfang an richtig planst. Alle fünf Sekunden zu polling klingt okay, bis du 200 Bieter hast, die gleichzeitig einen einzelnen Endpoint überlasten und deine Hosting-Rechnung sich über Nacht verdoppelt. Lass mich dir also genau zeigen, wie ich heute eine ordentliche Live-Auktionsplattform mit Next.js und Supabase bauen würde – basierend auf dem, was ich tatsächlich ausgeliefert habe.
---
Warum Next.js und Supabase speziell dafür
Es gibt ein Dutzend Wege, um Echtzeit zu realisieren. Socket.io auf einem Node-Server, Ably, Pusher, Firebase – ich habe alle zu verschiedenen Zeitpunkten genutzt. Aber die Kombination Next.js + Supabase verdient ihren Platz hier aus einem bestimmten Grund: Supabase Realtime basiert auf PostgreSQL's logischer Replikation, das heißt, deine Live-Gebotsaktualisierungen und deine persistente Datenschicht sind dasselbe System. Kein Synchronisieren von zwei Quellen der Wahrheit. Kein Wundern, ob ein Gebot, das in den WebSocket ging, auch wirklich in der Datenbank angekommen ist.
Supabase bietet dir auch Auth, Row Level Security und Storage von Anfang an. Bei einer Auktionsseite, bei der „nur der Auktionseigentümer eine Charge beenden kann" und „ein Nutzer kann nicht auf sein eigenes Objekt bieten" tatsächliche Geschäftsregeln sind, sind RLS-Richtlinien in Postgres wirklich das richtige Werkzeug.
Und Next.js, weil ehrlich gesagt: App Router mit Server Components bedeutet, du kannst den Auktionskatalog statisch rendern, die SEO glücklich machen und nur das Echtzeit-Gebots-Widget auf dem Client hydratisieren. Diese Aufteilung ist wichtig. Du möchtest nicht für dynamisches Rendering auf einer Seite zahlen, die zu 90 % aus statischem Content besteht.
---
Das Schema zuerst entwerfen (Überspring das nicht)
Das ist der Punkt, wo die meisten Menschen sich beeilen und es später bereuen. Ich habe peinliche drei Tage damit verbracht, das Schema des Bath-Antiquitäten-Kunden mitten im Projekt umzugestalten, weil ich das Gebotsverlaufsmodell nicht richtig durchdacht hatte.
Hier ist die Kernstruktur, die ich jetzt verwende:
- `profiles`, erweitert Supabase's
auth.users, speichert Display-Namen, verifiziertes-Bieter-Flag und einencredit_balance, falls du depositbasierte Gebotabgabe nutzt - `auctions`, das Event selbst;
starts_at,ends_at,status(draft | live | closed) undcreated_by - `lots`, einzelne Artikel innerhalb einer Auktion;
reserve_price,current_bid,current_bidder_id,lot_number,ends_at(Artikel können individuelle Countdowns haben) - `bids`, unveränderliches Append-Only-Log;
lot_id,bidder_id,amount,placed_at. Aktualisiere diese Tabelle niemals. Nie. - `auction_participants`, eine Join-Tabelle, die verfolgt, wer sich für welche Auktion registriert hat (nützlich für Deposit-Holds und Benachrichtigungs-Targeting)
Die Spalten current_bid und current_bidder_id in lots sind absichtlich denormalisiert. Ja, du könntest sie bei jedem Lesen aus der bids-Tabelle ableiten, aber unter gleichzeitiger Last wird diese Abfrage schnell teuer. Denormalisiere es, behalte die bids-Tabelle als dein Audit-Log, und nutze eine Postgres-Funktion, um lots atomar zu aktualisieren, wenn ein Gebot angenommen wird.
Die Atomic Bid Function
Das ist der Teil, den die meisten Tutorials überspringen. Race Conditions bei Auktionen sind real. Zwei Nutzer reichen £520 in derselben Millisekunde ein – was passiert?
Die Antwort ist eine Postgres-Funktion mit FOR UPDATE-Locking auf der lot-Zeile:
``` create or replace function place_bid(p_lot_id uuid, p_bidder_id uuid, p_amount numeric) returns json as $$ declare v_lot lots%rowtype; begin select * into v_lot from lots where id = p_lot_id for update;
if v_lot.status != 'live' then return json_build_object('success', false, 'error', 'Lot is not live'); end if;
if p_amount <= v_lot.current_bid then return json_build_object('success', false, 'error', 'Bid too low'); end if;
if p_bidder_id = v_lot.current_bidder_id then return json_build_object('success', false, 'error', 'You are already the highest bidder'); end if;
insert into bids (lot_id, bidder_id, amount) values (p_lot_id, p_bidder_id, p_amount);
update lots set current_bid = p_amount, current_bidder_id = p_bidder_id where id = p_lot_id;
return json_build_object('success', true, 'new_bid', p_amount); end; $$ language plpgsql security definer; ```
Rufe das aus deiner Next.js-API-Route über supabase.rpc('place_bid', {...}) auf. Das FOR UPDATE-Lock bedeutet, dass nur eine Transaktion pro lot in jedem Moment gewinnt. Die andere bekommt einen Serialisierungsfehler und du zeigst eine freundliche "jemand hat dich gerade übergeboten"-Nachricht auf dem Client.
---
Row Level Security, die Ebene der Auktionsregeln
RLS ist eine dieser Dinge, die Entwickler entweder sofort lieben oder meiden, weil es sich undurchsichtig anfühlt. Ich war im Meidungs-Lager, bis mich ein FinTech-Projekt bei Seahawk die harte Tour lehrte, dass die Erzwingung der Zugriffskontrolle nur im Anwendungscode eine falsch konfigurierte API-Route entfernt vom Desaster ist.
Für eine Auktionsseite sind dies die Richtlinien, die zählen:
- Jeder kann Live-Lose lesen,
SELECTauflotswobeiauctions.status = 'live' - Nur authentifizierte, verifizierte Bieter können Gebote eintragen, prüfe
profiles.verified_bidder = truein der Policy - Nur der Auktionsersteller kann den Lotstatus aktualisieren,
UPDATEauflotswobeiauctions.created_by = auth.uid() - Gebotsverlauf ist lesbar für den Auktionsersteller des Loses und den Bieter selbst, niemand sonst muss den vollständigen Gebotsverlauf in Echtzeit einsehen
Die Supabase RLS-Dokumentation ist hier wirklich gut, es lohnt sich, den Abschnitt zu Security-Definer-Funktionen zu lesen, denn er interagiert damit, wie RPC-Aufrufe wie place_bid funktionieren.
Eine Falle: Wenn du Security Definer auf deine Postgres-Funktion anwendest (wie oben), wird sie mit den Privilegien des Funktionseigentümers ausgeführt und umgeht RLS. Das ist beabsichtigt – du möchtest, dass die Gebotseintragung die RLS des Bieters umgeht, damit sie die Loszeile sperren und aktualisieren kann. Aber das bedeutet, du musst deine eigenen Business-Logic-Prüfungen innerhalb der Funktion erzwingen, was der Code oben tut.
---
Supabase Realtime in Next.js einrichten
Hier wird es wirklich befriedigend. Supabase Realtime lässt dich dich mit WebSockets darunter auf Änderungen an einer Postgres-Tabelle abonnieren, und das Client-SDK macht es fast beschämend einfach.
Auf deiner Auktionslot-Seite, einer Client Component im Next.js App Router, würdest du etwa folgendes tun:
``` 'use client'
import { useEffect, useState } from 'react' import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
export default function LotBidDisplay({ lotId, initialBid }) { const [currentBid, setCurrentBid] = useState(initialBid) const supabase = createClientComponentClient()
useEffect(() => { const channel = supabase.channel(lot-${lotId}).on( 'postgres_changes', { event: 'UPDATE', schema: 'public', table: 'lots', filter:id=eq.${lotId}}, (payload) => { setCurrentBid(payload.new.current_bid) } ).subscribe()
return () => { supabase.removeChannel(channel) } }, [lotId])
return <div>Current bid: £{currentBid.toLocaleString()}</div> } ```
Übergeben Sie initialBid von einer Server Component, die bei jeder Anfrage frische Daten abruft. Der Client übernimmt dann und lauscht auf UPDATE-Events in dieser speziellen Lot-Zeile. Jedes Mal, wenn place_bid erfolgreich ausgeführt wird, sendet Supabase die Änderung aus und die UI jedes verbundenen Bieters wird typischerweise innerhalb von etwa 100-300ms aktualisiert.
Handling des Countdown-Timers
Lose haben normalerweise einen Countdown „schließt in 3:42". Vertrau nicht der Client-Uhr dafür. Leite die Endzeit von lots.ends_at ab (gespeichert in UTC in Postgres) und berechne verbleibende Sekunden auf dem Client mit Date.now(). Synchronisiere es alle 60 Sekunden mit einem frischen Abruf neu, falls es driftet. Und füge „Soft-Close"-Logik hinzu: Wenn ein Gebot in den letzten 60 Sekunden ankommt, verlängere ends_at um zwei Minuten. Das ist standardmäßiges Auktionsverhalten und Bieter erwarten es.
---
Next.js App Router Architecture für die Auctions-UI
Die Seitenstruktur, die ich verwenden würde:
``app/ auctions/ page.tsx ← Server Component, listet Live-Auktionen auf (ISR, revalidate: 60) [auctionId]/ page.tsx ← Server Component, ruft Lots-Liste serverseitig ab LotGrid.tsx ← Client Component, abonniert Lot-Statusänderungen [lotId]/ page.tsx ← Server Component, initiale Lot-Daten + Metadaten für SEO BidPanel.tsx ← Client Component, Echtzeit-Gebotsanzeige + Gebotsform``
Der Katalog (/auctions) nutzt Incremental Static Regeneration mit einer 60-Sekunden-Revalidierung. Einzelne Lot-Seiten werden beim ersten Laden server-seitig gerendert (zum Teilen, Vorschau und og:image-Generierung), dann wird die Kontrolle an Client-Komponenten für die Live-Funktionen übergeben.
Eine Sache, die ich immer mache: die BidPanel-Komponente lazy-loaded hinter dynamic(() => import('./BidPanel'), { ssr: false }) halten. Das macht ohnehin nur client-seitig Sinn, und es hält deine initiale HTML-Payload für Nutzer mit langsamen Verbindungen schlank – was, wenn dein Publikum älter ist (wie bei Antiken-Auktionen üblich), deutlich wichtiger ist, als man erwarten würde.
---
Authentifizierung und der "Verified Bidder"-Flow
Standard-Supabase-Auth mit E-Mail/Passwort oder Magic Link funktioniert für die Registrierung einwandfrei. Aber Auktionen benötigen oft einen zusätzlichen Schritt: Bieter-Verifizierung. Du könntest einen Kreditkarteneintrag, eine ID-Verifizierung oder nur eine Admin-Genehmigung brauchen, bevor jemand tatsächlich ein Gebot abgeben kann.
Das Muster, das ich verwende: ein verified_bidder-Boolean auf der Profiles-Tabelle, standardmäßig falsch. Nach der Anmeldung sieht der Benutzer einen "Registrierung abschließen"-Bildschirm. Nach der Genehmigung (manuell durch Admin oder automatisch nach einer Stripe-Zahlungsgenehmigung) flipst du das Flag. Die RLS-Richtlinie auf Geboten prüft es. Sie können durchsuchen und beobachten, aber nicht bieten, bis sie verifiziert sind.
Bei Stripe-Zahlungsautorisierungen: Stripe's payment intents mit capture_method: manual ist der richtige Weg – du autorisierst eine £50-Blockierung, erfasst sie, wenn die Person gewinnt, gibst sie frei, wenn nicht. Das reduziert No-Pay-Situationen drastisch, die, vertrau mir, der Horror jedes Online-Auktionsbetreibers sind.
---
Deployment, Performance und die Stolpersteine
Deploye auf Vercel, das ist die offensichtliche Wahl für Next.js und das Edge-Netzwerk funktioniert gut mit Supabase's globaler Infrastruktur. Stelle sicher, dass dein Supabase-Projekt in der AWS-Region ist, die deiner Vercel-Deployment-Region am nächsten liegt. Ich habe 40–60ms völlig unnötiger Latenz gesehen, weil jemand Vercel in us-east-1 und Supabase in eu-west-2 deployed hat. Wähle eine Region, deploy beide dort.
Ein paar Dinge, die dir Kopfschmerzen bereiten, wenn du sie nicht von Anfang an richtig angehst:
- WebSocket-Verbindungsgrenzen. Supabase' kostenloses Tier erlaubt etwa 200 gleichzeitige Realtime-Verbindungen. Wenn Ihre Auktion viral wird, spielt diese Obergrenze eine Rolle. Überprüfen Sie Ihren Plan.
- Optimistische UI für Gebote. Zeigen Sie das Gebot sofort auf dem Bildschirm des Bieters an, bevor der Server bestätigt. Falls es fehlschlägt (überboten, Race Condition), machen Sie es mit einem Fehler rückgängig. Der 200-300ms-Server-Roundtrip ist unmerklich, wenn die UI nicht darauf wartet.
- Lot-Endzeitpuffer. Schließen Sie einen Lot niemals genau bei
ends_at. Geben Sie ihm einen 2-3 Sekunden großen server-seitigen Puffer, um in-flight Gebote zu ermöglichen, die kurz vor der Frist eingereicht wurden. Handhaben Sie dies in Ihrerclose_lot-Scheduled-Function. - E-Mail-Benachrichtigungen: Nutze Supabase Edge Functions mit Resend oder Postmark, um „Du wurdest überboten"- und „Du hast gewonnen!"-E-Mails zu versenden. Versuche das nicht von deinen Next.js API Routes aus, sie können timeout, und Auktionsgebote werden echte gereizt, wenn Benachrichtigungen unzuverlässig sind.
---
FAQ
Wie viele gleichzeitige Bieter kann Supabase Realtime verarbeiten?
Supabase's Pro-Plan unterstützt standardmäßig bis zu 500 gleichzeitige Realtime-Verbindungen, mit höheren Limits verfügbar. Für die meisten Auktionsseiten – es sei denn, du betreibst etwas in der Größe von Sotheby's online – ist das mehr als genug. Wenn du mit tausenden gleichzeitigen Zuschauern rechnest, erwäge, Lot-Updates über einen einzelnen server-seitigen Channel statt pro-Nutzer-Subscriptions zu broadcasten, und schau dir Supabase's Realtime Broadcast Feature an, das effizienter für High-Fan-Out-Szenarien ist.
Sollte ich Supabase Realtime oder einen dedizierten Service wie Ably nutzen?
Für die meisten Projekte ist Supabase Realtime vollkommen ausreichend und die Integration ist deutlich simpler, da deine Daten ohnehin schon in Supabase sind. Ich würde nur zu Ably oder Pusher greifen, wenn du Sub-50ms-Latenz global brauchst, oder wenn du etwas mit Millionen gleichzeitiger Verbindungen baust. Eine Antiken-Auktion, eine Charity-Fundraiser, der Online-Verkauf einer kleinen Kunstgalerie – Supabase verwaltet das alles problemlos.
Was passiert, wenn die WebSocket-Verbindung eines Benutzers während einer Auktion abbricht?
Supabase's Client SDK versucht automatisch, sich erneut zu verbinden. Aber du solltest den aktuellen Lot-Status (current_bid, ends_at) bei Wiederverbindung immer neu abrufen, anstatt darauf zu vertrauen, was zuvor im lokalen State war, bevor die Verbindung abbrach. Füge einen online/offline Event Listener in deiner Client Component hinzu und triggere einen frischen Server Fetch, wenn die Verbindung wiederhergestellt wird.
Kann ich Next.js Server Actions verwenden, um Gebote abzugeben statt einer API-Route?
Ja, und ich habe es getan. Server Actions in Next.js 14 sind praktisch, sie entfernen den Boilerplate einer dedizierten /api/bid-Route. Der Tradeoff ist, dass Server Actions ein bisschen schwerer einzeln zu rate-limitieren sind (du würdest Rate Limiting auf Middleware-Ebene statt pro-Action anwenden). Für eine Production-Auktionsseite würde ich Upstash Redis Rate Limiting in der Middleware hinzufügen, um zu verhindern, dass ein einzelner Nutzer Gebote spamt – egal, ob du Actions oder API Routes nutzt.
Wie gehe ich mit Unentschieden um, zwei identische Gebote zur gleichen Zeit?
Das FOR UPDATE Lock in der place_bid Postgres-Funktion serialisiert gleichzeitige Gebote, daher können technisch Unentschieden auf Datenbankebene nicht vorkommen. Eines wird erfolgreich sein, das andere schlägt mit einer „bid too low" Antwort fehl (da beide gleich current_bid sind und die Prüfung p_amount <= v_lot.current_bid ist). Wer zuerst kommt, mahlt zuerst. Das ist Standard-Auktionspraxis und die meisten Bieter verstehen das.
---
Der Antiken-Händler aus Bath betreibt für was es wert ist seine monatlichen Auktionen jetzt schon über zwei Jahre online. Peak-Bieter an einem Samstagabend: 84, sein ganzes Dorf scheinbar schauwend, wie eine umstrittene Georgian-Silber-Partie das Doppelte ihrer Reserve erzielt. Supabase zuckte nicht. Next.js zuckte nicht. Das Einzige, das brach, war sein Wi-Fi, weil er es vom Ladenboden aus betrieb.
Realtime ist schwer vorausschauend zu durchdenken, aber once das Schema solide ist und die atomare Bid-Funktion an Ort und Stelle, ist der Rest meistens Rohrleitungen. Mach die Grundlagen richtig und du verbringst deine Zeit mit den spaßigen Bits, die Countdown-Animationen, die „einmal, zweimal" UX, statt Race Conditions um Mitternacht zu debuggen.
