StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • Audyt SEO i Performance
  • Testy automatyczne i QA
  • Konsultacje Produktowe
  • Automatyzacja Procesów AI
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
Next.jsBackend

Supabase + Next.js — uwierzytelnianie, baza, storage i realtime w jednym stacku

Auth, baza, storage i realtime w jednym stacku — Supabase + Next.js App Router w 2026 z @supabase/ssr, bez przestarzałych Auth Helpers.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
10 kwietnia 2026 11:10
Czytanie
6 min czytania
Aktualizacja
25 maja 2026 10:55

Czym jest Supabase i dlaczego pasuje do Next.js?

Supabase to otwartoźródłowa platforma backendowa zbudowana wokół PostgreSQL. W jednym projekcie dostajesz bazę danych, Auth, Storage, Realtime, Edge Functions i automatycznie generowane API. Dla aplikacji w Next.js to często najkrótsza droga do sensownego MVP bez osobnego backendu pisanego od zera.

W 2026 roku najważniejsza zmiana w tym stacku jest prosta: nie zaczynaj od starych Auth Helpers. Do integracji z App Routerem używaj @supabase/ssr, cookies, osobnych klientów server/browser i middleware.ts, który odświeża sesję dla React Server Components — komponenty renderowane wyłącznie na serwerze, które mogą być funkcjami async i pobierać dane bezpośrednio, bez wysyłania własnego JavaScriptu do przeglądarki..

Druga zasada: Supabase nie zwalnia z myślenia o bezpieczeństwie. Publiczny klient w przeglądarce jest normalną częścią architektury, ale dane chroni baza przez Row Level Security to mechanizm PostgreSQL, który ogranicza dostęp do konkretnych wierszy tabeli na podstawie polityk dostępu.. Jeśli RLS jest źle ustawione, problemu nie naprawi elegancki komponent Reacta.

Uwaga

W Supabase publiczny klucz nie jest problemem sam w sobie. Problemem jest brak RLS albo zbyt szerokie polityki, bo to baza danych ma egzekwować dostęp do wierszy, plików i kanałów realtime.

Artykuł w skrócie

  • Nowy projekt Next.js + Supabase konfiguruj przez @supabase/ssr, nie przez przestarzałe Auth Helpers.
  • Używaj NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY; stary ANON_KEY traktuj jako legacy w nowych materiałach.
  • Middleware odświeża sesję, ale nie zastępuje sprawdzania uprawnień w Server Components, Route Handlers i Server Actions.
  • RLS, Storage policies i ograniczone Realtime subscriptions są częścią implementacji, nie dodatkiem na koniec.
  • Mutacje w App Routerze najczęściej trzymaj w Server Actions z walidacją danych i rewalidacją cache.

Warto widzieć ten stack jako kilka warstw kontroli dostępu, a nie jedno SDK wywoływane z Reacta. Middleware dba o świeżą sesję, kod serwerowy sprawdza użytkownika, a baza i polityki Supabase egzekwują dostęp do danych.

Diagram
Warstwy dostępu w aplikacji Next.js + Supabase

Setup projektu

Instalacja pakietów jest krótka:

Code
npm install @supabase/supabase-js @supabase/ssr

W nowych projektach Supabase promuje publishable keys:

Code
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://twoj-projekt.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx

Jeśli pracujesz na starszym projekcie, możesz jeszcze spotkać NEXT_PUBLIC_SUPABASE_ANON_KEY. To nadal może działać, ale przy nowym setupie warto przejść na Publishable key to publiczny klucz projektu Supabase przeznaczony do użycia w aplikacji klienckiej; jego bezpieczeństwo opiera się na RLS i politykach dostępu., bo to aktualny kierunek dokumentacji Supabase.

Nigdy nie dodawaj `service_role` to sekretny klucz Supabase z uprawnieniami administracyjnymi, który omija Row Level Security i nie może trafić do przeglądarki. jako zmiennej NEXT_PUBLIC_*. Ten klucz omija RLS i może istnieć tylko w bezpiecznym kontekście serwerowym, np. w zamkniętym Route Handlerze, zadaniu administracyjnym albo Edge Function.

Klient Supabase dla App Routera

W App Routerze potrzebujesz co najmniej dwóch klientów: jednego do kodu przeglądarkowego i jednego do kodu serwerowego.

Code
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
 
export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
  )
}
Code
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
 
export async function createClient() {
  const cookieStore = await cookies()
 
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) => {
              cookieStore.set(name, value, options)
            })
          } catch {
            // Server Components nie mogą zapisywać cookies.
            // Odświeżanie sesji obsługuje middleware.ts.
          }
        },
      },
    },
  )
}

