releases.shpreview
Supabase/Supabase Changelog

Supabase Changelog

Mon
Wed
Fri
JunJulAugSepOctNovDecJanFebMarAprMay
Less
More
Releases18Avg6/mo

There has been a growing trend of supply chain attacks on Node Package Manager (NPM). In addition, we have seen other creative attacks, including a typosquat package named supabase-javascript that appeared on npm, copying our name to phish developers. We reported it. npm took it down a few hours later, long enough that the package picked up real downloads.

If you build on Supabase, this matters to you. Edge Functions pull from npm. The Supabase CLI is on npm. supabase-js, @supabase/ssr, and @supabase/server are all on npm. Any of these is a credential leak waiting for the wrong update to land.

This post lays out what we are doing about it and what you should do today.

What we are doing about it at Supabase#

We kicked off a coordinated response across the company. The work in flight:

  • Publishing a canonical security guide in our docs. A single, agent-readable page that tells you exactly what to do.
  • Hardening our own GitHub Actions. Our security team finished a pass on pull_request_target usage across the Supabase org months ago and is close to enforcing pinned action SHAs across every repo.
  • Adding security notes to secret-handling APIs. TSDoc and JSDoc on functions like createClient so editor hovers warn when you are working with sensitive credentials.
  • Comms across every channel. Our goal is to educate as many people as we can, whether or not they are Supabase customers.

How npm supply chain attacks actually happen#

Supply chain attacks share a shape. The attacker does not break into your computer. They get you to invite their code in, and they do that by getting their code into a package you already trust. The recipes vary, but the three most common patterns are these:

  • Maintainer compromise. An attacker steals an npm publish token or phishes a maintainer, then publishes a new version of a popular package with malicious code added. The next time you run npm install against that range, you are running their code.
  • Typosquatting. An attacker registers a package name a few letters away from a real one, like supabase-javascript instead of @supabase/supabase-js. They wait for a developer or, increasingly, an AI coding agent to mistype the name. AI agents hallucinate package names regularly, and that is now a primary attack vector against teams that vibe-code their dependencies.
  • Build pipeline compromise. This is what hit TanStack. An attacker found a vulnerable GitHub Actions workflow, poisoned the build cache from a fork PR, and waited for the next legitimate release run to pick up the poisoned cache and publish their code under the real maintainer's identity. No stolen tokens. No compromised laptops. The attacker rode the official release train.

Once the malicious code lands on disk in your node_modules, npm's lifecycle scripts run it. By the time npm install returns, the attacker has already read your environment variables, your AWS instance metadata, your kubeconfig, your .npmrc token, your .git-credentials, and your SSH private keys. The TanStack payload exfiltrated through the Session messenger network, which is end-to-end encrypted and has no fixed command-and-control address. You cannot block it at the firewall.

The TanStack postmortem describes the full chain and is worth reading if you maintain a public open source project. The short version: every link in the chain (a pull_request_target workflow, an unsecured Actions cache, a long-lived OIDC publish token) was a known issue with public mitigations. The attack worked because nobody had connected the dots in advance.

Other things you should do today#

Most of what follows takes minutes. The goal is layered defense: no single mitigation stops every attack, but together they raise the cost enough that attackers go bother someone else.

Upgrade to pnpm 11 (or the npm v11 equivalent)#

pnpm 11 sets minimumReleaseAge to 24 hours by default, blocks exotic subdependencies by default, and ships a new Allow Builds model that controls which dependencies are permitted to run install scripts. If your AI coding agent picked pnpm 10.x for you, fix that. Tell it to use pnpm 11.

Then set minimumReleaseAge higher than the default. Three to seven days is a reasonable starting point for most projects. Most malicious npm packages are caught and pulled within twenty-four to forty-eight hours, so a three-day window catches the long tail of detections without throttling legitimate updates too much. Configure it in your project's pnpm-workspace.yaml or .npmrc:

minimumReleaseAge: 4320 # minutes, equals 3 days
Pin versions, especially for security-sensitive dependencies#

The ^ and ~ ranges in your package.json are a polite way of telling npm "trust me, take the next minor or patch version." Supply chain attacks exploit exactly that trust. Pin exact versions for anything that touches authentication, secrets, networking, or user data. Use ^ only where you actively want updates and have a process to vet them.

Commit your lockfile and review changes to it#

