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
Replication Slot Management and Database Health
All three CDC tools create PostgreSQL replication slots to track which WAL events have been consumed. Replication slots are powerful but carry a critical operational risk: if your CDC consumer stops processing events (network outage, application crash, deployment), the replication slot remains active and PostgreSQL cannot remove WAL segments that the slot has not yet consumed. Unchecked WAL accumulation can fill your PostgreSQL server's disk — causing database crashes that affect your entire application, not just the CDC consumer. Monitor the pg_replication_slots view's pg_current_wal_lsn() - restart_lsn metric and alert when WAL lag exceeds a threshold (for example, 1 GB). Prisma Pulse manages the replication slot on Prisma's infrastructure, shifting this operational responsibility to them — but you should still verify their SLA for slot management. For self-managed Debezium, configure max.queue.size and max.batch.size carefully and implement monitoring for the Debezium connector lag metric exposed via JMX. Supabase manages replication slots for Realtime internally and monitors WAL growth as part of their platform operations.
Event Ordering Guarantees and Consistency Semantics
CDC systems provide different ordering guarantees that matter for event-driven application correctness. Debezium via Kafka provides total ordering within a partition — all events for a given table are routed to the same Kafka partition by default, ensuring that an UPDATE always follows its INSERT in the event stream. Cross-table ordering (an order INSERT followed by an orderItem INSERT) is not guaranteed unless you use Kafka Streams to join events. Prisma Pulse delivers events from a single subscription in the order they occurred in the WAL, providing per-model ordering. Supabase Realtime delivers events per channel, with ordering preserved within a single WebSocket connection but not guaranteed across reconnections — clients that reconnect may miss events that occurred during the disconnection window unless you implement a catch-up mechanism using created_at timestamps. For event sourcing patterns where historical accuracy is critical (financial transactions, audit logs), Debezium's Kafka-backed total ordering is the only option that provides the guarantees needed.
Security and Data Exposure Considerations
CDC streams contain raw database change events including before and after values of every column — including columns that contain sensitive data (PII, tokens, hashed passwords). Debezium's column filtering configuration (column.mask.hash and column.exclude.list) lets you exclude or hash sensitive columns before events reach Kafka, preventing sensitive data from being written to Kafka topics that other consumers read. This is a critical security configuration step that teams frequently overlook when first deploying Debezium. Prisma Pulse events contain the full record data as typed by your Prisma schema — there is no column filtering mechanism, meaning a subscription on the User model receives all user fields including any sensitive ones. If your User model contains columns like passwordHash or phoneNumber, your CDC consumer receives that data regardless of whether it needs it. For security-sensitive deployments, consider creating database views that expose only the non-sensitive columns you need, and subscribe to the view rather than the base table. Supabase Realtime applies Row Level Security policies, which filter not just rows but also columns based on the authenticated user's policies.
Migrating from Polling to CDC Incrementally
Teams transitioning from a polling-based architecture to CDC do not need to perform a big-bang migration. The safest incremental approach starts by adding CDC alongside the existing polling, verifying that both systems see the same events, and then removing the polling once confidence is established. For Supabase Realtime, adding a table subscription alongside existing periodic supabase.from('orders').select() API calls is straightforward — run both in parallel for a sprint, compare event counts and latencies, then decommission the polling loop when the CDC path is validated. For Prisma Pulse, the prisma.model.subscribe() call starts consuming events immediately without modifying the existing polling code — add it to a separate worker process to avoid affecting the main application's event loop. When migrating to Debezium, the connector's initial snapshot mode (snapshot.mode = "initial") performs a full table scan before streaming WAL changes, ensuring the downstream Kafka consumers receive a complete state baseline before incremental changes begin — this makes the switchover to CDC-only consumption safe without requiring manual backfill logic.
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.
See also: Mongoose vs Prisma and Knex vs Prisma