Supabase Realtime + RLS support #856

Closed
opened 2026-03-13 08:07:24 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @Aymericr on GitHub (Mar 16, 2025).

Example setup for Supabase Realtime and Better Auth

Supabase realtime docs: https://supabase.com/docs/guides/realtime/authorization

@/lib/supabase/browser.ts

import { createBrowserClient } from '@supabase/ssr'

export const getSupabaseClient = (token: string) => {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      global: {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      },
      realtime: {
        params: {
          eventsPerSecond: 1000,
        },
      },
    },
  )
}

@/lib/supabase/server.ts

import { createClient } from '@supabase/supabase-js'
import jwt from 'jsonwebtoken'

export const getSupabaseAccessToken = ({ user, session }: Session) => {
  const signingSecret = process.env.SUPABASE_JWT_SECRET
  const payload = {
    aud: 'authenticated',
    exp: Math.floor(new Date(session.expiresAt).getTime() / 1000),
    sub: user.id,
    email: user.email,
    role: 'authenticated',
  }
  const token = jwt.sign(payload, signingSecret)
  return token
}

Server side page page.tsx

export default async function Page() {
  const { session, user } = await auth.api.getSession({
    headers: await headers(),
  })

  if (!user || !session) {
    redirect('/')
  }

  const supabaseAccessToken = getSupabaseAccessToken({ session, user })
   return ...
}

Client component: client.tsx

export function Realtime({ supabaseAccessToken } : { supabaseAccessToken: string }) {
  const supabase = getSupabaseClient(supabaseAccessToken)
  supabase.realtime.setAuth(supabaseAccessToken)
  return <div>...</div>
}

