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.
Зачем?
- Принцип единственной ответственности. Компонент рисует UI, сервис ходит за данными.
- Типизация. Мы генерируем типы из схемы БД и используем их везде.
- Централизация. Если завтра вы решите сменить 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):
// 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. Только вызов метода.
// 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) есть два пути:
- Public Buckets: Просто и быстро. Подходит для аватарок.
- 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 и типизацию.
Нужен похожий проект?
Свяжитесь с нами для бесплатной консультации.
