Next.js с Supabase — авторизация и база данных

18 сентября 2026
Автор

Антон Ларичев

Supabase — это open-source альтернатива Firebase, которая предоставляет PostgreSQL базу данных, авторизацию, хранилище файлов и realtime-подписки через единый API. В связке с Next.js App Router она позволяет строить полноценные fullstack-приложения без отдельного бэкенда.

В этой статье разберём, как подключить Supabase к проекту на Next.js 14+, настроить авторизацию через email и OAuth, работать с базой данных и защищать маршруты на серверной стороне.

Создание проекта Supabase

Перейдите на supabase.com, создайте аккаунт и новый проект. После инициализации (занимает около минуты) вам понадобятся две переменные из раздела Settings → API:

  • Project URL — адрес вашего проекта
  • anon public — публичный ключ для клиентского кода

Для серверных операций с повышенными привилегиями также сохраните service_role ключ — он даёт полный обход Row Level Security.

Курсы PurpleSchool

Учитесь у практикующих разработчиков — без воды

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Установка зависимостей

Supabase предоставляет официальный пакет для Next.js, который корректно работает с серверными компонентами и Server Actions:

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

Пакет @supabase/ssr решает проблему работы с cookies в среде Next.js — он умеет читать и писать cookies как на клиенте, так и в Server Components, Route Handlers и Middleware.

Настройка переменных окружения

Создайте файл .env.local в корне проекта:

NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

Префикс NEXT_PUBLIC_ делает переменную доступной в браузере. SUPABASE_SERVICE_ROLE_KEY без префикса остаётся только на сервере.

Создание клиентов Supabase

В App Router нужны разные клиенты для разных контекстов. Создайте директорию lib/supabase/ с тремя файлами.

Клиент для Server Components

// 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_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          )
        },
      },
    }
  )
}

Клиент для Client Components

// 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_ANON_KEY!
  )
}

Клиент для Middleware

// lib/supabase/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // Обновляем сессию пользователя
  await supabase.auth.getUser()

  return supabaseResponse
}

Настройка Middleware

Middleware запускается перед каждым запросом и обновляет JWT-токен в cookies. Без него сессия истекает и пользователь будет неожиданно разлогинен:

// 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)$).*)',
  ],
}

Авторизация через email

Форма входа

Создадим Server Action для авторизации — это позволяет обрабатывать форму без написания отдельного API-маршрута:

// app/auth/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export async function login(formData: FormData) {
  const supabase = await createClient()

  const { error } = await supabase.auth.signInWithPassword({
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  })

  if (error) {
    redirect('/auth/login?error=Неверный email или пароль')
  }

  revalidatePath('/', 'layout')
  redirect('/dashboard')
}

export async function signup(formData: FormData) {
  const supabase = await createClient()

  const { error } = await supabase.auth.signUp({
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  })

  if (error) {
    redirect('/auth/login?error=' + error.message)
  }

  redirect('/auth/confirm')
}

export async function logout() {
  const supabase = await createClient()
  await supabase.auth.signOut()
  redirect('/')
}
// app/auth/login/page.tsx
import { login, signup } from '../actions'

export default function LoginPage({
  searchParams,
}: {
  searchParams: { error?: string }
}) {
  return (
    <form>
      {searchParams.error && (
        <p style={{ color: 'red' }}>{searchParams.error}</p>
      )}
      <input name="email" type="email" placeholder="Email" required />
      <input name="password" type="password" placeholder="Пароль" required />
      <button formAction={login}>Войти</button>
      <button formAction={signup}>Зарегистрироваться</button>
    </form>
  )
}

OAuth авторизация (GitHub, Google)

Для OAuth нужен клиентский компонент, поскольку метод signInWithOAuth перенаправляет браузер на страницу провайдера:

// app/auth/components/OAuthButtons.tsx
'use client'

import { createClient } from '@/lib/supabase/client'

export function GitHubButton() {
  const supabase = createClient()

  const handleLogin = async () => {
    await supabase.auth.signInWithOAuth({
      provider: 'github',
      options: {
        redirectTo: `${window.location.origin}/auth/callback`,
      },
    })
  }

  return <button onClick={handleLogin}>Войти через GitHub</button>
}

После успешной OAuth-авторизации Supabase перенаправит пользователя на /auth/callback. Создайте Route Handler для обработки кода обмена:

// app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      return NextResponse.redirect(`${origin}/dashboard`)
    }
  }

  return NextResponse.redirect(`${origin}/auth/login?error=Ошибка авторизации`)
}

Защита маршрутов

Проверку сессии лучше делать прямо в Server Component, а не только в Middleware. Создайте переиспользуемую утилиту:

// lib/auth.ts
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export async function requireAuth() {
  const supabase = await createClient()
  const { data: { user }, error } = await supabase.auth.getUser()

  if (error || !user) {
    redirect('/auth/login')
  }

  return user
}

Теперь защитить любую страницу можно одной строкой:

// app/dashboard/page.tsx
import { requireAuth } from '@/lib/auth'

export default async function DashboardPage() {
  const user = await requireAuth()

  return (
    <div>
      <h1>Добро пожаловать, {user.email}</h1>
    </div>
  )
}

Работа с базой данных

Создание таблицы

В панели Supabase перейдите в SQL Editor и создайте таблицу для постов:

