Skip to main content

Command Palette

Search for a command to run...

Building Resumable WebSockets with Supabase Edge Functions and Postgres

Published
Building Resumable WebSockets with Supabase Edge Functions and Postgres
R

Support Engineer @Supabase | StackOverflow

Supabase makes it easy to build realtime applications with Postgres and Edge Functions. But building reliable WebSockets in serverless environments can still be challenging.

WebSockets help you build realtime apps such as chat systems, live dashboards, and AI assistants. However, connections can break in serverless environments like Supabase Edge Functions. Connections may drop during reconnects, messages can get lost, duplicate events may appear, and servers can restart without warning.

In this post, we will build a resumable AI chat using Supabase Edge Functions and Postgres. The experience stays smooth for users even after network problems or server restarts. We use Postgres as a durable event store so clients can reconnect and replay missed messages automatically.

The result is a lightweight and reliable realtime chat architecture built entirely on Supabase. No need for extra infrastructure like Pusher or Socket.IO.

If you enjoy building advanced realtime systems with Supabase, you may also like:

Architecture Overview

Here is how the system works:

  • Browser client: A simple HTML page with JavaScript that connects to the WebSocket and displays messages.

  • Supabase Edge Function: Acts as a WebSocket proxy and handles connections, authentication, and message delivery.

  • Postgres database: Stores sessions, events, and idempotency keys to make the system resumable.

  • OpenAI: Provides streaming AI responses using gpt-4o-mini.

Core idea: Do not treat WebSockets as stateful connections. Instead, treat them as resumable streams backed by Postgres events.

Every message becomes an event stored in the database. When the client reconnects, it asks for all events it missed.

This architecture solves many common problems with serverless WebSockets.

Designing the Database Schema

We need four simple tables. You can run the following SQL directly in the Supabase SQL Editor Documentation.

create extension if not exists pgcrypto;

create table ws_sessions (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null,
  created_at timestamptz default now(),
  updated_at timestamptz default now(),
  last_event_id bigint default 0,
  status text default 'active',
  metadata jsonb default '{}'::jsonb
);

create table ws_events (
  id bigint generated by default as identity primary key,
  session_id uuid not null references ws_sessions(id) on delete cascade,
  event_type text not null,
  payload jsonb not null,
  created_at timestamptz default now()
);
create index ws_events_session_id_id_idx on ws_events(session_id, id);

create table ws_idempotency_keys (
  session_id uuid not null references ws_sessions(id) on delete cascade,
  idempotency_key uuid not null,
  primary key(session_id, idempotency_key)
);

create unlogged table ws_live_connections (
  session_id uuid primary key,
  connected_at timestamptz default now(),
  last_seen_at timestamptz default now(),
  edge_region text
);

-- Enable Row Level Security on all tables
DO
$$
DECLARE
row record;
BEGIN
FOR row IN SELECT tablename FROM pg_tables AS t
WHERE t.schemaname = 'public'
LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY;', row.tablename);
END LOOP;
END;
$$;

Important Design Choices

  • ws_events uses BIGINT GENERATED BY DEFAULT AS IDENTITY to guarantee event ordering during replay.

  • ws_live_connections is an unlogged table because it only stores temporary connection state.

  • The replay index keeps reconnect queries fast.

  • Old sessions can be cleaned automatically using pg_cron Documentation.

This schema turns Postgres into a lightweight and reliable event log for durable WebSockets.

The Browser Client

The client is a single HTML file with JavaScript. It uses Supabase Anonymous Auth Documentation for quick testing. But it should feel similar when implementing this with authenticated users in Supabase.

Anonymous Login

const { data, error } = await supabase.auth.signInAnonymously();

Store Reconnect State in localStorage

The client stores:

  • sessionId

  • lastEventId

This allows the browser to reconnect and continue streaming from the last received event.

Smart Reconnection

The client also implements:

  • Exponential backoff

  • Heartbeats

  • Missed event replay

  • Stream rebuilding from assistant_delta

The complete browser client code, including all reconnect logic and streaming UI handling, is available in the GitHub repository:

resumable-ai-chat GitHub Repository

The client sends user_message events with an idempotency_key. It receives assistant_delta chunks and rebuilds the AI response in real time.

If the connection drops, the client reconnects and replays all missed events from Postgres automatically.

Building the WebSocket Edge Function

The Edge Function (index.ts) is the core of the system. It runs on Deno using Supabase Edge Functions Documentation.

Main Responsibilities

  • Validate the JWT from the query parameter.

  • Create or reuse a sessionId.

  • Upgrade the request immediately using Deno.upgradeWebSocket.

  • Replay missed events from Postgres.

  • Handle ping, resume, and user_message events.

The implementation uses EdgeRuntime.waitUntil() to continue cleanup tasks safely after the response lifecycle finishes. It also uses bufferedAmount to detect slow clients and close connections gracefully.

Streaming AI Responses Reliably

When a user sends a message:

  1. Save the message as an event using an idempotency check.

  2. Send an assistant_started event.

  3. Call OpenAI with streaming enabled.

  4. Forward every delta chunk to the client in realtime.

  5. Save the final assistant response when streaming completes.

Example code from the Edge Function:

const openaiRes = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
  },
  body: JSON.stringify({
    model: 'gpt-4o-mini',
    stream: true,
    messages: [{ role: 'user', content: msg.content }],
  }),
});

// Stream processing loop...
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  // parse SSE and send assistant_delta
}

The browser rebuilds the assistant response from streamed deltas. If the connection drops, the fully completed response is replayed from Postgres after reconnecting.

Handling Edge Runtime Reality

Edge Functions restart regularly, so the system must handle interruptions gracefully.

This implementation includes:

  • A server_restarting event sent before shutdown.

  • Automatic client reconnection.

  • PREEMPTIVE_RESTART_MS handling.

  • EdgeRuntime.waitUntil() for cleanup tasks.

  • Heartbeats every 10 seconds to keep connections alive.

This makes the experience feel continuous for users, even during infrastructure restarts.

Final Thoughts

Postgres works surprisingly well as a lightweight event store. You get durability, ordering, idempotency, and replay support using tools you already have inside Supabase.

Reconnects are treated as a first-class design concern. Reliable WebSockets are mostly an event replay problem. With Supabase Edge Functions and Postgres, you can build a powerful and low-cost alternative to heavier realtime infrastructure.

Try it yourself:

  1. Deploy the Edge Function.

  2. Run the SQL schema.

  3. Open the HTML client.

In just a few minutes, you will have a fully resumable AI chat running on Supabase.

Full source code:

resumable-ai-chat GitHub Repository

If you enjoyed this post, check out the other Supabase and Postgres tutorials on the blog for more practical backend engineering tips.