StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN
StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

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.

Astro

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

Doradztwo produktowe

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

QA & Automation

Testy automatyczne komponentów i E2E w oparciu o Cypress.

SEO & Performance

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

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
  • SEO & Performance Sprint
  • QA & Stabilizacja
  • Konsultacje Product / Delivery
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
Next.jsSupabaseBackendBazy danych

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

Aktualny poradnik Supabase + Next.js App Router: @supabase/ssr, publishable keys, Middleware, Server Actions, RLS, Storage i Realtime bez przestarzałych Auth Helpers.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano

10 kwietnia 2026 11:10

Czytanie

5 min czytania

Aktualizacja

27 kwietnia 2026 11:52

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 Server Components renderują się po stronie serwera i mogą pobierać dane bez wysyłania własnego kodu JavaScript 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.

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.

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 Next.js to kod uruchamiany przed obsługą requestu, używany m.in. do redirectów, nagłówków i odświeżania cookies sesji. 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 wykonywana na serwerze, którą można wywołać bezpośrednio z formularza lub komponentu React w App Routerze.. 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, oznacza generowanie HTML na serwerze dla konkretnego żądania. 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, przyspiesza dostarczanie treści z serwerów bliższych użytkownikowi.-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 po czasie bez pełnego rebuildu.,
  • 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.

Podsumowanie

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".

Źródła i dalsza lektura

  • 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

Często zadawane pytania

Tak, szczególnie gdy potrzebujesz szybko zbudować aplikację z uwierzytelnianiem, relacyjną bazą danych, storage i realtime. Najważniejsze jest używanie aktualnego pakietu `@supabase/ssr`, cookies zamiast localStorage w SSR oraz Row Level Security jako głównej warstwy ochrony danych.

Reakcje

Jak Ci się podobało?

Newsletter

Spodobał Ci się ten wpis?

Zapisz się na listę i dostawaj nowe artykuły o frontendzie, SEO i Next.js wprost na maila. Bez spamu, bez promocji — tylko praktyczna wiedza.

Pracuję z tym zawodowo.

Jeśli chcesz przełożyć ten temat na lepszą architekturę frontendu, uporządkować React lub Next.js i podnieść jakość pracy zespołu, skontaktuj się ze mną. Pomagam zamieniać wiedzę z artykułów w praktyczne decyzje technologiczne.

Skontaktuj się ze mną
Maciej Sala

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Moje artykułyWięcej o mnie
Poprzedni wpisGEO i AEO w Next.js — techniczna optymalizacja pod ChatGPT, Gemini i PerplexityTechniczny poradnik GEO i AEO dla Next.js: SSR/SSG, metadata, JSON-LD, sitemap, canonicale, dostępność dla botów i struktura treści pod ChatGPT, Gemini, Perplexity oraz AI Overviews.
Maciej Sala

Maciej Sala

Founder Strivelab

31 marca 2026
Następny wpisNext.js Sitemap i robots.txt — automatyczna generacja z App RouteraJak generować sitemap.xml i robots.txt w Next.js App Router? Natywne API konwencji plików vs next-sitemap — dynamiczne sitemaps, lastmod, changefreq i priorytety.
Maciej Sala

Maciej Sala

Founder Strivelab

10 kwietnia 2026

Spis treści

14 sekcji · 14 min

  • Czym jest Supabase i dlaczego pasuje do Next.js?1 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
  • Podsumowanie1 min
  • Źródła i dalsza lektura1 min

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Backend dla frontendowca — co musisz wiedzieć?

Backend dla frontendowca — co musisz wiedzieć?

Pełny przegląd backendu dla frontendowca: serwery, bazy, API, CORS, real-time, webhooks, auth, CSRF, cache, kolejki, deployment, monitoring, bezpieczeństwo. Kompletna mapa.

Maciej Sala

Maciej Sala

Founder Strivelab

28 lipca 2025
App Router czy Pages Router — co wybrać?

App Router czy Pages Router — co wybrać?

App Router czy Pages Router w Next.js 16? Konkretne różnice, koszty migracji i praktyczne kryteria wyboru dla nowych oraz istniejących projektów.

Maciej Sala

Maciej Sala

Founder Strivelab

23 grudnia 2025
Prognozy i trendy 2026: AI, GEO i React Server Components

Prognozy i trendy 2026: AI, GEO i React Server Components

Siedem trendów, które w 2026 realnie zmieniają web development i marketing: GEO, zero-click search, server-first Next.js, AI-assisted development i agentic commerce.

Maciej Sala

Maciej Sala

Founder Strivelab

8 stycznia 2026