W większym projekcie dodaj typy wygenerowane z bazy:

Code
npx supabase gen types typescript --project-id "$PROJECT_REF" > src/types/database.types.ts

Potem możesz stworzyć klienta jako createServerClient<Database>(...) i dostać typowane from('posts'), nazwy kolumn oraz relacje.

Middleware do odświeżania sesji

W kontekście Supabase Middleware w Astro to kod uruchamiany przy każdym żądaniu, zanim wyrenderuje się strona — może czytać żądanie, modyfikować context.locals, przekierowywać albo przepuścić żądanie dalej. jest potrzebny głównie dlatego, że Server Components nie mogą samodzielnie zapisać odświeżonych cookies.

Warto rozdzielić plik konwencji Next.js od logiki Supabase:

Code
// middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware'
 
export async function middleware(request: NextRequest) {
  return await updateSession(request)
}
 
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}
Code
// lib/supabase/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
 
export async function updateSession(request: NextRequest) {
  let response = NextResponse.next({ request })
 
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) => {
            request.cookies.set(name, value)
          })
 
          response = NextResponse.next({ request })
 
          cookiesToSet.forEach(({ name, value, options }) => {
            response.cookies.set(name, value, options)
          })
        },
      },
    },
  )
 
  await supabase.auth.getUser()
 
  return response
}

Middleware może robić wstępne przekierowania dla /dashboard, ale nie traktuj go jako pełnego systemu autoryzacji. Next.js uruchamia go przed renderowaniem i sam Middleware nie powinien stać się miejscem ciężkiej logiki biznesowej.

Uwierzytelnianie w Server Actions

Logowanie hasłem możesz zrobić w Client Component przez signInWithPassword, ale w App Routerze coraz częściej wygodniejszy jest Server Action to funkcja oznaczona dyrektywą 'use server', wykonywana na serwerze, ale wywoływana bezpośrednio z komponentu klienckiego. Tutaj odbiera kompletne dane formularza, ponownie waliduje je Zodem i zapisuje — bez ręcznego pisania endpointu API.. Formularz pozostaje prosty, cookies są ustawiane po stronie serwera, a po mutacji możesz odświeżyć cache.

Code
// app/login/actions.ts
'use server'
 
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
 
export async function login(formData: FormData) {
  const supabase = await createClient()
 
  const email = String(formData.get('email') ?? '')
  const password = String(formData.get('password') ?? '')
 
  const { error } = await supabase.auth.signInWithPassword({
    email,
    password,
  })
 
  if (error) {
    return { error: 'Nie udało się zalogować. Sprawdź adres e-mail i hasło.' }
  }
 
  revalidatePath('/', 'layout')
  redirect('/dashboard')
}
 
export async function signup(formData: FormData) {
  const supabase = await createClient()
 
  const email = String(formData.get('email') ?? '')
  const password = String(formData.get('password') ?? '')
 
  const { error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
    },
  })
 
  if (error) {
    return { error: 'Nie udało się utworzyć konta.' }
  }
 
  redirect('/check-email')
}
Code
// app/login/page.tsx
import { login } from './actions'
 
export default function LoginPage() {
  return (
    <form action={login} className="mx-auto max-w-md space-y-4 p-8">
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit">Zaloguj się</button>
    </form>
  )
}

W prawdziwej aplikacji dodaj walidację Zod, obsługę błędów przez useActionState, limity prób logowania i sensowny UX dla potwierdzania adresu e-mail. Przykład powyżej pokazuje przepływ, nie kompletny formularz produkcyjny.

OAuth i callback

OAuth nadal zaczyna się po stronie klienta, bo użytkownik musi zostać przekierowany do dostawcy (providera):

Code
'use client'
 
import { createClient } from '@/lib/supabase/client'
 
export function GoogleLoginButton() {
  const supabase = createClient()
 
  async function signInWithGoogle() {
    await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: `${window.location.origin}/auth/callback`,
      },
    })
  }
 
  return <button onClick={signInWithGoogle}>Kontynuuj z Google</button>
}

Callback wymienia code na sesję:

Code
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
 
export async function GET(request: Request) {
  const url = new URL(request.url)
  const code = url.searchParams.get('code')
  const next = url.searchParams.get('next') ?? '/dashboard'
 
  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
 
    if (!error) {
      return NextResponse.redirect(new URL(next, url.origin))
    }
  }
 
  return NextResponse.redirect(new URL('/login?error=auth', url.origin))
}

Ochrona stron i danych

Nie opieraj ochrony panelu wyłącznie na tym, że użytkownik nie widzi linku w nawigacji. Sprawdzenie musi odbywać się po stronie serwera.