CREATE TABLE posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  title TEXT NOT NULL,
  content TEXT,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Включаем Row Level Security
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Пользователь видит только свои посты
CREATE POLICY "Users can view own posts"
  ON posts FOR SELECT
  USING (auth.uid() = user_id);

-- Пользователь создаёт посты от своего имени
CREATE POLICY "Users can insert own posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = user_id);

-- Пользователь редактирует только свои посты
CREATE POLICY "Users can update own posts"
  ON posts FOR UPDATE
  USING (auth.uid() = user_id);

-- Пользователь удаляет только свои посты
CREATE POLICY "Users can delete own posts"
  ON posts FOR DELETE
  USING (auth.uid() = user_id);

Row Level Security (RLS) — это встроенный механизм PostgreSQL, который автоматически фильтрует строки на уровне базы данных в зависимости от текущего пользователя. Даже если в коде забыть добавить WHERE user_id = ..., RLS не позволит получить чужие данные.

Чтение данных в Server Component

// app/dashboard/posts/page.tsx
import { createClient } from '@/lib/supabase/server'
import { requireAuth } from '@/lib/auth'

interface Post {
  id: string
  title: string
  content: string | null
  created_at: string
}

export default async function PostsPage() {
  await requireAuth()

  const supabase = await createClient()
  const { data: posts, error } = await supabase
    .from('posts')
    .select('*')
    .order('created_at', { ascending: false })

  if (error) {
    return <p>Ошибка загрузки постов</p>
  }

  return (
    <ul>
      {posts.map((post: Post) => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </li>
      ))}
    </ul>
  )
}

Создание и удаление через Server Actions

// app/dashboard/posts/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
import { requireAuth } from '@/lib/auth'

export async function createPost(formData: FormData) {
  const user = await requireAuth()
  const supabase = await createClient()

  const { error } = await supabase.from('posts').insert({
    title: formData.get('title') as string,
    content: formData.get('content') as string,
    user_id: user.id,
  })

  if (error) throw new Error(error.message)

  revalidatePath('/dashboard/posts')
}

export async function deletePost(postId: string) {
  await requireAuth()
  const supabase = await createClient()

  const { error } = await supabase
    .from('posts')
    .delete()
    .eq('id', postId)

  if (error) throw new Error(error.message)

  revalidatePath('/dashboard/posts')
}
// app/dashboard/posts/components/CreatePostForm.tsx
'use client'

import { createPost } from '../actions'
import { useTransition } from 'react'

export function CreatePostForm() {
  const [isPending, startTransition] = useTransition()

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const formData = new FormData(e.currentTarget)
        startTransition(() => createPost(formData))
      }}
    >
      <input name="title" placeholder="Заголовок" required />
      <textarea name="content" placeholder="Содержание" />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Сохранение...' : 'Создать пост'}
      </button>
    </form>
  )
}

Типизация с генерацией типов

Supabase умеет генерировать TypeScript-типы из схемы вашей базы данных. Установите CLI и выполните:

npx supabase gen types typescript --project-id your-project-id > lib/database.types.ts

Затем передайте тип в клиент для автодополнения:

import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/lib/database.types'

export function createClient() {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

Теперь .from('posts').select('*') вернёт типизированный массив — TypeScript будет знать точный набор полей и их типы.

Получение профиля текущего пользователя

Для хранения дополнительных данных о пользователе (имя, аватар, роль) создайте отдельную таблицу profiles, связанную с auth.users:

CREATE TABLE profiles (
  id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
  display_name TEXT,
  avatar_url TEXT,
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Public profiles are viewable"
  ON profiles FOR SELECT USING (true);

CREATE POLICY "Users can update own profile"
  ON profiles FOR UPDATE USING (auth.uid() = id);

-- Автоматически создаём профиль при регистрации
CREATE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id)
  VALUES (NEW.id);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

Читать профиль можно вместе с данными пользователя:

const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()

const { data: profile } = await supabase
  .from('profiles')
  .select('display_name, avatar_url')
  .eq('id', user!.id)
  .single()

Итог

Supabase и Next.js App Router отлично дополняют друг друга: Server Components делают прямые запросы к базе без промежуточного API, Server Actions обрабатывают мутации, а Middleware поддерживает сессию актуальной на каждом запросе. Row Level Security на стороне PostgreSQL гарантирует безопасность данных независимо от логики приложения.

Чтобы глубже изучить Next.js и научиться строить production-ready приложения с авторизацией, базами данных и деплоем, приходите на курс PurpleSchool: https://purpleschool.ru/course/nextjs?utm_source=knowledgebase&utm_medium=text&utm_campaign=nextjs-supabase

Стрелочка влевоnotFound и redirect в серверных компонентах Next.jsNext.js с GraphQL и Apollo ClientСтрелочка вправо

Все гайды по Next-js

useSearchParams и usePathname в Next.js App RouterStreaming и Suspense в Next.jsServer Actions в Next.jsrevalidateTag и revalidatePath — инвалидация кэша в Next.jsPrisma ORM в Next.jsnotFound и redirect в серверных компонентах Next.jsNext.js с Supabase — авторизация и база данныхNext.js с GraphQL и Apollo ClientNext.js 15: новые возможности и breaking changesloading.js и error.js в Next.js App RouterИнтернационализация i18n в Next.js App RoutergenerateStaticParams — статическая генерация динамических маршрутовNext.js cookies и headers в App RouterNext.js с Clerk: аутентификация пользователейNext.js after() — выполнение кода после отправки ответа
Открыть базу знаний

Отправить комментарий