Skip to content

apply_rls: one subscription whose RLS evaluation raises silently kills postgres_changes delivery for ALL subscribers #2093

Description

@Raundahl

Bug report

One subscriber whose stored claims make RLS evaluation raise causes postgres_changes delivery to stop for every subscriber on the project — silently. We hit this in production on 2026-08-13 and lost all realtime delivery for ~2.5 hours, platform-wide.

Environment

  • Hosted Supabase, eu-north-1 (project ref available via support ticket on request)
  • Realtime 2.125.1 (slot supabase_realtime_replication_slot_2_125_1_*)
  • Postgres 15, RLS enabled on all published tables

What happened

  1. For ~2 hours (coinciding with a platform-side restart of our project's Auth + Realtime at ~05:04 UTC), GoTrue-issued access tokens carried the string "null" instead of JSON null in nullable app_metadata claims (e.g. app_id). Our access-token hook stamps proper JSON nulls and was byte-identical on our dev project, which was unaffected — the mangling was platform-side and stopped on its own at ~07:19 UTC. That is a separate Auth issue; it matters here only as the trigger.
  2. Our RLS policies call a helper that does NULLIF(auth.jwt() -> 'app_metadata' ->> 'app_id', '')::uuid. With the claim being the string "null", this raises 22P02 invalid input syntax for type uuid: "null".
  3. realtime.apply_rls() evaluates policies per subscription using the claims stored at subscribe time. When evaluation for one subscription raises, the error aborts processing of the WAL batch — and no subscriber receives anything, including service_role subscriptions and subscriptions on unrelated tables.

Observed symptoms

  • WebSocket connects (101), channels reach SUBSCRIBED, rows appear in realtime.subscription with correct claims/filters — everything looks healthy.
  • The replication slot stays active with confirmed_flush_lsn advancing to pg_current_wal_lsn() — WAL is consumed and acknowledged.
  • Zero postgres_changes frames are delivered to any subscriber (verified with a service_role client subscribed with and without filters, on multiple tables).
  • Reproduced directly: calling realtime.apply_rls() with a wal2json-shaped record while the poisoned subscriptions were present raises 22P02 with context SQL statement "execute walrus_rls_stmt"; after hardening our helper to not raise, the same call returns records and delivery resumed instantly for all subscribers.

Expected behavior

An error while evaluating RLS for one subscription should disqualify that subscription (arguably with an error surfaced to that channel), not abort delivery for every subscriber on the project. JWT claims are external input from the subscriber's perspective of the pipeline — any upstream serializer defect (as happened here, in the platform's own Auth layer) becomes a total, silent, project-wide realtime outage that no health signal exposes: sockets up, subscriptions registered, slot advancing, nothing delivered.

Workaround for anyone else hitting this

Make every function reachable from an RLS policy raise-proof against malformed claims — validate instead of casting:

CREATE OR REPLACE FUNCTION public.claim_uuid(p_claim text)
RETURNS uuid LANGUAGE sql IMMUTABLE SET search_path = '' AS $$
  SELECT CASE
    WHEN p_claim ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
    THEN p_claim::uuid
  END;
$$;

Diagnosis query that found our poisoned subscriptions, in case it helps others:

SELECT jsonb_typeof(claims->'app_metadata'->'app_id'), count(*)
FROM realtime.subscription GROUP BY 1;
-- 'string' where you expect 'null' = poisoned tokens

Happy to provide timestamps, the project ref, and full reproduction detail through a support ticket — please reference this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions