Skip to content

External DB init error #210

Description

@muellerelias

Bug report

Describe the bug

On a self-hosted Supabase deployment, the auth.uid() / auth.role() /
auth.email() helper functions installed by the init/bootstrap SQL only read
the legacy per-claim GUCs (request.jwt.claim.sub, etc.). Modern PostgREST
(v9+, here 14.8) no longer sets those — it only sets the aggregated JSON GUC
request.jwt.claims. As a result auth.uid() returns NULL for every
authenticated request, so every RLS policy of the form
user_id = auth.uid() matches zero rows
. All tables (profiles, report_labels,
reports, histories) become effectively unreadable/unwritable for authenticated
users, even though the JWT is valid and verified.

The init should ship the official coalesce-based definition that supports both
the old per-claim GUC and the new request.jwt.claims JSON, so it stays
compatible with the bundled PostgREST version.

To Reproduce

  1. Deploy self-hosted Supabase (Helm chart, PostgREST 14.8) and create the
    standard RLS policies, e.g.:
    create policy "profiles_select_own" on profiles
      for select using (user_id = auth.uid());
  2. Sign in a normal user (GoTrue password grant) and call REST with the user's
    access token (NOT the postgres/service_role key — those bypass RLS):
    curl -s -H "apikey: $PUB" -H "Authorization: Bearer $USER_TOKEN" \
      "$URL/rest/v1/profiles?select=id"
    → returns [] although a matching row exists (visible in Studio, which runs
    as postgres and bypasses RLS).
  3. Diagnose what the DB sees for that request:
    create or replace function public.debug_jwt() returns jsonb
    language sql stable as $$
      select jsonb_build_object(
        'uid', auth.uid(),
        'role', auth.role(),
        'claims', current_setting('request.jwt.claims', true)
      );
    $$;
    grant execute on function public.debug_jwt() to anon, authenticated;
    curl -s -X POST "$URL/rest/v1/rpc/debug_jwt" \
      -H "apikey: $PUB" -H "Authorization: Bearer $USER_TOKEN"
    Actual output:
    {
      "uid": null,
      "role": null,
      "claims": "{\"sub\":\"ae971efe-...\",\"role\":\"authenticated\",\"aud\":\"authenticated\", ...}"
    }
    request.jwt.claims is populated (contains sub and role), but
    auth.uid() and auth.role() are NULL, proving the helper functions read
    the wrong (now-empty) GUC.

Expected behavior

auth.uid() should return the sub claim of the verified JWT regardless of how
the bundled PostgREST version exposes claims, so RLS policies like
user_id = auth.uid() work out of the box. The init/bootstrap SQL should install
the GUC-compatible definition:

create or replace function auth.uid() returns uuid language sql stable as $$
  select coalesce(
    nullif(current_setting('request.jwt.claim.sub', true), ''),
    (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
  )::uuid
$$;

create or replace function auth.role() returns text language sql stable as $$
  select coalesce(
    nullif(current_setting('request.jwt.claim.role', true), ''),
    (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role')
  )
$$;

create or replace function auth.email() returns text language sql stable as $$
  select coalesce(
    nullif(current_setting('request.jwt.claim.email', true), ''),
    (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'email')
  )
$$;

Applying the above immediately fixes debug_jwt (uid becomes the user's UUID)
and all RLS policies start returning the correct rows — no policy or schema-cache
changes required.

Screenshots

N/A (CLI output included above).

System information

  • OS: Linux (Kubernetes, self-hosted)
  • Deployment: self-hosted Supabase via Helm chart on StackGres
  • PostgreSQL: 17.4 (StackGres)
  • PostgREST: 14.8
  • GoTrue (Auth): v2.186.0
  • JWT: HS256 (symmetric, shared JWT_SECRET)
  • Client: supabase-flutter 2.14.2 (Brick offline-first), but reproducible with
    plain curl as shown

Additional context

  • The standard/managed Supabase stack and the Supabase CLI ship the
    coalesce-based auth.uid(), so the issue does not appear there — it only
    surfaces on hand-assembled self-hosted stacks where an older auth.uid()
    definition is paired with a newer PostgREST.
  • Studio (role postgres) bypasses RLS, so the rows look present there, which
    makes the bug easy to misattribute to the app/policies rather than to
    auth.uid().
  • Root cause is the PostgREST change from per-claim GUCs (request.jwt.claim.*)
    to the aggregated request.jwt.claims JSON; the init SQL must use a definition
    that reads the JSON (with a fallback) to stay version-compatible.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions