Supabase + Next.js: Архитектура без костылей и хаоса
System Design / Backend

Supabase + Next.js: Архитектура без костылей и хаоса

Н
Никита
04 дек. 2025 г.

1. Введение: Хватит писать запросы в useEffect

Давайте начистоту. Supabase — это круто. Это Postgres, завернутый в удобный API с Realtime и Auth. Но то, как его используют 80% туториалов в сети — это преступление против архитектуры.

Я вижу это на каждом код-ревью: логика работы с БД, размазанная тонким слоем по React-компонентам. Прямые вызовы supabase.from('users').select('*') прямо внутри кнопки. Это не разработка, это прототипирование.

В 2025 году, если мы говорим о продакшене, а не о пет-проекте на выходные, нам нужен контроль. Нам нужна типизация, слой абстракции и понимание того, где выполняется код — на клиенте или на сервере. Если вы тащите Supabase в проект только чтобы "не писать бэкенд", вы уже выстрелили себе в ногу. Бэкенд никуда не делся, он просто переехал в Postgres RLS (Row Level Security) и Edge Functions.



2. Технический разбор: Строим Service Layer

Главная ошибка — использование supabase-js клиента как "серебряной пули" везде. В Next.js (особенно с App Router) у нас два мира: Server Components и Client Components. Смешивать их — значит ловить гидратационные ошибки и утечки данных.

Архитектура здорового человека

Мы не дергаем базу из компонентов. Мы создаем Data Access Layer.
Зачем?

  1. Принцип единственной ответственности. Компонент рисует UI, сервис ходит за данными.

  1. Типизация. Мы генерируем типы из схемы БД и используем их везде.
  1. Централизация. Если завтра вы решите сменить Supabase на собственный Postgres, вы перепишете только сервисы, а не 500 компонентов.

Реализация (TypeScript + Clean Code)

Сначала генерируем типы (вы же не пишете any, правда?):
npx supabase gen types typescript --project-id "$PROJECT_REF" > src/types/supabase.ts

Теперь пишем клиент-фактори. В Next.js нам нужны разные клиенты для сервера и браузера (используем @supabase/ssr).

1. Server Client (для Server Components/Actions):

TYPESCRIPT
// src/lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { Database } from '@/types/supabase'

export const createClient = () => {
  const cookieStore = cookies()

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          // В Server Components мы не можем сетить куки,
          // это нужно только для Server Actions или Middleware
          try {
            cookieStore.set({ name, value, ...options })
          } catch (error) {
            // Игнорируем ошибку в Server Components
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: '', ...options })
          } catch (error) {
             // Игнорируем
          }
        },
      },
    }
  )
}

2. Service Layer (Пример с профилем пользователя):

Никакой логики в UI. Только вызов метода.

TYPESCRIPT
// src/services/profile.service.ts
import { createClient } from '@/lib/supabase/server'
import { Database } from '@/types/supabase'

type Profile = Database['public']['Tables']['profiles']['Row']

export const ProfileService = {
  async getProfile(userId: string): Promise<Profile | null> {
    const supabase = createClient()
    
    const { data, error } = await supabase
      .from('profiles')
      .select('*')
      .eq('id', userId)
      .single()

    if (error) {
      console.error('Error fetching profile:', error.message)
      // Здесь можно кинуть кастомную ошибку для ErrorBoundary
      throw new Error('Failed to fetch profile')
    }

    return data
  },

  async updateAvatar(userId: string, file: File): Promise<string> {
    const supabase = createClient()
    const fileName = `${userId}/${Date.now()}_${file.name}`

    // Загрузка в Storage
    const { error: uploadError } = await supabase.storage
      .from('avatars')
      .upload(fileName, file)

    if (uploadError) throw uploadError

    // Получаем публичную ссылку (если бакет публичный)
    const { data } = supabase.storage
      .from('avatars')
      .getPublicUrl(fileName)

    return data.publicUrl
  }
}

Storage: Нюансы

С хранилищем (Storage) есть два пути:

  1. Public Buckets: Просто и быстро. Подходит для аватарок.

  1. Private Buckets + Signed URLs: Единственный верный путь для документов, чеков и всего чувствительного.

Не отдавайте прямые ссылки на приватные файлы. Генерируйте createSignedUrl с коротким TTL (временем жизни) на сервере и отдавайте клиенту временную ссылку.



3. Подводные камни: Где выстрелить себе в ногу

Опыт показывает, что на Supabase горят в трех местах:

1. RLS (Row Level Security) — это не опция, это закон

Если вы отключили RLS "на время разработки" и забыли — считайте, вас уже взломали. Любой школьник с консолью браузера может сделать supabase.from('users').delete().neq('id', '0').
Правило: RLS включен всегда. Политики пишутся сразу.

2. Проблема "Select *"

Frontend-разработчики привыкли тянуть весь объект. В SQL это плохо. В Supabase это еще и дорого (вы платите за egress трафик или упираетесь в лимиты).
Всегда указывайте конкретные поля: .select('id, name, avatar_url'). Это также облегчает жизнь постгресу при использовании индексов.

3. Waterfall запросов

Классика Next.js:

  • Компонент User тянет данные.

  • Внутри него компонент Posts тянет данные.
  • Внутри постов — компонент Comments.

Вы получаете последовательную цепочку запросов.
Решение: Используйте Promise.all в родительском Server Component или префетчинг. Supabase умеет делать join-ы (select('*, posts(*)')), но с этим осторожнее — можно положить базу сложным джойном.



4. Честные плюсы и минусы

АспектПлюсыМинусы
Скорость разработкиФеноменальная. CRUD поднимается за минуты.Легко написать говнокод, который сложно рефакторить.
База данныхЭто настоящий PostgreSQL. Вся мощь SQL, JSONB, GIS.Нужно знать SQL. Если вы привыкли к Mongo, будет больно.
AuthРаботает из коробки (OAuth, Magic Link).Кастомизация флоу регистрации иногда требует танцев с бубном (Triggers).
RealtimeПодписка на изменения БД — киллер-фича.Жрет коннекшены. На high-load нужно аккуратно настраивать pgbouncer.

5. Итоговое мнение: Вердикт на 2025 год

Стоит ли тащить Supabase + Next.js в продакшн в 2025 году?

Да, однозначно.

Но с одним условием: вы используете его как Postgres-as-a-Service, а не как "Firebase для бедных".
Архитектура Next.js App Router идеально ложится на модель Supabase, если вы держите логику доступа к данным на сервере (в Server Actions или API Routes), а клиент используете только для Realtime-подписок и мелкой интерактивности.

Это мощный стек, который позволяет команде из 2-3 сеньоров делать работу, для которой раньше требовалось 10 человек. Экономия на DevOps и инфраструктуре колоссальная. Главное — не забывайте про RLS и типизацию.

Нужен похожий проект?

Свяжитесь с нами для бесплатной консультации.

Обсудить задачу