Code
// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
 
export default async function DashboardPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
 
  if (!user) {
    redirect('/login')
  }
 
  return <div>Panel użytkownika: {user.email}</div>
}

W Server Actions powtarzaj ten sam typ kontroli. To, że formularz znajduje się na chronionej stronie, nie oznacza, że wywołanie akcji jest automatycznie bezpieczne.

CRUD z Server Components i Server Actions

Pobieranie danych w Server Component:

Code
// app/dashboard/posts/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
 
export default async function PostsPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
 
  if (!user) {
    redirect('/login')
  }
 
  const { data: posts, error } = await supabase
    .from('posts')
    .select('id, title, published, created_at')
    .eq('author_id', user.id)
    .order('created_at', { ascending: false })
 
  if (error) {
    throw new Error(error.message)
  }
 
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <span>{post.published ? 'Opublikowany' : 'Szkic'}</span>
        </li>
      ))}
    </ul>
  )
}

Mutacja w Server Action:

Code
// app/dashboard/posts/actions.ts
'use server'
 
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
 
const createPostSchema = z.object({
  title: z.string().trim().min(3).max(120),
  content: z.string().trim().min(10).max(20000),
})
 
export async function createPost(formData: FormData) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
 
  if (!user) {
    return { error: 'Musisz być zalogowany.' }
  }
 
  const parsed = createPostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  })
 
  if (!parsed.success) {
    return { error: 'Popraw dane formularza.' }
  }
 
  const { error } = await supabase.from('posts').insert({
    ...parsed.data,
    author_id: user.id,
    published: false,
  })
 
  if (error) {
    return { error: error.message }
  }
 
  revalidatePath('/dashboard/posts')
  return { success: true }
}

Nawet jeśli w insert() podstawiasz author_id na podstawie sesji, RLS nadal powinno to egzekwować. Kod aplikacji i polityki w bazie mają się wzajemnie uzupełniać.

Row Level Security

RLS w Supabase działa jak automatyczny filtr dodawany do zapytań. Po włączeniu RLS tabela nie zwróci danych przez publiczne API, dopóki nie dodasz polityk.

Code
alter table public.posts enable row level security;
 
create policy "Authenticated users can read their own posts"
on public.posts
for select
to authenticated
using ((select auth.uid()) = author_id);
 
create policy "Authenticated users can create their own posts"
on public.posts
for insert
to authenticated
with check ((select auth.uid()) = author_id);
 
create policy "Authenticated users can update their own posts"
on public.posts
for update
to authenticated
using ((select auth.uid()) = author_id)
with check ((select auth.uid()) = author_id);
 
create policy "Authenticated users can delete their own posts"
on public.posts
for delete
to authenticated
using ((select auth.uid()) = author_id);

Dla publicznych wpisów dodaj osobną politykę tylko do odczytu:

Code
create policy "Anyone can read published posts"
on public.posts
for select
to anon, authenticated
using (published = true);

Warto jawnie podawać role przez to authenticated albo to anon, authenticated. Jest to czytelniejsze i często wydajniejsze niż polityki bez określonej roli.

Storage — upload plików

Najprostszy wariant dla avatara to upload z klienta do bucketu avatars, z polityką ograniczającą ścieżkę do katalogu użytkownika.

Code
insert into storage.buckets (id, name, public)
values ('avatars', 'avatars', true)
on conflict (id) do nothing;
 
create policy "Users can upload their own avatar"
on storage.objects
for insert
to authenticated
with check (
  bucket_id = 'avatars'
  and (select auth.uid())::text = (storage.foldername(name))[1]
);
 
create policy "Users can update their own avatar"
on storage.objects
for update
to authenticated
using (
  bucket_id = 'avatars'
  and (select auth.uid())::text = (storage.foldername(name))[1]
);
 
create policy "Anyone can read avatars"
on storage.objects
for select
to anon, authenticated
using (bucket_id = 'avatars');

Komponent uploadu:

Code
// components/avatar-upload.tsx
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useMemo, useState } from 'react'
 
const MAX_FILE_SIZE = 2 * 1024 * 1024
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']
 