A lockfile records exactly which version of which package, with which hash, you installed. If an attacker republishes a tarball under the same version number, the hash mismatch fails the install. Commit pnpm-lock.yaml, package-lock.json, or yarn.lock to your repo. Treat lockfile diffs as code review surface, not noise. A pull request that bumps fifty transitive dependencies for no obvious reason deserves a careful read before it merges.

Disable npm install scripts where you can#

Most supply chain payloads run through preinstall, install, or postinstall lifecycle scripts. If your project does not need them, turn them off globally:

npm config set ignore-scripts true

Or scope it to the project via .npmrc:

ignore-scripts=true

The trade-off is that some packages with native code (bcrypt, sharp, and similar) will not build without scripts. Use pnpm's Allow Builds model to allowlist the specific packages you actually need rather than allowing every package on the registry to run code at install time.

Verify package names every single time you install#

Typosquats target the moment of carelessness. Before you pnpm add anything, especially anything an AI agent suggested, check that:

  • The scope is correct. Official Supabase packages live under @supabase/. A package named supabase-javascript or supabase-server without the scope is not ours and never has been.
  • The maintainer is the expected one. The npm page lists current maintainers; a brand-new maintainer on a long-established package is a signal worth a closer look.
  • The download counts and the linked GitHub repo match what you expect for a real, established package.
Pin your GitHub Actions to commit SHAs, not tags#

If you maintain a public repo, this is the single biggest change you can make. A tag like @v5 is a moving target. The maintainer of that action (or an attacker who compromised that maintainer) can republish the tag with new code, and your workflow will pick it up on the next run. Pin to the full commit SHA instead:

- uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656b67ce0c5b94 # v6.0.2

Renovate and Dependabot both understand this syntax and will update the comment when a new release is published, so you still get visibility without giving up safety.

Avoid pull_request_target with code checkout#

If your workflow uses pull_request_target and then checks out code from the PR, you are running attacker-controlled code in a context that has access to your repo's secrets and cache. This is the exact pattern that compromised TanStack. Use pull_request for anything that touches PR code. Reserve pull_request_target for trusted, no-checkout operations like labeling or commenting on the PR.

Rotate credentials if you think you were exposed#

If you ran npm install on a day when a package you depend on turned out to be compromised, treat the install host as potentially compromised too. Rotate everything reachable from that machine: AWS, GCP, Kubernetes, Vault, GitHub, npm, SSH, and any Supabase service-role keys. Audit your service-role key usage in the Supabase dashboard for access patterns you do not recognize. It is an annoying afternoon. It is not as annoying as a customer breach.

Consider a scanner as a second line of defense#

Tools like Socket.dev, npq, and Snyk monitor the npm registry and flag suspicious package behavior in real time. None of them are a silver bullet, and none of them substitute for the practices above. They are a useful second line of defense for teams that already have the basics in place.

Closing thought#

This kind of attack will keep happening. The cost of pulling one off keeps dropping; the payoff (credentials to dozens of production systems in a single shot) keeps rising. The good news is that the defenses are well understood, cheap to implement, and effective when stacked. Pin your versions. Wait for the dust to settle on new releases. Lock your CI down. Verify what you install. Tell your AI agents to do the same.

If you have suggestions, requests, or your own war stories about how you handle this on your team, find me on Discord or on Twitter.

Prompt for your coding agent#

Paste this into Claude Code, Codex, Cursor, or whatever agent you use. Read every change before you accept it. Do not skim.

Audit this repo for npm supply-chain hygiene. Apply the changes below and report what you did. Do not push, open PRs, install new dependencies, or rotate credentials without explicit approval.

Package manager:

- Upgrade to pnpm 11+ (or the latest yarn / npm / bun) if older.
- Set a 7-day quarantine on new versions for the package manager in use:
 - pnpm: `minimumReleaseAge: 10080` in `pnpm-workspace.yaml`.
 - npm: `min-release-age=7` in `.npmrc`.
 - yarn (berry): `npmMinimalAgeGate: '7d'` in `.yarnrc.yml`.
 - bun: `minimumReleaseAge = 604800` under `[install]` in `bunfig.toml`.
- Block lifecycle scripts by default. pnpm: declare an explicit `allowBuilds` list in `pnpm-workspace.yaml`. npm/bun: set `ignore-scripts=true`. yarn defaults to `enableScripts: false` — confirm it is not overridden.
- Block non-registry transitive refs. pnpm: `blockExoticSubdeps: true`. npm: set `allow-git=root`, `allow-remote=root`, `allow-file=root`, `allow-directory=root` in `.npmrc`. yarn: use `approvedGitRepositories` as an explicit allowlist.
- Pin the package manager itself: set `packageManager` in `package.json` to an exact version plus sha512 hash (e.g. `pnpm@10.4.1+sha512.<hash>`).

