Prisma Pulse vs Supabase Realtime vs Debezium: Postgres CDC 2026
Prisma Pulse vs Supabase Realtime vs Debezium: Postgres CDC 2026
TL;DR
Change Data Capture (CDC) streams database changes as events — every INSERT, UPDATE, and DELETE can trigger code without polling. Prisma Pulse brings CDC to TypeScript — subscribe to Prisma model changes with the same familiar ORM API, powered by PostgreSQL logical replication, works with any Postgres-compatible database. Supabase Realtime is the WebSocket layer for Supabase's Postgres — broadcast table changes to connected clients in real time, with Row Level Security filtering so users only receive changes they're authorized to see. Debezium is the enterprise-grade CDC platform — a Kafka Connect source connector that streams every Postgres change to Kafka, enabling microservices to react to database events without coupling to the application. For TypeScript app-level CDC: Prisma Pulse. For client-facing real-time UIs with RLS: Supabase Realtime. For microservice event streaming at scale: Debezium.
Key Takeaways
- Prisma Pulse uses PostgreSQL logical replication —
pg_logicalreplication slot under the hood - Supabase Realtime supports WebSocket and SSE — browser clients subscribe to table changes
- Debezium can capture every transaction in order — exactly-once, ordered event stream via Kafka
- Prisma Pulse filters by model —
prisma.user.$on('create', callback)for TypeScript type safety - Supabase Realtime applies RLS — users only receive changes matching their RLS policies
- Debezium supports schema evolution — schema registry tracks column additions/removals
- All three use PostgreSQL's logical replication —
wal_level = logicalrequired
What is Change Data Capture?
Without CDC — polling approach:
while (true) {
const newOrders = await db.orders.findMany({
where: { updatedAt: { gt: lastCheckedAt } }
});
processNewOrders(newOrders);
lastCheckedAt = new Date();
await sleep(5000); // Poll every 5 seconds
}
Problems:
- 5-second latency minimum
- Database load from constant polling
- Missed updates if multiple happen between polls
- Complex "updated_at" tracking logic
With CDC — event stream approach:
db.orders.subscribe("*", (change) => {
// INSERT, UPDATE, DELETE
processOrderChange(change);
});
Benefits:
- Millisecond latency
- Zero polling overhead
- Every change captured in order
- Built on WAL (Write-Ahead Log) — reliable and durable
PostgreSQL Logical Replication Setup
All three solutions require this PostgreSQL configuration:
-- postgresql.conf (or set via ALTER SYSTEM)
wal_level = logical -- Required for CDC
max_replication_slots = 10 -- One slot per CDC consumer
max_wal_senders = 10 -- Parallel WAL streaming connections
-- Create a replication slot (Prisma/Debezium do this automatically)
SELECT pg_create_logical_replication_slot('my_slot', 'pgoutput');
-- Grant replication permissions
ALTER USER your_app_user REPLICATION;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO your_app_user;
Prisma Pulse: TypeScript-Native CDC
Prisma Pulse wraps PostgreSQL logical replication with the Prisma ORM API — subscribe to model changes with TypeScript types automatically derived from your schema.
Installation
npm install @prisma/extension-pulse
# Requires Prisma ORM
Setup
// lib/db.ts
import { PrismaClient } from "@prisma/client";
import { withPulse } from "@prisma/extension-pulse";
const prisma = new PrismaClient().$extends(
withPulse({
apiKey: process.env.PULSE_API_KEY!, // From Prisma Platform
})
);
export { prisma };
Subscribing to Changes
import { prisma } from "@/lib/db";
// Subscribe to all changes on the Order model
async function watchOrders() {
const subscription = await prisma.order.subscribe();
// Async iterator — runs forever
for await (const event of subscription) {
switch (event.action) {
case "create": {
const order = event.created;
// TypeScript knows: order.id, order.userId, order.total, etc.
console.log("New order:", order.id, "Total:", order.total);
await sendOrderConfirmation(order);
break;
}
case "update": {
const { before, after } = event;
if (before.status !== after.status) {
await notifyStatusChange(after.userId, after.status);
}
break;
}
case "delete": {
const deleted = event.deleted;
await cleanupOrderData(deleted.id);
break;
}
}
}
}
watchOrders();
Filtered Subscriptions
// Only subscribe to specific events and conditions
const subscription = await prisma.order.subscribe({
create: {
after: {
status: "pending", // Only new orders with pending status
},
},
update: {
after: {
status: "shipped", // Only orders that were just shipped
},
},
});
for await (const event of subscription) {
if (event.action === "create") {
await processNewPendingOrder(event.created);
} else if (event.action === "update") {
await sendShippingNotification(event.after);
}
}
Real-World: Cache Invalidation
// Invalidate Redis cache when data changes
async function syncCacheWithDatabase() {
const userSub = await prisma.user.subscribe({
update: {},
delete: {},
});
for await (const event of userSub) {
if (event.action === "update" || event.action === "delete") {
const userId = event.action === "update" ? event.after.id : event.deleted.id;
// Invalidate all cache keys for this user
await redis.del(`user:${userId}`);
await redis.del(`user:${userId}:profile`);
await redis.del(`user:${userId}:posts`);
console.log(`Cache invalidated for user ${userId}`);
}
}
}
Supabase Realtime: Client-Facing WebSocket Changes
Supabase Realtime streams Postgres changes to connected browser/mobile clients via WebSocket. Row Level Security filters ensure users only receive their own data.
Client-Side Subscription
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // Anon key — RLS handles security
);
// Subscribe to all changes on a table
const subscription = supabase
.channel("orders-channel")
.on(
"postgres_changes",
{
event: "*", // INSERT | UPDATE | DELETE | *
schema: "public",
table: "orders",
// RLS automatically filters — user only sees their own orders
},
(payload) => {
console.log("Change received!", payload);
// payload.eventType: 'INSERT' | 'UPDATE' | 'DELETE'
// payload.new: new row data
// payload.old: old row data (updates + deletes)
if (payload.eventType === "INSERT") {
setOrders((prev) => [payload.new as Order, ...prev]);
} else if (payload.eventType === "UPDATE") {
setOrders((prev) =>
prev.map((o) => o.id === payload.new.id ? (payload.new as Order) : o)
);
} else if (payload.eventType === "DELETE") {
setOrders((prev) => prev.filter((o) => o.id !== payload.old.id));
}
}
)
.subscribe();
// Cleanup
return () => subscription.unsubscribe();
React Hook for Realtime
import { useEffect, useState } from "react";
import { createClient } from "@supabase/supabase-js";
import type { RealtimeChannel } from "@supabase/supabase-js";
function useRealtimeTable<T extends { id: string }>(
tableName: string,
initialData: T[]
) {
const [data, setData] = useState<T[]>(initialData);
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
useEffect(() => {
const channel: RealtimeChannel = supabase
.channel(`${tableName}-realtime`)
.on(
"postgres_changes",
{ event: "*", schema: "public", table: tableName },
(payload) => {
setData((prev) => {
if (payload.eventType === "INSERT") {
return [payload.new as T, ...prev];
}
if (payload.eventType === "UPDATE") {
return prev.map((item) =>
item.id === (payload.new as T).id ? (payload.new as T) : item
);
}
if (payload.eventType === "DELETE") {
return prev.filter((item) => item.id !== (payload.old as T).id);
}
return prev;
});
}
)
.subscribe();
return () => { supabase.removeChannel(channel); };
}, [tableName]);
return data;
}
// Usage
function LiveOrdersList() {
const orders = useRealtimeTable<Order>("orders", []);
return (
<ul>
{orders.map((order) => (
<li key={order.id}>{order.id} — {order.status}</li>
))}
</ul>
);
}
Broadcasting Custom Events
// Beyond table changes — broadcast custom events to a channel
const channel = supabase.channel("presence-channel", {
config: { presence: { key: userId } },
});
// Track who's online
channel
.on("presence", { event: "sync" }, () => {
const state = channel.presenceState();
setOnlineUsers(Object.keys(state));
})
.on("presence", { event: "join" }, ({ key, newPresences }) => {
console.log("User joined:", key);
})
.on("presence", { event: "leave" }, ({ key }) => {
console.log("User left:", key);
})
.subscribe(async (status) => {
if (status === "SUBSCRIBED") {
await channel.track({ userId, joinedAt: new Date().toISOString() });
}
});
Debezium: Enterprise Kafka Connect CDC
Debezium is the open-source CDC platform for event-driven microservices — streams Postgres changes to Kafka, enabling any service to consume database events.
Architecture
PostgreSQL
└── WAL (Write-Ahead Log)
└── Debezium Connector (Kafka Connect)
└── Kafka Topic: postgres.public.orders
├── Order Service (Kafka Consumer)
├── Notification Service (Kafka Consumer)
├── Analytics Service (Kafka Consumer)
└── Search Index Service (Kafka Consumer)
Docker Compose Setup
# docker-compose.yml — Debezium with Kafka
version: "3.8"
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
kafka:
image: confluentinc/cp-kafka:7.5.0
depends_on: [zookeeper]
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
debezium:
image: debezium/connect:2.5
depends_on: [kafka]
ports:
- "8083:8083"
environment:
BOOTSTRAP_SERVERS: kafka:9092
GROUP_ID: debezium
CONFIG_STORAGE_TOPIC: debezium_configs
OFFSET_STORAGE_TOPIC: debezium_offsets
STATUS_STORAGE_TOPIC: debezium_status
Registering the Postgres Connector
# Register Debezium connector via REST API
curl -X POST http://localhost:8083/connectors \
-H "Content-Type: application/json" \
-d '{
"name": "postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"tasks.max": "1",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "replication_user",
"database.password": "password",
"database.dbname": "myapp",
"topic.prefix": "postgres",
"table.include.list": "public.orders,public.users",
"plugin.name": "pgoutput",
"publication.autocreate.mode": "all_tables",
"decimal.handling.mode": "double",
"timestamp.converter": "io.debezium.time.IsoTimestampConverter"
}
}'
Consuming CDC Events in Node.js
import { Kafka } from "kafkajs";
const kafka = new Kafka({
clientId: "notification-service",
brokers: ["kafka:9092"],
});
const consumer = kafka.consumer({ groupId: "notification-service-group" });
async function processOrderChanges() {
await consumer.connect();
await consumer.subscribe({
topic: "postgres.public.orders",
fromBeginning: false,
});
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
if (!message.value) return;
const event = JSON.parse(message.value.toString());
const { op, before, after } = event.payload;
// op: 'c' = create, 'u' = update, 'd' = delete, 'r' = read (snapshot)
switch (op) {
case "c": {
// New order created
console.log("New order:", after);
await sendOrderConfirmationEmail(after.user_id, after.id);
break;
}
case "u": {
// Order updated
if (before.status !== after.status) {
await sendStatusUpdateEmail(after.user_id, after.status);
}
break;
}
case "d": {
// Order deleted
await cleanupOrderResources(before.id);
break;
}
}
},
});
}
Feature Comparison
| Feature | Prisma Pulse | Supabase Realtime | Debezium |
|---|---|---|---|
| Target consumer | Node.js backend | Browser/mobile clients | Kafka consumers |
| TypeScript types | ✅ From Prisma schema | ❌ | ❌ |
| Row Level Security | ❌ | ✅ | ❌ |
| Kafka integration | ❌ | ❌ | ✅ |
| WebSocket delivery | ❌ | ✅ | ❌ |
| Ordering guarantee | ✅ | ✅ | ✅ (Kafka) |
| Schema evolution | ✅ (Prisma) | Limited | ✅ (Avro + schema registry) |
| Self-hostable | ❌ (Prisma Cloud) | ✅ | ✅ |
| Multi-DB support | Postgres only | Supabase Postgres | 10+ databases |
| Setup complexity | Low | Low | High |
| Throughput | Medium | Medium | ✅ Very high |
| Pricing | $19/month+ | Included in Supabase | Open-source (infra costs) |
When to Use Each
Choose Prisma Pulse if:
- You're using Prisma ORM and want TypeScript-typed database change subscriptions
- Server-side CDC for cache invalidation, search indexing, or event-driven workflows
- Simple setup without Kafka infrastructure is preferred
- You're already on Prisma Platform (included in paid plans)
Choose Supabase Realtime if:
- Browser or mobile clients need to receive live database updates
- Row Level Security should automatically filter which changes clients receive
- Building collaborative features (real-time dashboards, live feeds, presence)
- You're already on Supabase (included at no extra cost)
Choose Debezium if:
- Microservices need to react to database events without coupling to the application
- Kafka is already in your infrastructure
- Exactly-once event delivery with full ordering guarantee is required
- Multi-database CDC (MySQL, MongoDB, SQL Server) in addition to Postgres
- Enterprise scale with millions of events per second
Methodology
Data sourced from official Prisma Pulse documentation (prisma.io/docs/data-platform/pulse), Supabase Realtime documentation (supabase.com/docs/guides/realtime), Debezium documentation (debezium.io), GitHub star counts and npm download statistics as of February 2026, and community discussions from the Prisma Discord, Supabase Discord, and the Debezium Zulip. Kafka throughput figures from Debezium performance benchmarks.
Related: Neon vs Supabase Postgres vs Tembo for the PostgreSQL hosting platforms that support logical replication, or Inngest vs Trigger.dev v3 vs QStash for processing the events that CDC captures.