Anonymous sign-ins mint real authenticated JWTs; gate on is_anonymous

signInAnonymously() returns a user with role authenticated, so every to authenticated policy admits it. Add a restrictive policy on is_anonymous; convert later.

RLS & Auth

· Chapter

19

·

2

min read

The answer. Anonymous sign-in gives a visitor a session before they have an identity: signInAnonymously() creates a row in auth.users with no email and returns a JWT whose role is authenticated and whose is_anonymous claim is true. That role is the catch. Every policy written to authenticated now includes guests, so before enabling the feature audit each one and add a restrictive policy where guests must not write. Conversion keeps the user id and everything they created: updateUser({ email }) starts email verification, linkIdentity({ provider }) attaches an OAuth identity. Because the endpoint is public, enable a captcha (Turnstile is supported natively) and know that an IP is limited to 30 anonymous sign-ins per hour by default.

The pattern.

create policy "guests cannot post" on public.posts as restrictive for insert to authenticated
  with check (((select auth.jwt())->>'is_anonymous')::boolean is not true);
const { data: { session } } = await supabase.auth.getSession();
if (!session) await supabase.auth.signInAnonymously();       // only when there is no session at all
// later, on conversion:
await supabase.auth.updateUser({ email });                   // sends the verification mail
delete from auth.users where is_anonymous is true and created_at < now() - interval '30 days';   -- pg_cron job

Watch out.

  • Calling signInAnonymously() on every page load creates a fresh user each time and orphans the previous one's rows. Check for an existing session first.
  • Anonymous users count toward MAU and never delete themselves; schedule the cleanup and cascade their data.
  • The Security Advisor flags to authenticated policies once anonymous sign-ins are on; treat that lint as a checklist, not noise.

Related: update-policies-have-two-halves-using-and-with-check · pg-cron-inside-postgres-vs-a-worker-cron-outside · security-advisor-lints-to-fix-before-launch