Lockfile and dependencies:

- Confirm `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock` is committed (not gitignored). CI installs must use `--frozen-lockfile` (pnpm/yarn) or `npm ci`. Flag any job that runs a non-frozen install.
- For dependencies handling auth, secrets, networking, crypto, or user data, replace `^`/`~` ranges with exact versions. List what you changed.
- Verify every Supabase import uses the exact `@supabase/` scope. Flag unscoped lookalikes (`supabase-js`, `supabase-javascript`, etc.) as possible typosquats.

GitHub Actions (if present):

- Repin every third-party `uses:` reference to a 40-character commit SHA, with the original tag as a trailing comment.
- Flag every workflow using `pull_request_target` that checks out PR code or runs PR-controlled build steps. Propose a `pull_request` rewrite. Do not silently change trigger types.
- Add a non-blocking `npm audit signatures` step to install workflows.

Flag for human (do not auto-enable):

- Dependabot alerts and secret scanning, if disabled.

Report:

- One line per file changed, with the reason.
- A separate list of items flagged for human review rather than automatically changed.

Supabase is now an official app in ChatGPT. Connect your Supabase projects and manage your entire database infrastructure by telling ChatGPT what you need.

With the Supabase ChatGPT app, you can execute SQL queries, modify schemas, deploy edge functions, manage branches, and troubleshoot your projects without leaving your conversation with ChatGPT.

Ask ChatGPT to check security advisors on your project and fix any issues. Request a schema change and ChatGPT executes it. Deploy an edge function with a single prompt.

What you can do#

The Supabase ChatGPT app includes 29 tools:

  • Database management: Execute SQL queries on Postgres databases, design and modify table schemas, list tables and extensions, get security recommendations.
  • Project operations: List and create projects, get cost estimates, pause and restore projects, access real-time logs.
  • Branching and migrations: Create development branches, merge changes, rebase and reset branches, list and apply migrations.
  • Edge functions: List, deploy, and manage serverless functions.
  • Documentation: Search Supabase docs directly from ChatGPT.

You can also pair the app with ChatGPT Projects to scope a conversation to a specific Supabase project. Set your project reference in the project instructions once, and every chat in that project connects to the right database automatically.

Getting started#

In ChatGPT, open the app directory and search for Supabase, or go directly to the app listing. Authorize ChatGPT to access your Supabase organization.

The app works on all Supabase plans and paid ChatGPT plans (Plus, Pro, Team, Enterprise).

If you don't have a Supabase account yet, start your project for free at supabase.com. Then connect it to ChatGPT and manage your projects by describing what you need.

Read the documentation at supabase.com/docs/guides/getting-started/mcp.

Today we're releasing @supabase/server in public beta.

This is a new package that handles auth verification, client setup, request context, and common server-side boilerplate for you. It works across Edge Functions, Vercel Functions, Cloudflare Workers, Hono and Bun.

We anonymously analyzed 25,000 deployed Edge Functions and saw the same pattern everywhere: developers were rebuilding the same setup code over and over just to get to their actual business logic.

