Read this first — what's shipped vs. planned

TopGun does NOT replace Supabase's full BaaS. TopGun is a sync layer in front of YOUR Postgres — it does not replace Supabase Auth, Edge Functions, Storage buckets, or row-level security policies. TopGun ships per-map ACL booleans (read/write) plus JWT roles claim extraction; first-class RBAC and Postgres RLS-style policies are on the roadmap (see /docs/roadmap). Supabase postgres_changes (CDC from the Postgres write-ahead log) maps loosely to TopGun's CRDT-write notifications, but TopGun owns the write path — clients write through the TopGun client, not directly to Postgres. Reference: /docs/roadmap.

DocsGuidesMigrating from Supabase Realtime

Migrating from Supabase Realtime

Supabase Realtime is a Postgres-native real-time library: clients open WebSocket channels, receive broadcast events, track presence, and listen to postgres_changes derived from the write-ahead log. TopGun is an offline-first sync layer that sits in front of YOUR Postgres — clients read and write locally, and the TopGun server merges and persists. This guide maps Supabase Realtime primitives one-to-one onto TopGun and names the gaps along the way.

Concept Mapping

Supabase ConceptTopGun EquivalentNotes
Channel (supabase.channel(‘room-1’))client.topic(‘room-1’)Named pub/sub topic; HLC-ordered delivery
Broadcast event (channel.send({type:‘broadcast’}))topic.publish(payload) / topic.subscribe(cb)Fan-out to all subscribers of the topic
Presence (channel.track(state) / channel.on(‘presence’, …))release_on_disconnect (single-node only)Cross-client presence broadcast is on the roadmap; release_on_disconnect is a lock-cleanup primitive
postgres_changes eventCRDT write notification via client.query().subscribe()TopGun owns the write path; notifications are emitted from CRDT merge, not Postgres WAL
Row Level Security (RLS) policiesPer-map ACL booleans (read/write)Full RLS-style policies on roadmap — see /docs/roadmap
Supabase Auth (signInWithPassword, etc.)BYO JWT issuer + client.setAuthToken(jwt)TopGun does not issue JWTs; pair with BetterAuth, Clerk, Supabase Auth, or any OIDC issuer
Supabase Edge FunctionsNot in scopeUse your own server endpoints (Node, Bun, Deno, Go, Rust, etc.)
Supabase Storage bucketsNot in scopeContinue using Supabase Storage, S3, or any object store independently
createClient(url, anonKey)new TopGunClient({ serverUrl, storage })Single client entry point; storage adapter pluggable (IndexedDB in browsers)
Client packages (@supabase/realtime-js, supabase-js)@topgunbuild/clientOne package covers data + realtime; auth/storage are BYO
Pricing tiers (Free / Pro / Team / Enterprise)Self-hosted, OSS (Apache-2.0)Cost = your infra; no usage-based pricing
Postgres-native (your-own-Postgres framing)TopGun also runs in front of YOUR PostgresParity — durable store is plain Postgres; pg_dump works
Offline writesFirst-class via IDBAdapterNot supported in Supabase Realtime; TopGun queues writes locally and syncs on reconnect

Side-by-Side Code Patterns

Pattern A — Initialize and authenticate

Supabase Realtime
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  'https://my-project.supabase.co',
  'public-anon-key',
);

await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'password',
});
TopGun
import { TopGunClient } from '@topgunbuild/client';
import { IDBAdapter } from '@topgunbuild/adapters';

const client = new TopGunClient({
  serverUrl: 'wss://topgun.example.com',
  storage: new IDBAdapter(),
});

// JWT issuance is handled by your own auth server.
// See /docs/guides/authentication for integration options.
const jwt = await myAuthServer.signIn(email, password);
client.setAuthToken(jwt);

TopGun does not issue JWTs itself. Bring your own auth provider (BetterAuth, Clerk, Supabase Auth, or any OIDC-compatible issuer) and pass the resulting JWT via client.setAuthToken(). See the Authentication guide for step-by-step setup.

Pattern B — Channel-subscribe vs query-subscribe

Supabase Realtime exposes two listening shapes — broadcast (fan-out messages) and postgres_changes (CDC from the WAL). TopGun maps these onto two distinct primitives: pub/sub topics and CRDT-backed query subscriptions. Both pairs are shown below.

Supabase Realtime
// Broadcast — fan-out messages
const channel = supabase.channel('room-1');

channel
  .on('broadcast', { event: 'msg' }, (payload) => {
    console.log('broadcast', payload);
  })
  .subscribe();

channel.send({
  type: 'broadcast',
  event: 'msg',
  payload: { text: 'hello' },
});

// postgres_changes — CDC from the WAL
const todosChannel = supabase.channel('todos-changes');

todosChannel
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'todos' },
    (payload) => {
      console.log('row inserted', payload.new);
    },
  )
  .subscribe();
TopGun
// Pub/sub topic — fan-out messages
const topic = client.topic('room-1');

const unsub = topic.subscribe((payload) => {
  console.log('topic', payload);
});

topic.publish({ text: 'hello' });

// CRDT-backed query subscription
// Server-pushed deltas via WebSocket; emitted from
// CRDT merge, not from Postgres WAL.
const query = client.query('todos');

const unsubQuery = query.subscribe((results) => {
  console.log('todos', results);
});

Supabase delivers WAL-derived row events; TopGun delivers CRDT-merge deltas. Functionally similar for “show me changes to this collection,” but TopGun owns the write path — see Pattern C.

Pattern C — Write to Postgres + listen vs CRDT write via client

Supabase Realtime
// Write goes through PostgREST (network round-trip).
await supabase.from('users').insert({
  id: 'alice',
  name: 'Alice',
  age: 30,
});

// Listen for the change via postgres_changes
// (separate WebSocket subscription).
const channel = supabase.channel('users-changes');
channel
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'users' },
    (payload) => {
      console.log('row inserted', payload.new);
    },
  )
  .subscribe();
TopGun
// Synchronous, local-first write. The CRDT merge
// propagates to the server (and to other clients
// subscribing via client.query) automatically.
client.getMap('users').set('alice', {
  id: 'alice',
  name: 'Alice',
  age: 30,
});

// Listen via the same query subscription; merges
// from this client and others both arrive here.
const query = client.query('users');
const unsub = query.subscribe((results) => {
  console.log('users', results);
});

In Supabase, the write path is client → PostgREST → Postgres → WAL → Realtime → subscribers. In TopGun, the write path is client (local LWWMap) → SyncEngine → server → CRDT merge → subscribers, and the durable Postgres write happens server-side. Clients do not write to Postgres directly.

If you’re using React

import { useQuery, useMutation } from '@topgunbuild/react';
import { type User } from './schema';

function UserList() {
  const { data: users } = useQuery<User>('users');
  const { create } = useMutation<User>('users');
  const add = () => create(crypto.randomUUID(), { name: 'Alice', age: 30 });
  return (
    <>
      <button onClick={add}>Add User</button>
      <ul>{users.map(u => <li key={u._key}>{u.name}</li>)}</ul>
    </>
  );
}

Deployment Mode Semantics

DeploymentStatusNotes
Single-node (recommended for migration)StableAll Supabase Realtime capabilities mapped above are production-ready in single-node mode
Cluster (current)Partition-routing, no RaftSafe when one node owns the partition; weaker guarantees under node failure — see /docs/roadmap
Cluster (planned)Raft-replicated — plannedMulti-region with QUORUM/STRONG consistency on roadmap — see /docs/roadmap

For a first migration, start on single-node. The partition-routed cluster mode works but does not yet have Raft-backed consensus; see the roadmap for cluster-safety status.

Why migrate?

  • Local-first reads/writes: Operations never wait for network; the UI updates immediately. Supabase Realtime is read-only on the client; writes still go through PostgREST + a network round-trip.
  • Offline persistence: First-class via IDBAdapter. Supabase Realtime has no offline mode.
  • CRDT auto-merge: LWW-Map and OR-Map merge deterministically via HLC. No “lost update” surprises when two clients write the same key offline.
  • Your-own-Postgres: Parity with Supabase’s “you own your Postgres” framing. TopGun’s durable store is plain Postgres; pg_dump works.
  • Performance: 483K ops/sec fire-and-forget, ~37K ops/sec fire-and-wait at 1.5ms p50 on M1 Max (200 connections). See benchmarks for methodology.

Migration checklist

  1. Audit Supabase Realtime features in use (channels, broadcast, presence, postgres_changes, RLS).
  2. Cross-reference each against the Concept Mapping table above; mark mappings vs gaps.
  3. Audit which Supabase services you use beyond Realtime (Auth, Storage, Edge Functions, RLS) — TopGun replaces only the Realtime + sync surface; the others stay on Supabase or move to alternative providers. Make explicit decisions per service.
  4. Port the client layer: replace createClient(url, anonKey) + the auth flow with new TopGunClient({ serverUrl, storage }) + setAuthToken (BYO JWT). See Authentication.
  5. Port channels/queries one resource at a time: replace channel.on('broadcast', ...) with client.topic(name).subscribe(cb), replace channel.on('postgres_changes', ...) with client.query().subscribe(), and replace direct supabase.from().insert() with client.getMap().set() (the write path moves through TopGun, not directly to Postgres).
  6. Backfill data via a one-time migration script that reads from Postgres (or via supabase-js) and writes to TopGun via the client SDK or direct Postgres insert (TopGun owns the write path going forward).