export function AvatarUpload({ userId }: { userId: string }) {
  const [uploading, setUploading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const supabase = useMemo(() => createClient(), [])
 
  async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0]
    if (!file) return
 
    setError(null)
 
    if (!ALLOWED_TYPES.includes(file.type) || file.size > MAX_FILE_SIZE) {
      setError('Wybierz obraz JPG, PNG albo WebP do 2 MB.')
      return
    }
 
    setUploading(true)
 
    const extension = file.name.split('.').pop()
    const filePath = `${userId}/avatar.${extension}`
 
    const { error } = await supabase.storage
      .from('avatars')
      .upload(filePath, file, {
        upsert: true,
        contentType: file.type,
        cacheControl: '3600',
      })
 
    setUploading(false)
 
    if (error) {
      setError('Nie udało się przesłać pliku.')
    }
  }
 
  return (
    <label>
      {uploading ? 'Przesyłanie...' : 'Zmień avatar'}
      <input type="file" accept="image/*" onChange={handleUpload} />
      {error ? <span>{error}</span> : null}
    </label>
  )
}

Walidacja w komponencie poprawia UX, ale nie jest granicą bezpieczeństwa. Granicą jest polityka Storage, limity bucketu i ewentualna kontrola po stronie serwera. Dla wrażliwych plików zamiast publicznego bucketu użyj prywatnego bucketu oraz podpisanych URL-i.

Realtime — subskrypcje na żywo

Realtime dla zmian w Postgresie wymaga dodania tabeli do publikacji:

Code
alter publication supabase_realtime add table public.comments;

Tabela nadal powinna mieć RLS:

Code
alter table public.comments enable row level security;
 
create policy "Users can read comments for visible posts"
on public.comments
for select
to authenticated
using (
  exists (
    select 1
    from public.posts
    where posts.id = comments.post_id
      and posts.author_id = (select auth.uid())
  )
);

Przykład komponentu:

Code
// components/live-comments.tsx
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useEffect, useMemo, useState } from 'react'
 
interface Comment {
  id: string
  post_id: string
  content: string
  author_name: string
  created_at: string
}
 