Drizzle custom migration into supabase/migrations/*

--- user_id() function to be used in RLS policies
CREATE OR REPLACE FUNCTION public.user_id() RETURNS text LANGUAGE sql STABLE AS $$
select coalesce(
    nullif(
      current_setting('request.jwt.claim.sub', true),
      ''
    ),
    (
      nullif(current_setting('request.jwt.claims', true), '')::jsonb->>'sub'
    )
  )::text $$;

Example policy through custom Drizzle migrations:

create policy "Can update own user data."
on "public"."auth_users"
as permissive
for update
to public
using ((user_id() = id));


create policy "Can view own user data."
on "public"."auth_users"
as permissive
for select
to public
using ((user_id() = id));


create policy "User can view messages"
on "public"."messages"
as permissive
for select
to public
using ((EXISTS ( SELECT 1
   FROM auth_users
  WHERE (( SELECT user_id() AS user_id) = auth_users.id))));

For listening to postgres_change via supabase realtime (broadcast recommended for scaling):

-- add a table called 'messages' to the publication
-- (update this to match your tables)
alter
  publication supabase_realtime add table messages;

Example frontend subscription:

  // ...
 const [messages, setMessages] = useState([])
  const supabase = getSupabaseClient(supabaseAccessToken)
  supabase.realtime.setAuth(supabaseAccessToken)
  useEffect(() => {
    const messageChannel = supabase.channel(`messages:${chatId}`)

    messageChannel.on(
      'postgres_changes',
      {
        event: '*',
        schema: 'public',
        table: 'messages',
        filter: `chat_id=eq.${chatId}`,
      },
      (payload: RealtimePostgresChangesPayload<MessageFromDatabase>) => {
        if (payload.eventType === 'INSERT') {
           // ...
        }
        if (payload.eventType === 'UPDATE') {
           // ...
        }
      },
    )
     return () => {
      if (messageChannel) supabase.removeChannel(messageChannel)
    }
  }, [setMessages, supabase, chatId])
Originally created by @Aymericr on GitHub (Mar 16, 2025). # Example setup for Supabase Realtime and Better Auth Supabase realtime docs: https://supabase.com/docs/guides/realtime/authorization `@/lib/supabase/browser.ts` ```typescript import { createBrowserClient } from '@supabase/ssr' export const getSupabaseClient = (token: string) => { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { global: { headers: { Authorization: `Bearer ${token}`, }, }, realtime: { params: { eventsPerSecond: 1000, }, }, }, ) } ``` `@/lib/supabase/server.ts` ```typescript import { createClient } from '@supabase/supabase-js' import jwt from 'jsonwebtoken' export const getSupabaseAccessToken = ({ user, session }: Session) => { const signingSecret = process.env.SUPABASE_JWT_SECRET const payload = { aud: 'authenticated', exp: Math.floor(new Date(session.expiresAt).getTime() / 1000), sub: user.id, email: user.email, role: 'authenticated', } const token = jwt.sign(payload, signingSecret) return token } ``` Server side page `page.tsx` ```typescript export default async function Page() { const { session, user } = await auth.api.getSession({ headers: await headers(), }) if (!user || !session) { redirect('/') } const supabaseAccessToken = getSupabaseAccessToken({ session, user }) return ... } ``` Client component: `client.tsx` ```typescript export function Realtime({ supabaseAccessToken } : { supabaseAccessToken: string }) { const supabase = getSupabaseClient(supabaseAccessToken) supabase.realtime.setAuth(supabaseAccessToken) return <div>...</div> } ``` Drizzle custom migration into `supabase/migrations/*` ```sql --- user_id() function to be used in RLS policies CREATE OR REPLACE FUNCTION public.user_id() RETURNS text LANGUAGE sql STABLE AS $$ select coalesce( nullif( current_setting('request.jwt.claim.sub', true), '' ), ( nullif(current_setting('request.jwt.claims', true), '')::jsonb->>'sub' ) )::text $$; ``` Example policy through custom Drizzle migrations: ```sql create policy "Can update own user data." on "public"."auth_users" as permissive for update to public using ((user_id() = id)); create policy "Can view own user data." on "public"."auth_users" as permissive for select to public using ((user_id() = id)); create policy "User can view messages" on "public"."messages" as permissive for select to public using ((EXISTS ( SELECT 1 FROM auth_users WHERE (( SELECT user_id() AS user_id) = auth_users.id)))); ``` For listening to `postgres_change` via supabase realtime (broadcast recommended for scaling): ```sql -- add a table called 'messages' to the publication -- (update this to match your tables) alter publication supabase_realtime add table messages; ``` Example frontend subscription: ```typescript // ... const [messages, setMessages] = useState([]) const supabase = getSupabaseClient(supabaseAccessToken) supabase.realtime.setAuth(supabaseAccessToken) useEffect(() => { const messageChannel = supabase.channel(`messages:${chatId}`) messageChannel.on( 'postgres_changes', { event: '*', schema: 'public', table: 'messages', filter: `chat_id=eq.${chatId}`, }, (payload: RealtimePostgresChangesPayload<MessageFromDatabase>) => { if (payload.eventType === 'INSERT') { // ... } if (payload.eventType === 'UPDATE') { // ... } }, ) return () => { if (messageChannel) supabase.removeChannel(messageChannel) } }, [setMessages, supabase, chatId]) ```
Author
Owner

@dosubot[bot] commented on GitHub (Jun 15, 2025):

Hi, @Aymericr. I'm Dosu, and I'm helping the better-auth team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • You opened an issue about setting up Supabase Realtime with Row Level Security (RLS).
  • Provided detailed code examples for client and server-side integration using Supabase and JWT authentication.
  • Included TypeScript snippets and SQL for custom migrations to implement RLS policies.
  • Referenced Supabase documentation for further guidance on authorization.
  • No comments or activity have been recorded on this issue so far.

Next Steps:

  • Please let me know if this issue is still relevant to the latest version of the better-auth repository by commenting here.
  • If there is no response, the issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Jun 15, 2025): Hi, @Aymericr. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog. I'm marking this issue as stale. **Issue Summary:** - You opened an issue about setting up Supabase Realtime with Row Level Security (RLS). - Provided detailed code examples for client and server-side integration using Supabase and JWT authentication. - Included TypeScript snippets and SQL for custom migrations to implement RLS policies. - Referenced Supabase documentation for further guidance on authorization. - No comments or activity have been recorded on this issue so far. **Next Steps:** - Please let me know if this issue is still relevant to the latest version of the better-auth repository by commenting here. - If there is no response, the issue will be automatically closed in 7 days. Thank you for your understanding and contribution!
Author
Owner

@ManuLpz4 commented on GitHub (Nov 19, 2025):

@Aymericr https://github.com/better-auth/better-auth/pull/579 this is what you need

@ManuLpz4 commented on GitHub (Nov 19, 2025): @Aymericr https://github.com/better-auth/better-auth/pull/579 this is what you need
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#856