Tous les articles
Next.jsSupabaseBackend

Supabase + Next.js 16: SSR Auth, Row Level Security, and Realtime Subscriptions

//
·9 min de lecture

Build a full-stack app with Supabase and Next.js 16 using @supabase/ssr for cookie-based server auth, Row Level Security policies, Realtime subscriptions for live UI updates, and Storage for file uploads.

Supabase is the go-to open-source Firebase alternative for Next.js projects. With @supabase/ssr, you get cookie-based authentication that works across Server Components, Server Actions, Route Handlers, and middleware — all reading from the same session without any client-server mismatch.

>Setup

bash
npm install @supabase/supabase-js @supabase/ssr
bash
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...

>Server client — Server Components and Actions

TypeScript
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
import type { Database } from '@/types/supabase';

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll:    () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options),
          );
        },
      },
    },
  );
}

// Usage in a Server Component
export default async function Dashboard() {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) redirect('/login');

  const { data: posts } = await supabase
    .from('posts')
    .select('*')
    .eq('user_id', user.id);

  return <PostsList posts={posts ?? []} />;
}

>Middleware — refresh sessions

TypeScript
// proxy.ts (Next.js 16)
import { createServerClient } from '@supabase/ssr';
import { NextRequest, NextResponse } from 'next/server';

export async function proxy(req: NextRequest) {
  let res = NextResponse.next({ request: req });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => req.cookies.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) => {
            req.cookies.set(name, value);
            res.cookies.set(name, value, options);
          });
        },
      },
    },
  );

  // Refreshes the session if expired — must be called in middleware
  const { data: { user } } = await supabase.auth.getUser();

  // Protect routes
  if (!user && req.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  return res;
}

export const config = { matcher: ['/((?!_next/static|favicon.ico).*)'] };

>Row Level Security policies

SQL
-- Enable RLS on the posts table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Users can only read their own posts
CREATE POLICY "select_own_posts" ON posts
  FOR SELECT USING (auth.uid() = user_id);

-- Users can only insert posts for themselves
CREATE POLICY "insert_own_posts" ON posts
  FOR INSERT WITH CHECK (auth.uid() = user_id);

-- Users can update and delete their own posts
CREATE POLICY "modify_own_posts" ON posts
  FOR ALL USING (auth.uid() = user_id);

-- Allow public read on published posts
CREATE POLICY "select_published" ON posts
  FOR SELECT USING (published = true);

NOTEWith RLS enabled, queries from the Supabase client automatically filter by the authenticated user. The service_role key bypasses RLS — never expose it client-side.

>Realtime subscriptions

TypeScript
'use client';
import { createBrowserClient } from '@supabase/ssr';
import { useEffect, useState } from 'react';

const supabase = createBrowserClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

export function LiveNotifications({ userId }: { userId: string }) {
  const [notifications, setNotifications] = useState<Notification[]>([]);

  useEffect(() => {
    const channel = supabase
      .channel('notifications')
      .on(
        'postgres_changes',
        {
          event:  'INSERT',
          schema: 'public',
          table:  'notifications',
          filter: `user_id=eq.${userId}`,
        },
        (payload) => {
          setNotifications((prev) => [payload.new as Notification, ...prev]);
        },
      )
      .subscribe();

    return () => { supabase.removeChannel(channel); };
  }, [userId]);

  return (
    <ul>{notifications.map((n) => <li key={n.id}>{n.message}</li>)}</ul>
  );
}

>Storage — file uploads

TypeScript
'use server';
import { createClient } from '@/lib/supabase/server';

export async function uploadAvatar(formData: FormData) {
  const supabase  = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) throw new Error('Unauthenticated');

  const file = formData.get('avatar') as File;
  const ext  = file.name.split('.').pop();
  const path = `avatars/${user.id}.${ext}`;

  const { error } = await supabase.storage
    .from('public-assets')
    .upload(path, file, { upsert: true });

  if (error) throw error;

  const { data: { publicUrl } } = supabase.storage
    .from('public-assets')
    .getPublicUrl(path);

  await supabase.from('profiles')
    .update({ avatar_url: publicUrl })
    .eq('id', user.id);
}