export function LiveComments({ postId }: { postId: string }) {
  const [comments, setComments] = useState<Comment[]>([])
  const supabase = useMemo(() => createClient(), [])
 
  useEffect(() => {
    let ignore = false
 
    async function loadComments() {
      const { data } = await supabase
        .from('comments')
        .select('id, post_id, content, author_name, created_at')
        .eq('post_id', postId)
        .order('created_at', { ascending: true })
 
      if (!ignore && data) {
        setComments(data)
      }
    }
 
    void loadComments()
 
    const channel = supabase
      .channel(`comments:${postId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'comments',
          filter: `post_id=eq.${postId}`,
        },
        (payload) => {
          setComments((current) => [...current, payload.new as Comment])
        },
      )
      .subscribe()
 
    return () => {
      ignore = true
      void supabase.removeChannel(channel)
    }
  }, [postId, supabase])
 
  return (
    <div>
      {comments.map((comment) => (
        <article key={comment.id}>
          <p>{comment.author_name}</p>
          <p>{comment.content}</p>
        </article>
      ))}
    </div>
  )
}

Przy Realtime łatwo przesadzić. Nie subskrybuj całych tabel, jeśli potrzebujesz zmian tylko dla jednego dokumentu, projektu albo organizacji. Filtr w subskrypcji zmniejsza ruch i ryzyko przypadkowego ujawnienia informacji.

Cache i prywatne dane

Supabase Auth z SSR, czyli Server-Side Rendering, to generowanie HTML na serwerze przy żądaniu — komponent client:only je pomija i renderuje się wyłącznie w przeglądarce. korzysta z cookies, a to oznacza, że odpowiedzi zawierające dane użytkownika nie powinny być traktowane jak statyczne strony ani cache'owane publicznie za CDN, czyli Content Delivery Network, to rozproszona sieć serwerów dostarczająca zasoby z węzła najbliższego użytkownikowi; CDN do obrazów dodatkowo transformuje je w locie.-em.

Praktyczne reguły:

  • strony panelu użytkownika trzymaj jako dynamiczne,
  • prywatnych danych nie pobieraj w generateStaticParams,
  • po Server Actions używaj revalidatePath() albo revalidateTag(),
  • nie mieszaj prywatnych danych użytkownika z publicznym ISR, czyli Incremental Static Regeneration, pozwala odświeżać strony statyczne w tle bez pełnego rebuildu — strona jest serwowana z cache, a Next.js regeneruje ją po upływie czasu revalidate.,
  • w Route Handlers jasno ustawiaj nagłówki cache, jeśli odpowiedź zależy od sesji.

To szczególnie ważne, gdy aplikacja działa za CDN-em lub używa agresywnego cache'owania odpowiedzi HTML.

Kiedy Supabase ma sens, a kiedy nie?

Supabase jest bardzo dobry dla:

  • MVP i SaaS z relacyjnym modelem danych,
  • paneli użytkownika, CRM, marketplace'ów i aplikacji B2B,
  • aplikacji, w których Postgres, RLS i szybki CRUD są ważniejsze niż własna infrastruktura,
  • produktów, które potrzebują Auth, Storage i Realtime bez składania kilku usług.

Rozważyłbym inne podejście, gdy:

  • masz bardzo specyficzne wymagania infrastrukturalne albo compliance,
  • logika domenowa jest ciężka i wymaga osobnego backendu,
  • Realtime ma obsługiwać bardzo duże wolumeny zdarzeń z własnym protokołem,
  • zespół już ma dojrzały backend w NestJS, Rails, Django, Laravel albo Go.

Supabase nie jest "backendiem bez backendu". To raczej zarządzany Postgres z zestawem usług, które pozwalają przesunąć dużo pracy z warstwy aplikacyjnej do bazy i polityk dostępu.

Werdykt Labu

Aktualny stack Supabase + Next.js opiera się na @supabase/ssr, cookies, middleware.ts, Server Actions i RLS. Największy błąd to traktowanie Supabase jak prostego SDK do frontendu. Poprawny model jest inny: frontend i Server Components korzystają z klienta Supabase, ale granice bezpieczeństwa są w bazie, politykach Storage i walidacji po stronie serwera.

Jeśli budujesz MVP albo aplikację produktową z panelem użytkownika, Supabase potrafi skrócić development o tygodnie. Warunek: od początku ustaw RLS, rozdziel klienta server/browser, nie wystawiaj sekretów do przeglądarki i nie zostawiaj uwierzytelniania i autoryzacji „na później".

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.
Automatyzacja AI
  • Czym jest Supabase i dlaczego pasuje do Next.js?2 min
  • Setup projektu1 min
  • Klient Supabase dla App Routera1 min
  • Middleware do odświeżania sesji1 min
  • Uwierzytelnianie w Server Actions1 min
  • Ochrona stron i danych1 min
  • CRUD z Server Components i Server Actions1 min
  • Row Level Security1 min
  • Storage — upload plików1 min
  • Realtime — subskrypcje na żywo1 min
  • Cache i prywatne dane1 min
  • Kiedy Supabase ma sens, a kiedy nie?1 min
  • Werdykt Labu1 min

Często zadawane pytania

Źródła i dalsza lekturaZweryfikowano: 19 maja 2026

Materiały wykorzystane do weryfikacji artykułu „Supabase + Next.js — uwierzytelnianie, baza, storage i realtime w jednym stacku”:

Supabase Docs — Creating a Supabase client for SSR, Supabase Docs — Build a User Management App with Next.js, Supabase Docs — Row Level Security, Supabase Docs — Postgres Changes, Next.js Docs — Middleware, Next.js Docs — Forms and Server Actions.

Maciej Sala

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty side-projecty.

Moje artykułyWięcej o mnie

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js w 2026 — kompleksowe porównanie frameworków
Astro.js vs Next.js w 2026 — kompleksowe porównanie frameworków

Astro 6 vs Next.js 16 — zupełnie różne założenia. Które wybrać do strony usługowej, bloga, SaaS i e-commerce? Decydujące kryteria.

Maciej Sala

Maciej Sala

Founder Strivelab

15 kwietnia 2026
Backend dla frontendowca: serwer, bazy danych i API
Backend dla frontendowca: serwer, bazy danych i API

Pierwsza część serii Backend dla frontendowca: architektura aplikacji, serwer, bazy danych, API, statusy HTTP, paginacja, idempotency, BFF i CORS.

Maciej Sala

Maciej Sala

Founder Strivelab

28 lipca 2025
Backend dla frontendowca: cache, deployment i bezpieczeństwo
Backend dla frontendowca: cache, deployment i bezpieczeństwo

Redis, cache HTTP, kolejki, deployment, monitoring, OWASP Top 10:2025 i RODO — backendowa wiedza, którą frontendowiec powinien znać.

Maciej Sala

Maciej Sala

Founder Strivelab

30 lipca 2025
Poprzedni wpisDrizzle ORM vs Prisma — co wybrać w 2026 do projektu Next.js?Drizzle ORM czy Prisma w 2026? Wydajność, DX i edge — porównanie, które pomoże zdecydować, zanim napiszesz pierwszy schemat bazy danych.
Maciej Sala

Maciej Sala

Founder Strivelab

10 kwietnia 2026
Następny wpisNext.js Sitemap i robots.txt — automatyczna generacja z App RouteraSitemap i robots.txt w Next.js App Router bez zewnętrznych paczek. Dynamiczne sitemaps, lastmod i jak nie wyeksponować staging przez przypadek.
Maciej Sala

Maciej Sala

Founder Strivelab

10 kwietnia 2026