Most functions needed to:

  • Create a Supabase client with SUPABASE_ANON_KEY
  • Create another admin client with SUPABASE_SERVICE_ROLE_KEY that can bypass Row Level Security
  • Verify the JWT
  • Parse claims
  • Handle CORS
  • Wire up auth context
  • Copy/paste the same _shared/*.ts files between functions

With @supabase/server you just declare who can call your endpoint and get a fully initialized context back:

  • User-scoped Supabase client
  • Admin client with service role access
  • Verified user identity
  • JWT claims
  • Built-in request/auth helpers
import { withSupabase } from 'npm:@supabase/server'

// Typical Deno.serve usage
Deno.serve(
  withSupabase({ auth: 'user' }, async (req, ctx) => {
    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  })
)

// New fetch style handler usage
export default {
  fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  }),
}

Note that export default { fetch } is equivalent to Deno.serve(...). Both define a request handler. We use export default throughout this post because it works across Edge Functions, Workers, and Bun. If you prefer Deno.serve, you can keep using it — it's still supported on Edge Functions.

How it works#

At the core of @supabase/server is the SupabaseContext: a request context that includes everything most Edge Functions need, already configured for you.

That includes:

  • A user-scoped Supabase client
  • An admin client with service role access
  • Verified user identity
  • JWT claims
  • Auth metadata

@supabase/server gives you multiple ways to get a SupabaseContext. The most common is withSupabase, a wrapper that handles auth, client creation, and CORS before your handler runs:

import { withSupabase } from 'npm:@supabase/server'

export default {
  fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  }),
}

If you need more control over error handling and responses, you can also call createSupabaseContext directly:

import { createSupabaseContext } from 'npm:@supabase/server'

export default {
  fetch: async (req) => {
    const { data: ctx, error } = await createSupabaseContext(req, { auth: 'user' })
    if (error) return Response.json({ error: error.message }, { status: error.status })

    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  },
}

Both approaches give you the same SupabaseContext. No shared utility files. No environment variable management. No manual JWT verification.

What's in the context#

Every withSupabase handler receives a ctx object with two pre-configured clients:

ctx.supabase — a user-scoped client that automatically respects RLS policies ctx.supabaseAdmin — an admin client using the service role for privileged operations

No manual client setup, JWT verification, or environment variable wiring required.

The full context looks like this:

interface SupabaseContext {
  supabase: SupabaseClient
  supabaseAdmin: SupabaseClient
  userClaims: UserIdentity | null
  jwtClaims: JWTClaims | null
  authMode: AuthMode
}

Declarative access control#

With @supabase/server, authentication happens before your handler runs.

You declare who is allowed to call the endpoint, and the package handles verification automatically.

For example, this endpoint allows unauthenticated requests:

export default {
  fetch: withSupabase({ auth: 'none' }, async (_req, _ctx) => {
    return Response.json({ status: 'ok' })
  }),
}

This endpoint requires a valid user JWT:

export default {
  fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  }),
}

If the request does not include a valid user token, the request is rejected before your handler executes.

Here's all of the auth modes included in the package:

// authenticated users only (default)
withSupabase({ auth: 'user' }, handler)

// no auth required, good for webhooks and health checks
withSupabase({ auth: 'none' }, handler)

// server-to-server with secret key
withSupabase({ auth: 'secret' }, handler)

// with publishable key
withSupabase({ auth: 'publishable' }, handler)

// accept either a user JWT or a secret key
withSupabase({ auth: ['user', 'secret'] }, handler)

Your function's security model is visible in one line.

Adopting new auth keys without the boilerplate#

Last year we improved project security with asymmetric JWT Signing Keys and new API keys. Better security for every project, but migrating existing functions was hard.

You had to install jose, configure a JWKS endpoint, build your own auth middleware, expose new secrets, and update every function individually.

We fixed it. @supabase/server handles new key validation and JWT verification internally. You adopt the package and the new security model comes with it. No jose. No JWKS configuration. No manual secret setup.

export default {
  // auth: 'user' will handle incoming user JWT validation for you
  fetch: withSupabase({ auth: 'user' }, async (req, { supabase }) => {
    const { data } = await supabase.from('subscriptions').select('*')
    return Response.json(data)
  }),
}

Now you get support for the new auth keys without manual JWT verification. Delete your shared utility files and focus on business logic.

Same code, every runtime#

withSupabase returns a standard (Request) => Promise<Response> handler. It works with any runtime that supports the Web API pattern.

Edge Functions, Vercel Functions, and Cloudflare Workers:

import { withSupabase } from '@supabase/server'

export default {
  fetch: withSupabase({ auth: 'user' }, handler),
}

On Edge Functions, declare the dependency in deno.json to import @supabase/server from npm:@supabase/server.

Hono (with the included adapter):

import { withSupabase } from '@supabase/server/adapters/hono'
import { Hono } from 'hono'

const app = new Hono()

app.get('/todos', withSupabase({ auth: 'user' }), async (c) => {
  const { supabase } = c.var.supabaseContext
  const { data } = await supabase.from('todos').select()
  return c.json(data)
})

export default { fetch: app.fetch }

Composable primitives#

Most developers don't need anything beyond withSupabase or createSupabaseContext. But you can use the underlying primitives directly.

import {
  createAdminClient,
  createContextClient,
  resolveEnv,
  verifyAuth,
} from '@supabase/server/core'

These are useful when you need more control: multiple routes with different auth, custom response headers, or domain-specific wrappers like MCP servers.

Here's an Edge Function with per-route auth:

import { createContextClient, verifyAuth } from '@supabase/server/core'

export default {
  fetch: async (req) => {
    const url = new URL(req.url)

    if (url.pathname === '/health') {
      return Response.json({ status: 'ok' })
    }

    if (url.pathname === '/todos') {
      const { data: auth, error } = await verifyAuth(req, { auth: 'user' })
      if (error) return Response.json({ error: error.message }, { status: error.status })

      const supabase = createContextClient(auth.token)
      const { data } = await supabase.from('todos').select()
      return Response.json(data)
    }

    return new Response('Not found', { status: 404 })
  },
}

These are the same primitives that power withSupabase. Teams building MCP servers, custom middleware, or framework adapters can compose them into their own patterns.

One pattern for humans and AI agents#

We designed @supabase/server with agentic development in mind. Every function follows the same structure: declare access, receive context, write logic.

During internal testing, Claude Code migrated an entire project's Edge Functions to @supabase/server in a single prompt. That included adopting new API keys, removing shared utility files, and switching every function to withSupabase. All functions worked on the first run.

When every function looks the same, agents produce correct code from a single example.

FAQ#

Does this replace @supabase/ssr?

No. @supabase/ssr handles cookie-based session management for frameworks like Next.js and SvelteKit. @supabase/server handles stateless, header-based auth for Edge Functions, Workers, and other backend runtimes. The two packages coexist and are not replacements for each other. Deeper integration with @supabase/ssr is on the roadmap.

If you would like to adopt the DX that this package provides, check our SSR frameworks documentation for implementation references.

Which runtimes does this support?

Any runtime or platform that supports the standard Request/Response Web API. withSupabase returns a standard (Request) => Promise<Response> handler, so it works on Supabase Edge Functions, Vercel Functions, Cloudflare Workers, Bun, Deno and more.

Is Hono the only supported framework?

No. Hono was the first framework adapter we shipped, and we have already merged a community PR for the H3 adapter. We expect to accept more community-contributed adapters.

See more in our adapters documentation.

Where is the documentation?

The package ships with full documentation in the GitHub repo. We're also working on adding guides to the Supabase docs.

What about environment variables?

On the Supabase platform and Local Development (CLI), your Edge Functions will receive the required environment variables to work out of the box (SUPABASE_PUBLISHABLE_KEYS, SUPABASE_SECRET_KEYS, SUPABASE_JWKS).

In local development or self-hosted environments, use the same plural form: SUPABASE_PUBLISHABLE_KEYS instead of SUPABASE_ANON_KEY, SUPABASE_SECRET_KEYS instead of SUPABASE_SERVICE_ROLE_KEY.

Check out the environment variables documentation for more details.

How can I leave feedback?

Open an issue on the GitHub repo or join the conversation in Discord.

Get started#

Install the package and the AI skill:

npm install @supabase/server@latest
npx skills add supabase/server

The skill gives Claude Code, Codex, Cursor and any agentic coding tool full context about the API surface, patterns, and migration paths. From there, you can prompt your way through most tasks.

Analyze all Edge Functions, and plan a full migration to use
the new API keys with @supabase/server

Scaffold a new REST API with Hono:

Create a Hono API with @supabase/server that has CRUD
endpoints for a todos table, using per-route auth

Add a protected Edge Function with admin operations:

Create an Edge Function that accepts user or secret key auth,
reads from a user's profile with RLS, and writes audit logs
with the admin client

Or write it by hand:

import { withSupabase } from 'npm:@supabase/server'

export default {
  fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  }),
}

@supabase/server is in public beta. We're looking for feedback on the API surface, the adapter patterns, and edge cases we haven't hit yet.

Check out the GitHub repo and the docs and let us know what you build.

Both Supabase Realtime and Supabase ETL read changes from your Postgres database. They both use logical replication under the hood. They even look similar when you squint. But they solve very different problems, and choosing the wrong one will frustrate you.

This post explains what each product does, how they differ, and when you should pick one over the other.

Two tools, two jobs#

Here is the simplest way to think about it:

  • Realtime sends database changes to your users' browsers and apps, right now, as they happen. It is built for live experiences.
  • ETL sends database changes in near real-time to analytical destinations like BigQuery and Analytics Buckets. It is built for reliable data movement.

Realtime answers the question: "How do I show my users what just happened?"

ETL answers the question: "How do I get my production data into my analytics warehouse?"

If you mix these up, you will run into problems. We see it happen regularly, and the rest of this post will help you avoid that.

What Realtime does#

Supabase Realtime is three features in one product:

  1. Broadcast. Send messages between connected clients in real time. No database required. Think cursor positions, typing indicators, or game state.
  2. Presence. Track who is online and what they are doing. Also no database required. Think "3 users are editing this document" or "Jane is typing..."
  3. Postgres Changes. Listen to INSERT, UPDATE, and DELETE events on your database tables and deliver them to subscribed clients over WebSocket.

Two of these three features, Broadcast and Presence, can work without any database interaction. Client-to-client Broadcast sends messages purely over WebSocket with nothing stored. However, Broadcast from Database lets you trigger broadcasts from database changes using triggers, giving you control over which events reach which channels. This matters because Realtime is not just a database change listener. It is a real-time communication layer for your application.

How Postgres Changes works#

When a client subscribes to a table, Realtime uses a PostgreSQL replication slot to read changes from the Write-Ahead Log (WAL). For each change, it checks Row Level Security (RLS) policies against every subscribed user. If a user is authorized to see the change, Realtime sends it over their WebSocket connection.

This is designed for live UI updates. A user inserts a message into a chat table. Other users see it appear instantly. A row updates in a dashboard table. The chart refreshes automatically.

What Realtime does not guarantee#

Realtime's Postgres Changes feature does not guarantee delivery. If a client disconnects for 30 seconds and reconnects, the changes that happened during those 30 seconds are gone. Realtime does not queue them and does not track how far each client has read.

Broadcast Replay offers limited catch-up for Broadcast from Database messages: clients can request up to 25 messages from the last 3 days using a since timestamp. But this only works on private channels, only for database-sourced broadcasts, and is currently in public alpha. It is not a general-purpose replay mechanism for all Realtime events.

Postgres Changes uses temporary replication slots. When no clients are subscribed, it stops replicating data entirely. When clients subscribe again, a new slot is created.

The Realtime team built it this way on purpose. Guaranteed delivery requires persistent state tracking, message queuing, and acknowledgment protocols. Those things add latency and complexity that would make Realtime worse at its actual job: delivering live updates as fast as possible.

If you need every change to arrive at its destination, no matter what, Realtime is not the right tool.

What ETL does#

Supabase ETL is a change-data-capture (CDC) pipeline. It reads every INSERT, UPDATE, DELETE, and TRUNCATE from your Postgres tables and writes them to a destination. Right now, the supported destinations are Analytics Buckets (built on Apache Iceberg) and BigQuery.

ETL replicates your data 1-to-1 in near real-time. If your destination disconnects or has problems, ETL does not skip over data.

How ETL works#

When you create an ETL pipeline, it connects to your database through a permanent replication slot. First, it performs a full copy of your existing data. Then it switches to streaming mode and captures every change as it happens, with latency measured in milliseconds to seconds (based on configuration parameters, data size, and destination type).

It's important to note that Supabase ETL doesn't respect Row-Level Security. Supabase ETL reads every piece of data. If you need to filter data, you should use publication filters.

Changes are batched and written to your destination. If the pipeline crashes, it restarts from the last acknowledged position. No data changes are lost. Note that schema changes (adding or removing columns) do not propagate automatically and require manual handling.

What ETL guarantees#

ETL provides at-least-once delivery. Every change that happens in your database will reach the destination at least once. In rare cases (like a crash during a long-running transaction), a change might be delivered more than once. Exactly-once processing is handled by the destination. Some destinations like BigQuery deduplicate automatically, while others may not.

ETL uses permanent replication slots. This means Postgres holds onto WAL data until ETL confirms it has been processed. If you stop the pipeline for maintenance and restart it later, it picks up exactly where it left off. Be aware that while the pipeline is paused, Postgres continues to retain WAL data. Extended pauses can lead to significant disk growth, and depending on your Postgres configuration, the pipeline may fail if the WAL retention limit is exceeded.

This is the opposite of Realtime's approach. ETL trades speed for reliability. It may not deliver changes to your warehouse in the same millisecond they happen, but it will deliver every single one.

The key differences#

Delivery guarantees#
RealtimeETL
GuaranteeBest effortAt-least-once
Missed changesLost foreverReplayed on reconnect
Replication slotTemporaryPermanent
Resume after disconnectNoYes

This is the most important difference. If you need every change to arrive, use ETL. If you need changes to arrive fast and can tolerate occasional gaps, use Realtime.

Where data goes#

Realtime sends data to client applications over WebSocket connections. Your users' browsers and mobile apps are the destination.

ETL sends data to analytical systems. BigQuery, Analytics Buckets, and eventually other data warehouses are the destination.

These are fundamentally different targets with fundamentally different needs. Client apps need low latency. Analytical systems need completeness.

Database dependency#

Realtime's Broadcast and Presence features can work without touching the database. You can build an entire collaborative experience (cursors, presence indicators, ephemeral messaging) without writing a single database query. However, Postgres Changes and Broadcast from Database both require database interaction.

ETL is entirely database-driven. Every byte of data it moves comes from your Postgres tables.

Scale characteristics#

Realtime's Broadcast and Presence features are built for high throughput and low latency. They do not run per-subscriber database queries and scale well across many concurrent connections.

Postgres Changes works differently. It processes changes sequentially to maintain ordering. For each change, it runs an RLS authorization check against every subscribed client. With 100 subscribers watching a table, one INSERT generates 100 authorization queries. This is a deliberate design choice that prioritizes correctness and low latency for typical workloads over raw throughput.

ETL processes changes in configurable batches with tunable parallelism. It does not need to authorize individual users because it is moving data to a system, not to end users.

When to use Realtime#

Use Realtime when you need to push live updates to your users:

  • Chat applications. Messages appear instantly for all participants.
  • Collaborative editing. See other users' cursors and changes in real time.
  • Live dashboards. Charts and metrics update without page refresh.
  • Notifications. Alert users when something relevant happens.
  • Multiplayer features. Synchronize game state or shared experiences.
  • Presence tracking. Show who is online, who is typing, who is viewing a document.

The common thread: a human is watching and needs to see changes as they happen.

When to use ETL#

Use ETL when you need reliable data movement to analytical systems:

  • Analytics and reporting. Move production data to BigQuery or Iceberg for querying without impacting your production database.
  • Audit trails. Analytics Buckets stores an append-only changelog of every INSERT, UPDATE, and DELETE. Nothing is lost.
  • Data warehousing. Replicate your operational data to a columnar format optimized for analytical queries.
  • Compliance. Maintain a complete, verifiable history of all data changes.
  • ML pipelines. Feed fresh data to training or feature stores without querying production.
  • Workload isolation. Run heavy analytical queries against your warehouse instead of your production database.

The common thread: a system needs a complete, reliable copy of your data.

The mistake we see most often#

Some developers discover Realtime's Postgres Changes feature and think: "I can use this to replicate data from one system to another." They write 20 lines of code with supabase-js, subscribe to table changes, and pipe them into another system.

It works great in development. It even works fine in production for a while. Then a WebSocket connection drops for a few seconds and data goes missing. Or the subscribing process restarts and misses a batch of changes. Or load spikes and the sequential RLS authorization checks cannot keep up.

The problem is not that Realtime is broken. The problem is that Realtime was not designed for this job.

If you are piping database changes into another system and you need every change to arrive, use ETL. That is exactly what it was built for.

Can I use both?#

Yes. In fact, many applications should.

Consider an e-commerce platform. You might use Realtime to push order status updates to customers in real time ("Your order has shipped!"). At the same time, you use ETL to replicate all order data to BigQuery for daily sales reports and trend analysis.

Same database. Same tables. Different tools for different jobs.

Realtime handles the live experience. ETL handles the analytical pipeline. Each does what it was designed to do.

Quick reference#

RealtimeETL
PurposeLive updates to client appsReliable data movement to analytics
DeliveryBest effortAt-least-once
DestinationsBrowsers, mobile apps (WebSocket)BigQuery, Analytics Buckets
Replication slotTemporaryPermanent
Resume on reconnectNoYes
Database requiredOnly for Postgres Changes and Broadcast from DatabaseYes, always
ProcessingSequential per-change with per-subscriber authorizationBatched with configurable parallelism
LatencyTypically under 100msSeconds (batched)
Best forHuman users watching live dataSystems consuming complete data
Built withElixir (Phoenix)Rust
Open sourcegithub.com/supabase/realtimegithub.com/supabase/etl

Getting started#

Supabase Realtime is available on all Supabase projects. Check out the Realtime documentation to get started.

Supabase ETL is currently in private alpha. You can request access through the Supabase Dashboard or contact your account manager. Read the ETL blog post for more details on how it works.

Branching without Git is now the default for all Supabase projects.

Supabase has supported database branching through a git-based workflow since Launch Week X. That workflow connects your GitHub repo to Supabase, tracks migrations in version control, and creates preview branches automatically when you open a pull request. It works well for teams that manage their database schema as code. But it requires a GitHub connection, which rules out anyone who doesn't work that way.

Branching 2.0 removed that requirement. You create a branch from the dashboard, make changes using the SQL Editor or Table Editor, review a schema diff, and merge. No git configuration required. It shipped behind a feature preview. Today, that preview is gone.

Branching without Git is on by default for every project. It adds a second path for developers who want to iterate on their schema without setting up a Git integration. Both approaches are fully supported, and you can switch between them or use them together. If you're already using git-based branching, nothing changes.

Two ways to branch

Branching without Git

Create branches directly from the Supabase Dashboard. Your branch gets its own Postgres instance with your current production schema. Make changes, preview the diff, and merge. The whole workflow stays inside Supabase.

Choose this when:

  • You're prototyping schema changes and want fast iteration
  • Your team manages the database primarily through the dashboard
  • You're working with AI tools that create and manage branches programmatically
  • You want to try branching without any upfront configuration

Git-based branching

Connect a GitHub repo to your Supabase project. Migrations live in version control. Branches are created automatically when you open a pull request and cleaned up when the PR is closed.

Choose this when:

  • Your team already manages database migrations in git
  • You want schema changes reviewed as part of your pull request workflow
  • You need an audit trail of every migration in version control
  • You prefer an infrastructure-as-code approach to database management

You can start with dashboard branching and add a git integration later when your workflow demands it. The two approaches use the same underlying infrastructure.

How it works

1. Create a branch

Open your project in the Supabase Dashboard and click "Create Branch." Pick a name. Your branch spins up with your current production schema.

2. Make changes

Use the branch like any Supabase project. Add tables, modify columns, update RLS policies, change functions. Every schema change you make in the SQL Editor or Table Editor is tracked.

3. Review the diff

When you're ready to merge, Supabase generates a migration diff showing exactly what changed between your branch and production. This diff is powered by pg-delta, our new schema diffing engine.

4. Merge

Review the generated migration, confirm, and merge. Your schema changes are applied to production.

pg-delta: a new diffing engine

The merge experience depends on accurate schema diffs. We built pg-delta from scratch inside pg-toolbelt to handle the full range of Postgres objects: tables, columns, RLS policies, functions, triggers, indexes, and extensions.

When you merge a branch, pg-delta compares the two schemas and generates the correct migration statements. It replaces migra as the default diffing engine for dashboard branching, with better coverage of Postgres-specific DDL.

pg-delta is also available in the Supabase CLI behind a flag for the diff command if you want to try it locally.

pg-delta is alpha software. If you discover any bugs related to diff review, please file a GitHub issue to help us improve the tooling.

Built for AI workflows

Every branch created through the Supabase MCP server uses dashboard branching automatically. When an AI tool needs to iterate on your database schema, it can create a branch, make changes, and merge, all without touching git. The branch exists for as long as the agent needs it and gets cleaned up when the work is done.

Getting started

Branching is available now in your dashboard.

If you're using the git-based workflow today, nothing changes. Your existing setup continues to work. Dashboard branching is the new default for new projects and for users who haven't configured a git integration.

Announcing the OSSCAR Index: a quarterly ranking of the fastest-growing open source organizations. The site, the data, and the scoring code are all open source.

Supabase hits 100,000 GitHub stars. A reflection on community, open source, and what got us here.

A look at recent ISP and government-directed blocks of Supabase domains in three regions—what triggered them, how we worked with authorities and customers to restore access, and what multi-tenant platforms can do to prepare.

Introduced rate limits on recursive/nested Edge Functions calls within your project. Rate limiting applies to outbound fetch() calls made by Edge Functions to other Edge Functions, including direct recursion, function chaining, circular calls, and fan-out patterns.

Rate limit budget: Each request chain has a minimum budget of 5,000 requests per minute. Inbound requests and external API calls are not affected.

Why introduced: Recursive function calls were causing increased response times across multiple regions. Based on metrics, the rate limit affects only 0.4% of projects. A guide on avoiding rate limits is available in the docs.

Supabase Pro users can now send their Supabase logs to their own logging backend, enabling them to debug in the same place as the rest of their stack.

The Hydra team, maintainers of pg_duckdb, is joining Supabase to focus on Postgres + Analytics and Open Warehouse Architecture.

Last Checked
1d ago
Latest
May 26, 2026
Tracking since Aug 8, 2023